Repository: encoredev/encore Branch: main Commit: 7825905c360d Files: 2021 Total size: 11.3 MB Directory structure: gitextract_3htyukto/ ├── .devcontainer/ │ ├── Dockerfile │ ├── devcontainer.json │ └── scripts/ │ ├── godeps.sh │ ├── install.sh │ └── prepare.sh ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ ├── help.yml │ │ └── suggestions.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug_report.yml │ │ └── config.yml │ ├── dockerimg/ │ │ ├── Dockerfile │ │ ├── encore-entrypoint.bash │ │ └── rename-binary-if-needed.bash │ ├── minimum-reproduction.md │ └── workflows/ │ ├── ci.yml │ ├── makefile │ ├── release-2.yml │ ├── release.yml │ ├── semgrep-to-rdjson.jq │ └── staticcheck-to-rdjsonl.jq ├── .gitignore ├── .prettierrc.toml ├── .reviewdog.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── check.bash ├── cli/ │ ├── cmd/ │ │ ├── encore/ │ │ │ ├── app/ │ │ │ │ ├── app.go │ │ │ │ ├── clone.go │ │ │ │ ├── create.go │ │ │ │ ├── create_form.go │ │ │ │ ├── create_test.go │ │ │ │ ├── initialize.go │ │ │ │ └── link.go │ │ │ ├── auth/ │ │ │ │ └── auth.go │ │ │ ├── bits/ │ │ │ │ ├── add.go │ │ │ │ ├── api.go │ │ │ │ ├── bits.go │ │ │ │ └── list.go │ │ │ ├── build.go │ │ │ ├── check.go │ │ │ ├── cmdutil/ │ │ │ │ ├── autocompletes.go │ │ │ │ ├── cmdutil.go │ │ │ │ ├── daemon.go │ │ │ │ ├── forms.go │ │ │ │ ├── language.go │ │ │ │ ├── output.go │ │ │ │ └── stream.go │ │ │ ├── config/ │ │ │ │ └── config.go │ │ │ ├── daemon/ │ │ │ │ ├── daemon.go │ │ │ │ └── migrations/ │ │ │ │ ├── 1_initial_schema.up.sql │ │ │ │ ├── 2_infra_namespaces.up.sql │ │ │ │ ├── 3_test_tracing.up.sql │ │ │ │ ├── 4_add_parent_span_id.up.sql │ │ │ │ └── 5_add_caller_event_id.up.sql │ │ │ ├── daemon.go │ │ │ ├── db.go │ │ │ ├── debug.go │ │ │ ├── deploy.go │ │ │ ├── exec.go │ │ │ ├── gen.go │ │ │ ├── init_windows.go │ │ │ ├── k8s/ │ │ │ │ ├── auth.go │ │ │ │ ├── config.go │ │ │ │ ├── kubernetes.go │ │ │ │ └── types/ │ │ │ │ ├── KUBERNETES_LICENSE.txt │ │ │ │ ├── README.md │ │ │ │ ├── clientauthentication_types.go │ │ │ │ ├── homedir.go │ │ │ │ ├── meta_types.go │ │ │ │ └── runtime_types.go │ │ │ ├── llm_rules/ │ │ │ │ ├── init.go │ │ │ │ ├── llm_rules.go │ │ │ │ └── tool.go │ │ │ ├── logs.go │ │ │ ├── main.go │ │ │ ├── mcp.go │ │ │ ├── namespace/ │ │ │ │ └── namespace.go │ │ │ ├── rand.go │ │ │ ├── root/ │ │ │ │ └── rootcmd.go │ │ │ ├── run.go │ │ │ ├── secrets/ │ │ │ │ ├── archive.go │ │ │ │ ├── delete.go │ │ │ │ ├── list.go │ │ │ │ ├── secrets.go │ │ │ │ └── set.go │ │ │ ├── sqlc.go │ │ │ ├── telemetry.go │ │ │ ├── test.go │ │ │ └── version.go │ │ ├── git-remote-encore/ │ │ │ └── main.go │ │ └── tsbundler-encore/ │ │ └── main.go │ ├── daemon/ │ │ ├── apps/ │ │ │ └── apps.go │ │ ├── check.go │ │ ├── common.go │ │ ├── create.go │ │ ├── daemon.go │ │ ├── dash/ │ │ │ ├── ai/ │ │ │ │ ├── assembler.go │ │ │ │ ├── client.go │ │ │ │ ├── codegen.go │ │ │ │ ├── conv.go │ │ │ │ ├── manager.go │ │ │ │ ├── overlay.go │ │ │ │ ├── parser.go │ │ │ │ ├── sql.go │ │ │ │ ├── types.go │ │ │ │ └── types_test.go │ │ │ ├── apiproxy/ │ │ │ │ └── apiproxy.go │ │ │ ├── dash.go │ │ │ ├── dash_test.go │ │ │ ├── dashproxy/ │ │ │ │ └── dashproxy.go │ │ │ ├── dbbrowser.go │ │ │ └── server.go │ │ ├── db.go │ │ ├── debug.go │ │ ├── engine/ │ │ │ ├── runtime.go │ │ │ ├── trace/ │ │ │ │ ├── parse_test.go │ │ │ │ └── trace.go │ │ │ └── trace2/ │ │ │ ├── recorder.go │ │ │ ├── sqlite/ │ │ │ │ ├── read.go │ │ │ │ └── write.go │ │ │ └── store.go │ │ ├── exec_script.go │ │ ├── export/ │ │ │ ├── download.go │ │ │ ├── export.go │ │ │ └── infra_config.go │ │ ├── export.go │ │ ├── internal/ │ │ │ ├── runlog/ │ │ │ │ └── runlog.go │ │ │ └── sym/ │ │ │ ├── sym.go │ │ │ ├── sym_darwin.go │ │ │ ├── sym_elf.go │ │ │ └── sym_windows.go │ │ ├── mcp/ │ │ │ ├── api_tools.go │ │ │ ├── bucket_tools.go │ │ │ ├── cache_tools.go │ │ │ ├── cron_tools.go │ │ │ ├── db_tools.go │ │ │ ├── docs_tools.go │ │ │ ├── mcp.go │ │ │ ├── metrics_tools.go │ │ │ ├── pubsub_tools.go │ │ │ ├── schema_json.go │ │ │ ├── secret_tools.go │ │ │ ├── src_tools.go │ │ │ ├── trace_tools.go │ │ │ └── util.go │ │ ├── namespace/ │ │ │ └── namespace.go │ │ ├── namespace.go │ │ ├── objects/ │ │ │ ├── manager.go │ │ │ ├── objects.go │ │ │ └── public.go │ │ ├── pubsub/ │ │ │ ├── nsq.go │ │ │ └── utils.go │ │ ├── redis/ │ │ │ └── redis.go │ │ ├── run/ │ │ │ ├── call.go │ │ │ ├── check.go │ │ │ ├── errors.go │ │ │ ├── exec_command.go │ │ │ ├── exec_script.go │ │ │ ├── http.go │ │ │ ├── infra/ │ │ │ │ ├── encorecloudtesting.go │ │ │ │ └── infra.go │ │ │ ├── manager.go │ │ │ ├── nsq_names.go │ │ │ ├── proc_groups.go │ │ │ ├── run.go │ │ │ ├── runtime_config2.go │ │ │ ├── tests.go │ │ │ └── watch.go │ │ ├── run.go │ │ ├── schema.go │ │ ├── secret/ │ │ │ └── secret.go │ │ ├── sqldb/ │ │ │ ├── cluster.go │ │ │ ├── db.go │ │ │ ├── db_test.go │ │ │ ├── docker/ │ │ │ │ └── docker.go │ │ │ ├── driver.go │ │ │ ├── external/ │ │ │ │ └── external.go │ │ │ ├── manager.go │ │ │ ├── migrate.go │ │ │ ├── proxy.go │ │ │ ├── remote.go │ │ │ └── utils.go │ │ ├── telemetry.go │ │ ├── test.go │ │ ├── tracing.go │ │ ├── userfacing.go │ │ └── watch.go │ └── internal/ │ ├── browser/ │ │ └── browser.go │ ├── bubbles/ │ │ ├── checklist/ │ │ │ └── checklist.go │ │ └── selector/ │ │ └── selector.go │ ├── dedent/ │ │ ├── dedent.go │ │ └── dedent_test.go │ ├── gosym/ │ │ ├── pclntab.go │ │ ├── symtab.go │ │ ├── symtab_test.go │ │ └── testdata/ │ │ ├── main.go │ │ ├── pclinetest.h │ │ └── pclinetest.s │ ├── jsonrpc2/ │ │ ├── conn.go │ │ ├── handler.go │ │ ├── jsonrpc2.go │ │ ├── jsonrpc2_test.go │ │ ├── messages.go │ │ ├── serve.go │ │ ├── serve_test.go │ │ ├── servertest/ │ │ │ ├── servertest.go │ │ │ └── servertest_test.go │ │ ├── stream.go │ │ ├── wire.go │ │ └── wire_test.go │ ├── login/ │ │ ├── deviceauth.go │ │ ├── interactive.go │ │ └── login.go │ ├── manifest/ │ │ └── manifest.go │ ├── onboarding/ │ │ └── onboarding.go │ ├── platform/ │ │ ├── api.go │ │ ├── client.go │ │ ├── gql/ │ │ │ ├── app.go │ │ │ ├── env.go │ │ │ └── secrets.go │ │ ├── jsoniter_ext.go │ │ ├── jsoniter_ext_test.go │ │ ├── login.go │ │ └── secrets.go │ ├── telemetry/ │ │ └── telemetry.go │ └── update/ │ └── update.go ├── clippy.toml ├── context7.json ├── docs/ │ ├── go/ │ │ ├── ai-integration.md │ │ ├── cli/ │ │ │ ├── cli-reference.md │ │ │ ├── client-generation.md │ │ │ ├── config-reference.md │ │ │ ├── infra-namespaces.md │ │ │ ├── mcp.md │ │ │ └── telemetry.md │ │ ├── community/ │ │ │ ├── contribute.md │ │ │ ├── get-involved.md │ │ │ ├── open-source.md │ │ │ ├── principles.md │ │ │ └── submit-template.md │ │ ├── concepts/ │ │ │ ├── application-model.md │ │ │ └── benefits.md │ │ ├── develop/ │ │ │ ├── api-docs.md │ │ │ ├── auth.md │ │ │ ├── config.md │ │ │ ├── cors.md │ │ │ ├── env-vars.md │ │ │ ├── metadata.md │ │ │ ├── middleware.md │ │ │ ├── mocking.md │ │ │ ├── testing.md │ │ │ └── validation.md │ │ ├── faq.md │ │ ├── how-to/ │ │ │ ├── atlas-gorm.md │ │ │ ├── auth0-auth.md │ │ │ ├── break-up-monolith.md │ │ │ ├── cgo.md │ │ │ ├── clerk-auth.md │ │ │ ├── debug.md │ │ │ ├── dependency-injection.md │ │ │ ├── entgo-orm.md │ │ │ ├── firebase-auth.md │ │ │ ├── grpc-connect.md │ │ │ ├── http-requests.md │ │ │ ├── integrate-frontend.mdx │ │ │ ├── logto-auth.md │ │ │ ├── pubsub-outbox.md │ │ │ └── temporal.md │ │ ├── install.md │ │ ├── migration/ │ │ │ ├── ai-migration.mdx │ │ │ └── migrate-away.md │ │ ├── observability/ │ │ │ ├── dev-dash.md │ │ │ ├── encore-flow.md │ │ │ ├── logging.md │ │ │ ├── metrics.md │ │ │ ├── service-catalog.md │ │ │ └── tracing.md │ │ ├── overview.md │ │ ├── primitives/ │ │ │ ├── api-calls.md │ │ │ ├── api-errors.md │ │ │ ├── api-schemas.md │ │ │ ├── app-structure.md │ │ │ ├── caching.md │ │ │ ├── change-db-schema.md │ │ │ ├── code-snippets.md │ │ │ ├── connect-existing-db.md │ │ │ ├── cron-jobs.md │ │ │ ├── database-extensions.md │ │ │ ├── database-troubleshooting.md │ │ │ ├── databases.md │ │ │ ├── defining-apis.md │ │ │ ├── insert-test-data-db.md │ │ │ ├── object-storage.md │ │ │ ├── pubsub.md │ │ │ ├── raw-endpoints.md │ │ │ ├── secrets.md │ │ │ ├── service-structs.md │ │ │ ├── services.md │ │ │ └── share-db-between-services.md │ │ ├── quick-start.mdx │ │ ├── self-host/ │ │ │ ├── ci-cd.md │ │ │ ├── configure-infra.md │ │ │ ├── deploy-to-digital-ocean-wip.md │ │ │ └── self-host.md │ │ └── tutorials/ │ │ ├── booking-system.mdx │ │ ├── graphql.mdx │ │ ├── incident-management-tool.md │ │ ├── meeting-notes.mdx │ │ ├── rest-api.mdx │ │ ├── slack-bot.md │ │ └── uptime.md │ ├── menu.cue │ ├── platform/ │ │ ├── ai-integration.md │ │ ├── deploy/ │ │ │ ├── deploying.md │ │ │ ├── environments.md │ │ │ ├── own-cloud.md │ │ │ ├── preview-environments.md │ │ │ └── security.md │ │ ├── infrastructure/ │ │ │ ├── aws.md │ │ │ ├── cloudflare.md │ │ │ ├── configuration.md │ │ │ ├── configure-kubectl.md │ │ │ ├── configure-network.md │ │ │ ├── gcp.md │ │ │ ├── import-cloud-sql.md │ │ │ ├── import-kubernetes-cluster.md │ │ │ ├── import-project.md │ │ │ ├── import-rds.md │ │ │ ├── infra.md │ │ │ ├── kubernetes.md │ │ │ ├── manage-db-users.md │ │ │ └── neon.md │ │ ├── integrations/ │ │ │ ├── api-reference.md │ │ │ ├── auth-keys.md │ │ │ ├── custom-domains.md │ │ │ ├── github.md │ │ │ ├── oauth-clients.md │ │ │ ├── terraform.md │ │ │ └── webhooks.md │ │ ├── introduction.md │ │ ├── management/ │ │ │ ├── billing.md │ │ │ ├── compliance.md │ │ │ ├── permissions.md │ │ │ ├── telemetry.md │ │ │ └── usage.md │ │ ├── migration/ │ │ │ ├── migrate-away.md │ │ │ ├── migrate-to-encore.md │ │ │ └── try-encore.md │ │ ├── observability/ │ │ │ ├── encore-flow.md │ │ │ ├── metrics.md │ │ │ ├── service-catalog.md │ │ │ └── tracing.md │ │ ├── other/ │ │ │ ├── vs-heroku.md │ │ │ ├── vs-supabase.md │ │ │ └── vs-terraform.md │ │ └── overview.md │ └── ts/ │ ├── ai-integration.md │ ├── cli/ │ │ ├── cli-reference.md │ │ ├── client-generation.md │ │ ├── config-reference.md │ │ ├── infra-namespaces.md │ │ ├── mcp.md │ │ └── telemetry.md │ ├── community/ │ │ ├── contribute.md │ │ ├── get-involved.md │ │ ├── open-source.md │ │ ├── principles.md │ │ └── submit-template.md │ ├── concepts/ │ │ ├── application-model.md │ │ ├── benefits.md │ │ └── hello-world.md │ ├── develop/ │ │ ├── auth.md │ │ ├── debug.md │ │ ├── env-vars.md │ │ ├── integrations/ │ │ │ ├── better-auth.md │ │ │ ├── polar.md │ │ │ └── resend.md │ │ ├── metadata.md │ │ ├── middleware.md │ │ ├── monorepo/ │ │ │ ├── nx.md │ │ │ └── turborepo.md │ │ ├── multithreading.md │ │ ├── orms/ │ │ │ ├── drizzle.md │ │ │ ├── knex.md │ │ │ ├── overview.md │ │ │ ├── prisma.md │ │ │ └── sequelize.md │ │ ├── running-scripts.md │ │ └── testing.md │ ├── faq.md │ ├── frontend/ │ │ ├── cors.md │ │ ├── hosting.mdx │ │ ├── mono-vs-multi-repo.mdx │ │ ├── request-client.mdx │ │ └── template-engine.md │ ├── how-to/ │ │ ├── file-uploads.md │ │ └── nestjs.md │ ├── install.md │ ├── migration/ │ │ ├── ai-migration.mdx │ │ ├── express-migration.md │ │ └── migrate-away.md │ ├── observability/ │ │ ├── dev-dash.md │ │ ├── flow.md │ │ ├── logging.md │ │ ├── metrics.md │ │ ├── service-catalog.md │ │ └── tracing.md │ ├── overview.md │ ├── primitives/ │ │ ├── api-calls.mdx │ │ ├── app-structure.md │ │ ├── caching.md │ │ ├── cookies.mdx │ │ ├── cron-jobs.md │ │ ├── database-extensions.md │ │ ├── databases.md │ │ ├── defining-apis.mdx │ │ ├── errors.md │ │ ├── graphql.mdx │ │ ├── object-storage.md │ │ ├── pubsub.md │ │ ├── raw-endpoints.mdx │ │ ├── secrets.md │ │ ├── services.mdx │ │ ├── static-assets.mdx │ │ ├── streaming-apis.mdx │ │ ├── types.mdx │ │ └── validation.mdx │ ├── quick-start.mdx │ ├── self-host/ │ │ ├── build.md │ │ ├── ci-cd.md │ │ ├── configure-infra.md │ │ ├── deploy-to-digital-ocean.md │ │ └── deploy-to-railway.md │ └── tutorials/ │ ├── graphql.mdx │ ├── rest-api.mdx │ ├── slack-bot.md │ └── uptime.md ├── e2e-tests/ │ ├── README.md │ ├── app_test.go │ ├── echo_app_test.go │ ├── testdata/ │ │ ├── echo/ │ │ │ ├── .gitignore │ │ │ ├── cache/ │ │ │ │ └── cache.go │ │ │ ├── di/ │ │ │ │ └── di.go │ │ │ ├── echo/ │ │ │ │ ├── config.cue │ │ │ │ ├── config.go │ │ │ │ ├── config_test.go │ │ │ │ ├── echo.go │ │ │ │ └── echo_test.go │ │ │ ├── empty_cfg/ │ │ │ │ └── service.go │ │ │ ├── encore.app │ │ │ ├── endtoend/ │ │ │ │ └── endtoend.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── middleware/ │ │ │ │ ├── middleware.go │ │ │ │ └── middleware_test.go │ │ │ ├── test/ │ │ │ │ └── endpoints.go │ │ │ └── validation/ │ │ │ └── validation.go │ │ ├── echo_client/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── golang/ │ │ │ │ └── client/ │ │ │ │ └── goclient.go │ │ │ ├── js/ │ │ │ │ ├── client.js │ │ │ │ └── main.js │ │ │ ├── main.go │ │ │ ├── package.json │ │ │ ├── ts/ │ │ │ │ ├── client.ts │ │ │ │ └── main.ts │ │ │ └── tsconfig.json │ │ ├── testscript/ │ │ │ ├── encore_currentrequest.txt │ │ │ ├── et_mocking.txt │ │ │ ├── et_override_user.txt │ │ │ ├── et_override_user_authdata.txt │ │ │ ├── experiment_local_secrets_override.txtar │ │ │ ├── fallback_routes.txt │ │ │ ├── graceful_shutdown.txt │ │ │ ├── pubsub_method_handler.txt │ │ │ ├── pubsub_ref.txt │ │ │ ├── ts_hello.txt │ │ │ └── ts_worker_pooling.txt │ │ └── tsapp/ │ │ ├── .gitignore │ │ ├── encore.app │ │ ├── package.json │ │ ├── service1/ │ │ │ ├── api.test.ts │ │ │ ├── api.ts │ │ │ └── encore.service.ts │ │ ├── service2/ │ │ │ ├── api.test.ts │ │ │ ├── api.ts │ │ │ └── encore.service.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── testscript_test.go │ └── ts_app_test.go ├── go.mod ├── go.sum ├── go_llm_instructions.txt ├── internal/ │ ├── conf/ │ │ └── conf.go │ ├── env/ │ │ └── env.go │ ├── etrace/ │ │ ├── etrace.go │ │ ├── gid.go │ │ └── protocol.go │ ├── gocodegen/ │ │ ├── helpers.go │ │ ├── marshalling.go │ │ └── package.go │ ├── goldfish/ │ │ └── goldfish.go │ ├── httpcache/ │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── diskcache/ │ │ │ ├── diskcache.go │ │ │ └── diskcache_test.go │ │ ├── httpcache.go │ │ ├── httpcache_test.go │ │ └── test/ │ │ ├── test.go │ │ └── test_test.go │ ├── lookpath/ │ │ └── lookpath.go │ ├── optracker/ │ │ ├── async.go │ │ └── optracker.go │ ├── userconfig/ │ │ ├── config.go │ │ ├── def.go │ │ ├── docs.go │ │ ├── files.go │ │ ├── gendocs/ │ │ │ └── gendocs.go │ │ ├── reflect.go │ │ ├── value.go │ │ └── write.go │ └── version/ │ └── version.go ├── miniredis/ │ ├── .gitignore │ ├── Cargo.toml │ ├── MINIREDIS_LICENSE.txt │ ├── src/ │ │ ├── bin/ │ │ │ └── miniredis-rs-server.rs │ │ ├── cmd/ │ │ │ ├── client.rs │ │ │ ├── cluster.rs │ │ │ ├── connection.rs │ │ │ ├── generic.rs │ │ │ ├── geo.rs │ │ │ ├── hash.rs │ │ │ ├── hll.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ ├── object.rs │ │ │ ├── pubsub.rs │ │ │ ├── scripting.rs │ │ │ ├── server.rs │ │ │ ├── set.rs │ │ │ ├── sorted_set.rs │ │ │ ├── stream.rs │ │ │ ├── string.rs │ │ │ └── transactions.rs │ │ ├── connection.rs │ │ ├── db.rs │ │ ├── dispatch.rs │ │ ├── error.rs │ │ ├── frame.rs │ │ ├── geo.rs │ │ ├── hll.rs │ │ ├── keys.rs │ │ ├── lib.rs │ │ ├── pubsub.rs │ │ ├── server.rs │ │ └── types.rs │ └── tests/ │ ├── cmd_auth.rs │ ├── cmd_bit.rs │ ├── cmd_client.rs │ ├── cmd_cluster.rs │ ├── cmd_connection.rs │ ├── cmd_generic.rs │ ├── cmd_geo.rs │ ├── cmd_hash.rs │ ├── cmd_hll.rs │ ├── cmd_list.rs │ ├── cmd_misc.rs │ ├── cmd_pubsub.rs │ ├── cmd_resp3.rs │ ├── cmd_scripting.rs │ ├── cmd_server.rs │ ├── cmd_set.rs │ ├── cmd_sorted_set.rs │ ├── cmd_stream.rs │ ├── cmd_string.rs │ ├── cmd_tls.rs │ ├── cmd_transactions.rs │ ├── direct_api.rs │ ├── helpers/ │ │ └── mod.rs │ ├── integration-go/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── cluster_test.go │ │ ├── command_test.go │ │ ├── connection_test.go │ │ ├── ephemeral.go │ │ ├── generic_test.go │ │ ├── geo_test.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── hash_test.go │ │ ├── hll_test.go │ │ ├── list_test.go │ │ ├── pubsub_test.go │ │ ├── script_test.go │ │ ├── server_test.go │ │ ├── set_test.go │ │ ├── sorted_set_test.go │ │ ├── stream_test.go │ │ ├── string_test.go │ │ ├── test.go │ │ ├── tls.go │ │ └── tx_test.go │ └── smoke.rs ├── parser/ │ └── encoding/ │ └── rpc.go ├── pkg/ │ ├── ansi/ │ │ └── ansi.go │ ├── appfile/ │ │ └── appfile.go │ ├── bits/ │ │ ├── bits.go │ │ └── download.go │ ├── builder/ │ │ ├── builder.go │ │ └── builderimpl/ │ │ └── builders.go │ ├── clientgen/ │ │ ├── client.go │ │ ├── client_test.go │ │ ├── clientgentypes/ │ │ │ └── clientgentypes.go │ │ ├── errors.go │ │ ├── golang.go │ │ ├── javascript.go │ │ ├── openapi/ │ │ │ ├── openapi.go │ │ │ └── schema.go │ │ ├── testdata/ │ │ │ ├── README.md │ │ │ ├── goapp/ │ │ │ │ ├── expected_baseauth_golang.go │ │ │ │ ├── expected_baseauth_javascript.js │ │ │ │ ├── expected_baseauth_openapi.json │ │ │ │ ├── expected_baseauth_typescript.ts │ │ │ │ ├── expected_golang.go │ │ │ │ ├── expected_httpstatus_golang.go │ │ │ │ ├── expected_httpstatus_typescript.ts │ │ │ │ ├── expected_javascript.js │ │ │ │ ├── expected_noauth_golang.go │ │ │ │ ├── expected_noauth_javascript.js │ │ │ │ ├── expected_noauth_openapi.json │ │ │ │ ├── expected_noauth_typescript.ts │ │ │ │ ├── expected_openapi.json │ │ │ │ ├── expected_typescript.ts │ │ │ │ ├── input.go │ │ │ │ ├── input_baseauth.go │ │ │ │ ├── input_httpstatus.go │ │ │ │ ├── input_noauth.go │ │ │ │ └── tsconfig.json │ │ │ └── tsapp/ │ │ │ ├── expected_decimal_golang.go │ │ │ ├── expected_decimal_javascript.js │ │ │ ├── expected_decimal_openapi.json │ │ │ ├── expected_decimal_typescript.ts │ │ │ ├── expected_golang.go │ │ │ ├── expected_httpstatus_golang.go │ │ │ ├── expected_httpstatus_typescript.ts │ │ │ ├── expected_javascript.js │ │ │ ├── expected_list_of_union_javascript.js │ │ │ ├── expected_list_of_union_openapi.json │ │ │ ├── expected_list_of_union_shared.ts │ │ │ ├── expected_list_of_union_typescript.ts │ │ │ ├── expected_openapi.json │ │ │ ├── expected_shared.ts │ │ │ ├── expected_stream_javascript.js │ │ │ ├── expected_stream_shared.ts │ │ │ ├── expected_stream_typescript.ts │ │ │ ├── expected_typescript.ts │ │ │ ├── input.ts │ │ │ ├── input_decimal.ts │ │ │ ├── input_httpstatus.ts │ │ │ ├── input_list_of_union.ts │ │ │ ├── input_stream.ts │ │ │ └── tsconfig.json │ │ ├── types.go │ │ ├── typescript.go │ │ └── utils.go │ ├── cueutil/ │ │ ├── build.go │ │ └── types.go │ ├── dockerbuild/ │ │ ├── dockerbuild.go │ │ ├── dockerbuild_test.go │ │ ├── features.go │ │ ├── manifest.go │ │ ├── spec.go │ │ ├── spec_test.go │ │ └── tarcopy.go │ ├── editors/ │ │ ├── LICENSE │ │ ├── doc.go │ │ ├── encore_names.go │ │ ├── encore_urls.go │ │ ├── launch.go │ │ ├── lookup.go │ │ ├── lookup_darwin.go │ │ ├── lookup_linux.go │ │ ├── lookup_test.go │ │ ├── lookup_unsupported.go │ │ ├── lookup_windows.go │ │ └── utils.go │ ├── eerror/ │ │ ├── error.go │ │ ├── stack.go │ │ └── zerolog.go │ ├── emulators/ │ │ └── storage/ │ │ ├── LICENSE │ │ ├── gcsemu/ │ │ │ ├── batch.go │ │ │ ├── client.go │ │ │ ├── errors.go │ │ │ ├── filestore.go │ │ │ ├── filestore_test.go │ │ │ ├── gcsemu.go │ │ │ ├── gcsemu_test.go │ │ │ ├── http_wrappers.go │ │ │ ├── memstore.go │ │ │ ├── memstore_test.go │ │ │ ├── meta.go │ │ │ ├── multipart.go │ │ │ ├── parse.go │ │ │ ├── range.go │ │ │ ├── range_test.go │ │ │ ├── raw_http_test.go │ │ │ ├── remote_test.go │ │ │ ├── server.go │ │ │ ├── store.go │ │ │ ├── util.go │ │ │ └── walk.go │ │ └── gcsutil/ │ │ ├── counted_lock.go │ │ ├── doc.go │ │ ├── gcspagetoken.go │ │ ├── gcspagetoken.pb.go │ │ ├── gcspagetoken.proto │ │ ├── gcspagetoken_test.go │ │ ├── transient_lock_map.go │ │ └── transient_lock_map_test.go │ ├── encorebuild/ │ │ ├── buildconf/ │ │ │ └── config.go │ │ ├── buildutil/ │ │ │ └── buildutil.go │ │ ├── cmd/ │ │ │ ├── build-local-binary/ │ │ │ │ └── build-local-binary.go │ │ │ └── make-release/ │ │ │ └── make-release.go │ │ ├── compile/ │ │ │ └── compile.go │ │ ├── dist_builder.go │ │ ├── gentypedefs/ │ │ │ ├── gentypedefs.go │ │ │ └── napi.cjs.tmpl │ │ ├── githubrelease/ │ │ │ └── githubrelease.go │ │ ├── jsruntimebuild.go │ │ ├── supervisorbuild.go │ │ └── windows/ │ │ ├── .gitignore │ │ ├── build.bat │ │ ├── manifest.xml │ │ └── resources.rc │ ├── environ/ │ │ └── environ.go │ ├── errinsrc/ │ │ ├── characters.go │ │ ├── errinsrc.go │ │ ├── internal/ │ │ │ ├── cuelocation.go │ │ │ ├── golocation.go │ │ │ ├── helper.go │ │ │ └── location.go │ │ ├── list.go │ │ ├── setup_test.go │ │ ├── srcerrors/ │ │ │ ├── errors.go │ │ │ ├── helpers.go │ │ │ └── helptext.go │ │ ├── srcrender.go │ │ ├── srcrender_test.go │ │ ├── stack.go │ │ ├── stack_dev.go │ │ ├── stack_release.go │ │ ├── testdata/ │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_ascii.golden │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_unicode.golden │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__on_same_line_ascii.golden │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__on_same_line_unicode.golden │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_ascii.golden │ │ │ ├── Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_unicode.golden │ │ │ ├── Test_renderSrc_MutlilineError_ascii.golden │ │ │ ├── Test_renderSrc_MutlilineError_unicode.golden │ │ │ ├── Test_renderSrc_Simple__error_no_text_ascii.golden │ │ │ ├── Test_renderSrc_Simple__error_no_text_unicode.golden │ │ │ ├── Test_renderSrc_Simple__multiline_message_ascii.golden │ │ │ ├── Test_renderSrc_Simple__multiline_message_unicode.golden │ │ │ ├── Test_renderSrc_Simple__simple_error_ascii.golden │ │ │ ├── Test_renderSrc_Simple__simple_error_unicode.golden │ │ │ ├── Test_renderSrc_Simple__simple_help_ascii.golden │ │ │ ├── Test_renderSrc_Simple__simple_help_unicode.golden │ │ │ ├── Test_renderSrc_Simple__simple_warning_ascii.golden │ │ │ ├── Test_renderSrc_Simple__simple_warning_unicode.golden │ │ │ ├── Test_renderSrc_Simple__single_character_error_ascii.golden │ │ │ ├── Test_renderSrc_Simple__single_character_error_unicode.golden │ │ │ ├── test.cue │ │ │ └── test.go │ │ └── utils.go │ ├── errlist/ │ │ └── errlist.go │ ├── errors/ │ │ ├── locations.go │ │ ├── range.go │ │ ├── template.go │ │ └── utils.go │ ├── fns/ │ │ └── fns.go │ ├── github/ │ │ └── github.go │ ├── golden/ │ │ └── golden.go │ ├── idents/ │ │ ├── identifiers.go │ │ └── identifiers_test.go │ ├── jsonext/ │ │ ├── listencoder.go │ │ ├── listencoder_test.go │ │ └── protojson.go │ ├── logging/ │ │ └── zerolog_adapter.go │ ├── make-release/ │ │ ├── compilers.go │ │ ├── dist_builder.go │ │ ├── js_packager.go │ │ ├── make-release.go │ │ ├── utils.go │ │ └── windows/ │ │ ├── .gitignore │ │ ├── build.bat │ │ ├── manifest.xml │ │ └── resources.rc │ ├── metascrub/ │ │ ├── metascrub.go │ │ └── metascrub_test.go │ ├── namealloc/ │ │ ├── namealloc.go │ │ └── namealloc_test.go │ ├── noopgateway/ │ │ ├── noopgateway.go │ │ └── retry_dialer.go │ ├── noopgwdesc/ │ │ └── gateway.go │ ├── option/ │ │ ├── option.go │ │ └── pkgfn.go │ ├── paths/ │ │ └── paths.go │ ├── pgproxy/ │ │ ├── README.md │ │ ├── pgproxy.go │ │ └── scram.go │ ├── promise/ │ │ └── prom.go │ ├── rtconfgen/ │ │ ├── base_builder.go │ │ ├── convert.go │ │ ├── infra_builder.go │ │ └── resource_map.go │ ├── supervisor/ │ │ ├── cmd/ │ │ │ └── supervisor-encore/ │ │ │ └── main.go │ │ └── supervisor.go │ ├── svcproxy/ │ │ ├── dialer.go │ │ ├── doc.go │ │ └── svcproxy.go │ ├── tarstream/ │ │ ├── LICENSE │ │ ├── datavec.go │ │ ├── datavec_test.go │ │ ├── tarstream.go │ │ └── tarstream_test.go │ ├── traceparser/ │ │ ├── binreader.go │ │ ├── parser.go │ │ └── parser_test.go │ ├── vcs/ │ │ ├── app.go │ │ └── vcs.go │ ├── vfs/ │ │ ├── directory.go │ │ ├── doc.go │ │ ├── file.go │ │ ├── node.go │ │ ├── testdata/ │ │ │ └── filteredglob/ │ │ │ ├── blahsvc/ │ │ │ │ ├── another.json │ │ │ │ └── test.json │ │ │ ├── foosystem/ │ │ │ │ ├── README.md │ │ │ │ ├── anotherservice/ │ │ │ │ │ └── test.txt │ │ │ │ └── barservice/ │ │ │ │ ├── blah.json │ │ │ │ └── test.txt │ │ │ └── nope/ │ │ │ └── ignored.txt │ │ ├── utils.go │ │ ├── vfs.go │ │ └── vfs_test.go │ ├── watcher/ │ │ ├── event.go │ │ ├── rlimit_nix.go │ │ ├── rlimit_noop.go │ │ ├── util.go │ │ └── watcher.go │ ├── words/ │ │ ├── funcs.go │ │ ├── shortwords.txt │ │ ├── words.go │ │ └── words_test.go │ └── xos/ │ ├── xos_unix.go │ └── xos_windows.go ├── proto/ │ ├── encore/ │ │ ├── daemon/ │ │ │ ├── daemon.pb.go │ │ │ ├── daemon.proto │ │ │ └── daemon_grpc.pb.go │ │ ├── engine/ │ │ │ ├── trace/ │ │ │ │ ├── trace.pb.go │ │ │ │ ├── trace.proto │ │ │ │ └── trace_util.go │ │ │ └── trace2/ │ │ │ ├── trace2.pb.go │ │ │ ├── trace2.proto │ │ │ └── trace_util.go │ │ ├── parser/ │ │ │ ├── meta/ │ │ │ │ └── v1/ │ │ │ │ ├── meta.pb.go │ │ │ │ ├── meta.pb.ts │ │ │ │ └── meta.proto │ │ │ └── schema/ │ │ │ └── v1/ │ │ │ ├── schema.pb.go │ │ │ ├── schema.pb.ts │ │ │ ├── schema.proto │ │ │ ├── walk.go │ │ │ └── walk_test.go │ │ └── runtime/ │ │ └── v1/ │ │ ├── infra.pb.go │ │ ├── infra.proto │ │ ├── runtime.pb.go │ │ ├── runtime.proto │ │ ├── secretdata.pb.go │ │ └── secretdata.proto │ ├── gen.go │ ├── gen.sh │ └── prompb/ │ ├── remote.proto │ └── types.proto ├── runtimes/ │ ├── core/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── resources/ │ │ │ └── test/ │ │ │ ├── infra.config.json │ │ │ └── runtime.pb │ │ └── src/ │ │ ├── api/ │ │ │ ├── auth/ │ │ │ │ ├── local.rs │ │ │ │ ├── mod.rs │ │ │ │ └── remote.rs │ │ │ ├── call.rs │ │ │ ├── cors/ │ │ │ │ ├── cors_headers_config/ │ │ │ │ │ ├── LICENSE │ │ │ │ │ ├── allow_credentials.rs │ │ │ │ │ ├── allow_headers.rs │ │ │ │ │ ├── allow_methods.rs │ │ │ │ │ ├── allow_origin.rs │ │ │ │ │ ├── allow_private_network.rs │ │ │ │ │ ├── expose_headers.rs │ │ │ │ │ ├── max_age.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── vary.rs │ │ │ │ ├── mod.rs │ │ │ │ └── tests.rs │ │ │ ├── encore_routes/ │ │ │ │ ├── healthz.rs │ │ │ │ └── mod.rs │ │ │ ├── endpoint.rs │ │ │ ├── error.rs │ │ │ ├── gateway/ │ │ │ │ ├── mod.rs │ │ │ │ ├── router.rs │ │ │ │ └── websocket.rs │ │ │ ├── http.rs │ │ │ ├── http_server.rs │ │ │ ├── httputil.rs │ │ │ ├── jsonschema/ │ │ │ │ ├── de.rs │ │ │ │ ├── meta.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── parse.rs │ │ │ │ ├── ser.rs │ │ │ │ └── validation.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── paths.rs │ │ │ ├── pvalue.rs │ │ │ ├── reqauth/ │ │ │ │ ├── caller.rs │ │ │ │ ├── encoreauth/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── ophash.rs │ │ │ │ │ └── sign.rs │ │ │ │ ├── meta.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── platform.rs │ │ │ │ └── svcauth.rs │ │ │ ├── schema/ │ │ │ │ ├── body.rs │ │ │ │ ├── cookie.rs │ │ │ │ ├── encoding.rs │ │ │ │ ├── header.rs │ │ │ │ ├── httpstatus.rs │ │ │ │ ├── method.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── path.rs │ │ │ │ └── query.rs │ │ │ ├── server.rs │ │ │ ├── snapshots/ │ │ │ │ ├── encore_runtime_core__api__paths__tests__basic.snap │ │ │ │ ├── encore_runtime_core__api__paths__tests__fallback.snap │ │ │ │ ├── encore_runtime_core__api__paths__tests__paths_to_register.snap │ │ │ │ ├── encore_runtime_core__api__paths__tests__tsr_conflict.snap │ │ │ │ └── encore_runtime_core__api__paths__tests__wildcard.snap │ │ │ ├── static_assets.rs │ │ │ ├── websocket.rs │ │ │ └── websocket_client.rs │ │ ├── base32.rs │ │ ├── cache/ │ │ │ ├── client.rs │ │ │ ├── client_tests.rs │ │ │ ├── error.rs │ │ │ ├── manager.rs │ │ │ ├── miniredis.rs │ │ │ ├── mod.rs │ │ │ ├── noop.rs │ │ │ └── tracer.rs │ │ ├── error/ │ │ │ ├── conversions.rs │ │ │ └── mod.rs │ │ ├── infracfg.rs │ │ ├── lib.rs │ │ ├── log/ │ │ │ ├── consolewriter.rs │ │ │ ├── fields.rs │ │ │ ├── logger.rs │ │ │ ├── mod.rs │ │ │ └── writers.rs │ │ ├── meta/ │ │ │ └── mod.rs │ │ ├── metadata/ │ │ │ ├── aws.rs │ │ │ ├── gce.rs │ │ │ └── mod.rs │ │ ├── metrics/ │ │ │ ├── atomic.rs │ │ │ ├── counter.rs │ │ │ ├── exporter/ │ │ │ │ ├── aws.rs │ │ │ │ ├── datadog.rs │ │ │ │ ├── gcp.rs │ │ │ │ ├── mod.rs │ │ │ │ └── prometheus.rs │ │ │ ├── gauge.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ ├── system.rs │ │ │ └── test.rs │ │ ├── model/ │ │ │ └── mod.rs │ │ ├── names.rs │ │ ├── objects/ │ │ │ ├── gcs/ │ │ │ │ ├── bucket.rs │ │ │ │ └── mod.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── noop/ │ │ │ │ └── mod.rs │ │ │ └── s3/ │ │ │ ├── bucket.rs │ │ │ └── mod.rs │ │ ├── proccfg.rs │ │ ├── pubsub/ │ │ │ ├── gcp/ │ │ │ │ ├── jwk.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── push_sub.rs │ │ │ │ ├── sub.rs │ │ │ │ └── topic.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── noop/ │ │ │ │ └── mod.rs │ │ │ ├── nsq/ │ │ │ │ ├── mod.rs │ │ │ │ ├── sub.rs │ │ │ │ └── topic.rs │ │ │ ├── push_registry.rs │ │ │ └── sqs_sns/ │ │ │ ├── fetcher.rs │ │ │ ├── mod.rs │ │ │ ├── sub.rs │ │ │ └── topic.rs │ │ ├── runtime_config/ │ │ │ └── mod.rs │ │ ├── secrets/ │ │ │ └── mod.rs │ │ ├── sqldb/ │ │ │ ├── client.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── numeric.rs │ │ │ ├── transaction.rs │ │ │ └── val.rs │ │ └── trace/ │ │ ├── eventbuf.rs │ │ ├── log.rs │ │ ├── mod.rs │ │ ├── protocol.rs │ │ └── time_anchor.rs │ ├── go/ │ │ ├── README.md │ │ ├── appruntime/ │ │ │ ├── apisdk/ │ │ │ │ ├── api/ │ │ │ │ │ ├── auth.go │ │ │ │ │ ├── auth_remote.go │ │ │ │ │ ├── call_context.go │ │ │ │ │ ├── call_meta.go │ │ │ │ │ ├── call_meta_test.go │ │ │ │ │ ├── callers.go │ │ │ │ │ ├── capture.go │ │ │ │ │ ├── encore_routes.go │ │ │ │ │ ├── errmarshalling/ │ │ │ │ │ │ ├── fallback.go │ │ │ │ │ │ ├── jsonextension.go │ │ │ │ │ │ ├── marshal.go │ │ │ │ │ │ └── marshal_test.go │ │ │ │ │ ├── gateway.go │ │ │ │ │ ├── handler.go │ │ │ │ │ ├── handler_test.go │ │ │ │ │ ├── middleware.go │ │ │ │ │ ├── pubsub_push_proxy.go │ │ │ │ │ ├── reflection.go │ │ │ │ │ ├── reflection_test.go │ │ │ │ │ ├── registry.go │ │ │ │ │ ├── reqtrack.go │ │ │ │ │ ├── server.go │ │ │ │ │ ├── server_test.go │ │ │ │ │ ├── services.go │ │ │ │ │ ├── singleton.go │ │ │ │ │ ├── svcauth/ │ │ │ │ │ │ ├── doc.go │ │ │ │ │ │ ├── encoreauth.go │ │ │ │ │ │ ├── noop.go │ │ │ │ │ │ ├── pkgfn.go │ │ │ │ │ │ └── svcauth.go │ │ │ │ │ ├── transport/ │ │ │ │ │ │ ├── doc.go │ │ │ │ │ │ ├── eh2c.go │ │ │ │ │ │ ├── http.go │ │ │ │ │ │ ├── meta.go │ │ │ │ │ │ └── transport.go │ │ │ │ │ └── util.go │ │ │ │ ├── app/ │ │ │ │ │ ├── app.go │ │ │ │ │ ├── appinit/ │ │ │ │ │ │ └── appinit.go │ │ │ │ │ └── setup.go │ │ │ │ ├── cors/ │ │ │ │ │ ├── cors.go │ │ │ │ │ └── cors_test.go │ │ │ │ └── service/ │ │ │ │ ├── service.go │ │ │ │ └── singleton.go │ │ │ ├── doc.go │ │ │ ├── exported/ │ │ │ │ ├── config/ │ │ │ │ │ ├── config.go │ │ │ │ │ ├── infra/ │ │ │ │ │ │ ├── config.go │ │ │ │ │ │ ├── config_test.go │ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ │ ├── infra.config.json │ │ │ │ │ │ │ └── runtime.json │ │ │ │ │ │ └── validation.go │ │ │ │ │ ├── parse.go │ │ │ │ │ └── parse_test.go │ │ │ │ ├── experiments/ │ │ │ │ │ ├── cli.go │ │ │ │ │ ├── errors.go │ │ │ │ │ ├── names.go │ │ │ │ │ └── set.go │ │ │ │ ├── model/ │ │ │ │ │ ├── request.go │ │ │ │ │ └── trace.go │ │ │ │ ├── scrub/ │ │ │ │ │ ├── benchmark_test.go │ │ │ │ │ ├── scanner.go │ │ │ │ │ ├── scanner_test.go │ │ │ │ │ ├── scrub.go │ │ │ │ │ ├── scrub_test.go │ │ │ │ │ └── token_string.go │ │ │ │ ├── stack/ │ │ │ │ │ ├── stack.go │ │ │ │ │ ├── stack_app.go │ │ │ │ │ ├── stack_noapp.go │ │ │ │ │ └── stack_test.go │ │ │ │ ├── trace/ │ │ │ │ │ ├── events.go │ │ │ │ │ ├── http.go │ │ │ │ │ ├── log.go │ │ │ │ │ ├── logger.go │ │ │ │ │ ├── mock_trace/ │ │ │ │ │ │ └── mock_trace.go │ │ │ │ │ ├── mutex_app.go │ │ │ │ │ ├── mutex_noapp.go │ │ │ │ │ └── version.go │ │ │ │ └── trace2/ │ │ │ │ ├── events.go │ │ │ │ ├── http.go │ │ │ │ ├── log.go │ │ │ │ ├── logger.go │ │ │ │ ├── mutex_app.go │ │ │ │ ├── mutex_noapp.go │ │ │ │ ├── timeanchor.go │ │ │ │ └── version.go │ │ │ ├── infrasdk/ │ │ │ │ ├── metadata/ │ │ │ │ │ ├── aws_collector.go │ │ │ │ │ ├── cloud_run_collector.go │ │ │ │ │ └── metadata.go │ │ │ │ ├── metrics/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── aws/ │ │ │ │ │ │ ├── cloudwatch.go │ │ │ │ │ │ └── cloudwatch_test.go │ │ │ │ │ ├── aws_cloudwatch_exporter.go │ │ │ │ │ ├── datadog/ │ │ │ │ │ │ └── datadog.go │ │ │ │ │ ├── datadog_exporter.go │ │ │ │ │ ├── encore_cloud_exporter.go │ │ │ │ │ ├── gcp/ │ │ │ │ │ │ ├── cloud_monitoring.go │ │ │ │ │ │ └── cloud_monitoring_test.go │ │ │ │ │ ├── gcp_cloud_monitoring_exporter.go │ │ │ │ │ ├── logs_based_exporter.go │ │ │ │ │ ├── logs_based_exporter_test.go │ │ │ │ │ ├── metrics.go │ │ │ │ │ ├── metrics_test.go │ │ │ │ │ ├── metricstest/ │ │ │ │ │ │ └── test_exporter.go │ │ │ │ │ ├── null_exporter.go │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ ├── prometheus.go │ │ │ │ │ │ ├── prometheus_test.go │ │ │ │ │ │ └── prompb/ │ │ │ │ │ │ ├── remote.pb.go │ │ │ │ │ │ └── types.pb.go │ │ │ │ │ ├── prometheus_exporter.go │ │ │ │ │ ├── system/ │ │ │ │ │ │ └── system.go │ │ │ │ │ └── zzz_singleton_internal.go │ │ │ │ └── secrets/ │ │ │ │ ├── manager_internal.go │ │ │ │ └── secrets.go │ │ │ └── shared/ │ │ │ ├── appconf/ │ │ │ │ └── appconf.go │ │ │ ├── cfgutil/ │ │ │ │ └── svc.go │ │ │ ├── cloud/ │ │ │ │ └── clouds.go │ │ │ ├── cloudtrace/ │ │ │ │ ├── extractors.go │ │ │ │ ├── gcp.go │ │ │ │ └── logfields.go │ │ │ ├── encoreenv/ │ │ │ │ ├── app.go │ │ │ │ ├── encoreenv.go │ │ │ │ └── noapp.go │ │ │ ├── etype/ │ │ │ │ ├── marshal.go │ │ │ │ └── unmarshal.go │ │ │ ├── health/ │ │ │ │ ├── check.go │ │ │ │ ├── health.go │ │ │ │ └── singleton.go │ │ │ ├── jsonapi/ │ │ │ │ ├── jsonapi.go │ │ │ │ └── jsonapi_nonapp.go │ │ │ ├── logging/ │ │ │ │ └── logging.go │ │ │ ├── nativehist/ │ │ │ │ ├── PROMETHEUS_LICENSE.txt │ │ │ │ └── nativehist.go │ │ │ ├── platform/ │ │ │ │ ├── platform.go │ │ │ │ ├── singleton.go │ │ │ │ └── streaming_trace.go │ │ │ ├── reqtrack/ │ │ │ │ ├── impl.go │ │ │ │ ├── impl_app.go │ │ │ │ ├── impl_noapp.go │ │ │ │ ├── reqtrack.go │ │ │ │ ├── singleton.go │ │ │ │ └── trace_stream.go │ │ │ ├── serde/ │ │ │ │ └── utils.go │ │ │ ├── shutdown/ │ │ │ │ ├── shutdown.go │ │ │ │ └── singleton.go │ │ │ ├── syncutil/ │ │ │ │ ├── once.go │ │ │ │ ├── once_test.go │ │ │ │ └── syncutil.go │ │ │ ├── testsupport/ │ │ │ │ ├── runtimehooks_app.go │ │ │ │ ├── testconfig.go │ │ │ │ └── testsupport.go │ │ │ └── traceprovider/ │ │ │ ├── mock_trace/ │ │ │ │ ├── factory.go │ │ │ │ └── mock_trace.go │ │ │ └── traceprovider.go │ │ ├── beta/ │ │ │ ├── auth/ │ │ │ │ ├── auth.go │ │ │ │ └── pkgfn.go │ │ │ ├── errs/ │ │ │ │ ├── builder.go │ │ │ │ ├── codes.go │ │ │ │ ├── details.go │ │ │ │ ├── error.go │ │ │ │ └── errs_internal.go │ │ │ └── package.go │ │ ├── config/ │ │ │ ├── helpers_internal.go │ │ │ ├── manager_internal.go │ │ │ ├── pkgfn.go │ │ │ ├── test_internal.go │ │ │ └── types.go │ │ ├── cron/ │ │ │ └── cron.go │ │ ├── et/ │ │ │ ├── auth.go │ │ │ ├── config.go │ │ │ ├── manager_internal.go │ │ │ ├── mocking.go │ │ │ ├── package.go │ │ │ ├── pkgfn.go │ │ │ ├── pubsub.go │ │ │ ├── singleton_internal.go │ │ │ └── sqldb.go │ │ ├── example_test.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal/ │ │ │ ├── limiter/ │ │ │ │ ├── limiter.go │ │ │ │ └── noop.go │ │ │ └── platformauth/ │ │ │ └── platformauth.go │ │ ├── meta.go │ │ ├── metrics/ │ │ │ ├── bits_internal.go │ │ │ ├── histogram_internal.go │ │ │ ├── metrics.go │ │ │ ├── metrics_test.go │ │ │ ├── pkgfn.go │ │ │ ├── registry_internal.go │ │ │ ├── singleton_internal.go │ │ │ └── units.go │ │ ├── middleware/ │ │ │ ├── middleware.go │ │ │ └── middleware_internal.go │ │ ├── package.go │ │ ├── pkgfn.go │ │ ├── pubsub/ │ │ │ ├── internal/ │ │ │ │ ├── aws/ │ │ │ │ │ ├── manager.go │ │ │ │ │ ├── topic.go │ │ │ │ │ └── topic_test.go │ │ │ │ ├── azure/ │ │ │ │ │ ├── clients.go │ │ │ │ │ └── topic.go │ │ │ │ ├── encorecloud/ │ │ │ │ │ ├── manager.go │ │ │ │ │ └── topic.go │ │ │ │ ├── gcp/ │ │ │ │ │ ├── clients.go │ │ │ │ │ ├── push_handler.go │ │ │ │ │ └── topic.go │ │ │ │ ├── noop/ │ │ │ │ │ └── topic.go │ │ │ │ ├── nsq/ │ │ │ │ │ ├── log_adapter.go │ │ │ │ │ └── topic.go │ │ │ │ ├── test/ │ │ │ │ │ └── topic.go │ │ │ │ ├── types/ │ │ │ │ │ ├── private.go │ │ │ │ │ ├── public.go │ │ │ │ │ └── push_registry.go │ │ │ │ └── utils/ │ │ │ │ ├── contexts.go │ │ │ │ ├── utils.go │ │ │ │ ├── utils_test.go │ │ │ │ ├── workers.go │ │ │ │ └── workers_test.go │ │ │ ├── manager_internal.go │ │ │ ├── package.go │ │ │ ├── pkgfn.go │ │ │ ├── provider_aws.go │ │ │ ├── provider_azure.go │ │ │ ├── provider_encorecloud.go │ │ │ ├── provider_gcp.go │ │ │ ├── provider_nsq.go │ │ │ ├── refs.go │ │ │ ├── subscription.go │ │ │ ├── test_internal.go │ │ │ ├── topic.go │ │ │ ├── types.go │ │ │ └── zzz_singleton_internal.go │ │ ├── request.go │ │ ├── rlog/ │ │ │ ├── pkgfn.go │ │ │ ├── rlog.go │ │ │ └── rlog_test.go │ │ ├── shutdown/ │ │ │ └── shutdown.go │ │ ├── storage/ │ │ │ ├── cache/ │ │ │ │ ├── basic.go │ │ │ │ ├── basic_test.go │ │ │ │ ├── cache.go │ │ │ │ ├── cache_test.go │ │ │ │ ├── error_internal.go │ │ │ │ ├── list.go │ │ │ │ ├── list_test.go │ │ │ │ ├── manager_internal.go │ │ │ │ ├── noop_internal.go │ │ │ │ ├── pkgfn.go │ │ │ │ ├── set.go │ │ │ │ ├── set_test.go │ │ │ │ ├── struct.go │ │ │ │ └── zzz_singleton_internal.go │ │ │ ├── objects/ │ │ │ │ ├── bucket.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── providers/ │ │ │ │ │ │ ├── gcs/ │ │ │ │ │ │ │ └── bucket.go │ │ │ │ │ │ ├── noop/ │ │ │ │ │ │ │ └── noop.go │ │ │ │ │ │ └── s3/ │ │ │ │ │ │ ├── bucket.go │ │ │ │ │ │ ├── mock_client_test.go │ │ │ │ │ │ ├── uploader.go │ │ │ │ │ │ └── uploader_test.go │ │ │ │ │ └── types/ │ │ │ │ │ └── types.go │ │ │ │ ├── manager_internal.go │ │ │ │ ├── objects.go │ │ │ │ ├── options.go │ │ │ │ ├── package.go │ │ │ │ ├── path_escape.go │ │ │ │ ├── provider_gcs.go │ │ │ │ ├── provider_s3.go │ │ │ │ ├── refs.go │ │ │ │ ├── registry_internal.go │ │ │ │ └── zzz_singleton_internal.go │ │ │ └── sqldb/ │ │ │ ├── db.go │ │ │ ├── db_hooks_test.go │ │ │ ├── errors.go │ │ │ ├── errors_internal.go │ │ │ ├── errors_test.go │ │ │ ├── internal/ │ │ │ │ └── stdlibdriver/ │ │ │ │ ├── LICENSE │ │ │ │ └── stdlibdriver.go │ │ │ ├── manager_internal.go │ │ │ ├── pgx_tracer_internal.go │ │ │ ├── pkgfn.go │ │ │ ├── sqldb.go │ │ │ ├── sqldb_test.go │ │ │ ├── sqlerr/ │ │ │ │ └── sqlerr.go │ │ │ ├── stdlib.go │ │ │ ├── stdlib_noop_internal.go │ │ │ ├── stdlib_wrapper_internal.go │ │ │ ├── test_db.go │ │ │ └── zzz_singleton_internal.go │ │ └── types/ │ │ ├── option/ │ │ │ └── option.go │ │ └── uuid/ │ │ ├── codec.go │ │ ├── codec_test.go │ │ ├── fuzz.go │ │ ├── generator.go │ │ ├── generator_test.go │ │ ├── sql.go │ │ ├── sql_test.go │ │ ├── uuid.go │ │ └── uuid_test.go │ └── js/ │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── encore.dev/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── api/ │ │ │ ├── error.ts │ │ │ ├── gateway.ts │ │ │ ├── httpstatus.ts │ │ │ ├── mod.ts │ │ │ └── stream.ts │ │ ├── app_meta.ts │ │ ├── auth/ │ │ │ └── mod.ts │ │ ├── config/ │ │ │ ├── mod.ts │ │ │ └── secrets.ts │ │ ├── cron/ │ │ │ └── mod.ts │ │ ├── internal/ │ │ │ ├── api/ │ │ │ │ ├── mod.ts │ │ │ │ └── node_http.ts │ │ │ ├── appinit/ │ │ │ │ └── mod.ts │ │ │ ├── auth/ │ │ │ │ └── mod.ts │ │ │ ├── codegen/ │ │ │ │ ├── api.ts │ │ │ │ ├── appinit.ts │ │ │ │ └── auth.ts │ │ │ ├── metrics/ │ │ │ │ ├── mod.ts │ │ │ │ └── registry.ts │ │ │ ├── reqtrack/ │ │ │ │ └── mod.ts │ │ │ ├── runtime/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .npmignore │ │ │ │ └── mod.ts │ │ │ ├── types/ │ │ │ │ └── mod.ts │ │ │ └── utils/ │ │ │ └── constraints.ts │ │ ├── log/ │ │ │ └── mod.ts │ │ ├── metrics/ │ │ │ └── mod.ts │ │ ├── mod.ts │ │ ├── package.json │ │ ├── pubsub/ │ │ │ ├── mod.ts │ │ │ ├── refs.ts │ │ │ ├── subscription.ts │ │ │ └── topic.ts │ │ ├── req_meta.ts │ │ ├── service/ │ │ │ └── mod.ts │ │ ├── storage/ │ │ │ ├── cache/ │ │ │ │ ├── basic.ts │ │ │ │ ├── cluster.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── expiry.ts │ │ │ │ ├── keyspace.ts │ │ │ │ ├── list.ts │ │ │ │ ├── mod.ts │ │ │ │ └── set.ts │ │ │ ├── objects/ │ │ │ │ ├── bucket.ts │ │ │ │ ├── error.ts │ │ │ │ ├── mod.ts │ │ │ │ └── refs.ts │ │ │ └── sqldb/ │ │ │ ├── database.ts │ │ │ └── mod.ts │ │ ├── tsconfig.json │ │ ├── types/ │ │ │ └── mod.ts │ │ └── validate/ │ │ └── mod.ts │ └── src/ │ ├── api.rs │ ├── cache.rs │ ├── cookies.rs │ ├── error.rs │ ├── gateway.rs │ ├── headers.rs │ ├── lib.rs │ ├── log.rs │ ├── meta.rs │ ├── metrics.rs │ ├── napi_util.rs │ ├── objects.rs │ ├── pubsub.rs │ ├── pvalue.rs │ ├── raw_api.rs │ ├── request_meta.rs │ ├── runtime.rs │ ├── runtime_config.rs │ ├── secret.rs │ ├── sqldb.rs │ ├── stream/ │ │ ├── mod.rs │ │ ├── read.rs │ │ └── write.rs │ ├── threadsafe_function.rs │ └── websocket_api.rs ├── rustfmt.toml ├── supervisor/ │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ ├── bin/ │ │ └── supervisor-encore.rs │ ├── config.rs │ ├── lib.rs │ ├── proxy.rs │ └── supervisor.rs ├── tools/ │ ├── publicapigen/ │ │ └── main.go │ └── semgrep-rules/ │ ├── README.md │ └── semgrep-go/ │ ├── LICENSE │ ├── README.md │ ├── badexponentiation.yml │ ├── badnilguard.yml │ ├── close-sql-query-rows.yml │ ├── contextCancelable.yml │ ├── contextTODO.yml │ ├── ctx-time.yml │ ├── errclosed.yml │ ├── errnilcheck.yml │ ├── errtodo.yml │ ├── gofuzz.yml │ ├── hashsum.yml │ ├── hmac-bytes.yml │ ├── hmac-hash.yml │ ├── hostport.yml │ ├── http-ctx-goroutine.yml │ ├── ioutil-discard.yml │ ├── ioutil-nop-closer.yml │ ├── ioutil-readall.yml │ ├── ioutil-readdir.yml │ ├── ioutil-readfile.yml │ ├── ioutil-tmpdir.yml │ ├── ioutil-tmpfile.yml │ ├── ioutil-writefile.yml │ ├── joinpath.yml │ ├── json-writer.yml │ ├── mail-address.yml │ ├── marshaljson.yml │ ├── marshalyaml.yml │ ├── mathbits.yml │ ├── nilerr.yml │ ├── nrtxn.yml │ ├── oddbitwise.yml │ ├── oddcompare-subtract-eq-zero.yml │ ├── oddcompare-subtract-gt-zero.yml │ ├── oddcompare-subtract-gte-zero.yml │ ├── oddcompare-subtract-lt-zero.yml │ ├── oddcompare-subtract-lte-zero.yml │ ├── oddcompare-subtract-neq-zero.yml │ ├── oddcompare-xor-eq-zero.yml │ ├── oddcompare-xor-neq-zero.yml │ ├── oddcompound.yml │ ├── oddifsequence.yml │ ├── oddmathbits.yml │ ├── os-error-is-exist.yml │ ├── os-error-is-not-exist.yml │ ├── os-error-is-permission.yml │ ├── os-error-is-timeout.yml │ ├── parseint-downcast.yml │ ├── readeof.yml │ ├── readfull.yml │ ├── returnnil.yml │ ├── ruleguard.rules.go │ ├── sortslice.yml │ ├── sprinterr.yml │ ├── timeafter.yml │ ├── unixnano-after.yml │ ├── unixnano-before.yml │ ├── unmarshaljson.yml │ ├── unmarshalyaml.yml │ ├── use-fprintf-not-write-fsprint.yml │ ├── use-write-not-fprint.yml │ ├── use-writer-not-writestring.yml │ ├── wrongerrcall.yml │ └── wronglock.yml ├── ts_llm_instructions.txt ├── tsparser/ │ ├── Cargo.toml │ ├── build.rs │ ├── examples/ │ │ └── testparse.rs │ ├── litparser/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── litparser-derive/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ └── integration_tests.rs │ ├── src/ │ │ ├── app/ │ │ │ └── mod.rs │ │ ├── bin/ │ │ │ └── tsparser-encore.rs │ │ ├── builder/ │ │ │ ├── codegen.rs │ │ │ ├── compile.rs │ │ │ ├── mod.rs │ │ │ ├── package_mgmt.rs │ │ │ ├── parse.rs │ │ │ ├── prepare.rs │ │ │ ├── templates/ │ │ │ │ ├── catalog/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth_ts.handlebars │ │ │ │ │ │ └── index_ts.handlebars │ │ │ │ │ └── clients/ │ │ │ │ │ ├── endpoints_d_ts.handlebars │ │ │ │ │ ├── endpoints_js.handlebars │ │ │ │ │ ├── endpoints_testing_js.handlebars │ │ │ │ │ ├── index_d_ts.handlebars │ │ │ │ │ └── index_js.handlebars │ │ │ │ └── entrypoints/ │ │ │ │ ├── combined/ │ │ │ │ │ └── main.handlebars │ │ │ │ ├── gateways/ │ │ │ │ │ └── main.handlebars │ │ │ │ └── services/ │ │ │ │ └── main.handlebars │ │ │ ├── test.rs │ │ │ └── transpiler.rs │ │ ├── exports.rs │ │ ├── legacymeta/ │ │ │ ├── api_schema.rs │ │ │ ├── mod.rs │ │ │ └── schema.rs │ │ ├── lib.rs │ │ ├── parser/ │ │ │ ├── doc_comments.rs │ │ │ ├── fileset.rs │ │ │ ├── memory_resolver.rs │ │ │ ├── mod.rs │ │ │ ├── module_loader.rs │ │ │ ├── parser.rs │ │ │ ├── resourceparser/ │ │ │ │ ├── bind.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── paths.rs │ │ │ │ └── resource_parser.rs │ │ │ ├── resources/ │ │ │ │ ├── apis/ │ │ │ │ │ ├── api.rs │ │ │ │ │ ├── authhandler.rs │ │ │ │ │ ├── encoding.rs │ │ │ │ │ ├── gateway.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── service.rs │ │ │ │ │ └── service_client.rs │ │ │ │ ├── infra/ │ │ │ │ │ ├── cache.rs │ │ │ │ │ ├── cron.rs │ │ │ │ │ ├── metrics.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── objects.rs │ │ │ │ │ ├── pubsub_subscription.rs │ │ │ │ │ ├── pubsub_topic.rs │ │ │ │ │ ├── secret.rs │ │ │ │ │ └── sqldb.rs │ │ │ │ ├── mod.rs │ │ │ │ └── parseutil.rs │ │ │ ├── respath.rs │ │ │ ├── service_discovery.rs │ │ │ ├── types/ │ │ │ │ ├── ast_id.rs │ │ │ │ ├── binding.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── object.rs │ │ │ │ ├── resolved.rs │ │ │ │ ├── snapshots/ │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@basic.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@call_expressions.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@call_signatures.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@export_default.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@export_wildcard.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@extends.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@generic.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@generics.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@infer.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@keyofenum.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@mapped_as.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@method_signatures.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@namespace_import.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@qualified_name.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@reexport_local.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@reexport_single.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@reexport_wildcard.txt.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@typeof.ts.snap │ │ │ │ │ ├── encore_tsparser__parser__types__tests__resolve_types@validation.ts.snap │ │ │ │ │ └── encore_tsparser__parser__types__tests__resolve_types@wirespec.ts.snap │ │ │ │ ├── testdata/ │ │ │ │ │ ├── basic.ts │ │ │ │ │ ├── call_expressions.ts │ │ │ │ │ ├── call_signatures.ts │ │ │ │ │ ├── export_default.ts │ │ │ │ │ ├── extends.ts │ │ │ │ │ ├── generic.ts │ │ │ │ │ ├── generics.ts │ │ │ │ │ ├── infer.txt │ │ │ │ │ ├── keyofenum.ts │ │ │ │ │ ├── mapped_as.ts │ │ │ │ │ ├── method_signatures.ts │ │ │ │ │ ├── namespace_import.txt │ │ │ │ │ ├── qualified_name.ts │ │ │ │ │ ├── reexport_local.txt │ │ │ │ │ ├── reexport_single.txt │ │ │ │ │ ├── reexport_wildcard.txt │ │ │ │ │ ├── typeof.ts │ │ │ │ │ ├── validation.ts │ │ │ │ │ └── wirespec.ts │ │ │ │ ├── tests.rs │ │ │ │ ├── typ.rs │ │ │ │ ├── type_resolve.rs │ │ │ │ ├── type_string.rs │ │ │ │ ├── utils.rs │ │ │ │ ├── validation.rs │ │ │ │ └── visitor.rs │ │ │ ├── universe.ts │ │ │ └── usageparser/ │ │ │ └── mod.rs │ │ ├── resolve_utils.rs │ │ ├── runtimeresolve/ │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ └── tsconfig.rs │ │ ├── span_err.rs │ │ ├── testutil/ │ │ │ ├── mod.rs │ │ │ ├── testparse.rs │ │ │ ├── testresolve.rs │ │ │ └── typeparse.rs │ │ └── tsconfig.rs │ ├── tests/ │ │ ├── common/ │ │ │ └── mod.rs │ │ ├── parse_tests.rs │ │ └── testdata/ │ │ ├── builtins.txt │ │ ├── cache.txt │ │ ├── cache_named.txt │ │ ├── mapped_as_clause.txt │ │ ├── mapped_types.txt │ │ ├── metrics.txt │ │ ├── query_header.txt │ │ └── tsconfig.txt │ ├── txtar/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── LICENSE-APACHE │ │ ├── LICENSE-MIT │ │ ├── README.md │ │ └── src/ │ │ ├── error.rs │ │ └── lib.rs │ └── wasm/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ └── lib.rs └── v2/ ├── app/ │ ├── api_framework.go │ ├── apiframework/ │ │ └── apiframework.go │ ├── app.go │ ├── errors.go │ ├── gateway.go │ ├── legacymeta/ │ │ ├── legacymeta.go │ │ ├── schema.go │ │ ├── selector_lookup.go │ │ └── trace_nodes.go │ ├── resource_usage.go │ ├── service.go │ ├── service_discovery.go │ ├── service_discovery_test.go │ ├── setup_test.go │ ├── testdata/ │ │ ├── auth_handler_call.txt │ │ ├── auth_handler_data.txt │ │ ├── auth_handler_invalid_builtin.txt │ │ ├── auth_handler_invalid_field_source.txt │ │ ├── auth_handler_invalid_named_type.txt │ │ ├── auth_handler_multiple.txt │ │ ├── auth_handler_reference.txt │ │ ├── auth_handler_simple.txt │ │ ├── auth_handler_struct.txt │ │ ├── auth_handler_svc_struct.txt │ │ ├── cache_cluster_outside_svc.txt │ │ ├── cache_definition.txt │ │ ├── cache_err_duplicate_cluster.txt │ │ ├── cache_err_duplicate_paths.txt │ │ ├── cache_err_generic_type_nonbasic.txt │ │ ├── cache_err_keyspace_invalid.txt │ │ ├── cache_err_keyspace_outside_svc.txt │ │ ├── cache_generic_type.txt │ │ ├── config.txt │ │ ├── config_err_unexported_field.txt │ │ ├── config_err_use_from_other_service.txt │ │ ├── config_err_wrapper_used_in_wrapper.txt │ │ ├── cron_job_definition.txt │ │ ├── cron_job_definition_init.txt │ │ ├── cron_job_definition_repeat.txt │ │ ├── cron_job_definition_rpc.txt │ │ ├── cron_job_err_not_api.txt │ │ ├── et.txt │ │ ├── metrics_counter.txt │ │ ├── metrics_gauge.txt │ │ ├── middleware.txt │ │ ├── middleware_err_no_matches.txt │ │ ├── middleware_err_not_in_service.txt │ │ ├── missing_generic_param.txt │ │ ├── pubsub.txt │ │ ├── pubsub_err_attributes_not_start_encore.txt │ │ ├── pubsub_err_duplicate_subscription_names.txt │ │ ├── pubsub_err_import_aliased_and_used_in_func.txt │ │ ├── pubsub_err_missing_delivery_guarantee.txt │ │ ├── pubsub_err_new_topic_func_aliased.txt │ │ ├── pubsub_err_ordering_attribute_missing.txt │ │ ├── pubsub_err_subscriber_different_service.txt │ │ ├── pubsub_err_subscriber_missing_handler.txt │ │ ├── pubsub_err_subscriber_nil_handler.txt │ │ ├── pubsub_err_subscriber_not_function.txt │ │ ├── pubsub_err_subscription_func_not_in_service.txt │ │ ├── pubsub_err_subscription_name_invalid.txt │ │ ├── pubsub_err_topic_declared_in_func.txt │ │ ├── pubsub_err_topic_invalid_usage.txt │ │ ├── pubsub_err_topic_must_be_unique.txt │ │ ├── pubsub_err_topic_name_invalid.txt │ │ ├── pubsub_publish_in_middleware.txt │ │ ├── pubsub_subscriber_creates_service.txt │ │ ├── pubsub_subscriber_in_same_service.txt │ │ ├── recursive_types.txt │ │ ├── rlog_call_outside_svc.txt │ │ ├── rpc_auth.txt │ │ ├── rpc_auth_no_authhandler.txt │ │ ├── rpc_call_selector.txt │ │ ├── rpc_err_any.txt │ │ ├── rpc_invalid_header_type.txt │ │ ├── rpc_invalid_path_param_name.txt │ │ ├── rpc_invalid_path_param_type.txt │ │ ├── rpc_invalid_path_too_few_params.txt │ │ ├── rpc_invalid_query_type.txt │ │ ├── rpc_legacy_syntax.txt │ │ ├── rpc_method.txt │ │ ├── rpc_non_raw_path.txt │ │ ├── rpc_option_types.txt │ │ ├── rpc_outside_service.txt │ │ ├── rpc_path_params.txt │ │ ├── rpc_raw_call.txt │ │ ├── rpc_raw_custom_path.txt │ │ ├── rpc_raw_duplicate_path.txt │ │ ├── rpc_raw_internal.txt │ │ ├── rpc_raw_public.txt │ │ ├── rpc_receiver_invalid.txt │ │ ├── rpc_receiver_typo.txt │ │ ├── rpc_without_calling.txt │ │ ├── secrets.txt │ │ ├── secrets_non_string.txt │ │ ├── servicestruct_creates_service.txt │ │ ├── servicestruct_duplicate.txt │ │ ├── servicestruct_ref.txt │ │ ├── sqldb_cross_service.txt │ │ ├── sqldb_err_unknown_db.txt │ │ ├── sqldb_err_unknown_db_stdlib.txt │ │ ├── sqldb_helper.txt │ │ ├── sqldb_outside_ref.txt │ │ ├── sqldb_outside_svc.txt │ │ ├── sqldb_outside_svc_test.txt │ │ ├── sqldb_success.txt │ │ ├── sqldb_without_call.txt │ │ ├── struct_duplicate_json_ignore.txt │ │ ├── svc_migration_db.txt │ │ └── type_ref_non_svc.txt │ ├── validate.go │ ├── validate_apis.go │ ├── validate_authhandlers.go │ ├── validate_caches.go │ ├── validate_config.go │ ├── validate_crons.go │ ├── validate_databases.go │ ├── validate_middleware.go │ ├── validate_objects.go │ ├── validate_pubsub.go │ ├── validate_servicestructs.go │ ├── validate_test.go │ └── validate_types.go ├── codegen/ │ ├── apigen/ │ │ ├── apigen.go │ │ ├── apigenutil/ │ │ │ └── apigenutil.go │ │ ├── authhandlergen/ │ │ │ ├── authhandlergen.go │ │ │ ├── authhandlergen_test.go │ │ │ └── testdata/ │ │ │ ├── authdata.txt │ │ │ ├── basic.txt │ │ │ ├── servicestruct.txt │ │ │ └── struct.txt │ │ ├── endpointgen/ │ │ │ ├── api_calls.go │ │ │ ├── endpointgen.go │ │ │ ├── endpointgen_test.go │ │ │ ├── handlers.go │ │ │ ├── request.go │ │ │ ├── response.go │ │ │ └── testdata/ │ │ │ ├── api_call.txt │ │ │ ├── api_call_servicestruct.txt │ │ │ ├── basic.txt │ │ │ ├── complex_omitempty.txt │ │ │ ├── endpoint_tags.txt │ │ │ ├── fallback_path.txt │ │ │ ├── path_params.txt │ │ │ ├── raw_endpoint.txt │ │ │ ├── recursive.txt │ │ │ ├── request_headers.txt │ │ │ ├── request_params.txt │ │ │ ├── request_query.txt │ │ │ ├── response_headers.txt │ │ │ ├── response_params.txt │ │ │ ├── response_status.txt │ │ │ ├── service_struct.txt │ │ │ └── unexported.txt │ │ ├── maingen/ │ │ │ ├── load_app.go │ │ │ ├── maingen.go │ │ │ ├── maingen_test.go │ │ │ ├── testdata/ │ │ │ │ ├── auth_handler.txt │ │ │ │ ├── basic.txt │ │ │ │ ├── multiple_services.txt │ │ │ │ ├── service_struct.txt │ │ │ │ └── subscription.txt │ │ │ └── testgen.go │ │ ├── middlewaregen/ │ │ │ ├── middlewaregen.go │ │ │ ├── middlewaregen_test.go │ │ │ └── testdata/ │ │ │ ├── basic.txt │ │ │ ├── global.txt │ │ │ └── service_struct.txt │ │ ├── servicestructgen/ │ │ │ ├── servicestructgen.go │ │ │ ├── servicestructgen_test.go │ │ │ └── testdata/ │ │ │ ├── basic.txt │ │ │ └── init_svc.txt │ │ ├── typescrub/ │ │ │ ├── jen.go │ │ │ ├── typescrub.go │ │ │ └── typescrub_test.go │ │ └── userfacinggen/ │ │ ├── testdata/ │ │ │ └── service_struct.txt │ │ ├── userfacinggen.go │ │ └── userfacinggen_test.go │ ├── config.go │ ├── cuegen/ │ │ ├── definition_generator.go │ │ ├── errors.go │ │ ├── generator.go │ │ ├── generator_test.go │ │ ├── service.go │ │ ├── testdata/ │ │ │ ├── basic_config.txt │ │ │ ├── basic_config_svc.cue │ │ │ ├── basic_inline_struct.txt │ │ │ ├── basic_inline_struct_svc.cue │ │ │ ├── basic_lists.txt │ │ │ ├── basic_lists_svc.cue │ │ │ ├── basic_maps.txt │ │ │ ├── basic_maps_svc.cue │ │ │ ├── basic_named_struct_multiple_uses.txt │ │ │ ├── basic_named_struct_multiple_uses_svc.cue │ │ │ ├── basic_named_struct_single_use.txt │ │ │ ├── basic_named_struct_single_use_svc.cue │ │ │ ├── basic_no_config.txt │ │ │ ├── basic_no_config_svc.cue │ │ │ ├── basic_with_cue_imports.txt │ │ │ ├── basic_with_cue_imports_svc.cue │ │ │ ├── basic_wrappers.txt │ │ │ ├── basic_wrappers_svc.cue │ │ │ ├── cue_optional_tag.txt │ │ │ ├── cue_optional_tag_svc.cue │ │ │ ├── cue_tags.txt │ │ │ ├── cue_tags_svc.cue │ │ │ ├── generic_named_types.txt │ │ │ ├── generic_named_types_svc.cue │ │ │ ├── generic_top_level_type.txt │ │ │ ├── generic_top_level_type_svc.cue │ │ │ ├── json_tags.txt │ │ │ ├── json_tags_svc.cue │ │ │ ├── merge_identical_comments.txt │ │ │ ├── merge_identical_comments_svc.cue │ │ │ ├── multiple_configs_in_service.txt │ │ │ ├── multiple_configs_in_service_svc.cue │ │ │ ├── types_from_multiple_packages.txt │ │ │ └── types_from_multiple_packages_svc.cue │ │ └── utils.go │ ├── decls.go │ ├── errors.go │ ├── gen.go │ ├── infragen/ │ │ ├── cachegen/ │ │ │ └── cachegen.go │ │ ├── configgen/ │ │ │ ├── configgen.go │ │ │ ├── configgen_test.go │ │ │ └── testdata/ │ │ │ ├── basic_config.txt │ │ │ ├── basic_inline_struct.txt │ │ │ ├── basic_lists.txt │ │ │ ├── basic_maps.txt │ │ │ ├── basic_named_struct_multiple_uses.txt │ │ │ ├── basic_named_struct_single_use.txt │ │ │ ├── basic_no_config.txt │ │ │ ├── basic_with_cue_imports.txt │ │ │ ├── basic_wrappers.txt │ │ │ ├── cue_optional_tag.txt │ │ │ ├── cue_tags.txt │ │ │ ├── generics.txt │ │ │ ├── json_tags.txt │ │ │ ├── merge_identical_comments.txt │ │ │ ├── multi_package.txt │ │ │ ├── multiple_configs_in_service.txt │ │ │ └── name_conflicts.txt │ │ ├── infragen.go │ │ ├── metricsgen/ │ │ │ └── metricsgen.go │ │ ├── pubsubgen/ │ │ │ ├── pubsubgen.go │ │ │ ├── pubsubgen_test.go │ │ │ └── testdata/ │ │ │ ├── basic.txt │ │ │ └── method_handler.txt │ │ └── secretsgen/ │ │ └── secretsgen.go │ ├── internal/ │ │ ├── codegentest/ │ │ │ └── codegentest.go │ │ └── genutil/ │ │ ├── etype.go │ │ └── types.go │ └── rewrite/ │ ├── rewrite.go │ └── rewrite_test.go ├── compiler/ │ └── build/ │ ├── build.go │ ├── build_test.go │ ├── errors.go │ ├── testdata/ │ │ ├── basic.txt │ │ ├── overlay.txt │ │ └── rewrite.txt │ └── tests.go ├── internals/ │ ├── overlay/ │ │ └── overlay.go │ ├── parsectx/ │ │ └── pctx.go │ ├── perr/ │ │ ├── aserror.go │ │ └── perr.go │ ├── pkginfo/ │ │ ├── errors.go │ │ ├── loader.go │ │ ├── loader_test.go │ │ ├── modresolve.go │ │ ├── modresolve_test.go │ │ ├── names.go │ │ ├── names_test.go │ │ ├── pkgparse.go │ │ └── types.go │ ├── posmap/ │ │ └── posmap.go │ ├── resourcepaths/ │ │ ├── errors.go │ │ ├── paths.go │ │ └── paths_test.go │ ├── scan/ │ │ ├── collect.go │ │ ├── collect_test.go │ │ ├── errors.go │ │ ├── scan.go │ │ └── scan_test.go │ ├── schema/ │ │ ├── decls.go │ │ ├── errors.go │ │ ├── schema_parser.go │ │ ├── schema_parser_test.go │ │ ├── schematest/ │ │ │ └── schematest.go │ │ ├── schemautil/ │ │ │ ├── astutil.go │ │ │ ├── astutil_test.go │ │ │ ├── errors.go │ │ │ └── schemautil.go │ │ ├── types.go │ │ └── types_string.go │ └── testutil/ │ ├── testutil.go │ ├── update_archive_file.go │ └── utils.go ├── parser/ │ ├── apis/ │ │ ├── api/ │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── apienc/ │ │ │ │ ├── encoding.go │ │ │ │ └── errors.go │ │ │ ├── errors.go │ │ │ └── usage.go │ │ ├── authhandler/ │ │ │ ├── authhandler.go │ │ │ ├── authhandler_test.go │ │ │ ├── errors.go │ │ │ └── usage.go │ │ ├── directive/ │ │ │ ├── directive.go │ │ │ ├── directive_test.go │ │ │ ├── errors.go │ │ │ └── fields.go │ │ ├── errors.go │ │ ├── middleware/ │ │ │ ├── errors.go │ │ │ ├── middleware.go │ │ │ └── middleware_test.go │ │ ├── parser.go │ │ ├── selector/ │ │ │ ├── errors.go │ │ │ ├── selector.go │ │ │ └── selector_test.go │ │ └── servicestruct/ │ │ ├── errors.go │ │ ├── servicestruct.go │ │ ├── servicestruct_test.go │ │ └── usage.go │ ├── infra/ │ │ ├── caches/ │ │ │ ├── cache_test.go │ │ │ ├── cluster.go │ │ │ ├── errors.go │ │ │ ├── keyspace.go │ │ │ ├── keyspace_test.go │ │ │ ├── testdata/ │ │ │ │ └── cluster.txt │ │ │ └── usage.go │ │ ├── config/ │ │ │ ├── config.go │ │ │ ├── errors.go │ │ │ └── usage.go │ │ ├── crons/ │ │ │ ├── cron.go │ │ │ ├── cron_test.go │ │ │ └── errors.go │ │ ├── internal/ │ │ │ ├── literals/ │ │ │ │ ├── constants.go │ │ │ │ ├── decode.go │ │ │ │ ├── decode_test.go │ │ │ │ ├── errors.go │ │ │ │ ├── literals.go │ │ │ │ └── literals_test.go │ │ │ ├── locations/ │ │ │ │ ├── locations.go │ │ │ │ └── locations_test.go │ │ │ └── parseutil/ │ │ │ ├── aststringer.go │ │ │ ├── errors.go │ │ │ ├── names.go │ │ │ ├── parseutil.go │ │ │ └── reference.go │ │ ├── metrics/ │ │ │ ├── errors.go │ │ │ ├── metrics.go │ │ │ ├── metrics_string.go │ │ │ └── metrics_test.go │ │ ├── objects/ │ │ │ ├── bucket.go │ │ │ ├── errors.go │ │ │ ├── usage.go │ │ │ └── usage_test.go │ │ ├── pubsub/ │ │ │ ├── errors.go │ │ │ ├── subscription.go │ │ │ ├── topic.go │ │ │ ├── usage.go │ │ │ └── usage_test.go │ │ ├── secrets/ │ │ │ ├── errors.go │ │ │ └── secrets.go │ │ └── sqldb/ │ │ ├── errors.go │ │ ├── implicit.go │ │ ├── named.go │ │ ├── sqldb.go │ │ ├── sqldb_test.go │ │ └── usage.go │ ├── internal/ │ │ └── utils/ │ │ └── prettyprint.go │ ├── parser.go │ ├── parser_test.go │ ├── resource/ │ │ ├── bind.go │ │ ├── resource.go │ │ ├── resource_string.go │ │ ├── resourceparser/ │ │ │ ├── registry.go │ │ │ └── resourceparser.go │ │ ├── resourcetest/ │ │ │ └── resourcetest.go │ │ └── usage/ │ │ ├── resolver.go │ │ ├── testdata/ │ │ │ ├── pubsub_usage.txt │ │ │ ├── secret_usage.txt │ │ │ └── sqldb_usage.txt │ │ ├── usage.go │ │ ├── usage_test.go │ │ └── usagetest/ │ │ └── usagetest.go │ └── result.go ├── tsbuilder/ │ └── tsbuilder.go └── v2builder/ └── v2builder.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM golang:1.24 RUN apt-get update && apt-get install -y sudo RUN curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \ apt-get install -y nodejs ADD scripts /scripts RUN bash /scripts/install.sh RUN bash /scripts/godeps.sh ENV ENCORE_GOROOT=/encore-release/encore-go ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "build": {"dockerfile": "Dockerfile"}, "containerEnv": { "ENCORE_DAEMON_DEV": "1", "ENCORE_RUNTIMES_PATH": "${containerWorkspaceFolder}/runtimes" }, "extensions": ["golang.go"], "postCreateCommand": "bash /scripts/prepare.sh", "forwardPorts": [4000, 9400] } ================================================ FILE: .devcontainer/scripts/godeps.sh ================================================ #!/usr/bin/env set -ex go install github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest go install github.com/ramya-rao-a/go-outline@latest go install github.com/cweill/gotests/gotests@latest go install github.com/fatih/gomodifytags@latest go install github.com/josharian/impl@latest go install github.com/haya14busa/goplay/cmd/goplay@latest go install github.com/go-delve/delve/cmd/dlv@latest go install honnef.co/go/tools/cmd/staticcheck@master go install golang.org/x/tools/gopls@latest GOBIN=/tmp/ go install github.com/go-delve/delve/cmd/dlv@master mv /tmp/dlv $GOPATH/bin/dlv-dap ================================================ FILE: .devcontainer/scripts/install.sh ================================================ #!/usr/bin/env bash set -e target="$(go env GOOS)_$(go env GOARCH)" encore_uri=$(curl -sSf -N "https://encore.dev/api/releases?target=${target}&show=url") if [ ! "$encore_uri" ]; then echo "Error: Unable to determine latest Encore release." 1>&2 exit 1 fi encore_install="/encore-release" bin_dir="$encore_install/bin" exe="$bin_dir/encore" tar="$encore_install/encore.tar.gz" if [ ! -d "$bin_dir" ]; then mkdir -p "$bin_dir" fi curl --fail --location --progress-bar --output "$tar" "$encore_uri" cd "$encore_install" tar -C "$encore_install" -xzf "$tar" chmod +x "$bin_dir"/* rm "$tar" "$exe" version echo "Encore was installed successfully to $exe" if command -v encore >/dev/null; then echo "Run 'encore --help' to get started" else case $SHELL in /bin/zsh) shell_profile=".zshrc" ;; *) shell_profile=".bash_profile" ;; esac echo "Manually add the directory to your \$HOME/$shell_profile (or similar)" echo " export ENCORE_INSTALL=\"$encore_install\"" echo " export PATH=\"\$ENCORE_INSTALL/bin:\$PATH\"" echo "Run '$exe --help' to get started" fi ================================================ FILE: .devcontainer/scripts/prepare.sh ================================================ #!/usr/bin/env bash set -e set -x go mod download ================================================ FILE: .github/DISCUSSION_TEMPLATE/help.yml ================================================ body: - type: markdown attributes: value: | Before asking a question, please check our [documentation](https://encore.dev/docs) to see if your question is already answered there. If you are not sure if your issue is a bug you can ask a question on our [Discord community](https://encore.dev/discord). **NOTE:** You don't need to answer questions that you know that aren't relevant. --- - type: checkboxes attributes: label: "Is there an existing issue/discussion for this?" description: "Please search in Issues and Discussions to see if this question has already been asked" options: - label: "I have searched the existing issues and discussions" required: true - type: input attributes: label: "Encore CLI version" description: | Which exact version of `encore` CLI are you using? Run `encore version` in your terminal to see your version. placeholder: "1.54.0" - type: input attributes: label: "Node.js version" description: "Which version of Node.js are you using?" placeholder: "24.0.0" - type: checkboxes validations: required: true attributes: label: "In which operating systems have you tested?" options: - label: macOS - label: Windows - label: Linux - type: markdown attributes: value: | --- - type: textarea attributes: label: "Question" description: | What is your question? **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in ================================================ FILE: .github/DISCUSSION_TEMPLATE/suggestions.yml ================================================ body: - type: markdown attributes: value: | Check out our [documentation](https://encore.dev/docs) to see if your suggestion is already implemented. If you are not sure if your suggestion is a feature request you can ask a question on our [Discord community](https://encore.dev/discord). --- - type: checkboxes attributes: label: "Is there an existing discussion that is already proposing this?" description: "Please search [here](https://github.com/encoredev/encore/discussions) to see if a discussion already exists for the feature you are requesting" options: - label: "I have searched the existing discussions" required: true - type: checkboxes validations: required: true attributes: label: "What part(s) of Encore does this feature request apply to?" options: - label: Encore.ts (TypeScript) - label: Encore.go (Go) - label: Encore CLI - label: Local Development Dashboard - label: Encore Cloud - label: Other - type: textarea validations: required: true attributes: label: "Is your feature request related to a problem? Please describe it" description: "A clear and concise description of what the problem is" placeholder: | I have an issue when ... - type: textarea validations: required: true attributes: label: "Describe the solution you'd like" description: "A clear and concise description of what you want to happen. Add any considered drawbacks" - type: textarea validations: required: true attributes: label: "What is the motivation / use case for changing the behavior?" description: "Describe the motivation or the concrete use case" ================================================ FILE: .github/ISSUE_TEMPLATE/Bug_report.yml ================================================ name: "\U0001F41B Bug Report" description: "If something isn't working as expected" labels: ["type: bug"] type: bug body: - type: markdown attributes: value: | ### We use GitHub Issues to track bug reports For suggestions and feature requests, please add those to our [GitHub discussions](https://github.com/encoredev/encore/discussions) forum. If you are not sure if your issue is a bug you can ask a question on our [Discord community](https://encore.dev/discord). **NOTE:** You don't need to answer questions that you know that aren't relevant. --- - type: checkboxes attributes: label: "Is there an existing issue for this?" description: "Please search [here](../issues?q=is%3Aissue) to see if an issue already exists for the bug you encountered" options: - label: "I have searched the existing issues" required: true - type: checkboxes id: area attributes: label: "What part(s) of Encore does this bug report apply to?" options: - label: Encore.ts (TypeScript) - label: Encore.go (Go) - label: Encore CLI - label: Local Development Dashboard - label: Encore Cloud - type: textarea validations: required: true attributes: label: "Current behavior" description: "How the issue manifests?" - type: input attributes: label: "Minimum reproduction code" placeholder: "https://github.com/..." description: | URL to a Git repository that reproduces your issue. [What is a minimum reproduction?](https://github.com/encoredev/encore/blob/main/.github/minimum-reproduction.md) - type: textarea attributes: label: "Steps to reproduce" description: | How the issue manifests? You could leave this blank if you already write this in your reproduction code placeholder: | 1. `encore run` 2. `curl localhost:4000/path` 3. See error... - type: textarea validations: required: true attributes: label: "Expected behavior" description: "A clear and concise description of what you expected to happened (or code)" - type: input attributes: label: "Encore CLI version" description: | Which exact version of `encore` CLI are you using? Run `encore version` in your terminal to see your version. placeholder: "1.54.0" - type: input attributes: label: "Node.js version" description: "Which version of Node.js are you using?" placeholder: "24.0.0" - type: checkboxes validations: required: true attributes: label: "In which operating systems have you tested?" options: - label: macOS - label: Windows - label: Linux - type: markdown attributes: value: | --- - type: textarea attributes: label: "Other" description: | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ ## To encourage contributors to use issue templates, we don't allow blank issues blank_issues_enabled: false contact_links: - name: "Suggestions and General Help" url: "https://github.com/encoredev/encore/discussions" about: "Please add suggestions or ask general help questions to our GitHub discussions forum." - name: "\U0001F4D5 Documentation" url: "https://encore.dev/docs" about: "Read about every Encore feature in-depth and search for things you are unsure about." ================================================ FILE: .github/dockerimg/Dockerfile ================================================ # syntax=docker/dockerfile:1.4 FROM --platform=$TARGETPLATFORM ubuntu:22.04 AS build ARG TARGETPLATFORM ARG RELEASE_VERSION RUN mkdir /encore ADD rename-binary-if-needed.bash rename-binary-if-needed.bash ADD artifacts /artifacts RUN /bin/bash -c 'SRC=encore-$(echo $TARGETPLATFORM | tr '/' '_'); tar -C /encore -xzf /artifacts/$SRC.tar.gz' RUN /bin/bash rename-binary-if-needed.bash FROM --platform=$TARGETPLATFORM ubuntu:22.04 RUN apt-get update && apt-get install -y -f ca-certificates ENV PATH="/encore/bin:${PATH}" WORKDIR /src ADD encore-entrypoint.bash /bin/encore-entrypoint.bash ENTRYPOINT ["/bin/encore-entrypoint.bash"] COPY --from=build /encore /encore ================================================ FILE: .github/dockerimg/encore-entrypoint.bash ================================================ #!/usr/bin/env bash set -eo pipefail # If the ENCORE_AUTHKEY environment variable is set, log in with it. if [ -n "$ENCORE_AUTHKEY" ]; then echo "Logging in to Encore using provided auth key..." encore auth login --auth-key "$ENCORE_AUTHKEY" fi # Run the encore command. encore "$@" ================================================ FILE: .github/dockerimg/rename-binary-if-needed.bash ================================================ #!/usr/bin/env bash set -eo pipefail # Check if `encore-nightly`, `encore-beta` or `encore-develop` are present, and if one of them are, rename it to `encore`. for binary in encore-nightly encore-beta encore-develop; do if [ -f "/encore/bin/$binary" ]; then echo "Renaming $binary to encore..." mv /encore/bin/$binary /encore/bin/encore fi done # Sanity check that /ecore/bin/encore exists. if [ ! -f "/encore/bin/encore" ]; then echo "ERROR: /encore/bin/encore does not exist. Did you mount the Encore binary directory to /encore/bin?" exit 1 fi ================================================ FILE: .github/minimum-reproduction.md ================================================ # Minimum Reproduction Repository A minimum reproduction repository is a git repository that can be shared publicly (doesn't expose private business logic), shows the problem you're running into, and has the fewest dependencies installed possible. It also has the steps in place for how to replicate the error you're running into. This is easiest to add to the README. ## Doesn't Expose Business Logic If your error resolves around a specific step in business logic, replicate the business logic in a way that doesn't make it evident what you're working on. ## Shows The Problem You're Running Into This is why the minimum reproduction should be created in the first place, cause you have an error you want someone to look into. ## Has The Fewest Dependencies Installed Possible If the reproduction doesn't need it, get rid of it. ## Steps To Replicate A set of clear, defined steps on how to replicate the error. You can separate the setup and reproduction steps as well if you'd like. An example would be something like ``` # Setup 1) npm install # Reproduction 1) encore run 2) open new terminal 3) curl http://localhost:4000/users 4) see the error ``` ## Okay I Understand What It Is, What Else Do I Need? Generally speaking, if you meet the above, it's good to go. This helps out those who debug errors and provide support immensely. ## So why am I being asked for this? There's a few reasons to provide a minimum reproduction: 1. it makes debugging where the error _could_ be so much easier. Instead of looking across 20 files and 5 directories, it's now 2 files in 1 directory. Much less to dig through and understand 2. half the time while creating the minimum reproduction, you'll find what the problem was yourself and grow as a developer and as a knowledge sharer. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: branches: - main schedule: - cron: "30 2 * * *" # Every night at 2:30am UTC (if you change this schedule, also change the if statement in the test steps) jobs: build: name: "Build" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: path: encr.dev - name: Set up Node uses: actions/setup-node@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: "encr.dev/go.mod" check-latest: true cache-dependency-path: "encr.dev/go.sum" - name: Build run: cd encr.dev && go build ./... - name: Build for Windows run: cd encr.dev && go build ./... env: GOOS: windows test: name: "Test" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: path: encr.dev - name: Set up Node uses: actions/setup-node@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: "encr.dev/go.mod" check-latest: true cache-dependency-path: "encr.dev/go.sum" - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Install Protoc uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo - name: Install encore-go run: | URL=$(curl -s https://api.github.com/repos/encoredev/go/releases/latest | grep "browser_download_url.*linux_x86-64.tar.gz" | cut -d : -f 2,3 | tr -d \" | tr -d '[:space:]') curl --fail -L -o encore-go.tar.gz $URL && tar -C . -xzf ./encore-go.tar.gz - name: Install tsparser run: cargo install --path encr.dev/tsparser --force --debug # If we're not running on a schedule, we only want to run tests on changed code - name: Run tests on changed code on the CLI run: cd encr.dev && go test -short -tags=dev_build 2>&1 ./... if: github.event.schedule != '30 2 * * *' env: ENCORE_GOROOT: ${{ github.workspace }}/encore-go ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes - name: Run tests on changed runtime code run: cd encr.dev/runtimes/go && go test -short -tags=dev_build ./... if: github.event.schedule != '30 2 * * *' # Each night we want to run all tests multiple times to catch any flaky tests # We will shuffle the order in which tests are run and run them 25 times looking # for failures. We will also fail fast so that we don't waste time running tests # that are already failing. - name: Run all tests multiple times on the CLI run: cd encr.dev && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=dev_build ./... if: github.event.schedule == '30 2 * * *' env: ENCORE_GOROOT: ${{ github.workspace }}/encore-go ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes - name: Run all tests multiple times on the runtime run: cd encr.dev/runtimes/go && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=dev_build ./... if: github.event.schedule == '30 2 * * *' - name: Report Nightly Failure uses: ravsamhq/notify-slack-action@bca2d7f5660b833a27bda4f6b8bef389ebfefd25 if: ${{ failure() && github.event.schedule == '30 2 * * *' }} with: status: ${{ job.status }} # required notification_title: "{workflow} has {status_message}" message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>" footer: "Linked Repo <{repo_url}|{repo}> | <{workflow_url}|View Workflow>" env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ALERT_WEBHOOK_URL }} # required test-e2e: name: "Test e2e" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: path: encr.dev - name: Set up Node uses: actions/setup-node@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: "encr.dev/go.mod" check-latest: true cache-dependency-path: "encr.dev/go.sum" - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Install Protoc uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo - name: Install encore-go run: | URL=$(curl -s https://api.github.com/repos/encoredev/go/releases/latest | grep "browser_download_url.*linux_x86-64.tar.gz" | cut -d : -f 2,3 | tr -d \" | tr -d '[:space:]') curl --fail -L -o encore-go.tar.gz $URL && tar -C . -xzf ./encore-go.tar.gz - name: Install tsparser run: cargo install --path encr.dev/tsparser --force --debug - name: Install tsbundler run: cd encr.dev && go install ./cli/cmd/tsbundler-encore - name: Build jsruntime run: cd encr.dev && go run ./pkg/encorebuild/cmd/build-local-binary encore-runtime.node # If we're not running on a schedule, we only want to run tests on changed code - name: Run tests on changed code on the CLI run: cd encr.dev && go test -short -tags=e2e 2>&1 ./e2e-tests if: github.event.schedule != '30 2 * * *' env: ENCORE_GOROOT: ${{ github.workspace }}/encore-go ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes # Each night we want to run all tests multiple times to catch any flaky tests # We will shuffle the order in which tests are run and run them 25 times looking # for failures. We will also fail fast so that we don't waste time running tests # that are already failing. - name: Run all tests multiple times on the CLI run: cd encr.dev && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=e2e ./e2e-tests if: github.event.schedule == '30 2 * * *' env: ENCORE_GOROOT: ${{ github.workspace }}/encore-go ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes - name: Report Nightly Failure uses: ravsamhq/notify-slack-action@bca2d7f5660b833a27bda4f6b8bef389ebfefd25 if: ${{ failure() && github.event.schedule == '30 2 * * *' }} with: status: ${{ job.status }} # required notification_title: "{workflow} has {status_message}" message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>" footer: "Linked Repo <{repo_url}|{repo}> | <{workflow_url}|View Workflow>" env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ALERT_WEBHOOK_URL }} # required # Run static analysis on the PR static-analysis: name: "Static Analysis" # We're using buildjet for this as it's very slow on Github's own runners runs-on: buildjet-4vcpu-ubuntu-2204 # Skip any PR created by dependabot to avoid permission issues: if: (github.actor != 'dependabot[bot]') permissions: checks: write contents: read pull-requests: write steps: - uses: actions/checkout@v4 - name: Install jq uses: dcarbone/install-jq-action@91d8da7268538e8a0ae0c8b72af44f1763228455 - name: Install semgrep run: | python3 -m pip install semgrep python3 -m pip install --upgrade requests - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: "go.mod" cache: false - name: Install ci tools run: | go install honnef.co/go/tools/cmd/staticcheck@master go install github.com/kisielk/errcheck@latest go install github.com/gordonklaus/ineffassign@latest rust_core: name: "Test core runtime" runs-on: ubuntu-latest steps: - name: Checkout codebase uses: actions/checkout@v4 - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: toolchain: stable components: rustfmt,clippy - name: Install Protoc uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo - uses: taiki-e/install-action@nextest - name: Run test run: cargo nextest run env: CARGO_TERM_COLOR: always - name: Run rustfmt run: cargo fmt --all --check - name: Run clippy run: cargo clippy --all-targets --all-features -- -D warnings wasm_build: name: "Build tsparser WASM" runs-on: ubuntu-latest steps: - name: Checkout codebase uses: actions/checkout@v4 - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: toolchain: stable targets: wasm32-unknown-unknown components: clippy - name: Install Protoc uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-wasm - name: Build run: cargo build --target wasm32-unknown-unknown -p tsparser-wasm - name: Run clippy for WASM run: cargo clippy --target wasm32-unknown-unknown -p tsparser-wasm -- -D warnings ================================================ FILE: .github/workflows/makefile ================================================ # This makefile is used inconjunction with the .reviewdog.yml file in the root of the repo .PHONY: list-modules go-vet staticcheck errcheck ineffassign go-fmt # Automatically gather all information ALL_SRC := $(shell find ../../ -name "*.go") ALL_MODS = $(shell find ../../ -name go.mod) MOD_DIRS = $(sort $(realpath $(dir $(ALL_MODS)))) REPO_DIR := $(realpath ../../) SEMGREP_DIR := "$(REPO_DIR)/tools/semgrep-rules" # List modules reports all found Go modules within the repository list-modules: @echo $(MOD_DIRS) # Function to run a command in each Go module with appropriate build tags # # REL_DIR is the relative path to the file from the repository root # it is computed by removing the REPO_DIR prefix from the $dir variable, # then we remove the prefix "/" to make it relative # and finally escaping the slashes so we can use it in sed define run_for_each_module @for dir in $(MOD_DIRS); do \ TAGS=""; \ if [ "$$dir" != "$(REPO_DIR)" ]; then \ TAGS="-tags encore,encore_internal,encore_app"; \ fi; \ REL_DIR=$$(echo "$${dir#$(REPO_DIR)}/" | sed 's/^\///' | sed 's/\//\\\//g'); \ (cd "$$dir" && $(1) $$TAGS $(2) | sed "s/^\.\//$$REL_DIR/"); \ done; endef # Run Go vet go-vet: $(ALL_SRC) # The sed statements are: # # 1. Remove any lines starting with "#" (go vet uses these for each package) # 2. Remove any "vet: " prefix from the output (sometimes we get this sometimes we dont) # 3. Remove any "./" prefix from the output (we'll get this for files which exist directly in the module root folder - this is done so we don't double up next) # 4. Add a "./" prefix to the output (this is so the sed within the run_for_each_module function can add the module path to each line) $(call run_for_each_module,go vet,./... 2>&1 | sed '/^#/d' | sed 's/^vet: //' | sed 's/^\.\///' | sed "s/^/\.\//") ## Run staticcheck staticcheck: $(ALL_SRC) $(call run_for_each_module,staticcheck -tests=false -f=json,./... | jq -f "$(REPO_DIR)/.github/workflows/staticcheck-to-rdjsonl.jq" -c) # Run errcheck errcheck: $(ALL_SRC) $(call run_for_each_module,errcheck -abspath,./...) ## Run ineffassign ineffassign: $(ALL_SRC) $(call run_for_each_module,ineffassign,./... 2>&1) semgrep: $(ALL_SRC) @cd $(REPO_DIR) && semgrep scan --quiet --config=auto --config=$(SEMGREP_DIR) --json | jq -f "$(REPO_DIR)/.github/workflows/semgrep-to-rdjson.jq" -c go-fmt: $(ALL_SRC) @cd $(REPO_DIR) && gofmt -s -d . || exit 0 ================================================ FILE: .github/workflows/release-2.yml ================================================ name: Release (2.0) on: workflow_dispatch: inputs: version: description: 'Version to build ("v1.2.3", "v1.2.3-nightly.20231231", "v1.2.3-beta.1" or "v0.0.0-develop+[commitHash]")' type: string required: true jobs: release: name: "Run Release Script" runs-on: self-hosted env: GOROOT: /usr/local/go-1.21.4 RUSTUP_HOME: /usr/local/rust/rustup steps: - name: Checkout the repo uses: actions/checkout@v4 with: path: encr.dev - name: Trigger release script env: NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} run: | cd ${{ github.workspace }}/encr.dev go run ./pkg/encorebuild/cmd/make-release/ -dst "${{ github.workspace }}/build" -v "${{ github.event.inputs.version }}" -publish-npm=true - name: Publish artifact (darwin_amd64) uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-darwin_amd64 path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-darwin_amd64.tar.gz - name: Publish artifact (darwin_arm64) uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-darwin_arm64 path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-darwin_arm64.tar.gz - name: Publish artifact (linux_amd64) uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-linux_amd64 path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_amd64.tar.gz - name: Publish artifact (linux_arm64) uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-linux_arm64 path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_arm64.tar.gz - name: Publish artifact (windows_amd64) uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-windows_amd64 path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-windows_amd64.tar.gz - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Registry uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Copy linux artifacts to docker context folder run: | mkdir -p ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts cp ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_amd64.tar.gz ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts/encore-linux_amd64.tar.gz cp ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_arm64.tar.gz ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts/encore-linux_arm64.tar.gz - name: Create metadata (tags, labels) for Docker image id: docker-meta uses: docker/metadata-action@v5 with: images: encoredotdev/encore labels: | org.opencontainers.image.title=Encore org.opencontainers.image.vendor=encore.dev org.opencontainers.image.authors=support@encore.dev org.opencontainers.image.description=Encore is the end-to-end Backend Development Platform that lets you escape cloud complexity. tags: | type=raw,value=latest,enable=${{ !contains(github.event.inputs.version, '-') }} type=semver,pattern={{version}},value=${{ github.event.inputs.version }} type=sha type=schedule,pattern=nightly,enable=${{ contains(github.event.inputs.version, '-nightly.') }} type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.version }},enable=${{ !contains(github.event.inputs.version, '-') }} type=semver,pattern={{major}},value=${{ github.event.inputs.version }},enable=${{ !contains(github.event.inputs.version, '-') }} - name: Build and push docker images uses: docker/build-push-action@v4 with: context: encr.dev/.github/dockerimg platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.docker-meta.outputs.tags }} labels: ${{ steps.docker-meta.outputs.labels }} cache-from: type=inline cache-to: type=inline build-args: | RELEASE_VERSION=${{ github.event.inputs.version }} notify_release_success: name: "Notify release system of successful build" runs-on: self-hosted needs: - release steps: - name: Webhook uses: distributhor/workflow-webhook@f5a294e144d6ef44cfac4d3d5e20b613bcee0d4b # v3.0.7 env: webhook_type: "json" webhook_url: ${{ secrets.RELEASE_WEBHOOK }} data: '{ "version": "${{ github.event.inputs.version }}", "run_id": "${{ github.run_id }}" }' ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: inputs: version: description: 'Version to build ("1.2.3")' required: true encorego_version: description: 'Encore-Go version to use ("encore-go1.17.7")' required: true jobs: build: strategy: matrix: include: - builder: ubuntu-24.04 goos: linux goarch: amd64 release_key: linux_x86-64 - builder: ubuntu-24.04 goos: linux goarch: arm64 release_key: linux_arm64 - builder: macos-11 goos: darwin goarch: amd64 release_key: macos_x86-64 - builder: macos-11 goos: darwin goarch: arm64 release_key: macos_arm64 - builder: windows-latest goos: windows goarch: amd64 release_key: windows_x86-64 runs-on: ${{ matrix.builder }} steps: - name: Check out repo uses: actions/checkout@v4 with: path: encr.dev - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: "encr.dev/go.mod" check-latest: true cache-dependency-path: "encr.dev/go.sum" - name: Set up Zig uses: goto-bus-stop/setup-zig@7ab2955eb728f5440978d5824358023be3a2802d # v2.2.0 with: version: 0.10.1 - name: Install encore-go run: curl --fail -o encore-go.tar.gz -L https://github.com/encoredev/go/releases/download/${{ github.event.inputs.encorego_version }}/${{ matrix.release_key }}.tar.gz && tar -C ${{ github.workspace }} -xzf ./encore-go.tar.gz - name: Build run: cd encr.dev && go run ./pkg/make-release/make-release.go -v="${{ github.event.inputs.version }}" -dst=dist -goos=${{ matrix.goos }} -goarch=${{ matrix.goarch }} -encore-go="../encore-go" env: GO111MODULE: "on" if: runner.os != 'windows' - name: Build run: cd encr.dev && .\pkg\make-release\windows\build.bat env: GO111MODULE: "on" ENCORE_VERSION: "${{ github.event.inputs.version }}" ENCORE_GOROOT: "../encore-go" if: runner.os == 'windows' - name: "Tar artifacts" run: tar -czvf encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz -C encr.dev/dist/${{ matrix.goos }}_${{ matrix.goarch }} . - name: Publish artifact uses: actions/upload-artifact@v3 with: name: encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }} path: encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz publish-docker-images: name: "publish docker images" runs-on: ubuntu-24.04 needs: build permissions: contents: read packages: write steps: - uses: actions/checkout@v4 with: sparse-checkout: .github - name: Download Artifacts uses: actions/download-artifact@v3 with: path: .github/dockerimg/artifacts - name: Setup Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to Docker Registry uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Cache Docker layers uses: actions/cache@v2 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx- - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v4 with: images: encoredotdev/encore labels: | org.opencontainers.image.title=Encore org.opencontainers.image.vendor=encore.dev org.opencontainers.image.authors=support@encore.dev org.opencontainers.image.description=Encore is the end-to-end Backend Development Platform that lets you escape cloud complexity. tags: | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=semver,pattern={{version}},value=v${{ github.event.inputs.version }} type=semver,pattern={{major}}.{{minor}},value=v${{ github.event.inputs.version }} type=semver,pattern={{major}},value=v${{ github.event.inputs.version }} - name: Build and push uses: docker/build-push-action@v4 with: context: .github/dockerimg platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | RELEASE_VERSION=${{ github.event.inputs.version }} notify_release_success: needs: - build - publish-docker-images runs-on: ubuntu-24.04 steps: - name: Webhook uses: distributhor/workflow-webhook@v3.0.7 env: webhook_type: "json" webhook_url: ${{ secrets.RELEASE_WEBHOOK }} data: '{ "version": "${{ github.event.inputs.version }}", "run_id": "${{ github.run_id }}" }' ================================================ FILE: .github/workflows/semgrep-to-rdjson.jq ================================================ # See https://github.com/reviewdog/reviewdog/tree/master/proto/rdf { source: { name: "semgrep", url: "https://semgrep.dev/", }, diagnostics: [ .results[] | { code: { value: .check_id, url: [ .extra.metadata.shortlink?, .extra.metadata.source?, .extra."semgrep.dev".rule.url?, "https://github.com/encoredev/encore/blob/main/\(.check_id | gsub("\\."; "/")).yml" ] | map(select(. != null)) | first, }, message: .extra.message, location: { path: .path, range: { start: { line: .start.line, column: .start.col }, end: { line: .end.line, column: .end.col }, }, }, severity: .extra.severity, # Temporary variable we store to track the fix _res: . } | if ._res.extra.fix then .suggestions = [{ range: .location.range, text: ._res.extra.fix, }] else . end | del(._res) ] } ================================================ FILE: .github/workflows/staticcheck-to-rdjsonl.jq ================================================ # See https://github.com/reviewdog/reviewdog/tree/master/proto/rdf { source: { name: "staticcheck", url: "https://staticcheck.io" }, message: .message, code: {value: .code, url: "https://staticcheck.io/docs/checks#\(.code)"}, location: { path: .location.file, range: { start: { line: .location.line, column: .location.column } } }, severity: ((.severity|ascii_upcase|select(match("ERROR|WARNING|INFO")))//null) } ================================================ FILE: .gitignore ================================================ # Prevent built binaries from being checked in accidentally. /dist /encore /git-remote-encore /target /__debug_* # Don't commit dotfiles /.encore /.vscode /.zed # Build artifact that must be placed alongside go files for Windows *.syso # JetBrains .idea .fleet .run # MacOS .DS_Store runtimes/supervisor-encore runtimes/supervisor-encore-linux-amd64 encore-runtime.node-linux-amd64 ================================================ FILE: .prettierrc.toml ================================================ trailingComma = "none" ================================================ FILE: .reviewdog.yml ================================================ # Encore's reviewdog configuration file. # # This runs in our CI pipeline when you open a PR. To run this locally # and get the same results as our CI pipeline, run: `./check.bash` # # We use a makefile rather than the commands directly as this repo # has multiple Go modules within it and most tools only look at the # module in the current directory. Thus our make file runs the tool # for each module, combining the results into a single standardised # that review dog can then parse and display as a single "run" for # each tool. runner: go-vet: cmd: make -s -C .github/workflows go-vet format: govet go-fmt: cmd: make -s -C .github/workflows go-fmt format: diff # Disable staticcheck until it supports Go 1.21: https://github.com/dominikh/go-tools/issues/1431 # staticcheck: # cmd: make -s -C .github/workflows staticcheck # format: rdjsonl errcheck: cmd: make -s -C .github/workflows errcheck errorformat: - "%f:%l:%c:\t%m" ineffassign: cmd: make -s -C .github/workflows ineffassign errorformat: - "%f:%l:%c: %m" semgrep: cmd: make -s -C .github/workflows semgrep format: rdjson ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [info@encore.dev](mailto:info@encore.dev). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Encore We're so excited that you are interested in contributing to Encore! All contributions are welcome, and there are several valuable ways to contribute. Below is a technical walkthrough of developing the `encore` command for contributing code to the Encore project. Head over to the community section for [more ways to contribute](https://encore.dev/docs/community/contribute)! ## GitHub Codespaces / VS Code Remote Containers The easiest way to get started with developing Encore is using GitHub Codespaces. Simply open this repository in a new Codespace and your development environment will be set up with everything preconfigured for building the `encore` CLI and running applications with it. This also works just as well with [Visual Studio Code's Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). ## Building the encore command from source To build from the source simply run `go build ./cli/cmd/encore` and `go install ./cli/cmd/git-remote-encore`. Running an Encore application requires both the Encore runtime (the `encore.dev` package) as well as a custom-built [Go runtime](https://github.com/encoredev/go) to implement Encore's request semantics and automated instrumentation. As a result, the Encore Daemon must know where these two things exist on the filesystem to compile the Encore application properly. This must be done in one of two ways: embedding the installation path at compile time (similar to `GOROOT`) or by setting an environment variable at runtime. The environment variables are: - `ENCORE_RUNTIMES_PATH` – the path to the `encore.dev` runtime implementation. - `ENCORE_GOROOT` – the path to encore-go on disk **ENCORE_RUNTIMES_PATH** This must be set to the location of the `encore.dev` runtime package. It's located in this Git repository in the `runtimes` directory: ```bash export ENCORE_RUNTIMES_PATH=/path/to/encore/runtimes ``` **ENCORE_GOROOT** The `ENCORE_GOROOT` must be set to the path to the [Encore Go runtime](https://github.com/encoredev/go). Unless you want to make changes to the Go runtime it's easiest to point this to an existing Encore installation. To do that, run `encore daemon env` and grab the value of `ENCORE_GOROOT`. For example (yours is probably different): ```bash export ENCORE_GOROOT=/opt/homebrew/Cellar/encore/0.16.2/libexec/encore-go ``` ### Running applications when building from source Once you've built your own `encore` binary and set the environment variables above, you're ready to go! Start the daemon with the built binary: `./encore daemon -f` Note that when you run commands like `encore run` must use the same `encore` binary the daemon is running. ### Testing the Daemon run logic The codegen tests in the `internal/clientgen/client_test.go` file uses many auto generated files from the `e2e-tests/testdata` directory. To generate the client files and other test files, run `go test -golden-update` from the `e2e-tests` directory. This will generate client files for all the supported client generation languages. Running `go test ./internal/clientgen` will now work and use the most recent client generated files. If you change the client or content of the `testdata` folder, you may need to regenerate the client files again. ## Architecture The code base is divided into several parts: ### cli The `encore` command line interface. The encore background daemon is located at `cli/daemon` and is responsible for managing processes, setting up databases and talking with the Encore servers for operations like fetching production logs. ### parser The Encore Parser statically analyzes Encore apps to build up a model of the application dubbed the Encore Syntax Tree (EST) that lives in `parser/est`. For speed the parser does not perform traditional type-checking; it does limited type-checking for enforcing Encore-specific rules but otherwise relies on the underlying Go compiler to perform type-checking as part of building the application. ### compiler The Encore Compiler rewrites the source code based on the parsed Encore Syntax Tree to create a fully functioning application. It rewrites API calls & API handlers, injects instrumentation and secret values, and more. ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "runtimes/core", "runtimes/js", "tsparser", "tsparser/wasm", "supervisor", "miniredis", ] [profile.dev.package] insta.opt-level = 3 [profile.release] lto = true [patch.crates-io] tokio-postgres = { git = "https://github.com/encoredev/rust-postgres", branch = "encore-patches-sync" } postgres-protocol = { git = "https://github.com/encoredev/rust-postgres", branch = "encore-patches-sync" } postgres-types = { git = "https://github.com/encoredev/rust-postgres", branch = "encore-patches-sync" } swc_ecma_parser = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_ecma_ast = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_ecma_transforms_base = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_atoms = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_common = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_ecma_loader = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } swc_ecma_visit = { git = "https://github.com/encoredev/swc", branch = "node-resolve-exports" } ================================================ FILE: Cross.toml ================================================ [build] pre-build = [ "apt-get install unzip &&", "curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip &&", "unzip protoc-24.4-linux-x86_64.zip -d /usr/local &&", "rm protoc-24.4-linux-x86_64.zip &&", "export PATH=$PATH:/usr/local/bin", ] [build.env] volumes = ["ENCORE_WORKDIR"] passthrough = ["TYPE_DEF_TMP_PATH", "ENCORE_VERSION"] ================================================ FILE: LICENSE ================================================ Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================

encore icon


Open Source Framework for creating type-safe distributed systems with declarative infrastructure

- **Framework:** The Encore framework, available for TypeScript and Go, lets you define APIs, services, and infrastructure (databases, Pub/Sub, caching, buckets, cron jobs) as type-safe objects in your code. Write your application once, then deploy it anywhere without code changes by [exporting a Docker image](https://encore.dev/docs/ts/self-host/build) and supplying the infra configuration. - **Local Dev Tools:** The Encore CLI runs your app locally and automatically provisions local infrastructure. Encore's local dev dashboard provides tools for a productive workflow: Tracing, API Explorer, Service Catalog, Architecture Diagrams, and Database Explorer. - **DevOps Platform (Optional):** [Encore Cloud](https://encore.cloud) parses your application and automatically provisions the required infrastructure in your own AWS/GCP account. Other tools include Preview Environments for each PR, Service Catalog, Distributed Tracing, Metrics, and Cost Analytics. **⭐ Star this repository** to help spread the word and stay up to date. ### Get started **Install Encore:** - **macOS:** `brew install encoredev/tap/encore` - **Linux:** `curl -L https://encore.dev/install.sh | bash` - **Windows:** `iwr https://encore.dev/install.ps1 | iex` **Create your first app:** - **TypeScript:** `encore app create --example=ts/hello-world` - **Go:** `encore app create --example=hello-world` **Use with AI coding assistants:** Add Encore's [LLM instructions](https://encore.dev/docs/ts/ai-integration) to your project, so your AI tools can understand your architecture, generate type-safe code, and use Encore's infrastructure primitives. Use the built-in [MCP server](https://encore.dev/docs/ts/ai-integration) to give your AI runtime context (query databases, call APIs, analyze traces) for seamless debugging and faster iterations. [Learn more](https://encore.dev/docs/ts/ai-integration) https://github.com/user-attachments/assets/461b902f-8fd3-46f1-a73c-0ebbfa789ce3 _Encore's local development dashboard_ ## How it works Encore's open source backend frameworks, [Encore.ts](https://encore.dev/docs/ts) and [Encore.go](https://encore.dev/docs/primitives/overview), enable you to define resources like services, databases, Pub/Sub, caches, buckets, and cron jobs, as type-safe objects in your application code. You only define **infrastructure semantics** (_what matters for the behavior of the application_), not configuration for specific cloud services. Here's how you define a database in Encore.ts: ```typescript const db = new SQLDatabase("users", { migrations: "./migrations" }); ``` Encore parses your application to understand your infrastructure requirements, then sets up infrastructure in different environments: - **Locally:** The Encore CLI sets up local infrastructure (microservices, Postgres, Pub/Sub, etc.) and provides a development dashboard with distributed tracing, API documentation, service catalog, architecture diagrams, and database explorer. Works offline, no Docker Compose needed. - **AWS/GCP:** Encore Cloud deploys to your AWS/GCP account without any Terraform or YAML needed. It automatically sets up compute instances (serverless or Kubernetes), databases (RDS/Cloud SQL), Pub/Sub (SQS/GCP Pub/Sub), storage (S3/GCS), caching (ElastiCache/Memorystore), and all other required resources like security groups and IAM policies, according to best practices. - **Self-hosted:** Use the Encore CLI to export your app as Docker images, then supply your infra config to host anywhere. Encore overview _Encore orchestrates infrastructure from local development and testing, to production in your cloud._ #### Encore makes it simpler to build distributed systems - **Microservices without boilerplate:** Call APIs in other services like regular functions. Encore handles service discovery, networking, and serialization. Get cross-service type-safety and auto-complete in your IDE. - **Modular monolith to microservices:** Structure your application using independent services for clarity. Then deploy them colocated in a single process or as distributed microservices, without changing a single line of code. Encore handles service communication whether in-process or over the network. - **Testing built-in:** Mock API calls, get dedicated test infrastructure, and use distributed tracing for tests. ### Example: Hello World Defining microservices and API endpoints is very simple. With less than 10 lines of code, you can create a production-ready, deployable service. **Hello World in Encore.ts** ```typescript import { api } from "encore.dev/api"; export const get = api( { expose: true, method: "GET", path: "/hello/:name" }, async ({ name }: { name: string }): Promise => { const msg = `Hello ${name}!`; return { message: msg }; } ); interface Response { message: string; } ``` **Hello World in Encore.go** ```go package hello //encore:api public path=/hello/:name func World(ctx context.Context, name string) (*Response, error) { msg := fmt.Sprintf("Hello, %s!", name) return &Response{Message: msg}, nil } type Response struct { Message string } ``` ### Example: Using Pub/Sub If you want a Pub/Sub Topic, you declare it directly in your application code and Encore will integrate the infrastructure and generate the boilerplate code necessary. Encore orchestrates the relevant Pub/Sub infrastructure for different environments: - **NSQ** for local environments - **GCP Pub/Sub** for environments on GCP - **SNS/SQS** for environments on AWS **Using Pub/Sub in Encore.ts** ```typescript import { Topic } "encore.dev/pubsub" export interface SignupEvent { userID: string; } export const signups = new Topic("signups", { deliveryGuarantee: "at-least-once", }); ``` **Using Pub/Sub in Encore.go** ```go import "encore.dev/pubsub" type User struct { /* fields... */ } var Signup = pubsub.NewTopic[*User]("signup", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) // Publish messages by calling a method Signup.Publish(ctx, &User{...}) ``` ### Need some infrastructure Encore doesn't provide? Encore never prevents you from using arbitrary infrastructure. You can use any external resource as you normally would, directly integrating standard SDKs (AWS SDK, GCP client libraries, third-party APIs, etc.). You then provision that resource yourself as you normally would. ### Want to use Encore in an existing system? You don't need a complete rewrite, Encore supports incremental adoption. **Service-by-service adoption (recommended):** Build new services with Encore and run them alongside your existing system, integrated via APIs. Then incrementally migrate existing services as needed. Each migrated service immediately gets Encore's full feature set: infrastructure provisioning, tracing, architecture diagrams. #### Deployment options: - **Your Kubernetes cluster:** Deploy directly to your existing Kubernetes infrastructure. Run Encore alongside legacy systems in the same environment. - **Encore-managed infrastructure:** Encore provisions and manages infrastructure in your AWS/GCP account, deployed within your existing VPC and security setup. - **Terraform provider:** Encore provides a [Terraform provider](https://encore.dev/docs/platform/integrations/terraform) to make it simple to integrate Encore managed infrastructure with your existing infrastructure landscape. Start with low-risk, frequently-changed services to validate the approach. Learn more in our [migration guide](https://encore.dev/docs/platform/migration/migrate-to-encore). ## Learn more - **Documentation:** [Encore Docs](https://encore.dev/docs) - **See example apps:** [Example Apps Repo](https://github.com/encoredev/examples/) - **See products built with Encore:** [Showcase](https://encore.cloud/showcase) - **Hear from teams using Encore:** [Case studies](https://encore.cloud/customers) - **Have questions?** Join the friendly developer community on [Discord](https://encore.dev/discord) - **Talk to a human:** [Book a 1:1 demo](https://encore.dev/book) with one of our founders - **Videos:** - Intro: Encore concepts & features - Demo video: Getting started with Encore.ts - Demo: Building and deploying a simple Go service - Demo: Building an event-driven system in Go - Find more videos in our [YouTube channel](youtube.com/channel/UCvqeAqMPotfuA6SPXa4VhNQ/). ### How Encore compares to other tools | Tool | What it does | How Encore differs | | ---------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Pulumi / CDK / Terraform / SST** | Infrastructure-as-Code tools for provisioning cloud resources | Other solutions define configuration for specific infra services, coupling your application to one set of infrastructure. Encore uses semantic infrastructure (services, databases, Pub/Sub), write once, deploy anywhere (AWS/GCP/local), colocated or distributed services | | **Serverless Framework / Chalice** | Frameworks for deploying serverless functions to AWS | Encore supports any architecture (monolith, microservices, serverless) and any cloud (AWS, GCP, self-hosted) | | **NestJS / Express / Fiber** | Web frameworks for building APIs and services | Encore provides the same capabilities plus infrastructure primitives, local dev tooling, observability, and optional cloud deployment | | **Convex / Supabase / Firebase** | Managed backend-as-a-service platforms | Encore gives you similar productivity but supports microservices and event-driven systems, and deploys to your own cloud account with no vendor lock-in | | **Vercel / Netlify / Railway** | Deployment platforms (primarily frontend/full-stack) | Encore is backend-specialized with deeper primitives (Pub/Sub, cron, caching) and multi-cloud infrastructure automation | ### Why teams use Encore - **Faster Development**: Encore streamlines the development process by providing guardrails, clear abstractions, and removing manual infrastructure tasks from development iterations. - **Scalability & Performance**: Encore simplifies building large-scale microservices applications that can handle growing user bases and demands, without the normal boilerplate and complexity. - **Control & Standardization**: Built-in tools like automated architecture diagrams, infrastructure tracking and approval workflows, make it easy for teams and leaders to get an overview of the entire application. - **Security & Compliance**: Encore Cloud helps ensure your application is secure and compliant by enforcing security standards like least privilege IAM, and provisioning infrastructure according to best practices for each cloud provider. - **Reduced Costs**: Encore Cloud's automatic infrastructure management removes common cloud expenses like overprovisioned test environments, and reduces DevOps workload. ## Open Source Everything needed to develop and deploy Encore applications is Open Source, including the backend frameworks, parser, compiler, runtime, and CLI. This includes all code needed for local development, everything that runs in your application when it is deployed, and everything needed to generate a Docker image for your application, so you can easily deploy your application anywhere. [Learn more in the docs](https://encore.dev/docs/ts/self-host/build). ## Join our growing developer community Developers building with Encore are part of fast-moving teams that want to focus on creative programming and building great software to solve meaningful problems. It's a friendly place, great for exchanging ideas and learning new things! **Join the community on [Discord](https://encore.dev/discord).** We rely on your contributions and feedback to improve Encore for everyone who is using it. Here's how you can contribute: - ⭐ **Star and watch this repository to help spread the word and stay up to date.** - Meet fellow Encore developers and chat on [Discord](https://encore.dev/discord). - Follow Encore on [Twitter](https://twitter.com/encoredotdev). - Share feedback or ask questions via [email](mailto:hello@encore.dev). - Leave feedback on the [Public Roadmap](https://encore.dev/roadmap). - Send a pull request here on GitHub with your contribution. ## Frequently Asked Questions (FAQ) ### Who's behind Encore? Encore was founded by long-time engineers from Spotify and Google. We've lived through the challenges of building complex distributed systems with thousands of services, and scaling to hundreds of millions of users. Encore grew out of these experiences and is a solution to the frustrations that came with them: unnecessary infrastructure complexity and tedious repetitive work that suffocates developers' productivity and creativity. ### Who is Encore for? **Individual developers:** Build cloud applications without managing infrastructure configuration. Go from idea to deployed application in minutes instead of days. **Startup teams:** Get a production-ready backend on AWS/GCP without dedicated DevOps engineers. Focus your time on your product instead of reinventing infrastructure patterns and building platform tooling. **Large organizations:** Standardize backend development across teams. Reduce onboarding time and operational overhead. Spin up new services in minutes with consistent patterns, without needing days of back and forth between development and DevOps teams. ### Does defining infrastructure in code couple my app to infrastructure? No. Encore keeps your application code cloud-agnostic by letting you refer only to **logical resources** (like "a Postgres database" or "a Pub/Sub topic"). A backend-agnostic interface means your code has no cloud-specific imports or configurations. Encore's compiler and runtime handle the mapping of logical resources to actual infrastructure, which is configured per environment. Your code stays identical whether the environment uses e.g.: - **AWS RDS** or **GCP Cloud SQL** for databases - **SQS/SNS** or **GCP Pub/Sub** for messaging - **AWS Fargate**, **Cloud Run**, or **Kubernetes** for compute This **reduces coupling** compared to traditional Infrastructure-as-Code or cloud SDKs, which embed cloud-specific decisions directly in your codebase. With Encore, swapping cloud providers or infrastructure services requires no code changes. ### What kind of support does Encore offer? Encore is fully open source and maintained by a dedicated full-time team. Support options include: - **Community Support:** Free support via [Discord](https://encore.dev/discord) - **Documentation:** Comprehensive guides and API references at [encore.dev/docs](https://encore.dev/docs) - **Paid Support:** For teams requiring guaranteed response times or dedicated support, [contact us](mailto:hello@encore.dev) about support plans ### What if I want to migrate away from Encore? Encore is designed to let you go outside of the framework when you want to, and easily drop down in abstraction level when you need to, so you never run into any dead-ends. Should you want to migrate away, it's straightforward and does not require a big rewrite. 99% of your code is regular Go or TypeScript. Encore provides tools for [self-hosting](https://encore.dev/docs/ts/self-host/build) your application, by using the Open Source CLI to produce a standalone Docker image that can be deployed anywhere you'd like. Learn more in the [migration guide](https://encore.dev/docs/ts/migration/migrate-away) ## Roadmap We're actively expanding Encore's capabilities. Here's what's on the horizon: **Languages** - **Python**: Next on the roadmap for broader ecosystem support **Cloud Providers** - **Azure**: Planned to complement existing AWS and GCP support **Infrastructure Primitives** - Expanding storage, compute, and queue options See the full [Public Roadmap](https://encore.dev/roadmap) and share your feedback on what you'd like to see next. ## Contributing to Encore and building from source See [CONTRIBUTING.md](CONTRIBUTING.md). ================================================ FILE: check.bash ================================================ #!/usr/bin/env bash # # This script will run the same checks as Encore's CI pipeline and report the same static analysis errors # as the pipeline by default. It can be used to check for what errors might be reported by the pipeline # before you commit and open a PR. # # Usage: # ./check.bash [options] # # Options: # --base The merge base to compare against (default: origin/main) # --diff Show the diff against base instead of running the checks # --filter-mode The filter mode to use for reviewdog; added, file, diff_context, nofilter (default: file) # --all Alias for `--filter-mode nofilter` (runs checks against all files in the working directory) # # Examples: # # # Run the checks against files changed since branching from origin/main # # (This is the default behavior and what our CI process does) # ./check.bash # # # Show the diff between the current working directory and origin/main # ./check.bash --diff # # # Run the checks against the entire working directory (regardless of changes made) # ./check.bash --all ############################################################################################################################## # Step 0: Setup the script with basic error handling # ############################################################################################################################## set -euo pipefail # nosemgrep IFS=$'\n\t' function errHandler() { echo "Exiting due to an error line $1" >&2 echo "" >&2 awk 'NR>L-4 && NR> ":""),$0 }' L="$1" "$0" >&2 } trap 'errHandler $LINENO' ERR ############################################################################################################################## # Step 1: Configure the script with the parameters the use wants # ############################################################################################################################## # Parameters WORK_DIR=$( dirname "${BASH_SOURCE[0]}" ) # Get the directory this script is in BASE_REF="origin/main" # The merge base to compare against DIFF_ONLY="false" # If true, show the diff instead of running the checks FILTER_MODE="file" # The filter mode to use for reviewdog (added, file, diff_context, nofilter) # Parse the command line arguments while [[ $# -gt 0 ]]; do case "$1" in --base) BASE_REF="$2" shift 2 ;; --diff) DIFF_ONLY="true" shift 1 ;; --filter-mode) FILTER_MODE="$2" shift 2 ;; --all) FILTER_MODE="nofilter" shift 1 ;; *) echo "Unknown argument: $1" exit 1 ;; esac done ############################################################################################################################## # Step 2: Check for required tools and error out if anything is missing which we can't install for the user # ############################################################################################################################## # Check for tools we can't install using go command -v go >/dev/null 2>&1 || { echo >&2 "go is required but not installed. Aborting."; exit 1; } command -v git >/dev/null 2>&1 || { echo >&2 "git is required but not installed. Aborting."; exit 1; } command -v sed >/dev/null 2>&1 || { echo >&2 "sed is required but not installed. Aborting."; exit 1; } command -v semgrep >/dev/null 2>&1 || { echo >&2 "semgrep is required but not installed. Aborting."; exit 1; } # Now install all missing tools command -v reviewdog >/dev/null 2>&1 || go install github.com/reviewdog/reviewdog/cmd/reviewdog@latest || { echo >&2 "Unable to install reviewdog. Aborting."; exit 1; } command -v staticcheck >/dev/null 2>&1 || go install honnef.co/go/tools/cmd/staticcheck@latest || { echo >&2 "Unable to install staticcheck. Aborting."; exit 1; } command -v errcheck >/dev/null 2>&1 || go install github.com/kisielk/errcheck@latest || { echo >&2 "Unable to install errcheck. Aborting."; exit 1; } command -v ineffassign >/dev/null 2>&1 || go install github.com/gordonklaus/ineffassign@latest || { echo >&2 "Unable to install ineffassign. Aborting."; exit 1; } ############################################################################################################################## # Step 3: Create a diff of the changes in the working directory against the common ancestor of the current branch and main # # This will be used to run static analysis checks on only the files that have changed. This diff should mimic the # # diff that would be created by GitHub when all current changes are committed and pushed into a PR on GitHub. # ############################################################################################################################## # Don't generate the diff if we don't need it to filter! if [[ "$FILTER_MODE" != "nofilter" ]]; then # Create a temp directory to store the common ancestor commit TMP_DIR=$(mktemp -d) if [[ ! "$TMP_DIR" || ! -d "$TMP_DIR" ]]; then echo "Could not create temp dir" exit 1 fi # Create a temp file to store the diff we need DIFF_FILE=$(mktemp) if [[ ! "$DIFF_FILE" || ! -f "$DIFF_FILE" ]]; then echo "Could not create temp diff file" exit 1 fi # Create a blank file to use as a comparison when a file is missing because either it's new or been deleted BLANK_FILE=$(mktemp) if [[ ! "$BLANK_FILE" || ! -f "$BLANK_FILE" ]]; then echo "Could not create blank file" exit 1 fi # Clean up on exit and delete all the temp files we just created function cleanup() { rm -rf "$TMP_DIR" rm -f "$DIFF_FILE" rm -f "$BLANK_FILE" } trap cleanup EXIT # Clone the repo into the temp directory git clone -q "$WORK_DIR" "$TMP_DIR" # Change our temp directory to be a clean copy of the common ancestor commit pushd "$TMP_DIR" > /dev/null git reset -q --hard HEAD git checkout -q "$(git merge-base "$BASE_REF" HEAD)" TRACKED_FILES_FROM_MAIN=$(git ls-files) popd > /dev/null # Create a list of files that we care about MODIFICATIONS_IN_WORKING_DIR=$(git status --short | awk '{print $2}') TRACKED_FILES_IN_WORKING_DIR=$(git ls-files) ALL_FILES=$(echo "$TRACKED_FILES_IN_WORKING_DIR $MODIFICATIONS_IN_WORKING_DIR $TRACKED_FILES_FROM_MAIN" | tr ' ' '\n' | sort -u) # Create a diff of the changes in the working directory against the common ancestor of the current branch and main for file in $ALL_FILES; do # If the original file doesn't exist, use a blank file instead # (This means it was a new file that was added in the current version of the code base) ORIGINAL_FILE="$TMP_DIR/$file" if [[ ! -f "$ORIGINAL_FILE" ]]; then ORIGINAL_FILE="$BLANK_FILE" fi # If the updated file doesn't exist, use a blank file instead # (This means the file was deleted in the current version of the code base) UPDATED_FILE="$WORK_DIR/$file" if [[ ! -f "$UPDATED_FILE" ]]; then UPDATED_FILE="$BLANK_FILE" fi # Run git diff between the original file and the updated file # Replace the file paths in the diff to match the relative path in the working directory # Then write the diff into our diff file git diff "$ORIGINAL_FILE" "$UPDATED_FILE" | sed "s|$ORIGINAL_FILE|/$file|g" | sed "s|$UPDATED_FILE|/$file|g" >> "$DIFF_FILE" || true # Suppress the exit code done if [[ "$DIFF_ONLY" == "true" ]]; then cat "$DIFF_FILE" exit 0 fi fi ############################################################################################################################## # Step 4: Run review dog using the diff we just created, allowing reviewdog to only show errors from changes we've made # ############################################################################################################################## if [[ "$FILTER_MODE" == "nofilter" ]]; then reviewdog -filter-mode=nofilter else reviewdog -filter-mode="$FILTER_MODE" -diff="cat $DIFF_FILE" fi ================================================ FILE: cli/cmd/encore/app/app.go ================================================ package app import ( "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/root" ) // These can be overwritten using // `go build -ldflags "-X encr.dev/cli/cmd/encore/app.defaultGitRemoteName=encore"`. var ( defaultGitRemoteName = "encore" defaultGitRemoteURL = "encore://" ) var appCmd = &cobra.Command{ Use: "app", Short: "Commands to create and link Encore apps", } func init() { root.Cmd.AddCommand(appCmd) } ================================================ FILE: cli/cmd/encore/app/clone.go ================================================ package app import ( "os" "os/exec" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" ) var cloneAppCmd = &cobra.Command{ Use: "clone [app-id] [directory]", Short: "Clone an Encore app to your computer", Args: cobra.MinimumNArgs(1), DisableFlagsInUseLine: true, Run: func(c *cobra.Command, args []string) { cmdArgs := append([]string{"clone", "--origin", defaultGitRemoteName, defaultGitRemoteURL + args[0]}, args[1:]...) cmd := exec.Command("git", cmdArgs...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { os.Exit(1) } }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { switch len(args) { case 0: return cmdutil.AutoCompleteAppSlug(cmd, args, toComplete) case 1: return nil, cobra.ShellCompDirectiveFilterDirs default: return nil, cobra.ShellCompDirectiveDefault } }, } func init() { appCmd.AddCommand(cloneAppCmd) } ================================================ FILE: cli/cmd/encore/app/create.go ================================================ package app import ( "bytes" "context" "encoding/json" "fmt" "io/fs" "os" "os/exec" "path/filepath" "strings" "time" "github.com/briandowns/spinner" "github.com/cockroachdb/errors" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/tailscale/hujson" "golang.org/x/term" "encr.dev/cli/cmd/encore/auth" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/llm_rules" "encr.dev/cli/internal/platform" "encr.dev/cli/internal/telemetry" "encr.dev/internal/conf" "encr.dev/internal/env" "encr.dev/internal/userconfig" "encr.dev/internal/version" "encr.dev/pkg/github" "encr.dev/pkg/xos" daemonpb "encr.dev/proto/encore/daemon" ) var ( createAppTemplate string createAppOnPlatform bool createAppLang = cmdutil.Oneof{ Value: "", Allowed: cmdutil.LanguageFlagValues(), Flag: "lang", FlagShort: "l", Desc: "Programming language to use for the app", TypeDesc: "string", } createAppLLMRules = cmdutil.Oneof{ Value: "", Allowed: llm_rules.LLMRulesFlagValues(), Flag: "llm-rules", FlagShort: "r", Desc: "Initialize the app with llm rules for a specific tool", TypeDesc: "string", } ) var createAppCmd = &cobra.Command{ Use: "create [name]", Short: "Create a new Encore app", Args: cobra.MaximumNArgs(1), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { name := "" if len(args) > 0 { name = args[0] } var tool llm_rules.Tool if createAppLLMRules.Value == "" { cfg, err := userconfig.Global().Get() if err != nil { cmdutil.Fatalf("Couldn't read user config: %s", err) } tool = llm_rules.Tool(cfg.LLMRules) } else { tool = llm_rules.Tool(createAppLLMRules.Value) } if err := createApp(context.Background(), name, createAppTemplate, cmdutil.Language(createAppLang.Value), tool); err != nil { cmdutil.Fatal(err) } }, } func init() { appCmd.AddCommand(createAppCmd) createAppCmd.Flags().BoolVar(&createAppOnPlatform, "platform", true, "whether to create the app with the Encore Platform") createAppCmd.Flags().StringVar(&createAppTemplate, "example", "", "URL to example code to use.") createAppLang.AddFlag(createAppCmd) createAppLLMRules.AddFlag(createAppCmd) } func promptAccountCreation() { // If shell is non-interactive, don't prompt if !term.IsTerminal(int(os.Stdin.Fd())) { return } cyan := color.New(color.FgCyan) red := color.New(color.FgRed) // Prompt the user for creating an account if they're not logged in. if _, err := conf.CurrentUser(); errors.Is(err, fs.ErrNotExist) && createAppOnPlatform { PromptLoop: for { _, _ = cyan.Fprint(os.Stderr, "Log in / Sign up for a free Encore Cloud account to enable automated cloud deployments? (Y/n): ") var input string _, _ = fmt.Scanln(&input) input = strings.TrimSpace(input) switch input { case "Y", "y", "yes", "": telemetry.Send("app.create.account", map[string]any{"response": true}) if err := auth.DoLogin(auth.AutoFlow); err != nil { cmdutil.Fatal(err) } case "N", "n", "no": telemetry.Send("app.create.account", map[string]any{"response": false}) // Continue without creating an account. case "q", "quit", "exit": os.Exit(1) default: // Try again. _, _ = red.Fprintln(os.Stderr, "Unexpected answer, please enter 'y' or 'n'.") continue PromptLoop } break } } } func promptRunApp() bool { // If shell is non-interactive, don't prompt if !term.IsTerminal(int(os.Stdin.Fd())) { return false } cyan := color.New(color.FgCyan) red := color.New(color.FgRed) for { _, _ = cyan.Fprint(os.Stderr, "Run your app now? (Y/n): ") var input string _, _ = fmt.Scanln(&input) input = strings.TrimSpace(input) switch input { case "Y", "y", "yes", "": telemetry.Send("app.create.run", map[string]any{"response": true}) return true case "N", "n", "no": telemetry.Send("app.create.run", map[string]any{"response": false}) return false case "q", "quit", "exit": telemetry.Send("app.create.run", map[string]any{"response": false}) return false default: // Try again. _, _ = red.Fprintln(os.Stderr, "Unexpected answer, please enter 'y' or 'n'.") } } } // createApp is the implementation of the "encore app create" command. func createApp(ctx context.Context, name, template string, lang cmdutil.Language, llmRules llm_rules.Tool) (err error) { defer func() { // We need to send the telemetry synchronously to ensure it's sent before the command exits. telemetry.SendSync("app.create", map[string]any{ "template": template, "lang": lang, "error": err != nil, }) }() cyan := color.New(color.FgCyan) green := color.New(color.FgGreen) promptAccountCreation() if name == "" || template == "" || llmRules == "" { name, template, lang, llmRules = createAppForm(name, template, lang, llmRules, false) } // Treat the special name "empty" as the empty app template // (the rest of the code assumes that's the empty string). if template == "empty" { template = "" } if template == "" && lang == cmdutil.LanguageTS { template = "ts/empty" } if err := validateName(name); err != nil { return err } else if _, err := os.Stat(name); err == nil { return fmt.Errorf("directory %s already exists", name) } // Parse template information, if provided. var ex *github.Tree if template != "" { var err error ex, err = parseTemplate(ctx, template) if err != nil { return err } } if err := os.Mkdir(name, 0755); err != nil { return err } defer func() { if err != nil { // Clean up the directory we just created in case of an error. _ = os.RemoveAll(name) } }() if ex != nil { s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Downloading template %s ", ex.Name()) s.Start() err := github.ExtractTree(ctx, ex, name) s.Stop() fmt.Println() if err != nil { return fmt.Errorf("failed to download template %s: %v", ex.Name(), err) } gray := color.New(color.Faint) _, _ = gray.Printf("Downloaded template %s.\n", ex.Name()) } else { // Set up files that we need when we don't have an example if err := xos.WriteFile(filepath.Join(name, ".gitignore"), []byte("/.encore\n"), 0644); err != nil { cmdutil.Fatal(err) } encoreModData := []byte("module encore.app\n") if err := xos.WriteFile(filepath.Join(name, "go.mod"), encoreModData, 0644); err != nil { cmdutil.Fatal(err) } } _, err = conf.CurrentUser() loggedIn := err == nil exCfg, err := parseExampleConfig(name) if err != nil { return fmt.Errorf("failed to parse example config: %v", err) } // Delete the example config file. _ = os.Remove(exampleJSONPath(name)) var app *platform.App if loggedIn && createAppOnPlatform { s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Creating app on encore.dev " s.Start() app, err = createAppOnServer(name, exCfg) s.Stop() if err != nil { return fmt.Errorf("creating app on encore.dev: %v", err) } } appRootRelpath := filepath.FromSlash(exCfg.EncoreAppPath) encoreAppPath := filepath.Join(name, appRootRelpath, "encore.app") appData, err := os.ReadFile(encoreAppPath) if err != nil { appData, err = []byte("{}"), nil } if app != nil { appData, err = setEncoreAppID(appData, app.Slug, []string{}) } else { appData, err = setEncoreAppID(appData, "", []string{ "The app is not currently linked to the encore.dev platform.", `Use "encore app link" to link it.`, }) } if err != nil { return errors.Wrap(err, "write encore.app file") } if err := xos.WriteFile(encoreAppPath, appData, 0644); err != nil { return errors.Wrap(err, "write encore.app file") } // Update to latest encore.dev release if _, err := os.Stat(filepath.Join(name, appRootRelpath, "go.mod")); err == nil { lang = cmdutil.LanguageGo s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Running go get encore.dev@latest" s.Start() if err := gogetEncore(filepath.Join(name, appRootRelpath)); err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) } s.Stop() } else if _, err := os.Stat(filepath.Join(name, appRootRelpath, "package.json")); err == nil { lang = cmdutil.LanguageTS s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Running npm install encore.dev@latest" s.Start() if err := npmInstallEncore(filepath.Join(name, appRootRelpath)); err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) } s.Stop() } // Rewrite any existence of ENCORE_APP_ID to the allocated app id. if app != nil { if err := rewritePlaceholders(name, app); err != nil { red := color.New(color.FgRed) _, _ = red.Printf("Failed rewriting source code placeholders, skipping: %v\n", err) } } if err := initGitRepo(name, app); err != nil { return err } // Try to generate wrappers. Don't error out if it fails for some reason, // it's a nice-to-have to avoid IDEs thinking there are compile errors before 'encore run' runs. _ = generateWrappers(filepath.Join(name, appRootRelpath)) // Create the app on the daemon. appRoot, err := filepath.Abs(filepath.Join(name, appRootRelpath)) if err != nil { cmdutil.Fatalf("failed to get absolute path: %v", err) } daemon := cmdutil.ConnectDaemon(ctx) appResp, err := daemon.CreateApp(ctx, &daemonpb.CreateAppRequest{ AppRoot: appRoot, Tutorial: exCfg.Tutorial, Template: template, }) if err != nil { color.Red("Failed to create app on daemon: %s\n", err) } if err := llm_rules.SetupLLMRules(llmRules, lang, filepath.Join(name, appRootRelpath), appResp.AppId); err != nil { color.Red("Failed to setup LLM rules: %s\n", err) } cmdutil.ClearTerminalExceptFirstNLines(0) _, _ = green.Printf("Successfully created app %s!\n", name) cyanf := cyan.SprintfFunc() fmt.Println() if app != nil { fmt.Printf("App ID: %s\n", cyanf(app.Slug)) fmt.Printf("Web URL: %s%s", cyanf("https://app.encore.cloud/"+app.Slug), cmdutil.Newline) } fmt.Printf("App Root: %s\n", cyanf(appRoot)) llm_rules.PrintLLMRulesInfo(llmRules) greenBoldF := green.Add(color.Bold).SprintfFunc() fmt.Printf("Run your app with: %s\n", greenBoldF("cd %s && encore run", filepath.Join(name, appRootRelpath))) fmt.Println() if promptRunApp() { cmdutil.ClearTerminalExceptFirstNLines(0) stream, err := daemon.Run(ctx, &daemonpb.RunRequest{ AppRoot: appRoot, Watch: true, WorkingDir: ".", Environ: os.Environ(), ListenAddr: "127.0.0.1:4000", Browser: daemonpb.RunRequest_BROWSER_ALWAYS, }) if err != nil { cmdutil.Fatalf("failed to run app: %v", err) } converter := cmdutil.ConvertJSONLogs(cmdutil.Colorize(true)) _ = cmdutil.StreamCommandOutput(stream, converter) return nil } cmdutil.ClearTerminalExceptFirstNLines(0) fmt.Print("Useful commands:\n\n") _, _ = cyan.Printf(" encore run\n") fmt.Print(" Run your app locally\n\n") if lang == cmdutil.LanguageGo { _, _ = cyan.Printf(" encore test ./...\n") } else { _, _ = cyan.Printf(" encore test\n") } fmt.Print(" Run tests\n\n") if app != nil { _, _ = cyan.Printf(" git push encore\n") fmt.Print(" Deploys your app\n\n") } fmt.Printf("Get started now: %s\n", greenBoldF("cd %s && encore run", filepath.Join(name, appRootRelpath))) return nil } // detectLang attempts to detect the application language for an Encore application // situated at appRoot. func detectLang(appRoot string) cmdutil.Language { if _, err := os.Stat(filepath.Join(appRoot, "go.mod")); err == nil { return cmdutil.LanguageGo } else if _, err := os.Stat(filepath.Join(appRoot, "package.json")); err == nil { return cmdutil.LanguageTS } return cmdutil.LanguageGo } func validateName(name string) error { ln := len(name) if ln == 0 { return fmt.Errorf("name must not be empty") } else if ln > 50 { return fmt.Errorf("name too long (max 50 chars)") } for i, s := range name { // Outside of [a-z], [0-9] and != '-'? if !((s >= 'a' && s <= 'z') || (s >= '0' && s <= '9') || s == '-') { return fmt.Errorf("name must only contain lowercase letters, digits, or dashes") } else if s == '-' { if i == 0 { return fmt.Errorf("name cannot start with a dash") } else if (i + 1) == ln { return fmt.Errorf("name cannot end with a dash") } else if name[i-1] == '-' { return fmt.Errorf("name cannot contain repeated dashes") } } } return nil } func gogetEncore(dir string) error { var goBinPath string // Prefer the 'go' binary from the Encore GOROOT if available. if goroot, ok := env.OptEncoreGoRoot().Get(); ok { goBinPath = filepath.Join(goroot, "bin", "go") } else { // Otherwise fall back to just "go", so that exec.Command // does a path lookup. goBinPath = "go" } // Use the 'go' binary from the Encore GOROOT in case the user // does not have Go installed separately from Encore. // nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.Command(goBinPath, "get", "encore.dev@latest") cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { return errors.Newf("go get failed: %v: %s", err, out) } return nil } func npmInstallEncore(dir string) error { args := []string{"install"} if version.Channel == version.DevBuild { args = append(args, filepath.Join(env.EncoreRuntimesPath(), "js", "encore.dev")) } else { args = append(args, fmt.Sprintf("encore.dev@%s", strings.TrimPrefix(version.Version, "v"))) } // First install the 'encore.dev' package. cmd := exec.Command("npm", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { err = fmt.Errorf("installing encore.dev package failed: %v: %s", err, out) } // Then run 'npm install'. cmd = exec.Command("npm", "install") cmd.Dir = dir if out2, err2 := cmd.CombinedOutput(); err2 != nil && err == nil { err = fmt.Errorf("'npm install' failed: %v: %s", err2, out2) } return err } func createAppOnServer(name string, cfg exampleConfig) (*platform.App, error) { if _, err := conf.CurrentUser(); err != nil { return nil, err } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() params := &platform.CreateAppParams{ Name: name, InitialSecrets: cfg.InitialSecrets, AppRootDir: cfg.EncoreAppPath, } return platform.CreateApp(ctx, params) } func parseTemplate(ctx context.Context, tmpl string) (*github.Tree, error) { // If the template does not contain a colon or a dot, it's definitely // not a github.com URL. Assume it's a simple template name. if !strings.Contains(tmpl, ":") && !strings.Contains(tmpl, ".") { tmpl = "https://github.com/encoredev/examples/tree/main/" + tmpl } return github.ParseTree(ctx, tmpl) } // initGitRepo initializes the git repo. // If app is not nil, it configures the repo to push to the given app. // If git does not exist, it reports an error matching exec.ErrNotFound. func initGitRepo(path string, app *platform.App) (err error) { defer func() { if e := recover(); e != nil { if ee, ok := e.(error); ok { err = ee } else { panic(e) } } }() git := func(args ...string) []byte { cmd := exec.Command("git", args...) cmd.Dir = path out, err := cmd.CombinedOutput() if err != nil && !errors.Is(err, exec.ErrNotFound) { panic(fmt.Errorf("git %s: %s (%w)", strings.Join(args, " "), out, err)) } return out } // Initialize git repo git("init") if app != nil && app.MainBranch != nil { git("checkout", "-b", *app.MainBranch) } git("config", "--local", "push.default", "current") git("add", "-A") cmd := exec.Command("git", "commit", "-m", "Initial commit") cmd.Dir = path // Configure the committer if the user hasn't done it themselves yet. if ok, _ := gitUserConfigured(); !ok { cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=Encore", "GIT_AUTHOR_EMAIL=git-bot@encore.dev", "GIT_COMMITTER_NAME=Encore", "GIT_COMMITTER_EMAIL=git-bot@encore.dev", ) } if out, err := cmd.CombinedOutput(); err != nil && !errors.Is(err, exec.ErrNotFound) { return fmt.Errorf("create initial commit repository: %s (%v)", out, err) } if app != nil { git("remote", "add", defaultGitRemoteName, defaultGitRemoteURL+app.Slug) } return nil } func addEncoreRemote(root, appID string) { // Determine if there are any remotes cmd := exec.Command("git", "remote") cmd.Dir = root out, err := cmd.CombinedOutput() if err != nil { return } out = bytes.TrimSpace(out) if len(out) == 0 { cmd = exec.Command("git", "remote", "add", defaultGitRemoteName, defaultGitRemoteURL+appID) cmd.Dir = root if err := cmd.Run(); err == nil { fmt.Println("Configured git remote 'encore' to push/pull with Encore.") } } } // gitUserConfigured reports whether the user has configured // user.name and user.email in git. func gitUserConfigured() (bool, error) { for _, s := range []string{"user.name", "user.email"} { out, err := exec.Command("git", "config", s).CombinedOutput() if err != nil { return false, err } else if len(bytes.TrimSpace(out)) == 0 { return false, nil } } return true, nil } // rewritePlaceholders recursively rewrites all files within basePath // to replace placeholders with the actual values for this particular app. func rewritePlaceholders(basePath string, app *platform.App) error { var first error err := filepath.WalkDir(basePath, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } if !info.Type().IsRegular() { return nil } if err := rewritePlaceholder(path, info, app); err != nil { if first == nil { first = err } } return nil }) if err == nil { err = first } return err } // rewritePlaceholder rewrites a file to replace placeholders with the // actual values for this particular app. If the file contains none of // the placeholders, this is a no-op. func rewritePlaceholder(path string, info fs.DirEntry, app *platform.App) error { data, err := os.ReadFile(path) if err != nil { return err } placeholders := []string{ "{{ENCORE_APP_ID}}", app.Slug, } var replaced bool for i := 0; i < len(placeholders); i += 2 { placeholder := []byte(placeholders[i]) target := []byte(placeholders[i+1]) if bytes.Contains(data, placeholder) { data = bytes.ReplaceAll(data, placeholder, target) replaced = true } } if replaced { return xos.WriteFile(path, data, info.Type().Perm()) } return nil } // exampleConfig is the optional configuration file for example apps. type exampleConfig struct { // Relative path to the directory where the `encore.app` should be located. // Defaults to ".". EncoreAppPath string `json:"encore_app_path"` InitialSecrets map[string]string `json:"initial_secrets"` Tutorial bool `json:"tutorial"` } func parseExampleConfig(repoPath string) (cfg exampleConfig, err error) { baseConfig := exampleConfig{ EncoreAppPath: ".", } data, err := os.ReadFile(exampleJSONPath(repoPath)) if errors.Is(err, fs.ErrNotExist) { return baseConfig, nil } else if err != nil { return baseConfig, err } data, err = hujson.Standardize(data) if err != nil { return baseConfig, err } else if err := json.Unmarshal(data, &cfg); err != nil { return baseConfig, err } if cfg.EncoreAppPath == "" { cfg.EncoreAppPath = "." } if !filepath.IsLocal(cfg.EncoreAppPath) { return baseConfig, errors.New("encore_app_path must be a local path") } return cfg, nil } func exampleJSONPath(repoPath string) string { return filepath.Join(repoPath, "example-initial-setup.json") } // setEncoreAppID rewrites the encore.app file to replace the app id, preserving comments. // It optionally adds comment lines before the "id" field if commentLines is not nil. func setEncoreAppID(data []byte, id string, commentLines []string) ([]byte, error) { if len(data) == 0 { data = []byte("{}") } root, err := hujson.Parse(data) if err != nil { return data, errors.Wrap(err, "parse encore.app") } obj, ok := root.Value.(*hujson.Object) if !ok { return data, errors.New("invalid encore.app format: not a json object") } var buf bytes.Buffer for i, ln := range commentLines { if i == 0 { fmt.Fprintf(&buf, "\n") } fmt.Fprintf(&buf, "\t// %s\n", strings.TrimSpace(ln)) } extra := hujson.Extra(buf.Bytes()) jsonValue, _ := json.Marshal(id) value := hujson.Value{ Value: hujson.Literal(jsonValue), } found := false for i := range obj.Members { m := &obj.Members[i] if lit, ok := m.Name.Value.(hujson.Literal); ok && lit.String() == "id" { if commentLines != nil { m.Name.BeforeExtra = extra } m.Value = value found = true break } } if !found { obj.Members = append([]hujson.ObjectMember{{ Name: hujson.Value{ BeforeExtra: extra, Value: hujson.Literal(`"id"`), }, Value: value, }}, obj.Members...) } root.Format() return root.Pack(), nil } // generateWrappers runs 'encore gen wrappers' in the given directory. func generateWrappers(dir string) error { // Use this executable if we can. exe, err := os.Executable() if err != nil { exe = "encore" } // nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.Command(exe, "gen", "wrappers") cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("encore gen wrappers failed: %v: %s", err, out) } return nil } ================================================ FILE: cli/cmd/encore/app/create_form.go ================================================ package app import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "slices" "strings" "sync" "time" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/tailscale/hujson" "golang.org/x/term" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/llm_rules" "encr.dev/pkg/option" ) type templateItem struct { ItemTitle string `json:"title"` Desc string `json:"desc"` Template string `json:"template"` Lang cmdutil.Language `json:"lang"` } func (i templateItem) Title() string { return i.ItemTitle } func (i templateItem) Description() string { return i.Desc } func (i templateItem) FilterValue() string { return i.ItemTitle } type CreateStep int const ( CreateStepLang CreateStep = iota CreateStepTemplate CreateStepAppName CreateStepLLMRules ) type createFormModel struct { steps []CreateStep lang langSelectModel templates templateListModel appName appNameModel llmRules llm_rules.ToolSelectModel initExistingApp bool width int height int aborted bool } func (m createFormModel) currentStep() option.Option[CreateStep] { if len(m.steps) == 0 { return option.None[CreateStep]() } return option.Some(m.steps[0]) } func (m createFormModel) hasStep(s CreateStep) bool { return slices.Contains(m.steps, s) } func (m *createFormModel) removeStep(s CreateStep) { m.steps = slices.DeleteFunc(m.steps, func(step CreateStep) bool { return step == s }) } func (m createFormModel) Init() tea.Cmd { return tea.Batch( m.appName.Init(), m.templates.Init(), ) } const checkmark = "✔" type appNameDone struct{} type appNameModel struct { predefined string text textinput.Model dirExists bool } func (m appNameModel) Init() tea.Cmd { return tea.Batch( textinput.Blink, ) } func (m appNameModel) Selected() string { if m.predefined != "" { return m.predefined } return m.text.Value() } func (m appNameModel) Update(msg tea.Msg) (appNameModel, tea.Cmd) { var cmds []tea.Cmd var c tea.Cmd m.text, c = m.text.Update(msg) cmds = append(cmds, c) if val := m.text.Value(); val != "" { _, err := os.Stat(val) m.dirExists = err == nil } switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: if m.text.Value() != "" && !m.dirExists { cmds = append(cmds, func() tea.Msg { return appNameDone{} }) } } } return m, tea.Batch(cmds...) } func (m appNameModel) View() string { var b strings.Builder if m.text.Focused() { b.WriteString(cmdutil.InputStyle.Render("App Name")) b.WriteString(cmdutil.DescStyle.Render(" [Use only lowercase letters, digits, and dashes]")) b.WriteByte('\n') b.WriteString(m.text.View()) if m.dirExists { b.WriteString(cmdutil.ErrorStyle.Render(" error: dir already exists")) } } else { fmt.Fprintf(&b, "%s App Name: %s", checkmark, m.text.Value()) } b.WriteByte('\n') return b.String() } type templateListModel struct { predefined string filter cmdutil.Language all []templateItem list list.Model loading spinner.Model } func (m templateListModel) Init() tea.Cmd { return tea.Batch( loadTemplates, m.loading.Tick, ) } func (m *templateListModel) SetSize(width, height int) { m.list.SetWidth(width) m.list.SetHeight(max(height-1, 0)) } type templateSelectDone struct{} func (m templateListModel) Update(msg tea.Msg) (templateListModel, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: // Have we selected a template? if idx := m.list.Index(); idx >= 0 { return m, func() tea.Msg { return templateSelectDone{} } } } case spinner.TickMsg: m.loading, _ = m.loading.Update(msg) case loadedTemplates: m.all = msg m.refreshFilter() newList, c := m.list.Update(msg) m.list = newList cmds = append(cmds, c) } newList, c := m.list.Update(msg) m.list = newList cmds = append(cmds, c) return m, tea.Batch(cmds...) } func (m *templateListModel) UpdateFilter(lang cmdutil.Language) { m.filter = lang m.refreshFilter() } func (m *templateListModel) refreshFilter() { var listItems []list.Item for _, it := range m.all { if it.Lang == m.filter { listItems = append(listItems, it) } } m.list.SetItems(listItems) } func (m templateListModel) View() string { var b strings.Builder b.WriteString(cmdutil.InputStyle.Render("Template")) b.WriteString(cmdutil.DescStyle.Render(" [Use arrows to move]")) b.WriteByte('\n') b.WriteString(m.list.View()) return b.String() } func (m templateListModel) Selected() string { if m.predefined != "" { return m.predefined } idx := m.list.Index() if idx < 0 { return "" } return m.list.Items()[idx].FilterValue() } func (m createFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmds []tea.Cmd c tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": m.aborted = true return m, tea.Quit case "q": // Only quit if no text input is focused if step, ok := m.currentStep().Get(); ok && step == CreateStepAppName { if m.appName.text.Focused() { break } } m.aborted = true return m, tea.Quit } if step, ok := m.currentStep().Get(); ok { switch step { case CreateStepLang: m.lang, c = m.lang.Update(msg) cmds = append(cmds, c) case CreateStepTemplate: m.templates, c = m.templates.Update(msg) cmds = append(cmds, c) case CreateStepAppName: m.appName, c = m.appName.Update(msg) cmds = append(cmds, c) case CreateStepLLMRules: m.llmRules, c = m.llmRules.Update(msg) cmds = append(cmds, c) } } return m, tea.Batch(cmds...) case langSelectDone: m.removeStep(CreateStepLang) m.templates.UpdateFilter(msg.Selected) m.SetSize(m.width, m.height) case llm_rules.ToolSelectDone: m.removeStep(CreateStepLLMRules) m.SetSize(m.width, m.height) case templateSelectDone: m.removeStep(CreateStepTemplate) if m.appName.predefined != "" { m.removeStep(CreateStepAppName) } m.SetSize(m.width, m.height) case appNameDone: m.removeStep(CreateStepAppName) m.SetSize(m.width, m.height) case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height m.SetSize(msg.Width, msg.Height) return m, nil } // No more steps, quit if !m.currentStep().Present() { cmds = append(cmds, tea.Quit) } // Update all submodels for other messages. m.lang, c = m.lang.Update(msg) cmds = append(cmds, c) m.templates, c = m.templates.Update(msg) cmds = append(cmds, c) m.llmRules, c = m.llmRules.Update(msg) cmds = append(cmds, c) m.appName, c = m.appName.Update(msg) cmds = append(cmds, c) return m, tea.Batch(cmds...) } func (m *createFormModel) SetSize(width, height int) { doneHeight := lipgloss.Height(m.doneView()) availHeight := height - doneHeight // CreateStepLang m.lang.SetSize(width, availHeight) // CreateStepTemplate m.templates.SetSize(width, availHeight) // CreateStepLLMRules m.llmRules.SetSize(width, availHeight) } func (m createFormModel) doneView() string { var b strings.Builder renderDone := func(title, value string) { b.WriteString(cmdutil.SuccessStyle.Render(fmt.Sprintf("%s %s: ", checkmark, title))) b.WriteString(value) b.WriteByte('\n') } renderLangDone := func() { renderDone("Language", m.lang.Selected().Display()) } renderNameDone := func() { renderDone("App Name", m.appName.Selected()) } renderTemplateDone := func() { renderDone("Template", m.templates.Selected()) } renderLLMRulesDone := func() { renderDone("LLM Rules", m.llmRules.Selected().Display()) } if m.appName.predefined != "" { renderNameDone() } if m.templates.predefined == "" && !m.hasStep(CreateStepLang) { renderLangDone() } if !m.initExistingApp { if m.templates.predefined != "" || !m.hasStep(CreateStepTemplate) { renderTemplateDone() } if m.llmRules.Predefined != "" || !m.hasStep(CreateStepLLMRules) { if m.llmRules.Selected() != llm_rules.LLMRulesToolNone { renderLLMRulesDone() } } } if m.appName.predefined == "" && !m.hasStep(CreateStepAppName) { renderNameDone() } return b.String() } func (m createFormModel) View() string { var b strings.Builder doneView := m.doneView() b.WriteString(doneView) if doneView != "" { b.WriteByte('\n') } if step, ok := m.currentStep().Get(); ok { if step == CreateStepLang { b.WriteString(m.lang.View()) } if step == CreateStepTemplate { b.WriteString(m.templates.View()) } if step == CreateStepAppName { b.WriteString(m.appName.View()) } if step == CreateStepLLMRules { b.WriteString(m.llmRules.View()) } } return cmdutil.DocStyle.Render(b.String()) } func (m templateListModel) SelectedItem() (templateItem, bool) { if m.predefined != "" { return templateItem{}, false } idx := m.list.Index() items := m.list.Items() if idx >= 0 && len(items) > idx { return items[idx].(templateItem), true } return templateItem{}, false } func createAppForm(inputName, inputTemplate string, inputLang cmdutil.Language, inputLLMRules llm_rules.Tool, initExistingApp bool) (appName, template string, selectedLang cmdutil.Language, selectedRules llm_rules.Tool) { // If all is set, just return if inputName != "" && inputTemplate != "" && inputLLMRules != "" { return inputName, inputTemplate, inputLang, inputLLMRules } // If shell is non-interactive, don't prompt if !term.IsTerminal(int(os.Stdin.Fd())) { if inputName == "" { cmdutil.Fatal("specify an app name") } return inputName, inputTemplate, inputLang, inputLLMRules } var langModel langSelectModel { ls := list.NewDefaultItemStyles() ls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) ls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) del := list.NewDefaultDelegate() del.Styles = ls del.ShowDescription = false del.SetSpacing(0) items := []list.Item{ langItem{ lang: cmdutil.LanguageGo, desc: "Build performant and scalable backends with Go", }, langItem{ lang: cmdutil.LanguageTS, desc: "Build backend and full-stack applications with TypeScript", }, } ll := list.New(items, del, 0, 0) ll.SetShowTitle(false) ll.SetShowHelp(false) ll.SetShowPagination(true) ll.SetShowFilter(false) ll.SetFilteringEnabled(false) ll.SetShowStatusBar(false) ll.DisableQuitKeybindings() // quit handled by createFormModel langModel = langSelectModel{ List: ll, Predefined: inputLang, } langModel.SetSize(0, 20) } var templateModel templateListModel { ls := list.NewDefaultItemStyles() ls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) ls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) del := list.NewDefaultDelegate() del.Styles = ls ll := list.New(nil, del, 0, 20) ll.SetShowTitle(false) ll.SetShowHelp(false) ll.SetShowPagination(true) ll.SetShowFilter(false) ll.SetFilteringEnabled(false) ll.SetShowStatusBar(false) ll.DisableQuitKeybindings() // quit handled by createFormModel sp := spinner.New() sp.Spinner = spinner.Dot sp.Style = cmdutil.InputStyle.Copy().Inline(true) templateModel = templateListModel{ predefined: inputTemplate, list: ll, loading: sp, } } var llmRulesModel llm_rules.ToolSelectModel { ls := list.NewDefaultItemStyles() ls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) ls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) del := list.NewDefaultDelegate() del.Styles = ls del.ShowDescription = false del.SetSpacing(0) items := make([]list.Item, 0, len(llm_rules.AllLLMRules)+1) items = append(items, llm_rules.NewLLMRulesItem(llm_rules.LLMRulesToolNone)) for _, rule := range llm_rules.AllLLMRules { items = append(items, llm_rules.NewLLMRulesItem(rule)) } ll := list.New(items, del, 0, 0) ll.SetShowTitle(false) ll.SetShowHelp(false) ll.SetShowPagination(true) ll.SetShowFilter(false) ll.SetFilteringEnabled(false) ll.SetShowStatusBar(false) ll.DisableQuitKeybindings() // quit handled by createFormModel llmRulesModel = llm_rules.ToolSelectModel{ List: ll, Predefined: inputLLMRules, } llmRulesModel.SetSize(0, 20) } var nameModel appNameModel { text := textinput.New() text.Focus() text.CharLimit = 20 text.Width = 30 text.Validate = incrementalValidateNameInput nameModel = appNameModel{predefined: inputName, text: text} } // Setup what steps and in what order they should be presented var steps []CreateStep if initExistingApp { if langModel.Predefined == "" { steps = append(steps, CreateStepLang) } } else { if templateModel.predefined == "" { if langModel.Predefined == "" { steps = append(steps, CreateStepLang) } else { templateModel.UpdateFilter(inputLang) } steps = append(steps, CreateStepTemplate) } if llmRulesModel.Predefined == "" { steps = append(steps, CreateStepLLMRules) } } if nameModel.predefined == "" { steps = append(steps, CreateStepAppName) } m := createFormModel{ steps: steps, lang: langModel, templates: templateModel, llmRules: llmRulesModel, appName: nameModel, initExistingApp: initExistingApp, } // If we have a name, start the list without any selection. if m.appName.predefined != "" { m.templates.list.Select(-1) } p := tea.NewProgram(m) result, err := p.Run() if err != nil { cmdutil.Fatal(err) } // Validate the result. res := result.(createFormModel) if res.aborted { os.Exit(1) } appName, template = inputName, inputTemplate if appName == "" { appName = res.appName.text.Value() } if template == "" && !initExistingApp { sel, ok := res.templates.SelectedItem() if !ok { cmdutil.Fatal("no template selected") } template = sel.Template } return appName, template, res.lang.Selected(), res.llmRules.Selected() } type langItem struct { lang cmdutil.Language desc string } func (i langItem) FilterValue() string { return i.lang.Display() } func (i langItem) Title() string { return i.FilterValue() } func (i langItem) Description() string { return "" } func (i langItem) SelectedID() cmdutil.Language { return i.lang } type langSelectModel = cmdutil.SimpleSelectModel[cmdutil.Language, langItem] type langSelectDone = cmdutil.SimpleSelectDone[cmdutil.Language] type loadedTemplates []templateItem var defaultTutorials = []templateItem{ { ItemTitle: "Intro to Encore.ts", Desc: "An interactive tutorial", Template: "ts/introduction", Lang: "ts", }, } var defaultTemplates = []templateItem{ { ItemTitle: "Hello World", Desc: "A simple REST API", Template: "hello-world", Lang: "go", }, { ItemTitle: "Hello World", Desc: "A simple REST API", Template: "ts/hello-world", Lang: "ts", }, { ItemTitle: "Uptime Monitor", Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", Template: "uptime", Lang: "go", }, { ItemTitle: "Uptime Monitor", Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", Template: "ts/uptime", Lang: "ts", }, { ItemTitle: "GraphQL", Desc: "GraphQL API, Microservices, SQL Database", Template: "graphql", Lang: "go", }, { ItemTitle: "URL Shortener", Desc: "REST API, SQL Database", Template: "url-shortener", Lang: "go", }, { ItemTitle: "URL Shortener", Desc: "REST API, SQL Database", Template: "ts/url-shortener", Lang: "ts", }, { ItemTitle: "SaaS Starter", Desc: "Complete app with Clerk auth, Stripe billing, etc. (advanced)", Template: "ts/saas-starter", Lang: "ts", }, { ItemTitle: "Empty app", Desc: "Start from scratch (experienced users only)", Template: "", Lang: "go", }, { ItemTitle: "Empty app", Desc: "Start from scratch (experienced users only)", Template: "ts/empty", Lang: "ts", }, } func fetchTemplates(url string, defaults []templateItem) []templateItem { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if req, err := http.NewRequestWithContext(ctx, "GET", url, nil); err == nil { if resp, err := http.DefaultClient.Do(req); err == nil { if data, err := io.ReadAll(resp.Body); err == nil { data, err = hujson.Standardize(data) if err == nil { var items []templateItem if err := json.Unmarshal(data, &items); err == nil && len(items) > 0 { return items } } } } } return defaults } func loadTemplates() tea.Msg { var wg sync.WaitGroup var templates, tutorials []templateItem wg.Add(1) go func() { defer wg.Done() templates = fetchTemplates("https://raw.githubusercontent.com/encoredev/examples/main/cli-templates.json", defaultTemplates) }() wg.Add(1) go func() { defer wg.Done() tutorials = fetchTemplates("https://raw.githubusercontent.com/encoredev/examples/main/cli-tutorials.json", defaultTutorials) }() wg.Wait() return loadedTemplates(append(tutorials, templates...)) } // incrementalValidateNameInput is like validateName but only // checks for valid/invalid characters. It can't check for // whether the last character is a dash, since if we treat that // as an error the user won't be able to enter dashes at all. func incrementalValidateNameInput(name string) error { ln := len(name) if ln == 0 { return fmt.Errorf("name must not be empty") } else if ln > 50 { return fmt.Errorf("name too long (max 50 chars)") } for i, s := range name { // Outside of [a-z], [0-9] and != '-'? if !((s >= 'a' && s <= 'z') || (s >= '0' && s <= '9') || s == '-') { return fmt.Errorf("name must only contain lowercase letters, digits, or dashes") } else if s == '-' { if i == 0 { return fmt.Errorf("name cannot start with a dash") } else if name[i-1] == '-' { return fmt.Errorf("name cannot contain repeated dashes") } } } return nil } ================================================ FILE: cli/cmd/encore/app/create_test.go ================================================ package app import ( "fmt" "testing" ) func Test_setEncoreAppID(t *testing.T) { tests := []struct { data []byte id string commentLines []string want string }{ { data: []byte(`{}`), id: "foo", commentLines: []string{"bar"}, want: `{ // bar "id": "foo", } `, }, { data: []byte(``), id: "foo", commentLines: []string{"bar"}, want: `{ // bar "id": "foo", } `, }, { data: []byte(`{ // foo "id": "test", }`), id: "foo", commentLines: []string{"bar", "baz"}, want: `{ // bar // baz "id": "foo", } `, }, { data: []byte(`{ "some_other_field": true, // foo "id": "test", }`), id: "foo", commentLines: []string{"bar", "baz"}, want: `{ "some_other_field": true, // bar // baz "id": "foo", } `, }, } for i, tt := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { got, err := setEncoreAppID(tt.data, tt.id, tt.commentLines) if err != nil { t.Fatal(err) } gotStr := string(got) if gotStr != tt.want { t.Errorf("setEncoreAppID() = %q, want %q", gotStr, tt.want) } }) } } ================================================ FILE: cli/cmd/encore/app/initialize.go ================================================ package app import ( "errors" "fmt" "os" "strings" "time" "github.com/briandowns/spinner" "github.com/fatih/color" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/llm_rules" "encr.dev/internal/conf" "encr.dev/pkg/xos" ) const ( tsEncoreAppData = `{%s "id": "%s", "lang": "typescript", } ` goEncoreAppData = `{%s "id": "%s", } ` ) var ( initAppLang = cmdutil.Oneof{ Value: "", Allowed: cmdutil.LanguageFlagValues(), Flag: "lang", FlagShort: "l", Desc: "Programming language to use for the app", TypeDesc: "string", } ) // Create a new app from scratch: `encore app create` // Link an existing app to an existing repo: `encore app link ` // Link an existing repo to a new app: `encore app init ` func init() { initAppCmd := &cobra.Command{ Use: "init [name]", Short: "Create a new Encore app from an existing repository", Args: cobra.MaximumNArgs(1), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { var name string if len(args) > 0 { name = args[0] } if err := initializeApp(name); err != nil { cmdutil.Fatal(err) } }, } appCmd.AddCommand(initAppCmd) initAppLang.AddFlag(initAppCmd) } func initializeApp(name string) error { // Check if encore.app file exists _, _, err := cmdutil.MaybeAppRoot() if errors.Is(err, cmdutil.ErrNoEncoreApp) { // expected } else if err != nil { cmdutil.Fatal(err) } else { // There is already an app here or in a parent directory. cmdutil.Fatal("an encore.app file already exists (here or in a parent directory)") } cyan := color.New(color.FgCyan) promptAccountCreation() name, _, lang, _ := createAppForm(name, "", cmdutil.Language(initAppLang.Value), llm_rules.LLMRulesToolNone, true) if err := validateName(name); err != nil { return err } appSlug := "" appSlugComments := "" // Create the app on the server. if _, err := conf.CurrentUser(); err == nil { s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Creating app on encore.dev " s.Start() app, err := createAppOnServer(name, exampleConfig{}) s.Stop() if err != nil { return fmt.Errorf("creating app on encore.dev: %v", err) } appSlug = app.Slug } // Create the encore.app file var encoreAppTemplate = goEncoreAppData if lang == "ts" { encoreAppTemplate = tsEncoreAppData } if appSlug == "" { appSlugComments = strings.Join([]string{ "", "The app is not currently linked to the encore.dev platform.", `Use "encore app link" to link it.`, }, "\n\t//") } encoreAppData := fmt.Appendf(nil, encoreAppTemplate, appSlugComments, appSlug) if err := xos.WriteFile("encore.app", encoreAppData, 0644); err != nil { return err } // Update to latest encore.dev release if _, err := os.Stat("go.mod"); err == nil { lang = cmdutil.LanguageGo s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Running go get encore.dev@latest" s.Start() if err := gogetEncore("."); err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) } s.Stop() } else if _, err := os.Stat("package.json"); err == nil { lang = cmdutil.LanguageTS s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Running npm install encore.dev@latest" s.Start() if err := npmInstallEncore("."); err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) } s.Stop() } green := color.New(color.FgGreen) _, _ = green.Fprint(os.Stdout, "Successfully initialized application on Encore Cloud!\n") if appSlug == "" { _, _ = fmt.Fprintf(os.Stdout, "The app is not currently linked to the encore.dev platform.\n") _, _ = fmt.Fprintf(os.Stdout, "Use \"encore app link\" to link it.\n") return nil } _, _ = fmt.Fprintf(os.Stdout, "- App ID: %s\n", cyan.Sprint(appSlug)) _, _ = fmt.Fprintf(os.Stdout, "- Cloud Dashboard: %s\n\n", cyan.Sprintf("https://app.encore.cloud/%s", appSlug)) return nil } ================================================ FILE: cli/cmd/encore/app/link.go ================================================ package app import ( "bytes" "context" "errors" "fmt" "io/fs" "os" "path/filepath" "time" "github.com/spf13/cobra" "github.com/tailscale/hujson" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" "encr.dev/pkg/xos" ) var forceLink bool var linkAppCmd = &cobra.Command{ Use: "link [app-id]", Short: "Link an Encore app with the server", Args: cobra.MaximumNArgs(1), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { var appID string if len(args) > 0 { appID = args[0] } linkApp(appID, forceLink) }, ValidArgsFunction: cmdutil.AutoCompleteAppSlug, } func init() { appCmd.AddCommand(linkAppCmd) linkAppCmd.Flags().BoolVarP(&forceLink, "force", "f", false, "Force link even if the app is already linked.") } func linkApp(appID string, force bool) { // Determine the app root. root, _, err := cmdutil.MaybeAppRoot() if errors.Is(err, cmdutil.ErrNoEncoreApp) { root, err = os.Getwd() } if err != nil { cmdutil.Fatal(err) } filePath := filepath.Join(root, "encore.app") data, err := os.ReadFile(filePath) if err != nil && !errors.Is(err, fs.ErrNotExist) { cmdutil.Fatal(err) os.Exit(1) } if len(bytes.TrimSpace(data)) == 0 { // Treat missing and empty files as an empty object. data = []byte("{}") } val, err := hujson.Parse(data) if err != nil { cmdutil.Fatal("could not parse encore.app: ", err) } appData, ok := val.Value.(*hujson.Object) if !ok { cmdutil.Fatal("could not parse encore.app: expected JSON object") } // Find the "id" value, if any. var idValue *hujson.Value for i := 0; i < len(appData.Members); i++ { kv := &appData.Members[i] lit, ok := kv.Name.Value.(hujson.Literal) if !ok || lit.String() != "id" { continue } idValue = &kv.Value } if idValue != nil { val, ok := idValue.Value.(hujson.Literal) if ok && val.String() != "" && val.String() != appID && !force { cmdutil.Fatal("the app is already linked.\n\nNote: to link to a different app, specify the --force flag.") } } if appID == "" { // The app is not linked. Prompt the user for an app ID. fmt.Println("Make sure the app is created on app.encore.cloud, and then enter its ID to link it.") fmt.Print("App ID: ") if _, err := fmt.Scanln(&appID); err != nil { cmdutil.Fatal(err) } else if appID == "" { cmdutil.Fatal("no app id given.") } } if linked, err := validateAppSlug(appID); err != nil { cmdutil.Fatal(err) } else if !linked { fmt.Fprintln(os.Stderr, "Error: that app does not exist, or you don't have access to it.") os.Exit(1) } // Write it back to our data structure. if idValue != nil { idValue.Value = hujson.String(appID) } else { appData.Members = append(appData.Members, hujson.ObjectMember{ Name: hujson.Value{Value: hujson.String("id")}, Value: hujson.Value{Value: hujson.String(appID)}, }) } val.Format() if err := xos.WriteFile(filePath, val.Pack(), 0644); err != nil { cmdutil.Fatal(err) os.Exit(1) } addEncoreRemote(root, appID) fmt.Println("Successfully linked app!") } func validateAppSlug(slug string) (ok bool, err error) { if _, err := conf.CurrentUser(); errors.Is(err, fs.ErrNotExist) { cmdutil.Fatal("not logged in. Run 'encore auth login' first.") } else if err != nil { return false, err } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if _, err := platform.GetApp(ctx, slug); err != nil { var e platform.Error if errors.As(err, &e) && e.HTTPCode == 404 { return false, nil } return false, err } return true, nil } ================================================ FILE: cli/cmd/encore/auth/auth.go ================================================ package auth import ( "errors" "fmt" "os" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/cli/internal/login" "encr.dev/internal/conf" ) var authKey string func init() { authCmd := &cobra.Command{ Use: "auth", Short: "Commands to authenticate with Encore", } signupCmd := &cobra.Command{ Use: "signup", Short: "Create a new Encore account", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { if err := DoLogin(DeviceAuth); err != nil { cmdutil.Fatal(err) } }, } loginCmd := &cobra.Command{ Use: "login [--auth-key=]", Short: "Log in to Encore", Run: func(cmd *cobra.Command, args []string) { if authKey != "" { if err := DoLoginWithAuthKey(); err != nil { cmdutil.Fatal(err) } } else { if err := DoLogin(DeviceAuth); err != nil { cmdutil.Fatal(err) } } }, } logoutCmd := &cobra.Command{ Use: "logout", Short: "Logs out the currently logged in user", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { DoLogout() }, } whoamiCmd := &cobra.Command{ Use: "whoami", Short: "Show the current logged in user", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { Whoami() }, } authCmd.AddCommand(signupCmd) authCmd.AddCommand(loginCmd) loginCmd.Flags().StringVarP(&authKey, "auth-key", "k", "", "Auth Key to use for login") authCmd.AddCommand(logoutCmd) authCmd.AddCommand(whoamiCmd) root.Cmd.AddCommand(authCmd) } type Flow int const ( AutoFlow Flow = iota Interactive DeviceAuth ) func DoLogin(flow Flow) (err error) { var fn func() (*conf.Config, error) switch flow { case Interactive: fn = login.Interactive case DeviceAuth: fn = login.DeviceAuth default: fn = login.DecideFlow } cfg, err := fn() if err != nil { return err } if err := conf.Write(cfg); err != nil { return fmt.Errorf("write credentials: %v", err) } fmt.Fprintln(os.Stdout, "Successfully logged in!") return nil } func DoLogout() { if err := conf.Logout(); err != nil { fmt.Fprintln(os.Stderr, "could not logout:", err) os.Exit(1) } // Stop running daemon to clear any cached credentials cmdutil.StopDaemon() fmt.Fprintln(os.Stdout, "encore: logged out.") } func DoLoginWithAuthKey() error { cfg, err := login.WithAuthKey(authKey) if err != nil { return err } if err := conf.Write(cfg); err != nil { return fmt.Errorf("write credentials: %v", err) } fmt.Fprintln(os.Stdout, "Successfully logged in!") return nil } func Whoami() { cfg, err := conf.CurrentUser() if err != nil { if errors.Is(err, os.ErrNotExist) { fmt.Fprint(os.Stdout, "not logged in.", cmdutil.Newline) return } cmdutil.Fatal(err) } if cfg.AppSlug != "" { fmt.Fprintf(os.Stdout, "logged in as app %s%s", cfg.AppSlug, cmdutil.Newline) } else { fmt.Fprintf(os.Stdout, "logged in as %s%s", cfg.Email, cmdutil.Newline) } } ================================================ FILE: cli/cmd/encore/bits/add.go ================================================ package bits import ( "context" "fmt" "os" "github.com/cockroachdb/errors" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/pkg/bits" ) var addCmd = &cobra.Command{ Use: "add []", Short: "Add an Encore Bit to your application", Args: cobra.MinimumNArgs(1), DisableFlagsInUseLine: true, Run: func(c *cobra.Command, args []string) { slug := args[0] ctx := context.Background() bit, err := bits.Get(ctx, slug) if errors.Is(err, errBitNotFound) { cmdutil.Fatalf("encore bit not found: %s", slug) } else if err != nil { cmdutil.Fatalf("could not lookup encore bit: %v", err) } workdir, err := os.MkdirTemp("", "encore-bit") if err != nil { cmdutil.Fatal(err) } defer os.RemoveAll(workdir) //prefix := args[0] //if len(args) > 1 { // prefix = args[1] //} fmt.Fprintf(os.Stderr, "Downloading Encore Bit: %s\n", bit.Title) if err := bits.Extract(ctx, bit, workdir); err != nil { cmdutil.Fatalf("download failed: %v", err) } meta, err := bits.Describe(ctx, workdir) if err != nil { cmdutil.Fatalf("could not parse bit metadata: %v", err) } fmt.Fprintf(os.Stderr, "successfully got bit: %+v\n", meta) //fmt.Fprintf(os.Stderr, "\n\nSuccessfully added Encore Bit: %s!\n", bit.Title) //fmt.Fprintf(os.Stderr, "You can find the new bit under the %s/ directory.\n", prefix) }, } func init() { bitsCmd.AddCommand(addCmd) } ================================================ FILE: cli/cmd/encore/bits/api.go ================================================ package bits import ( "context" "encoding/json" "io" "net/http" "net/url" "github.com/cockroachdb/errors" ) type Bit struct { ID int64 Slug string Title string Description string GitRepo string GitBranch string } type ListResponse struct { Bits []*Bit } func List(ctx context.Context) ([]*Bit, error) { resp, err := http.Get("https://automativity.encore.dev/bits") if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { slurp, _ := io.ReadAll(resp.Body) return nil, errors.Newf("got status %d: %s", resp.StatusCode, slurp) } var data ListResponse if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, errors.Wrap(err, "decode json response") } return data.Bits, nil } var errBitNotFound = errors.New("bit not found") func Get(ctx context.Context, slug string) (*Bit, error) { resp, err := http.Get("https://automativity.encore.dev/bits/" + url.PathEscape(slug)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == 404 { return nil, errBitNotFound } else if resp.StatusCode != 200 { slurp, _ := io.ReadAll(resp.Body) return nil, errors.Newf("got status %d: %s", resp.StatusCode, slurp) } var bit Bit if err := json.NewDecoder(resp.Body).Decode(&bit); err != nil { return nil, errors.Wrap(err, "decode json response") } return &bit, nil } ================================================ FILE: cli/cmd/encore/bits/bits.go ================================================ package bits import ( "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/root" ) var bitsCmd = &cobra.Command{ Use: "bits", Short: "Commands to manage encore bits, reusable functionality for Encore applications", } func init() { root.Cmd.AddCommand(bitsCmd) } ================================================ FILE: cli/cmd/encore/bits/list.go ================================================ package bits import ( "context" "fmt" "os" "text/tabwriter" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/pkg/bits" ) var listCmd = &cobra.Command{ Use: "list", Short: "Lists available Encore Bits to add to your application", Args: cobra.ExactArgs(0), Run: func(c *cobra.Command, args []string) { bits, err := bits.List(context.Background()) if err != nil { cmdutil.Fatalf("could not list encore bits: %v", err) } tw := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) fmt.Fprintln(tw, "ID\tTitle\tDescription") for _, bit := range bits { fmt.Fprintf(tw, "%s\t%s\t%s\n", bit.Slug, bit.Title, bit.Description) fmt.Fprintln(tw) } tw.Flush() }, } func init() { bitsCmd.AddCommand(listCmd) } ================================================ FILE: cli/cmd/encore/build.go ================================================ package main import ( "context" "fmt" "os" "os/signal" "path/filepath" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/pkg/appfile" daemonpb "encr.dev/proto/encore/daemon" ) var ( targetOS = cmdutil.Oneof{ Value: "linux", Allowed: []string{"linux"}, Flag: "os", Desc: "the target operating system", } targetArch = cmdutil.Oneof{ Value: "amd64", Allowed: []string{"amd64", "arm64"}, Flag: "arch", Desc: "the target architecture", } ) func init() { buildCmd := &cobra.Command{ Use: "build", Aliases: []string{"eject"}, Short: "build provides ways to build your application for deployment", } p := buildParams{ CgoEnabled: os.Getenv("CGO_ENABLED") == "1", } dockerBuildCmd := &cobra.Command{ Use: "docker IMAGE_TAG", Short: "docker builds a portable docker image of your Encore application", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { p.Goarch = targetArch.Value p.Goos = targetOS.Value p.AppRoot, _ = determineAppRoot() p.WorkspaceRoot = determineWorkspaceRoot(p.AppRoot) file, err := appfile.ParseFile(filepath.Join(p.AppRoot, appfile.Name)) if err == nil { if !cmd.Flag("base").Changed && file.Lang == appfile.LangTS { p.BaseImg = "node:slim" } if !cmd.Flag("cgo").Changed { p.CgoEnabled = file.Build.CgoEnabled } } p.ImageTag = args[0] dockerBuild(p) }, } dockerBuildCmd.Flags().BoolVarP(&p.Push, "push", "p", false, "push image to remote repository") dockerBuildCmd.Flags().StringVar(&p.BaseImg, "base", "scratch", "base image to build from") dockerBuildCmd.Flags().BoolVar(&p.CgoEnabled, "cgo", false, "enable cgo") dockerBuildCmd.Flags().BoolVar(&p.SkipInfraConf, "skip-config", false, "do not read or generate a infra configuration file") dockerBuildCmd.Flags().StringVar(&p.InfraConfPath, "config", "", "infra configuration file path") p.Services = dockerBuildCmd.Flags().StringSlice("services", nil, "services to include in the image") p.Gateways = dockerBuildCmd.Flags().StringSlice("gateways", nil, "gateways to include in the image") targetOS.AddFlag(dockerBuildCmd) targetArch.AddFlag(dockerBuildCmd) rootCmd.AddCommand(buildCmd) buildCmd.AddCommand(dockerBuildCmd) } type buildParams struct { AppRoot string WorkspaceRoot string ImageTag string Push bool BaseImg string Goos string Goarch string CgoEnabled bool SkipInfraConf bool InfraConfPath string Services *[]string Gateways *[]string } func dockerBuild(p buildParams) { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-interrupt cancel() }() daemon := setupDaemon(ctx) params := &daemonpb.DockerExportParams{ BaseImageTag: p.BaseImg, } if p.Push { params.PushDestinationTag = p.ImageTag } else { params.LocalDaemonTag = p.ImageTag } var services, gateways []string if p.Services != nil { services = *p.Services } if p.Gateways != nil { gateways = *p.Gateways } var err error cfgPath := "" if p.InfraConfPath != "" { cfgPath, err = filepath.Abs(p.InfraConfPath) if err != nil { cmdutil.Fatalf("failed to resolve absolute path for %s: %v", p.InfraConfPath, err) } } stream, err := daemon.Export(ctx, &daemonpb.ExportRequest{ AppRoot: p.AppRoot, WorkspaceRoot: p.WorkspaceRoot, CgoEnabled: p.CgoEnabled, Goos: p.Goos, Goarch: p.Goarch, Environ: os.Environ(), Format: &daemonpb.ExportRequest_Docker{ Docker: params, }, InfraConfPath: cfgPath, Services: services, Gateways: gateways, SkipInfraConf: p.SkipInfraConf, }) if err != nil { fmt.Fprintln(os.Stderr, "fatal: ", err) os.Exit(1) } if code := cmdutil.StreamCommandOutput(stream, cmdutil.ConvertJSONLogs()); code != 0 { os.Exit(code) } } func or(a, b string) string { if a != "" { return a } return b } ================================================ FILE: cli/cmd/encore/check.go ================================================ package main import ( "context" "fmt" "os" "os/signal" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" daemonpb "encr.dev/proto/encore/daemon" ) var ( codegenDebug bool checkParseTests bool ) var checkCmd = &cobra.Command{ Use: "check", Short: "Checks your application for compile-time errors using Encore's compiler.", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { appRoot, relPath := determineAppRoot() runChecks(appRoot, relPath) }, } func init() { rootCmd.AddCommand(checkCmd) checkCmd.Flags().BoolVar(&codegenDebug, "codegen-debug", false, "Dump generated code (for debugging Encore's code generation)") checkCmd.Flags().BoolVar(&checkParseTests, "tests", false, "Parse tests as well") } func runChecks(appRoot, relPath string) { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-interrupt cancel() }() daemon := setupDaemon(ctx) stream, err := daemon.Check(ctx, &daemonpb.CheckRequest{ AppRoot: appRoot, WorkingDir: relPath, CodegenDebug: codegenDebug, ParseTests: checkParseTests, Environ: os.Environ(), }) if err != nil { fmt.Fprintln(os.Stderr, "fatal: ", err) os.Exit(1) } os.Exit(cmdutil.StreamCommandOutput(stream, nil)) } ================================================ FILE: cli/cmd/encore/cmdutil/autocompletes.go ================================================ package cmdutil import ( "fmt" "strings" "github.com/spf13/cobra" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" ) func AutoCompleteFromStaticList(args ...string) func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, _ []string, toComplete string) (rtn []string, dir cobra.ShellCompDirective) { toComplete = strings.ToLower(toComplete) for _, option := range args { before, _, _ := strings.Cut(option, "\t") if strings.HasPrefix(before, toComplete) { rtn = append(rtn, option) } } return rtn, cobra.ShellCompDirectiveNoFileComp } } func AutoCompleteAppSlug(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { // incase of not being logged in or an error, we give no auto competition _, err := conf.CurrentUser() if err != nil { return nil, cobra.ShellCompDirectiveError } apps, err := platform.ListApps(cmd.Context()) if err != nil { return nil, cobra.ShellCompDirectiveError } toComplete = strings.ToLower(toComplete) rtn := make([]string, 0, len(apps)) for _, app := range apps { if strings.HasPrefix(strings.ToLower(app.Slug), toComplete) { desc := app.Description if desc == "" { desc = app.Name } rtn = append(rtn, fmt.Sprintf("%s\t%s", app.Slug, desc)) } } return rtn, cobra.ShellCompDirectiveNoFileComp } func AutoCompleteEnvSlug(cmd *cobra.Command, args []string, toComplete string) (rtn []string, dir cobra.ShellCompDirective) { toComplete = strings.ToLower(toComplete) // Support the local environment if strings.HasPrefix("local", toComplete) { rtn = append(rtn, "local\tThis local development environment") } _, err := conf.CurrentUser() if err != nil { return rtn, cobra.ShellCompDirectiveError } // Assume the app slug is the first argument appSlug := args[len(args)-1] // Get the environments for the app and filter by what the user has already entered envs, err := platform.ListEnvs(cmd.Context(), appSlug) if err != nil { return rtn, cobra.ShellCompDirectiveError } for _, env := range envs { if strings.HasPrefix(strings.ToLower(env.Slug), toComplete) { rtn = append(rtn, fmt.Sprintf("%s\tA %s enviroment running on %s", env.Slug, env.Type, env.Cloud)) } } return rtn, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cli/cmd/encore/cmdutil/cmdutil.go ================================================ package cmdutil import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "runtime" "github.com/fatih/color" "golang.org/x/crypto/ssh/terminal" "google.golang.org/grpc/status" "encr.dev/cli/internal/manifest" "encr.dev/pkg/appfile" "encr.dev/pkg/errinsrc" "encr.dev/pkg/errlist" ) var ( ErrNoEncoreApp = errors.New("no encore.app found in directory (or any of the parent directories)") ErrEncoreAppIsDir = errors.New("encore.app is a directory, not a file") ) // MaybeAppRoot determines the app root by looking for the "encore.app" file, // initially in the current directory and then recursively in parent directories // up to the filesystem root. // // It reports the absolute path to the app root, and the // relative path from the app root to the working directory. func MaybeAppRoot() (appRoot, relPath string, err error) { dir, err := os.Getwd() if err != nil { return "", "", err } return FindAppRootFromDir(dir) } func FindAppRootFromDir(dir string) (appRoot, relPath string, err error) { rel := "." for { path := filepath.Join(dir, "encore.app") fi, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { dir2 := filepath.Dir(dir) if dir2 == dir { return "", "", ErrNoEncoreApp } rel = filepath.Join(filepath.Base(dir), rel) dir = dir2 continue } else if err != nil { return "", "", err } else if fi.IsDir() { return "", "", ErrEncoreAppIsDir } else { return dir, rel, nil } } } // AppRoot is like MaybeAppRoot but instead of returning an error // it prints it to stderr and exits. func AppRoot() (appRoot, relPath string) { appRoot, relPath, err := MaybeAppRoot() if err != nil { Fatal(err) } return appRoot, relPath } // WorkspaceRoot determines the workspace root by looking for the .git folder in app root or parents to it. // It reports the absolute path to the workspace root. func WorkspaceRoot(appRoot string) string { dir := appRoot for { path := filepath.Join(dir, ".git") fi, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { dir2 := filepath.Dir(dir) if dir2 == dir { return appRoot } dir = dir2 continue } else if err != nil { Fatal(err) } else if !fi.IsDir() { continue } else { return dir } } } func AppSlugOrLocalID() string { appRoot, _ := AppRoot() appID, _ := appfile.Slug(appRoot) if appID == "" { mf, err := manifest.ReadOrCreate(appRoot) if err != nil { Fatalf("failed to read app manifest: %v", err) } appID = mf.LocalID } return appID } // AppSlug reports the current app's app slug. // It throws a fatal error if the app is not connected with the Encore Platform. func AppSlug() string { appRoot, _ := AppRoot() appSlug, err := appfile.Slug(appRoot) if err != nil { Fatal(err) } else if appSlug == "" { Fatal("app is not linked with the Encore Platform (see 'encore app link')") } return appSlug } func Fatal(args ...any) { // Prettify gRPC errors for i, arg := range args { if err, ok := arg.(error); ok { if s, ok := status.FromError(err); ok { args[i] = s.Message() } } } red := color.New(color.FgRed) _, _ = red.Fprint(os.Stderr, "error: ") _, _ = red.Fprintln(os.Stderr, args...) os.Exit(1) } func Fatalf(format string, args ...any) { // Prettify gRPC errors for i, arg := range args { if err, ok := arg.(error); ok { if s, ok := status.FromError(err); ok { args[i] = s.Message() } } } Fatal(fmt.Sprintf(format, args...)) } func DisplayError(out *os.File, err []byte) { if len(err) == 0 { return } // Get the width of the terminal we're rendering in // if we can so we render using the most space possible. width, _, sizeErr := terminal.GetSize(int(out.Fd())) if sizeErr == nil { errinsrc.TerminalWidth = width } // Unmarshal the error into a structured errlist errList := errlist.New(nil) if err := json.Unmarshal(err, &errList); err != nil { Fatalf("unable to parse error: %v", err) } if errList.Len() == 0 { return } _, _ = os.Stderr.Write([]byte(errList.Error())) } var Newline string func init() { switch runtime.GOOS { case "windows": Newline = "\r\n" default: Newline = "\n" } } ================================================ FILE: cli/cmd/encore/cmdutil/daemon.go ================================================ package cmdutil import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "time" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/internal/version" "encr.dev/pkg/xos" daemonpb "encr.dev/proto/encore/daemon" ) func IsDaemonRunning(ctx context.Context) bool { socketPath, err := daemonSockPath() if err != nil { return false } if _, err := xos.SocketStat(socketPath); err == nil { // The socket exists; check that it is responsive. if cc, err := dialDaemon(ctx, socketPath); err == nil { _ = cc.Close() return true } // socket is not responding, remove it _ = os.Remove(socketPath) } return false } // ConnectDaemon returns a client connection to the Encore daemon. // By default, it will start the daemon if it is not already running. func ConnectDaemon(ctx context.Context) daemonpb.DaemonClient { socketPath, err := daemonSockPath() if err != nil { fmt.Fprintln(os.Stderr, "fatal: ", err) os.Exit(1) } if _, err := xos.SocketStat(socketPath); err == nil { // The socket exists; check that it is responsive. if cc, err := dialDaemon(ctx, socketPath); err == nil { // Make sure the daemon is running an up-to-date version; // restart it otherwise. cl := daemonpb.NewDaemonClient(cc) if resp, err := cl.Version(ctx, &empty.Empty{}); err == nil { diff := version.Compare(resp.Version) switch { case diff < 0: // Daemon is running a newer version return cl case diff == 0: if configHash, err := version.ConfigHash(); err != nil { Fatal("unable to get config path: ", err) } else if configHash == resp.ConfigHash { return cl } // If we're running a development release, and so is the daemon, don't restart. // This is to avoid spurious restarts during development. if version.Channel == version.DevBuild && version.ChannelFor(resp.Version) == version.DevBuild { return cl } // Daemon is running the same version but different config fmt.Fprintf(os.Stderr, "encore: restarting daemon due to configuration change.\n") case diff > 0: fmt.Fprintf(os.Stderr, "encore: daemon is running an outdated version (%s), restarting.\n", resp.Version) } } } // Remove the socket file which triggers the daemon to exit. _ = os.Remove(socketPath) } // Start the daemon. if err := StartDaemonInBackground(ctx); err != nil { Fatal("starting daemon: ", err) } cc, err := dialDaemon(ctx, socketPath) if err != nil { Fatal("dialing daemon: ", err) } return daemonpb.NewDaemonClient(cc) } func StopDaemon() { socketPath, err := daemonSockPath() if err != nil { Fatal("stopping daemon: ", err) } if _, err := xos.SocketStat(socketPath); err == nil { _ = os.Remove(socketPath) } } // daemonSockPath reports the path to the Encore daemon unix socket. func daemonSockPath() (string, error) { cacheDir, err := os.UserCacheDir() if err != nil { return "", fmt.Errorf("could not determine cache dir: %v", err) } return filepath.Join(cacheDir, "encore", "encored.sock"), nil } // StartDaemonInBackground starts the Encore daemon in the background. func StartDaemonInBackground(ctx context.Context) error { socketPath, err := daemonSockPath() if err != nil { return err } // nosemgrep exe, err := os.Executable() if err != nil { exe, err = exec.LookPath("encore") } if err != nil { return fmt.Errorf("could not determine location of encore executable: %v", err) } // nosemgrep cmd := exec.Command(exe, "daemon", "-f") cmd.SysProcAttr = xos.CreateNewProcessGroup() if err := cmd.Start(); err != nil { return fmt.Errorf("could not start encore daemon: %v", err) } // Wait for it to come up for i := 0; i < 50; i++ { if err := ctx.Err(); err != nil { return err } time.Sleep(100 * time.Millisecond) if _, err := xos.SocketStat(socketPath); err == nil { return nil } } return fmt.Errorf("timed out waiting for daemon to start") } func dialDaemon(ctx context.Context, socketPath string) (*grpc.ClientConn, error) { ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) defer cancel() dialer := func(ctx context.Context, addr string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", socketPath) } // Set max message size to 16mb (up from default 4mb) for json formatted debug metadata for large applications. return grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithBlock(), grpc.WithUnaryInterceptor(errInterceptor), grpc.WithContextDialer(dialer), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16*1024*1024)), ) } func errInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { err := invoker(ctx, method, req, reply, cc, opts...) if err != nil { if st, ok := status.FromError(err); ok { if st.Code() == codes.Unauthenticated { Fatal("not logged in: run 'encore auth login' first") } for _, detail := range st.Details() { switch t := detail.(type) { case *errdetails.PreconditionFailure: for _, violation := range t.Violations { if violation.Type == "INVALID_REFRESH_TOKEN" { Fatal("OAuth refresh token was invalid. Please run `encore auth login` again.") } } } } } } return err } ================================================ FILE: cli/cmd/encore/cmdutil/forms.go ================================================ package cmdutil import ( "strings" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) const ( CodeBlue = "#6D89FF" CodePurple = "#A36C8C" CodeGreen = "#B3D77E" ValidationFail = "#CB1010" ) var ( InputStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: CodeBlue, Light: CodeBlue}) DescStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: CodeGreen, Light: CodePurple}) DocStyle = lipgloss.NewStyle().Padding(0, 2, 0, 2) ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(ValidationFail)) SuccessStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00C200")) ) type SelectedID[T any] interface { SelectedID() T } type Selectable interface { comparable SelectPrompt() string } type SimpleSelectDone[T any] struct { Selected T } type SimpleSelectModel[T Selectable, S SelectedID[T]] struct { Predefined T List list.Model } func (m SimpleSelectModel[T, S]) Selected() T { var empty T if m.Predefined != empty { return m.Predefined } sel := m.List.SelectedItem() if sel == nil { return empty } return sel.(S).SelectedID() } func (m SimpleSelectModel[T, I]) Update(msg tea.Msg) (SimpleSelectModel[T, I], tea.Cmd) { var c tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: // Have we selected an item? if idx := m.List.Index(); idx >= 0 { return m, func() tea.Msg { return SimpleSelectDone[T]{ Selected: m.Selected(), } } } } } m.List, c = m.List.Update(msg) return m, c } func (m *SimpleSelectModel[T, I]) SetSize(width, height int) { m.List.SetWidth(width) m.List.SetHeight(max(height-1, 0)) } func (m SimpleSelectModel[T, I]) View() string { var b strings.Builder // Get the prompt from the type T var zero T prompt := zero.SelectPrompt() b.WriteString(InputStyle.Render(prompt)) b.WriteString(DescStyle.Render(" [Use arrows to move]")) b.WriteString("\n") b.WriteString(m.List.View()) return b.String() } ================================================ FILE: cli/cmd/encore/cmdutil/language.go ================================================ package cmdutil type Language string const ( LanguageGo Language = "go" LanguageTS Language = "ts" ) var AllLanguages = []Language{ LanguageGo, LanguageTS, } func LanguageFlagValues() []string { result := make([]string, 0, len(AllLanguages)) for _, r := range AllLanguages { result = append(result, string(r)) } return result } func (lang Language) Display() string { switch lang { case LanguageGo: return "Go" case LanguageTS: return "TypeScript" default: return string(lang) } } func (lang Language) SelectPrompt() string { return "Select language for your application" } ================================================ FILE: cli/cmd/encore/cmdutil/output.go ================================================ package cmdutil import ( "errors" "slices" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" ) type Oneof struct { Value string Allowed []string Flag string // defaults to "output" if empty FlagShort string // defaults to "o" if both Flag and FlagShort are empty Desc string // usage desc TypeDesc string // type description, defaults to the name of the flag NoOptDefVal string // default value when no option is provided } func (o *Oneof) AddFlag(cmd *cobra.Command) { name, short := o.FlagName() cmd.Flags().AddFlag( &pflag.Flag{ Name: name, NoOptDefVal: o.NoOptDefVal, Shorthand: short, Usage: o.Usage(), Value: o, DefValue: o.String(), }) _ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return o.Allowed, cobra.ShellCompDirectiveNoFileComp }) } func (o *Oneof) FlagName() (name, short string) { name, short = o.Flag, o.FlagShort if name == "" { name, short = "output", "o" } return name, short } func (o *Oneof) String() string { return o.Value } func (o *Oneof) Type() string { if o.TypeDesc != "" { return o.TypeDesc } name, _ := o.FlagName() return name } func (o *Oneof) Set(v string) error { if slices.Contains(o.Allowed, v) { o.Value = v return nil } var b strings.Builder b.WriteString("must be one of ") o.oneOf(&b) return errors.New(b.String()) } func (o *Oneof) Usage() string { var b strings.Builder desc := o.Desc if desc == "" { desc = "Output format" } b.WriteString(desc + ". One of (") o.oneOf(&b) b.WriteString(").") return b.String() } // Alternatives lists the alternatives in the format "a|b|c". func (o *Oneof) Alternatives() string { var b strings.Builder for i, s := range o.Allowed { if i > 0 { b.WriteByte('|') } b.WriteString(s) } return b.String() } func (o *Oneof) oneOf(b *strings.Builder) { n := len(o.Allowed) for i, s := range o.Allowed { if i > 0 { switch { case n == 2: b.WriteString(" or ") case i == n-1: b.WriteString(", or ") default: b.WriteString(", ") } } b.WriteString(strconv.Quote(s)) } } ================================================ FILE: cli/cmd/encore/cmdutil/stream.go ================================================ package cmdutil import ( "bufio" "bytes" "encoding/json" "fmt" "io" "os" "strings" "sync" "github.com/logrusorgru/aurora/v3" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/crypto/ssh/terminal" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/pkg/ansi" "encr.dev/proto/encore/daemon" ) // CommandOutputStream is the interface for gRPC streams that // stream the output of a command. type CommandOutputStream interface { Recv() (*daemon.CommandMessage, error) } type OutputConverter func(line []byte) []byte // StreamCommandOutput streams the output from the given command stream, // and reports the command's exit code. // If convertJSON is true, lines that look like JSON are fed through // zerolog's console writer. func StreamCommandOutput(stream CommandOutputStream, converter OutputConverter) int { var outWrite io.Writer = os.Stdout var errWrite io.Writer = os.Stderr var writesDone sync.WaitGroup defer writesDone.Wait() if converter != nil { // Create a pipe that we read from line-by-line so we can detect JSON lines. outRead, outw := io.Pipe() errRead, errw := io.Pipe() outWrite = outw errWrite = errw defer func() { _ = outw.Close() }() defer func() { _ = errw.Close() }() for i, read := range []io.Reader{outRead, errRead} { read := read stdout := i == 0 writesDone.Add(1) go func() { defer writesDone.Done() for { scanner := bufio.NewScanner(read) for scanner.Scan() { line := append(scanner.Bytes(), '\n') line = converter(line) if stdout { _, _ = os.Stdout.Write(line) } else { _, _ = os.Stderr.Write(line) } } if err := scanner.Err(); err != nil { // The scanner failed, likely due to a too-long line. Log an error // and create a new scanner since the old one is in an unrecoverable state. fmt.Fprintln(os.Stderr, "failed to read output:", err) scanner = bufio.NewScanner(read) continue } else { break } } }() } } for { msg, err := stream.Recv() if err != nil { st := status.Convert(err) switch { case st.Code() == codes.FailedPrecondition: _, _ = fmt.Fprintln(os.Stderr, st.Message()) return 1 case err == io.EOF || st.Code() == codes.Canceled || strings.HasSuffix(err.Error(), "error reading from server: EOF"): return 0 default: log.Fatal().Err(err).Msg("connection failure") } } switch m := msg.Msg.(type) { case *daemon.CommandMessage_Output: if m.Output.Stdout != nil { _, _ = outWrite.Write(m.Output.Stdout) } if m.Output.Stderr != nil { _, _ = errWrite.Write(m.Output.Stderr) } case *daemon.CommandMessage_Errors: DisplayError(os.Stderr, m.Errors.Errinsrc) case *daemon.CommandMessage_Exit: return int(m.Exit.Code) } } } type ConvertLogOptions struct { Color bool } type ConvertLogOption func(*ConvertLogOptions) func Colorize(enable bool) ConvertLogOption { return func(clo *ConvertLogOptions) { clo.Color = enable } } func ConvertJSONLogs(opts ...ConvertLogOption) OutputConverter { // Default to colorized output. options := ConvertLogOptions{Color: true} for _, opt := range opts { opt(&options) } var logMutex sync.Mutex logLineBuffer := bytes.NewBuffer(make([]byte, 0, 1024)) cout := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { w.Out = logLineBuffer w.FieldsExclude = []string{"stack"} w.FormatExtra = func(vals map[string]any, buf *bytes.Buffer) error { if stack, ok := vals["stack"]; ok { return FormatStack(stack, buf) } return nil } }) if !options.Color { cout.NoColor = true } return func(line []byte) []byte { // If this isn't a JSON log line, just return it as-is if len(line) == 0 || line[0] != '{' { return line } // Otherwise grab the converter buffer and reset it logMutex.Lock() defer logMutex.Unlock() logLineBuffer.Reset() // Then convert the JSON log line to pretty formatted text _, err := cout.Write(line) if err != nil { return line } out := make([]byte, len(logLineBuffer.Bytes())) copy(out, logLineBuffer.Bytes()) return out } } func FormatStack(val any, buf *bytes.Buffer) error { var frames []struct { File string Line int Func string } if jsonRepr, err := json.Marshal(val); err != nil { return err } else if err := json.Unmarshal(jsonRepr, &frames); err != nil { return err } for _, f := range frames { fmt.Fprintf(buf, "\n %s\n %s", f.Func, aurora.Gray(12, fmt.Sprintf("%s:%d", f.File, f.Line))) } return nil } func ClearTerminalExceptFirstNLines(n int) { // Clear the screen except for the first line. if _, height, err := terminal.GetSize(int(os.Stdout.Fd())); err == nil { count := height - (1 + n) if count > 0 { _, _ = os.Stdout.Write(bytes.Repeat([]byte{'\n'}, count)) } _, _ = fmt.Fprint(os.Stdout, ansi.SetCursorPosition(2, 1)+ansi.ClearScreen(ansi.CursorToBottom)) } } ================================================ FILE: cli/cmd/encore/config/config.go ================================================ package config import ( "fmt" "os" "strings" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/internal/userconfig" "github.com/spf13/cobra" ) var ( forceApp, forceGlobal bool viewAllSettings bool ) var autoCompleteConfigKeys = cmdutil.AutoCompleteFromStaticList(userconfig.Keys()...) var longDocs = `Gets or sets configuration values for customizing the behavior of the Encore CLI. Configuration options can be set both for individual Encore applications, as well as globally for the local user. Configuration options can be set using ` + bt("encore config ") + `, and options can similarly be read using ` + bt("encore config ") + `. When running ` + bt("encore config") + ` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the ` + bt("--global") + ` flag. Available configuration settings are: ` + userconfig.CLIDocs() var configCmd = &cobra.Command{ Use: "config []", Short: "Get or set a configuration value", Long: longDocs, Args: cobra.RangeArgs(0, 2), Run: func(cmd *cobra.Command, args []string) { appRoot, _, _ := cmdutil.MaybeAppRoot() appScope := appRoot != "" if forceApp { appScope = true } else if forceGlobal { appScope = false } if appScope && appRoot == "" { // If the user specified --app, error if there is no app. cmdutil.Fatal(cmdutil.ErrNoEncoreApp) } if len(args) == 2 { var err error if appScope { err = userconfig.SetForApp(appRoot, args[0], args[1]) } else { err = userconfig.SetGlobal(args[0], args[1]) } if err != nil { cmdutil.Fatal(err) } } else { var ( cfg *userconfig.Config err error ) if appScope { appRoot, _ := cmdutil.AppRoot() cfg, err = userconfig.ForApp(appRoot).Get() } else { cfg, err = userconfig.Global().Get() } if err != nil { cmdutil.Fatal(err) } if viewAllSettings { if len(args) > 0 { cmdutil.Fatalf("cannot specify a settings key when using --all") } s := strings.TrimSuffix(cfg.Render(), "\n") fmt.Println(s) return } if len(args) == 0 { // No args are only allowed when --all is specified. _ = cmd.Usage() os.Exit(1) } val, ok := cfg.GetByKey(args[0]) if !ok { cmdutil.Fatalf("unknown key %q", args[0]) } fmt.Printf("%v\n", val) } }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // Completing the first argument, the config key return autoCompleteConfigKeys(cmd, args, toComplete) } return nil, cobra.ShellCompDirectiveNoFileComp }, } func init() { configCmd.Flags().BoolVar(&viewAllSettings, "all", false, "view all settings") configCmd.Flags().BoolVar(&forceApp, "app", false, "set the value for the current app") configCmd.Flags().BoolVar(&forceGlobal, "global", false, "set the value at the global level") configCmd.MarkFlagsMutuallyExclusive("app", "global") root.Cmd.AddCommand(configCmd) } // bt renders a backtick-enclosed string. func bt(val string) string { return fmt.Sprintf("`%s`", val) } ================================================ FILE: cli/cmd/encore/daemon/daemon.go ================================================ package daemon import ( "context" "database/sql" "embed" _ "embed" // for go:embed "fmt" "io" "io/fs" "net" "net/http" "net/http/pprof" "net/netip" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/cenkalti/backoff/v4" "github.com/cockroachdb/errors" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database" "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/mattn/go-sqlite3" // for "sqlite3" driver "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/daemon" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/dash" "encr.dev/cli/daemon/engine" "encr.dev/cli/daemon/engine/trace2" "encr.dev/cli/daemon/engine/trace2/sqlite" "encr.dev/cli/daemon/mcp" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/objects" "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/secret" "encr.dev/cli/daemon/sqldb" "encr.dev/cli/daemon/sqldb/docker" "encr.dev/cli/daemon/sqldb/external" "encr.dev/internal/conf" "encr.dev/internal/env" "encr.dev/pkg/eerror" "encr.dev/pkg/option" "encr.dev/pkg/watcher" "encr.dev/pkg/xos" daemonpb "encr.dev/proto/encore/daemon" ) // Main runs the daemon. func Main() { watcher.BumpRLimitSoftToHardLimit() if err := redirectLogOutput(); err != nil { log.Error().Err(err).Msg("could not setup daemon log file, skipping") } if err := runMain(); err != nil { log.Fatal().Err(err).Msg("daemon failed") } } func runMain() (err error) { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT) defer cancel() // exit receives signals from the different subsystems // that something went wrong and it's time to exit. // Sending nil indicates it's time to gracefully exit. exit := make(chan error) d := &Daemon{dev: conf.DevDaemon, exit: exit} defer handleBailout(&err) defer d.closeAll() d.init(ctx) d.serve() select { case err := <-exit: return err case <-ctx.Done(): return nil } } // Daemon orchestrates setting up the different daemon subsystems. type Daemon struct { Daemon *net.UnixListener Runtime *retryingTCPListener DBProxy *retryingTCPListener Dash *retryingTCPListener Debug *retryingTCPListener ObjectStorage *retryingTCPListener MCP *retryingTCPListener EncoreDB *sql.DB Apps *apps.Manager Secret *secret.Manager RunMgr *run.Manager NS *namespace.Manager ClusterMgr *sqldb.ClusterManager ObjectsMgr *objects.ClusterManager MCPMgr *mcp.Manager PublicBuckets *objects.PublicBucketServer Trace trace2.Store Server *daemon.Server dev bool // whether we're in development mode // exit is a channel that shuts down the daemon when sent on. // A nil error indicates graceful exit. exit chan<- error // close are the things to close when exiting. close []io.Closer } func (d *Daemon) init(ctx context.Context) { d.Daemon = d.listenDaemonSocket() d.Dash = d.listenTCPRetry("dashboard", env.EncoreDevDashListenAddr(), 9400) d.DBProxy = d.listenTCPRetry("dbproxy", option.None[string](), 9500) d.Runtime = d.listenTCPRetry("runtime", option.None[string](), 9600) d.Debug = d.listenTCPRetry("debug", option.None[string](), 9700) d.ObjectStorage = d.listenTCPRetry("objectstorage", env.EncoreObjectStorageListAddr(), 9800) d.MCP = d.listenTCPRetry("mcp", env.EncoreMCPSSEListenAddr(), 9900) d.EncoreDB = d.openDB() d.Apps = apps.NewManager(d.EncoreDB) d.close = append(d.close, d.Apps) // If ENCORE_SQLDB_HOST is set, use the external cluster instead of // creating our own docker container cluster. var sqldbDriver sqldb.Driver = &docker.Driver{} if host := os.Getenv("ENCORE_SQLDB_HOST"); host != "" { sqldbDriver = &external.Driver{ Host: host, Database: os.Getenv("ENCORE_SQLDB_DATABASE"), SuperuserUsername: os.Getenv("ENCORE_SQLDB_USER"), SuperuserPassword: os.Getenv("ENCORE_SQLDB_PASSWORD"), } log.Info().Msgf("using external postgres cluster: %s", host) } d.NS = namespace.NewManager(d.EncoreDB) d.Secret = secret.New() d.ClusterMgr = sqldb.NewClusterManager(sqldbDriver, d.Apps, d.NS, d.Secret) d.ObjectsMgr = objects.NewClusterManager(d.NS) d.PublicBuckets = objects.NewPublicBucketServer("http://"+d.ObjectStorage.ClientAddr(), d.ObjectsMgr.PersistentStoreFallback) traceStore := sqlite.New(d.EncoreDB) go traceStore.CleanEvery(ctx, 1*time.Minute, 500, 100, 10000) d.Trace = traceStore d.RunMgr = &run.Manager{ RuntimePort: d.Runtime.Port(), DBProxyPort: d.DBProxy.Port(), DashBaseURL: fmt.Sprintf("http://%s", d.Dash.ClientAddr()), Secret: d.Secret, ClusterMgr: d.ClusterMgr, ObjectsMgr: d.ObjectsMgr, PublicBuckets: d.PublicBuckets, } d.MCPMgr = mcp.NewManager( d.Apps, d.ClusterMgr, d.NS, d.Trace, d.RunMgr, fmt.Sprintf("http://%s", d.MCP.ClientAddr()), ) // Register namespace deletion handlers. d.NS.RegisterDeletionHandler(d.ClusterMgr) d.NS.RegisterDeletionHandler(d.RunMgr) d.NS.RegisterDeletionHandler(d.ObjectsMgr) d.Server = daemon.New(d.Apps, d.RunMgr, d.ClusterMgr, d.Secret, d.NS, d.MCPMgr) } func (d *Daemon) serve() { go d.serveDaemon() go d.serveRuntime() go d.serveDBProxy() go d.serveDash() go d.serveDebug() go d.serveObjects() go d.serveMCP() } // listenDaemonSocket listens on the encored.sock UNIX socket // and arranges to exit when the socket is closed. func (d *Daemon) listenDaemonSocket() *net.UnixListener { userCacheDir, err := os.UserCacheDir() if err != nil { fatal(err) } socketPath := filepath.Join(userCacheDir, "encore", "encored.sock") if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil { fatal(err) } // If the daemon socket already exists, remove it so we can take over listening. if _, err := xos.SocketStat(socketPath); err == nil { _ = os.Remove(socketPath) } ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) if err != nil { fatal(err) } d.closeOnExit(ln) // Detect when the socket is closed. go func() { d.exit <- detectSocketClose(ln, socketPath) }() return ln } func failedPreconditionError(msg, typ, desc string) error { st, err := status.New(codes.FailedPrecondition, msg).WithDetails( &errdetails.PreconditionFailure{ Violations: []*errdetails.PreconditionFailure_Violation{ { Type: typ, Description: desc, }, }, }, ) if err != nil { panic(err) } return st.Err() } func ErrInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { resp, err = handler(ctx, req) if errors.Is(err, conf.ErrInvalidRefreshToken) { return nil, failedPreconditionError("invalid refresh token", "INVALID_REFRESH_TOKEN", "invalid refresh token") } else if errors.Is(err, conf.ErrNotLoggedIn) { return nil, status.Error(codes.Unauthenticated, "not logged in") } return resp, err } func (d *Daemon) serveDaemon() { log.Info().Stringer("addr", d.Daemon.Addr()).Msg("serving daemon") srv := grpc.NewServer(grpc.UnaryInterceptor(ErrInterceptor)) daemonpb.RegisterDaemonServer(srv, d.Server) d.exit <- srv.Serve(d.Daemon) } func (d *Daemon) serveRuntime() { log.Info().Stringer("addr", d.Runtime.Addr()).Msg("serving runtime") rec := trace2.NewRecorder(d.Trace) srv := engine.NewServer(d.RunMgr, rec) d.exit <- http.Serve(d.Runtime, srv) } func (d *Daemon) serveDBProxy() { log.Info().Stringer("addr", d.DBProxy.Addr()).Msg("serving dbproxy") d.exit <- d.ClusterMgr.ServeProxy(d.DBProxy) } func (d *Daemon) serveMCP() { log.Info().Stringer("addr", d.MCP.Addr()).Msg("serving mcp") d.exit <- d.MCPMgr.Serve(d.MCP) } func (d *Daemon) serveObjects() { log.Info().Stringer("addr", d.ObjectStorage.Addr()).Msg("serving object storage") d.exit <- d.PublicBuckets.Serve(d.ObjectStorage) } func (d *Daemon) serveDash() { log.Info().Stringer("addr", d.Dash.Addr()).Msg("serving dash") srv := dash.NewServer(d.Apps, d.RunMgr, d.NS, d.Trace, d.Dash.Port()) d.exit <- http.Serve(d.Dash, srv) } func (d *Daemon) serveDebug() { log.Info().Stringer("addr", d.Debug.Addr()).Msg("serving debug") mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) d.exit <- http.Serve(d.Debug, mux) } // listenTCPRetry listens for TCP connections on the given port, retrying // in the background if it's already in use. func (d *Daemon) listenTCPRetry(component string, addrOverride option.Option[string], defaultPort uint16) *retryingTCPListener { addr, err := parseInterface(addrOverride.GetOrElse("127.0.0.1:0")) if err != nil { log.Fatal().Str("component", component).Err(err).Msg("failed to parse interface") } if addr.Port() == 0 { addr = netip.AddrPortFrom(addr.Addr(), defaultPort) } ln := listenLocalhostTCP(component, addr) d.closeOnExit(ln) return ln } func (d *Daemon) openDB() *sql.DB { dir, err := conf.Dir() if err != nil { fatal(err) } else if err := os.MkdirAll(dir, 0755); err != nil { fatal(err) } dbPath := filepath.Join(dir, "encore.db") // Create the database file if it doesn't exist, as // we've observed some failures to open the database file when it doesn't already exist. if _, err := os.Stat(dbPath); os.IsNotExist(err) { if f, err := os.OpenFile(dbPath, os.O_CREATE|os.O_WRONLY, 0600); err == nil { _ = f.Close() } } db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_journal=wal&_txlock=immediate", dbPath)) if err != nil { fatal(err) } // Initialize db schema if err := d.runDBMigrations(db); err != nil { fatalf("unable to migrate management database: %v", err) } d.closeOnExit(db) return db } //go:embed migrations var dbMigrations embed.FS func (d *Daemon) runDBMigrations(db *sql.DB) error { { // Convert old-style schema definition to golang-migrate, if necessary. var isLegacy bool err := db.QueryRow(` SELECT COUNT(*) > 0 FROM pragma_table_info('schema_migrations') WHERE name = 'dummy' `).Scan(&isLegacy) if err != nil { return err } else if isLegacy { _, _ = db.Exec("DROP TABLE schema_migrations;") } } src, err := iofs.New(dbMigrations, "migrations") if err != nil { return fmt.Errorf("read db migrations: %v", err) } instance, err := sqlite3.WithInstance(db, &sqlite3.Config{}) if err != nil { return fmt.Errorf("initialize migration instance: %v", err) } m, err := migrate.NewWithInstance("iofs", src, "encore", instance) if err != nil { return fmt.Errorf("setup migrate instance: %v", err) } err = m.Up() if errors.Is(err, migrate.ErrNoChange) { return nil } // If we have a dirty migration, reset the dirty flag and try again. // This is safe since all migrations run inside transactions. var dirty migrate.ErrDirty if errors.As(err, &dirty) { // Find the version that preceded the dirty version so // we can force the migration to that version and then // re-apply the migration. var prevVer uint prevVer, err = src.Prev(uint(dirty.Version)) targetVer := int(prevVer) if errors.Is(err, fs.ErrNotExist) { // No previous migration exists targetVer = database.NilVersion } else if err != nil { return errors.Wrap(err, "failed to find previous version") } if err = m.Force(targetVer); err == nil { err = m.Up() } } return err } // detectSocketClose polls for the unix socket at socketPath to be removed // or changed to a different underlying inode. func detectSocketClose(ln *net.UnixListener, socketPath string) error { orig, err := xos.SocketStat(socketPath) if err != nil { return err } // When this function exits, the socket has been changed. // In that case, don't unlink the socket since it has already been changed. defer ln.SetUnlinkOnClose(false) // Sleep until the socket changes errs := 0 for { time.Sleep(200 * time.Millisecond) fi, err := xos.SocketStat(socketPath) if errors.Is(err, fs.ErrNotExist) { // Socket was removed; don't remove it again return nil } else if err != nil { errs++ if errs == 3 { return err } time.Sleep(1 * time.Second) continue } if !xos.SameSocket(orig, fi) { return nil } } } func (d *Daemon) closeOnExit(c io.Closer) { d.close = append(d.close, c) } func (d *Daemon) closeAll() { for _, c := range d.close { _ = c.Close() } } type bailout struct { err error } func fatal(err error) { panic(bailout{err}) } func fatalf(format string, args ...interface{}) { panic(bailout{fmt.Errorf(format, args...)}) } func handleBailout(err *error) { if e := recover(); e != nil { if b, ok := e.(bailout); ok { *err = b.err } else { panic(e) } } } // redirectLogOutput redirects the global logger to also write to a file. func redirectLogOutput() error { logPath := env.EncoreDaemonLogPath() if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { return err } f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err } log.Info().Msgf("writing output to %s", logPath) zerolog.TimeFieldFormat = time.RFC3339Nano consoleWriter := zerolog.ConsoleWriter{ Out: os.Stderr, FieldsExclude: []string{zerolog.ErrorStackFieldName}, } consoleWriter.FormatExtra = eerror.ZeroLogConsoleExtraFormatter consoleWriter.TimeFormat = time.TimeOnly zerolog.ErrorStackMarshaler = eerror.ZeroLogStackMarshaller log.Logger = log.With().Caller().Stack().Logger().Output(io.MultiWriter(consoleWriter, f)) return nil } // retryingTCPListener is a TCP listener that attempts multiple times // to listen on a given port. It is designed to handle race conditions // between multiple daemon processes handing off to each other // and the port still being in use momentarily. type retryingTCPListener struct { component string addr netip.AddrPort ctx context.Context cancel func() // call to cancel ctx // doneListening is closed when the underlying listener is open, // or it gave up due to an error. doneListening chan struct{} underlying net.Listener listenErr error } func listenLocalhostTCP(component string, addr netip.AddrPort) *retryingTCPListener { ctx, cancel := context.WithCancel(context.Background()) ln := &retryingTCPListener{ component: component, addr: addr, ctx: ctx, cancel: cancel, doneListening: make(chan struct{}), } go ln.listen() return ln } func (ln *retryingTCPListener) Accept() (net.Conn, error) { select { case <-ln.ctx.Done(): return nil, net.ErrClosed case <-ln.doneListening: if ln.listenErr != nil { return nil, ln.listenErr } return ln.underlying.Accept() } } func (ln *retryingTCPListener) Close() error { ln.cancel() select { case <-ln.doneListening: if ln.listenErr == nil { return ln.underlying.Close() } default: } return nil } func (ln *retryingTCPListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IP(ln.addr.Addr().AsSlice()), Port: int(ln.addr.Port())} } func (ln *retryingTCPListener) ClientAddr() string { // If our addr is 0.0.0.0 or the ipv6 equivalent, return 127.0.0.1 instead // so that clients can connect to us. if ln.addr.Addr().IsUnspecified() { if ln.addr.Addr().Is6() { return fmt.Sprintf("[::1]:%d", ln.addr.Port()) } return fmt.Sprintf("127.0.0.1:%d", ln.addr.Port()) } return ln.addr.String() } func (ln *retryingTCPListener) Port() int { return int(ln.addr.Port()) } func (ln *retryingTCPListener) listen() { defer close(ln.doneListening) logger := log.With().Str("component", ln.component).Int("port", ln.Port()).Logger() addr := ln.addr.String() b := backoff.NewExponentialBackOff() b.InitialInterval = 50 * time.Millisecond b.MaxInterval = 500 * time.Millisecond b.MaxElapsedTime = 5 * time.Second ln.listenErr = backoff.Retry(func() (err error) { if err := ln.ctx.Err(); err != nil { return backoff.Permanent(err) } ln.underlying, err = net.Listen("tcp", addr) if err != nil { logger.Error().Err(ln.listenErr).Msg("unable to listen, retrying") } return err }, b) if ln.listenErr != nil { logger.Error().Err(ln.listenErr).Msg("unable to listen, giving up") } else { logger.Info().Msg("listening on port") } } func parseInterface(s string) (netip.AddrPort, error) { addr, portStr, _, err := splitAddrPort(s) if err != nil { return netip.AddrPort{}, err } port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return netip.AddrPort{}, err } // Is addr a valid ip? If so we're done. if ip, err := netip.ParseAddr(addr); err == nil { return netip.AddrPortFrom(ip, uint16(port)), nil } // Otherwise perform name resolution. ips, err := net.LookupIP(addr) if err != nil { return netip.AddrPort{}, err } if len(ips) == 0 { return netip.AddrPort{}, fmt.Errorf("no IP addresses found for %s", addr) } // Prefer IPv4 addresses. for _, ip := range ips { if ip.To4() != nil { if addr, err := netip.ParseAddr(ip.String()); err == nil { return netip.AddrPortFrom(addr, uint16(port)), nil } } } if addr, err := netip.ParseAddr(ips[0].String()); err == nil { return netip.AddrPortFrom(addr, uint16(port)), nil } return netip.AddrPort{}, fmt.Errorf("unable to parse IP address %s", addr) } // splitAddrPort splits s into an IP address string and a port // string. It splits strings shaped like "foo:bar" or "[foo]:bar", // without further validating the substrings. v6 indicates whether the // ip string should parse as an IPv6 address or an IPv4 address, in // order for s to be a valid ip:port string. func splitAddrPort(s string) (ip, port string, v6 bool, err error) { i := strings.LastIndexByte(s, ':') if i == -1 { return "", "", false, errors.New("not an ip:port") } ip, port = s[:i], s[i+1:] if len(ip) == 0 { return "", "", false, errors.New("no IP") } if len(port) == 0 { return "", "", false, errors.New("no port") } if ip[0] == '[' { if len(ip) < 2 || ip[len(ip)-1] != ']' { return "", "", false, errors.New("missing ]") } ip = ip[1 : len(ip)-1] v6 = true } return ip, port, v6, nil } ================================================ FILE: cli/cmd/encore/daemon/migrations/1_initial_schema.up.sql ================================================ CREATE TABLE IF NOT EXISTS app ( root TEXT PRIMARY KEY, local_id TEXT NOT NULL, platform_id TEXT NULL, -- NULL if not linked updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS trace_event ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_id TEXT NOT NULL, -- platform_id or local_id trace_id TEXT NOT NULL, span_id TEXT NOT NULL, event_data TEXT NOT NULL -- json ); CREATE INDEX IF NOT EXISTS trace_event_span_key ON trace_event (trace_id, span_id); CREATE TABLE IF NOT EXISTS trace_span_index ( trace_id TEXT NOT NULL, span_id TEXT NOT NULL, app_id TEXT NOT NULL, -- platform_id or local_id span_type INTEGER NOT NULL, -- enum -- request fields started_at INTEGER NULL, -- unix nanosecond is_root BOOLEAN NULL, service_name TEXT NULL, endpoint_name TEXT NULL, topic_name TEXT NULL, subscription_name TEXT NULL, message_id TEXT NULL, external_request_id TEXT NULL, -- response fields has_response BOOLEAN NOT NULL, is_error BOOLEAN NULL, duration_nanos INTEGER NULL, user_id TEXT NULL, PRIMARY KEY (trace_id, span_id) ); ================================================ FILE: cli/cmd/encore/daemon/migrations/2_infra_namespaces.up.sql ================================================ CREATE TABLE IF NOT EXISTS namespace ( id TEXT PRIMARY KEY, -- uuid app_id TEXT NOT NULL, -- platform_id or local_id name TEXT NOT NULL, active BOOL NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL, last_active_at TIMESTAMP NULL, UNIQUE (app_id, name) ); -- Ensure there's a single active namespace per app. CREATE UNIQUE INDEX active_namespace ON namespace (app_id) WHERE active = true; ================================================ FILE: cli/cmd/encore/daemon/migrations/3_test_tracing.up.sql ================================================ ALTER TABLE trace_span_index ADD COLUMN test_skipped BOOLEAN NOT NULL DEFAULT FALSE; ALTER TABLE trace_span_index ADD COLUMN src_file TEXT NULL; ALTER TABLE trace_span_index ADD COLUMN src_line INTEGER NULL; ================================================ FILE: cli/cmd/encore/daemon/migrations/4_add_parent_span_id.up.sql ================================================ ALTER TABLE trace_span_index ADD COLUMN parent_span_id TEXT NULL; ================================================ FILE: cli/cmd/encore/daemon/migrations/5_add_caller_event_id.up.sql ================================================ ALTER TABLE trace_span_index ADD COLUMN caller_event_id INTEGER NULL; ================================================ FILE: cli/cmd/encore/daemon.go ================================================ package main import ( "context" "fmt" "os" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" daemonpkg "encr.dev/cli/cmd/encore/daemon" "encr.dev/internal/env" daemonpb "encr.dev/proto/encore/daemon" ) var daemonizeForeground bool var daemonCmd = &cobra.Command{ Use: "daemon", Short: "Starts the encore daemon", Run: func(cc *cobra.Command, args []string) { if daemonizeForeground { daemonpkg.Main() } else { if err := cmdutil.StartDaemonInBackground(context.Background()); err != nil { fatal(err) } fmt.Fprintln(os.Stdout, "encore daemon is now running") } }, } func init() { rootCmd.AddCommand(daemonCmd) daemonCmd.Flags().BoolVarP(&daemonizeForeground, "foreground", "f", false, "Start the daemon in the foreground") daemonCmd.AddCommand(daemonEnvCmd) } func setupDaemon(ctx context.Context) daemonpb.DaemonClient { return cmdutil.ConnectDaemon(ctx) } var daemonEnvCmd = &cobra.Command{ Use: "env", Short: "Prints Encore environment information", Run: func(cc *cobra.Command, args []string) { envs := env.List() for _, e := range envs { fmt.Println(e) } }, } ================================================ FILE: cli/cmd/encore/db.go ================================================ package main import ( "context" "fmt" "os" "os/exec" "os/signal" "path/filepath" "runtime" "strings" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/daemon/sqldb/docker" daemonpb "encr.dev/proto/encore/daemon" ) var dbCmd = &cobra.Command{ Use: "db", Short: "Database management commands", } var ( resetAll bool testDB bool shadowDB bool write bool admin bool superuser bool nsName string ) func getDBRole() daemonpb.DBRole { switch { case superuser: return daemonpb.DBRole_DB_ROLE_SUPERUSER case admin: return daemonpb.DBRole_DB_ROLE_ADMIN case write: return daemonpb.DBRole_DB_ROLE_WRITE default: return daemonpb.DBRole_DB_ROLE_READ } } var dbResetCmd = &cobra.Command{ Use: "reset ", Short: "Resets the databases with the given names. Use --all to reset all databases.", Run: func(command *cobra.Command, args []string) { appRoot, _ := determineAppRoot() dbNames := args if resetAll { if len(dbNames) > 0 { fatal("cannot specify both --all and database names") } dbNames = nil } else { if len(dbNames) == 0 { fatal("no database names given") } } ctx := context.Background() daemon := setupDaemon(ctx) stream, err := daemon.DBReset(ctx, &daemonpb.DBResetRequest{ AppRoot: appRoot, DatabaseNames: dbNames, ClusterType: dbClusterType(), Namespace: nonZeroPtr(nsName), }) if err != nil { fatal("reset databases: ", err) } os.Exit(cmdutil.StreamCommandOutput(stream, nil)) }, } var dbEnv string var dbShellCmd = &cobra.Command{ Use: "shell DATABASE_NAME [--env=] [--test|--shadow]", Short: "Connects to the database via psql shell", Long: `Defaults to connecting to your local environment. Specify --env to connect to another environment. Use --test to connect to databases used for integration testing. Use --shadow to connect to the shadow database, used for database drift detection when using tools like Prisma. --test and --shadow imply --env=local. `, Args: cobra.MaximumNArgs(1), DisableFlagsInUseLine: true, Run: func(command *cobra.Command, args []string) { appRoot, relPath := determineAppRoot() ctx := context.Background() daemon := setupDaemon(ctx) dbName := "" if len(args) > 0 { dbName = args[0] // Ignore the trailing slash to support auto-completion of directory names dbName = strings.TrimSuffix(dbName, "/") } else { // Find the enclosing service by looking for the "migrations" folder SvcNameLoop: for p := relPath; p != "."; p = filepath.Dir(p) { absPath := filepath.Join(appRoot, p) if _, err := os.Stat(filepath.Join(absPath, "migrations")); err == nil { pkgs, err := resolvePackages(absPath, ".") if err == nil && len(pkgs) > 0 { dbName = filepath.Base(pkgs[0]) break SvcNameLoop } } } if dbName == "" { fatal("could not find an Encore service with a database in this directory (or any of the parent directories).\n\n" + "Note: You can specify a service name to connect to it directly using the command 'encore db shell '.") } } if testDB || shadowDB { dbEnv = "local" } resp, err := daemon.DBConnect(ctx, &daemonpb.DBConnectRequest{ AppRoot: appRoot, DbName: dbName, EnvName: dbEnv, ClusterType: dbClusterType(), Namespace: nonZeroPtr(nsName), Role: getDBRole(), }) if err != nil { fatalf("could not connect to db %s: %v", dbName, err) } // If we have the psql binary, use that. // Otherwise fall back to docker. var cmd *exec.Cmd if p, err := exec.LookPath("psql"); err == nil { cmd = exec.Command(p, resp.Dsn) } else { fmt.Fprintln(os.Stderr, "encore: no 'psql' executable found in $PATH; using docker to run 'psql' instead.\n\nNote: install psql to hide this message.") dsn := resp.Dsn if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { // Docker for {Mac, Windows}'s networking setup requires // using "host.docker.internal" instead of "localhost" for _, rep := range []string{"localhost", "127.0.0.1"} { dsn = strings.Replace(dsn, rep, "host.docker.internal", -1) } } cmd = exec.Command("docker", "run", "-it", "--rm", "--network=host", docker.Image, "psql", dsn) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Start(); err != nil { log.Fatal().Err(err).Msg("failed to start psql") } signal.Ignore(os.Interrupt) if err := cmd.Wait(); err != nil { log.Fatal().Err(err).Msg("psql failed") } }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return nil, cobra.ShellCompDirectiveFilterDirs }, } var dbProxyPort int32 var dbProxyCmd = &cobra.Command{ Use: "proxy [--env=] [--test|--shadow]", Short: "Sets up a proxy tunnel to the database", Long: `Set up a proxy tunnel to a database for use with other tools. Use --test to connect to databases used for integration testing. Use --shadow to connect to the shadow database, used for database drift detection when using tools like Prisma. --test and --shadow imply --env=local. `, Run: func(command *cobra.Command, args []string) { appRoot, _ := determineAppRoot() interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-interrupt cancel() }() if testDB || shadowDB { dbEnv = "local" } daemon := setupDaemon(ctx) stream, err := daemon.DBProxy(ctx, &daemonpb.DBProxyRequest{ AppRoot: appRoot, EnvName: dbEnv, Port: dbProxyPort, ClusterType: dbClusterType(), Namespace: nonZeroPtr(nsName), Role: getDBRole(), }) if err != nil { log.Fatal().Err(err).Msg("could not setup db proxy") } os.Exit(cmdutil.StreamCommandOutput(stream, nil)) }, } var dbConnURICmd = &cobra.Command{ Use: "conn-uri [] [--test|--shadow]", Short: "Outputs the database connection string", Long: `Retrieve a stable connection uri for connecting to a database. Use --test to connect to databases used for integration testing. Use --shadow to connect to the shadow database, used for database drift detection when using tools like Prisma. --test and --shadow imply --env=local. `, Args: cobra.MaximumNArgs(1), Run: func(command *cobra.Command, args []string) { appRoot, relPath := determineAppRoot() ctx := context.Background() daemon := setupDaemon(ctx) dbName := "" if len(args) > 0 { dbName = args[0] } else { // Find the enclosing service by looking for the "migrations" folder DBNameLoop: for p := relPath; p != "."; p = filepath.Dir(p) { absPath := filepath.Join(appRoot, p) if _, err := os.Stat(filepath.Join(absPath, "migrations")); err == nil { pkgs, err := resolvePackages(absPath, ".") if err == nil && len(pkgs) > 0 { dbName = filepath.Base(pkgs[0]) break DBNameLoop } } } if dbName == "" { fatal("could not find Encore service with a database in this directory (or any parent directory).\n\n" + "Note: You can specify a service name to connect to it directly using the command 'encore db conn-uri '.") } } if testDB || shadowDB { dbEnv = "local" } resp, err := daemon.DBConnect(ctx, &daemonpb.DBConnectRequest{ AppRoot: appRoot, DbName: dbName, EnvName: dbEnv, ClusterType: dbClusterType(), Namespace: nonZeroPtr(nsName), Role: getDBRole(), }) if err != nil { st, ok := status.FromError(err) if ok { if st.Code() == codes.NotFound { fatalf("no such database found: %s", dbName) } } fatalf("could not connect to the database for service %s: %v", dbName, err) } _, _ = fmt.Fprintln(os.Stdout, resp.Dsn) }, } func init() { rootCmd.AddCommand(dbCmd) dbResetCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") dbResetCmd.Flags().BoolVar(&resetAll, "all", false, "Reset all services in the application") dbResetCmd.Flags().BoolVarP(&testDB, "test", "t", false, "Reset databases in the test cluster instead") dbResetCmd.Flags().BoolVar(&shadowDB, "shadow", false, "Reset databases in the shadow cluster instead") dbCmd.AddCommand(dbResetCmd) dbShellCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") dbShellCmd.Flags().StringVarP(&dbEnv, "env", "e", "local", "Environment name to connect to (such as \"prod\")") dbShellCmd.Flags().BoolVarP(&testDB, "test", "t", false, "Connect to the integration test database (implies --env=local)") dbShellCmd.Flags().BoolVar(&shadowDB, "shadow", false, "Connect to the shadow database (implies --env=local)") dbShellCmd.Flags().BoolVar(&write, "write", false, "Connect with write privileges") dbShellCmd.Flags().BoolVar(&admin, "admin", false, "Connect with admin privileges") dbShellCmd.Flags().BoolVar(&superuser, "superuser", false, "Connect as a superuser") dbShellCmd.MarkFlagsMutuallyExclusive("write", "admin", "superuser") dbCmd.AddCommand(dbShellCmd) dbProxyCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") dbProxyCmd.Flags().StringVarP(&dbEnv, "env", "e", "local", "Environment name to connect to (such as \"prod\")") dbProxyCmd.Flags().Int32VarP(&dbProxyPort, "port", "p", 0, "Port to listen on (defaults to a random port)") dbProxyCmd.Flags().BoolVarP(&testDB, "test", "t", false, "Connect to the integration test database (implies --env=local)") dbProxyCmd.Flags().BoolVar(&shadowDB, "shadow", false, "Connect to the shadow database (implies --env=local)") dbProxyCmd.Flags().BoolVar(&write, "write", false, "Connect with write privileges") dbProxyCmd.Flags().BoolVar(&admin, "admin", false, "Connect with admin privileges") dbProxyCmd.Flags().BoolVar(&superuser, "superuser", false, "Connect as a superuser") dbProxyCmd.MarkFlagsMutuallyExclusive("write", "admin", "superuser") dbCmd.AddCommand(dbProxyCmd) dbConnURICmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") dbConnURICmd.Flags().StringVarP(&dbEnv, "env", "e", "local", "Environment name to connect to (such as \"prod\")") dbConnURICmd.Flags().BoolVarP(&testDB, "test", "t", false, "Connect to the integration test database (implies --env=local)") dbConnURICmd.Flags().BoolVar(&shadowDB, "shadow", false, "Connect to the shadow database (implies --env=local)") dbConnURICmd.Flags().BoolVar(&write, "write", false, "Connect with write privileges") dbConnURICmd.Flags().BoolVar(&admin, "admin", false, "Connect with admin privileges") dbConnURICmd.Flags().BoolVar(&superuser, "superuser", false, "Connect as a superuser") dbConnURICmd.MarkFlagsMutuallyExclusive("write", "admin", "superuser") dbCmd.AddCommand(dbConnURICmd) } func dbClusterType() daemonpb.DBClusterType { if testDB && shadowDB { fatal("cannot specify both --test and --shadow") } switch { case testDB: return daemonpb.DBClusterType_DB_CLUSTER_TYPE_TEST case shadowDB: return daemonpb.DBClusterType_DB_CLUSTER_TYPE_SHADOW default: return daemonpb.DBClusterType_DB_CLUSTER_TYPE_RUN } } ================================================ FILE: cli/cmd/encore/debug.go ================================================ package main import ( "context" "os" "os/signal" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" daemonpb "encr.dev/proto/encore/daemon" ) func init() { debugCmd := &cobra.Command{ Use: "debug", Short: "debug is a collection of debug commands", Hidden: true, } format := cmdutil.Oneof{ Value: "proto", Allowed: []string{"proto", "json"}, Flag: "format", FlagShort: "f", Desc: "Output format", } toFormat := func() daemonpb.DumpMetaRequest_Format { switch format.Value { case "proto": return daemonpb.DumpMetaRequest_FORMAT_PROTO case "json": return daemonpb.DumpMetaRequest_FORMAT_JSON default: return daemonpb.DumpMetaRequest_FORMAT_UNSPECIFIED } } var p dumpMetaParams dumpMeta := &cobra.Command{ Use: "meta", Short: "Outputs the parsed metadata", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { p.AppRoot, p.WorkingDir = determineAppRoot() p.Environ = os.Environ() p.Format = toFormat() dumpMeta(p) }, } format.AddFlag(dumpMeta) dumpMeta.Flags().BoolVar(&p.ParseTests, "tests", false, "Parse tests as well") rootCmd.AddCommand(debugCmd) debugCmd.AddCommand(dumpMeta) } type dumpMetaParams struct { AppRoot string WorkingDir string ParseTests bool Format daemonpb.DumpMetaRequest_Format Environ []string } func dumpMeta(p dumpMetaParams) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() daemon := setupDaemon(ctx) resp, err := daemon.DumpMeta(ctx, &daemonpb.DumpMetaRequest{ AppRoot: p.AppRoot, WorkingDir: p.WorkingDir, ParseTests: p.ParseTests, Environ: p.Environ, Format: p.Format, }) if err != nil { fatal(err) } _, _ = os.Stdout.Write(resp.Meta) } ================================================ FILE: cli/cmd/encore/deploy.go ================================================ package main import ( "encoding/hex" "encoding/json" "fmt" "strings" "github.com/cockroachdb/errors" "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/platform" "encr.dev/pkg/appfile" ) var ( appSlug string envName string commit string branch string format = cmdutil.Oneof{ Value: "text", Allowed: []string{"text", "json"}, Flag: "format", FlagShort: "f", Desc: "Output format", } ) var deployAppCmd = &cobra.Command{ Use: "deploy --commit COMMIT_SHA | --branch BRANCH_NAME", Short: "Deploy an Encore app to a cloud environment", DisableFlagsInUseLine: true, Run: func(c *cobra.Command, args []string) { if commit != "" { hb, err := hex.DecodeString(commit) if err != nil || len(hb) != 20 { cmdutil.Fatalf("invalid commit: %s", commit) } } if appSlug == "" { appRoot, _, err := cmdutil.MaybeAppRoot() if err != nil { cmdutil.Fatalf("no app found. Run deploy inside an encore app directory or specify the app with --app") } appSlug, err = appfile.Slug(appRoot) if err != nil { cmdutil.Fatalf("no app found. Run deploy inside an encore app directory or specify the app with --app") } } rollout, err := platform.Deploy(c.Context(), appSlug, envName, commit, branch) var pErr platform.Error if ok := errors.As(err, &pErr); ok { switch pErr.Code { case "app_not_found": cmdutil.Fatalf("app not found: %s", appSlug) case "validation": var details platform.ValidationDetails err := json.Unmarshal(pErr.Detail, &details) if err != nil { cmdutil.Fatalf("failed to deploy: %v", err) } switch details.Field { case "commit": cmdutil.Fatalf("could not find commit: %s. Is it pushed to the remote repository?", commit) case "branch": cmdutil.Fatalf("could not find branch: %s. Is it pushed to the remote repository?", branch) case "env": cmdutil.Fatalf("could not find environment: %s/%s", appSlug, envName) } } } if err != nil { cmdutil.Fatalf("failed to deploy: %v", err) } url := fmt.Sprintf("https://app.encore.cloud/%s/deploys/%s/%s", appSlug, rollout.EnvName, strings.TrimPrefix(rollout.ID, "roll_")) switch format.Value { case "text": fmt.Println(aurora.Sprintf("\n%s %s\n", aurora.Bold("Started Deploy:"), url)) case "json": output, _ := json.Marshal(map[string]string{ "id": strings.TrimPrefix(rollout.ID, "roll_"), "env": rollout.EnvName, "app": appSlug, "url": url, }) fmt.Println(string(output)) } }, } func init() { alphaCmd.AddCommand(deployAppCmd) deployAppCmd.Flags().StringVar(&appSlug, "app", "", "app slug to deploy to (default current app)") deployAppCmd.Flags().StringVarP(&envName, "env", "e", "", "environment to deploy to (default primary env)") deployAppCmd.Flags().StringVar(&commit, "commit", "", "commit to deploy") deployAppCmd.Flags().StringVar(&branch, "branch", "", "branch to deploy") format.AddFlag(deployAppCmd) _ = deployAppCmd.MarkFlagRequired("env") deployAppCmd.MarkFlagsMutuallyExclusive("commit", "branch") deployAppCmd.MarkFlagsOneRequired("commit", "branch") } ================================================ FILE: cli/cmd/encore/exec.go ================================================ package main import ( "context" "errors" "os" "os/exec" "os/signal" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/pkg/appfile" daemonpb "encr.dev/proto/encore/daemon" ) var execCmd = &cobra.Command{ Use: "exec path/to/script [args...]", Short: "Runs executable scripts against the local Encore app", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { args = []string{"."} // current directory } appRoot, wd := determineAppRoot() execScript(appRoot, wd, args) }, } var execCmdAlpha = &cobra.Command{ Use: "exec path/to/script [args...]", Short: "Runs executable scripts against the local Encore app", Hidden: true, Deprecated: "use \"encore exec\" instead", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { args = []string{"."} // current directory } appRoot, wd := determineAppRoot() execScript(appRoot, wd, args) }, } func execScript(appRoot, relWD string, args []string) { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-interrupt cancel() }() daemon := setupDaemon(ctx) // For TypeScript apps, use ExecSpec to get the command spec and run it // locally. This allows interactive commands (stdin) to work properly. lang, err := appfile.AppLang(appRoot) if err != nil { fatal(err) } if lang == appfile.LangTS { tempDir, err := os.MkdirTemp("", "encore-exec") if err != nil { fatal(err) } defer func() { _ = os.RemoveAll(tempDir) }() stream, err := daemon.ExecSpec(ctx, &daemonpb.ExecSpecRequest{ AppRoot: appRoot, WorkingDir: relWD, ScriptArgs: args, Environ: os.Environ(), Namespace: nonZeroPtr(nsName), TempDir: tempDir, }) if err != nil { fatal(err) } cmdutil.ClearTerminalExceptFirstNLines(1) // Read progress messages until we get the spec. var spec *daemonpb.ExecSpecResponse for { msg, err := stream.Recv() if err != nil { fatal(err) } switch m := msg.Msg.(type) { case *daemonpb.ExecSpecMessage_Output: if len(m.Output.Stdout) > 0 { os.Stdout.Write(m.Output.Stdout) } if len(m.Output.Stderr) > 0 { os.Stderr.Write(m.Output.Stderr) } case *daemonpb.ExecSpecMessage_Spec: spec = m.Spec } if spec != nil { break } } cmd := exec.Command(spec.Command, spec.Args...) cmd.Env = spec.Environ cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { os.Exit(exitErr.ExitCode()) } fatal(err) } return } // For Go apps, use the streaming ExecScript RPC. stream, err := daemon.ExecScript(ctx, &daemonpb.ExecScriptRequest{ AppRoot: appRoot, WorkingDir: relWD, ScriptArgs: args, Environ: os.Environ(), TraceFile: root.TraceFile, Namespace: nonZeroPtr(nsName), }) if err != nil { fatal(err) } cmdutil.ClearTerminalExceptFirstNLines(1) code := cmdutil.StreamCommandOutput(stream, cmdutil.ConvertJSONLogs()) os.Exit(code) } var alphaCmd = &cobra.Command{ Use: "alpha", Short: "Pre-release functionality in alpha stage", Hidden: true, } func init() { rootCmd.AddCommand(alphaCmd) } func init() { execCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") alphaCmd.AddCommand(execCmdAlpha) rootCmd.AddCommand(execCmd) } ================================================ FILE: cli/cmd/encore/gen.go ================================================ package main import ( "context" "errors" "fmt" "os" "time" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/manifest" "encr.dev/pkg/appfile" "encr.dev/pkg/clientgen" daemonpb "encr.dev/proto/encore/daemon" ) func init() { genCmd := &cobra.Command{ Use: "gen", Short: "Code generation commands", } rootCmd.AddCommand(genCmd) var ( output string lang string envName string genServiceNames []string excludedServices []string endpointTags []string excludedEndpointTags []string openAPIExcludePrivateEndpoints bool tsSharedTypes bool target string tsDefaultClient string ) genClientCmd := &cobra.Command{ Use: "client [] [--env=] [--services=foo,bar] [--excluded-services=baz,qux] [--tags=cache,mobile] [--excluded-tags=internal] [--openapi-exclude-private-endpoints]", Short: "Generates an API client for your app", Long: `Generates an API client for your app. By default generates the API based on your local environment. Use '--env=' to generate it based on your cloud environments. Supported language codes are: typescript: A TypeScript client using the Fetch API javascript: A JavaScript client using the Fetch API go: A Go client using net/http" openapi: An OpenAPI specification (EXPERIMENTAL) By default all services with a non-private API endpoint are included. To further narrow down the services to generate, use the '--services' flag. `, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if target == "leap" { lang = "typescript" tsDefaultClient = "import.meta.env.VITE_CLIENT_TARGET" if output == "" { output = "../frontend/client.ts" } excludedServices = append(excludedServices, "frontend") tsSharedTypes = true } if output == "" && lang == "" { fatal("specify at least one of --output or --lang.") } // Determine the app id, either from the argument or from the current directory. var appID, appRoot string if len(args) == 0 { var err error // First check the encore.app file. appRoot, _, err = cmdutil.MaybeAppRoot() if err != nil && !errors.Is(err, cmdutil.ErrNoEncoreApp) { fatal(err) } else if appRoot != "" { if slug, err := appfile.Slug(appRoot); err == nil { appID = slug } } // If we still don't have an app id, read it from the manifest. if appID == "" { mf, err := manifest.ReadOrCreate(appRoot) if err != nil { fatal(err) } appID = mf.AppID if appID == "" { appID = mf.LocalID } } } else { appID = args[0] } if lang == "" { var ok bool l, ok := clientgen.Detect(output) if !ok { fatal("could not detect language from output.\n\nNote: you can specify the language explicitly with --lang.") } lang = string(l) } else { // Validate the user input for the language l, err := clientgen.GetLang(lang) if err != nil { fatal(fmt.Sprintf("%s: supported languages are `typescript`, `javascript`, `go` and `openapi`", err)) } lang = string(l) } ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() daemon := setupDaemon(ctx) if genServiceNames == nil { genServiceNames = []string{"*"} } resp, err := daemon.GenClient(ctx, &daemonpb.GenClientRequest{ AppId: appID, EnvName: envName, Lang: lang, Services: genServiceNames, ExcludedServices: excludedServices, EndpointTags: endpointTags, ExcludedEndpointTags: excludedEndpointTags, OpenapiExcludePrivateEndpoints: &openAPIExcludePrivateEndpoints, TsSharedTypes: &tsSharedTypes, TsClientTarget: &tsDefaultClient, AppRoot: appRoot, }) if err != nil { fatal(err) } if output == "" { _, _ = os.Stdout.Write(resp.Code) } else { if err := os.WriteFile(output, resp.Code, 0755); err != nil { fatal(err) } } }, ValidArgsFunction: cmdutil.AutoCompleteAppSlug, } genWrappersCmd := &cobra.Command{ Use: "wrappers", Short: "Generates user-facing wrapper code", Long: `Manually regenerates user-facing wrapper code. This is typically not something you ever need to call during regular development, as Encore automatically regenerates the wrappers whenever the code-base changes. Its core use case is for CI/CD workflows where you want to run custom linters, which may require the user-facing wrapper code to be manually generated.`, Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { appRoot, _ := determineAppRoot() ctx := context.Background() daemon := setupDaemon(ctx) _, err := daemon.GenWrappers(ctx, &daemonpb.GenWrappersRequest{ AppRoot: appRoot, }) if err != nil { fatal(err) } else { fmt.Println("successfully generated encore wrappers.") } }, } genCmd.AddCommand(genClientCmd) genCmd.AddCommand(genWrappersCmd) genClientCmd.Flags().StringVarP(&lang, "lang", "l", "", "The language to generate code for (\"typescript\", \"javascript\", \"go\", and \"openapi\" are supported)") _ = genClientCmd.RegisterFlagCompletionFunc("lang", cmdutil.AutoCompleteFromStaticList( "typescript\tA TypeScript client using the in-browser Fetch API", "javascript\tA JavaScript client using the in-browser Fetch API", "go\tA Go client using net/http", "openapi\tAn OpenAPI specification", )) genClientCmd.Flags().StringVarP(&output, "output", "o", "", "The filename to write the generated client code to") _ = genClientCmd.MarkFlagFilename("output", "go", "ts", "tsx", "js", "jsx") genClientCmd.Flags().StringVarP(&envName, "env", "e", "local", "The environment to fetch the API for (defaults to the local environment)") _ = genClientCmd.RegisterFlagCompletionFunc("env", cmdutil.AutoCompleteEnvSlug) genClientCmd.Flags().StringSliceVarP(&genServiceNames, "services", "s", nil, "The names of the services to include in the output") genClientCmd.Flags().StringSliceVarP(&excludedServices, "excluded-services", "x", nil, "The names of the services to exclude in the output") genClientCmd.Flags().StringSliceVarP(&endpointTags, "tags", "t", nil, "The names of endpoint tags to include in the output") genClientCmd.Flags(). StringSliceVar(&excludedEndpointTags, "excluded-tags", nil, "The names of endpoint tags to exclude in the output") genClientCmd.Flags(). BoolVar(&openAPIExcludePrivateEndpoints, "openapi-exclude-private-endpoints", false, "Exclude private endpoints from the OpenAPI spec") genClientCmd.Flags(). BoolVar(&tsSharedTypes, "ts:shared-types", false, "Import types from ~backend instead of re-generating them") genClientCmd.Flags().StringVar(&target, "target", "", "An optional target for the client (\"leap\")") _ = genClientCmd.RegisterFlagCompletionFunc("target", cmdutil.AutoCompleteFromStaticList( "leap\tA TypeScript client for apps created with Leap (https://leap.new) ", )) } ================================================ FILE: cli/cmd/encore/init_windows.go ================================================ //go:build windows // +build windows package main import ( "golang.org/x/sys/windows" ) // init activates virtual terminal feature on "windows", this enables colored // terminal output. func init() { setConsoleMode(windows.Stdout, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) setConsoleMode(windows.Stderr, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) } // setConsoleMode enables VT processing on stout and stderr. func setConsoleMode(handle windows.Handle, flag uint32) { var mode uint32 if err := windows.GetConsoleMode(handle, &mode); err == nil { windows.SetConsoleMode(handle, mode|flag) } } ================================================ FILE: cli/cmd/encore/k8s/auth.go ================================================ package k8s import ( "encoding/json" "os" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/k8s/types" "encr.dev/internal/conf" ) var genAuthCmd = &cobra.Command{ Use: "exec-credentials", Short: "Used by kubectl to get an authentication token for the Encore Kubernetes Proxy", Args: cobra.NoArgs, Hidden: true, DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { generateExecCredentials() }, } func init() { kubernetesCmd.AddCommand(genAuthCmd) } // GenerateExecCredentials generates the Kubernetes exec credentials and writes them to stdout. // // If an error occurs, it is written to stderr and the program exits with a non-zero exit code. func generateExecCredentials() { // Get the OAuth token from the Encore API token, err := conf.DefaultTokenSource.Token() if err != nil { cmdutil.Fatalf("error getting token: %v", err) } // Generate the kuberentes exec credentials datastructures expiryTime := types.NewTime(token.Expiry) execCredentials := &types.ExecCredential{ TypeMeta: types.TypeMeta{ APIVersion: "client.authentication.k8s.io/v1", Kind: "ExecCredential", }, Status: &types.ExecCredentialStatus{ Token: token.AccessToken, ExpirationTimestamp: &expiryTime, }, } // Marshal the exec credentials to JSON and write to stdout output, err := json.MarshalIndent(execCredentials, "", " ") if err != nil { cmdutil.Fatalf("error marshalling exec credentials: %v", err) } _, _ = os.Stdout.Write(output) } ================================================ FILE: cli/cmd/encore/k8s/config.go ================================================ package k8s import ( "context" "fmt" "io/fs" "os" "path/filepath" "slices" "strings" "text/tabwriter" "time" "github.com/cockroachdb/errors" "github.com/fatih/color" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/k8s/types" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" "encr.dev/pkg/xos" "sigs.k8s.io/yaml" ) var configCmd = &cobra.Command{ Use: "configure --env=ENV_NAME", Short: "Updates your kubectl config to point to the Kubernetes cluster(s) for the specified environment", Run: func(cmd *cobra.Command, args []string) { appSlug := cmdutil.AppSlug() ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) defer cancel() if k8sEnvName == "" { _ = cmd.Help() cmdutil.Fatal("must specify environment name with --env") } err := configureForAppEnv(ctx, appSlug, k8sEnvName) if err != nil { cmdutil.Fatalf("error configuring kubectl: %v", err) } }, } var ( k8sEnvName string ) func init() { configCmd.Flags().StringVarP(&k8sEnvName, "env", "e", "", "Environment name") _ = configCmd.MarkFlagRequired("env") kubernetesCmd.AddCommand(configCmd) } func configureForAppEnv(ctx context.Context, appID string, envName string) error { appSlug, envName, clusters, err := platform.KubernetesClusters(ctx, appID, envName) if err != nil { return errors.Wrap(err, "unable to get Kubernetes clusters for environment") } if len(clusters) == 0 { return errors.New("no Kubernetes clusters found for environment") } // Read the existing kubeconfig file configFilePath := filepath.Join(types.HomeDir(), ".kube", "config") cfg, err := readKubeConfig(configFilePath) if err != nil { return err } // Add the clusters contextPrefix := fmt.Sprintf("encore_%s_%s", appSlug, envName) authName := "encore-proxy-auth" contextNames := make([]string, len(clusters)) for i, cluster := range clusters { // Create a context name for the cluster // by default we use the app slug and env name seperated by a underscore (e.g. encore-myapp_prod) // however if the environment has multiple clusters then we also include the cluster name (e.g. encore-myapp_prod_cluster1) contextName := contextPrefix if len(clusters) > 1 { contextName += "_" + cluster.Name } contextNames[i] = contextName // Add the cluster using the cluster name as the context name cfg.clusters = appendOrUpdate(cfg.clusters, map[string]any{ "name": contextName, "cluster": map[string]any{ "server": fmt.Sprintf("%s/k8s-api-proxy/%s/%s/", conf.APIBaseURL, cluster.EnvID, cluster.ResID), }, }) k8sContext := map[string]any{ "cluster": contextName, "user": authName, } if cluster.DefaultNamespace != "" { k8sContext["namespace"] = cluster.DefaultNamespace } cfg.contexts = appendOrUpdate(cfg.contexts, map[string]any{ "name": contextName, "context": k8sContext, }) } // Remove any old contexts or clusters // We do this by iterating over the existing contexts and clusters and removing any that are not in the new list for i := len(cfg.contexts) - 1; i >= 0; i-- { if foundContext, ok := cfg.contexts[i].(map[string]any); ok { if contextName, ok := foundContext["name"].(string); ok { if strings.HasPrefix(contextName, contextPrefix) && !slices.Contains(contextNames, contextName) { cfg.contexts = append(cfg.contexts[:i], cfg.contexts[i+1:]...) } } } } for i := len(cfg.clusters) - 1; i >= 0; i-- { if foundCluster, ok := cfg.clusters[i].(map[string]any); ok { if clusterName, ok := foundCluster["name"].(string); ok { if strings.HasPrefix(clusterName, contextPrefix) && !slices.Contains(contextNames, clusterName) { cfg.clusters = append(cfg.clusters[:i], cfg.clusters[i+1:]...) } } } } // If we added a cluster then we need to update the encore-k8s-proxy user cfg.users = appendOrUpdate(cfg.users, map[string]any{ "name": authName, "user": map[string]any{ "exec": map[string]any{ "apiVersion": "client.authentication.k8s.io/v1", "args": []string{"kubernetes", "exec-credentials"}, "command": "encore", "env": nil, "installHint": "Install encore for use with kubectl, see https://encore.dev", "interactiveMode": "Never", "provideClusterInfo": false, }, }, }) // Update the current context to the first cluster for the environment cfg.raw["current-context"] = contextNames[0] if err := writeKubeConfig(configFilePath, cfg); err != nil { return err } if len(clusters) == 1 { _, _ = fmt.Fprintf(os.Stdout, "kubectl configured for cluster %s under context %s.\n", color.CyanString(clusters[0].Name), color.CyanString(contextNames[0])) } else { _, _ = fmt.Fprintf(os.Stdout, "kubectl configured for %d clusters:\n\n", len(clusters)) w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.StripEscape) _, _ = fmt.Fprint(w, "CLUSTER\tCONTEXT\tACTIVE\n") for i, cluster := range clusters { active := "" if i == 0 { active = "yes" } _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", cluster.Name, contextNames[0], active) } _ = w.Flush() } return nil } // readKubeConfig reads the existing kubeconfig file and returns a Cfg struct. // however this is as untyped as possible, so that we can easily marshal it back without losing any data. func readKubeConfig(file string) (*Cfg, error) { b, err := os.ReadFile(file) if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, errors.Wrap(err, "unable to read kubeconfig file") } // Read the existing kubeconfig file var kubeConfig map[string]any if len(b) > 0 { if err = yaml.Unmarshal(b, &kubeConfig); err != nil { return nil, errors.Wrap(err, "unable to parse kubeconfig file") } } // Ensure the kubeConfig struct is valid if kubeConfig == nil { kubeConfig = map[string]any{ "apiVersion": "v1", "kind": "Config", } } else if kubeConfig["apiVersion"] != "v1" || kubeConfig["kind"] != "Config" { return nil, errors.New("invalid existing kubeconfig file") } cfg := &Cfg{ raw: kubeConfig, } if clusters, ok := kubeConfig["clusters"]; ok { if clusters, ok := clusters.([]any); ok { cfg.clusters = clusters } else { return nil, errors.Newf("clusters is not an array got %T", clusters) } } if users, ok := kubeConfig["users"]; ok { if users, ok := users.([]any); ok { cfg.users = users } else { return nil, errors.Newf("users is not an array got %T", users) } } if contexts, ok := kubeConfig["contexts"]; ok { if contexts, ok := contexts.([]any); ok { cfg.contexts = contexts } else { return nil, errors.Newf("contexts is not an array got %T", contexts) } } return cfg, nil } // writeKubeConfig writes the kubeconfig back to the file. func writeKubeConfig(file string, cfg *Cfg) error { // Update the raw kubeconfig struct cfg.raw["clusters"] = cfg.clusters cfg.raw["users"] = cfg.users cfg.raw["contexts"] = cfg.contexts b, err := yaml.Marshal(cfg.raw) if err != nil { return errors.Wrap(err, "unable to marshal kubeconfig back into yaml") } // Ensure the directory exists if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { return errors.Wrap(err, "unable to create kubeconfig directory") } // Then write the file err = xos.WriteFile(file, b, 0600) if err != nil { return errors.Wrap(err, "unable to write kubeconfig file") } return nil } type Cfg struct { raw map[string]any clusters []any users []any contexts []any } // appendOrUpdate looks at the array for an entry which is a map and has a "name" key which matches the name in val, if found // it will update the entry with val, otherwise it will append val to the array. func appendOrUpdate(dst []any, val map[string]any) []any { idx := slices.IndexFunc(dst, func(entry any) bool { if entry, ok := entry.(map[string]any); ok { if entry["name"] == val["name"] { return true } } return false }) if idx == -1 { return append(dst, val) } else { dst[idx] = val return dst } } ================================================ FILE: cli/cmd/encore/k8s/kubernetes.go ================================================ package k8s import ( "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/root" ) var kubernetesCmd = &cobra.Command{ Use: "kubernetes", Short: "Kubernetes management commands", Aliases: []string{"k8s"}, } func init() { root.Cmd.AddCommand(kubernetesCmd) } ================================================ FILE: cli/cmd/encore/k8s/types/KUBERNETES_LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: cli/cmd/encore/k8s/types/README.md ================================================ # Kubernetes Types This package contains types copied directly from the [Kubernetes](https://github.com/kubernetes/kubernetes) project, this is to prevent the Encore CLI needing to have a dependency on the Kubernetes project for just these types. ================================================ FILE: cli/cmd/encore/k8s/types/clientauthentication_types.go ================================================ /* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types // ExecCredential is used by exec-based plugins to communicate credentials to // HTTP transports. type ExecCredential struct { TypeMeta `json:",inline"` // Spec holds information passed to the plugin by the transport. Spec ExecCredentialSpec `json:"spec,omitempty"` // Status is filled in by the plugin and holds the credentials that the transport // should use to contact the API. // +optional Status *ExecCredentialStatus `json:"status,omitempty"` } // ExecCredentialSpec holds request and runtime specific information provided by // the transport. type ExecCredentialSpec struct { // Cluster contains information to allow an exec plugin to communicate with the // kubernetes cluster being authenticated to. Note that Cluster is non-nil only // when provideClusterInfo is set to true in the exec provider config (i.e., // ExecConfig.ProvideClusterInfo). // +optional Cluster *Cluster `json:"cluster,omitempty"` // Interactive declares whether stdin has been passed to this exec plugin. Interactive bool `json:"interactive"` } // ExecCredentialStatus holds credentials for the transport to use. // // Token and ClientKeyData are sensitive fields. This data should only be // transmitted in-memory between client and exec plugin process. Exec plugin // itself should at least be protected via file permissions. type ExecCredentialStatus struct { // ExpirationTimestamp indicates a time when the provided credentials expire. // +optional ExpirationTimestamp *Time `json:"expirationTimestamp,omitempty"` // Token is a bearer token used by the client for request authentication. Token string `json:"token,omitempty" datapolicy:"token"` // PEM-encoded client TLS certificates (including intermediates, if any). ClientCertificateData string `json:"clientCertificateData,omitempty"` // PEM-encoded private key for the above certificate. ClientKeyData string `json:"clientKeyData,omitempty" datapolicy:"security-key"` } // Cluster contains information to allow an exec plugin to communicate // with the kubernetes cluster being authenticated to. // // To ensure that this struct contains everything someone would need to communicate // with a kubernetes cluster (just like they would via a kubeconfig), the fields // should shadow "k8s.io/client-go/tools/clientcmd/api/v1".Cluster, with the exception // of CertificateAuthority, since CA data will always be passed to the plugin as bytes. type Cluster struct { // Server is the address of the kubernetes cluster (https://hostname:port). Server string `json:"server"` // TLSServerName is passed to the server for SNI and is used in the client to // check server certificates against. If ServerName is empty, the hostname // used to contact the server is used. // +optional TLSServerName string `json:"tls-server-name,omitempty"` // InsecureSkipTLSVerify skips the validity check for the server's certificate. // This will make your HTTPS connections insecure. // +optional InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` // CAData contains PEM-encoded certificate authority certificates. // If empty, system roots should be used. // +listType=atomic // +optional CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` // ProxyURL is the URL to the proxy to be used for all requests to this // cluster. // +optional ProxyURL string `json:"proxy-url,omitempty"` // DisableCompression allows client to opt-out of response compression for all requests to the server. This is useful // to speed up requests (specifically lists) when client-server network bandwidth is ample, by saving time on // compression (server-side) and decompression (client-side): https://github.com/kubernetes/kubernetes/issues/112296. // +optional DisableCompression bool `json:"disable-compression,omitempty"` // Config holds additional config data that is specific to the exec // plugin with regards to the cluster being authenticated to. // // This data is sourced from the clientcmd Cluster object's // extensions[client.authentication.k8s.io/exec] field: // // clusters: // - name: my-cluster // cluster: // ... // extensions: // - name: client.authentication.k8s.io/exec # reserved extension name for per cluster exec config // extension: // audience: 06e3fbd18de8 # arbitrary config // // In some environments, the user config may be exactly the same across many clusters // (i.e. call this exec plugin) minus some details that are specific to each cluster // such as the audience. This field allows the per cluster config to be directly // specified with the cluster info. Using this field to store secret data is not // recommended as one of the prime benefits of exec plugins is that no secrets need // to be stored directly in the kubeconfig. // +optional Config RawExtension `json:"config,omitempty"` } ================================================ FILE: cli/cmd/encore/k8s/types/homedir.go ================================================ /* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types import ( "os" "path/filepath" "runtime" ) // HomeDir returns the home directory for the current user. // On Windows: // 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\config` file is returned. // 2. if none of those locations contain a `.kube\config` file, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned. // 3. if none of those locations are writeable, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned. // 4. if none of those locations exists, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned. func HomeDir() string { if runtime.GOOS == "windows" { home := os.Getenv("HOME") homeDriveHomePath := "" if homeDrive, homePath := os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"); len(homeDrive) > 0 && len(homePath) > 0 { homeDriveHomePath = homeDrive + homePath } userProfile := os.Getenv("USERPROFILE") // Return first of %HOME%, %HOMEDRIVE%/%HOMEPATH%, %USERPROFILE% that contains a `.kube\config` file. // %HOMEDRIVE%/%HOMEPATH% is preferred over %USERPROFILE% for backwards-compatibility. for _, p := range []string{home, homeDriveHomePath, userProfile} { if len(p) == 0 { continue } if _, err := os.Stat(filepath.Join(p, ".kube", "config")); err != nil { continue } return p } firstSetPath := "" firstExistingPath := "" // Prefer %USERPROFILE% over %HOMEDRIVE%/%HOMEPATH% for compatibility with other auth-writing tools for _, p := range []string{home, userProfile, homeDriveHomePath} { if len(p) == 0 { continue } if len(firstSetPath) == 0 { // remember the first path that is set firstSetPath = p } info, err := os.Stat(p) if err != nil { continue } if len(firstExistingPath) == 0 { // remember the first path that exists firstExistingPath = p } if info.IsDir() && info.Mode().Perm()&(1<<(uint(7))) != 0 { // return first path that is writeable return p } } // If none are writeable, return first location that exists if len(firstExistingPath) > 0 { return firstExistingPath } // If none exist, return first location that is set if len(firstSetPath) > 0 { return firstSetPath } // We've got nothing return "" } return os.Getenv("HOME") } ================================================ FILE: cli/cmd/encore/k8s/types/meta_types.go ================================================ /* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types import ( "encoding/json" "time" ) // TypeMeta describes an individual object in an API response or request // with strings representing the type of the object and its API schema version. // Structures that are versioned or persisted should inline TypeMeta. // // +k8s:deepcopy-gen=false type TypeMeta struct { // Kind is a string value representing the REST resource this object represents. // Servers may infer this from the endpoint the client submits requests to. // Cannot be updated. // In CamelCase. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds // +optional Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"` // APIVersion defines the versioned schema of this representation of an object. // Servers should convert recognized schemas to the latest internal value, and // may reject unrecognized values. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources // +optional APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"` } // Time is a wrapper around time.Time which supports correct // marshaling to YAML and JSON. Wrappers are provided for many // of the factory methods that the time package offers. // // +protobuf.options.marshal=false // +protobuf.as=Timestamp // +protobuf.options.(gogoproto.goproto_stringer)=false type Time struct { time.Time `protobuf:"-"` } // NewTime returns a wrapped instance of the provided time func NewTime(time time.Time) Time { return Time{time} } // UnmarshalJSON implements the json.Unmarshaller interface. func (t *Time) UnmarshalJSON(b []byte) error { if len(b) == 4 && string(b) == "null" { t.Time = time.Time{} return nil } var str string err := json.Unmarshal(b, &str) if err != nil { return err } pt, err := time.Parse(time.RFC3339, str) if err != nil { return err } t.Time = pt.Local() return nil } // MarshalJSON implements the json.Marshaler interface. func (t Time) MarshalJSON() ([]byte, error) { if t.IsZero() { // Encode unset/nil objects as JSON's "null". return []byte("null"), nil } buf := make([]byte, 0, len(time.RFC3339)+2) buf = append(buf, '"') // time cannot contain non escapable JSON characters buf = t.UTC().AppendFormat(buf, time.RFC3339) buf = append(buf, '"') return buf, nil } ================================================ FILE: cli/cmd/encore/k8s/types/runtime_types.go ================================================ /* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types // RawExtension is used to hold extensions in external versions. // // To use this, make a field which has RawExtension as its type in your external, versioned // struct, and Object in your internal struct. You also need to register your // various plugin types. // // // Internal package: // // type MyAPIObject struct { // runtime.TypeMeta `json:",inline"` // MyPlugin runtime.Object `json:"myPlugin"` // } // // type PluginA struct { // AOption string `json:"aOption"` // } // // // External package: // // type MyAPIObject struct { // runtime.TypeMeta `json:",inline"` // MyPlugin runtime.RawExtension `json:"myPlugin"` // } // // type PluginA struct { // AOption string `json:"aOption"` // } // // // On the wire, the JSON will look something like this: // // { // "kind":"MyAPIObject", // "apiVersion":"v1", // "myPlugin": { // "kind":"PluginA", // "aOption":"foo", // }, // } // // So what happens? Decode first uses json or yaml to unmarshal the serialized data into // your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. // The next step is to copy (using pkg/conversion) into the internal struct. The runtime // package's DefaultScheme has conversion functions installed which will unpack the // JSON stored in RawExtension, turning it into the correct object type, and storing it // in the Object. (TODO: In the case where the object is of an unknown type, a // runtime.Unknown object will be created and stored.) // // +k8s:deepcopy-gen=true // +protobuf=true // +k8s:openapi-gen=true type RawExtension struct { // Raw is the underlying serialization of this object. // // TODO: Determine how to detect ContentType and ContentEncoding of 'Raw' data. Raw []byte `json:"-" protobuf:"bytes,1,opt,name=raw"` // Object can hold a representation of this extension - useful for working with versioned // structs. Object any `json:"-"` } ================================================ FILE: cli/cmd/encore/llm_rules/init.go ================================================ package llm_rules import ( "os" "path/filepath" "strings" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/internal/userconfig" "encr.dev/pkg/appfile" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/cockroachdb/errors" "github.com/spf13/cobra" ) var ( llmRulesToolFlag = cmdutil.Oneof{ Value: "", Allowed: LLMRulesFlagValues(), Flag: "llm-rules", FlagShort: "r", Desc: "Initialize the app with llm rules for a specific tool", TypeDesc: "string", } ) func init() { llmRules := &cobra.Command{ Use: "init", Short: "Initialize llm rules for this project", Args: cobra.ExactArgs(0), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { var tool Tool if llmRulesToolFlag.Value == "" { cfg, err := userconfig.Global().Get() if err != nil { cmdutil.Fatalf("Couldn't read user config: %s", err) } tool = Tool(cfg.LLMRules) } else { tool = Tool(llmRulesToolFlag.Value) } if err := initLLMRules(tool); err != nil { cmdutil.Fatal(err) } }, } llmRulesCmd.AddCommand(llmRules) llmRulesToolFlag.AddFlag(llmRules) } func initLLMRules(tool Tool) error { if tool == "" { var llmRulesModel ToolSelectModel { ls := list.NewDefaultItemStyles() ls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) ls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue)) del := list.NewDefaultDelegate() del.Styles = ls del.ShowDescription = false del.SetSpacing(0) items := make([]list.Item, 0, len(AllLLMRules)) for _, rule := range AllLLMRules { items = append(items, ToolItem{rule}) } ll := list.New(items, del, 0, 0) ll.SetShowTitle(false) ll.SetShowHelp(false) ll.SetShowPagination(true) ll.SetShowFilter(false) ll.SetFilteringEnabled(false) ll.SetShowStatusBar(false) ll.DisableQuitKeybindings() // quit handled by toolSelectModel llmRulesModel = ToolSelectModel{ List: ll, Predefined: LLMRulesToolNone, } llmRulesModel.SetSize(0, 20) } t := toolSelectorModel{ toolModel: llmRulesModel, } p := tea.NewProgram(t) result, err := p.Run() if err != nil { cmdutil.Fatal(err) } res := result.(toolSelectorModel) if res.aborted { os.Exit(1) } tool = res.toolModel.Selected() } // Determine the app root. root, _, err := cmdutil.MaybeAppRoot() if errors.Is(err, cmdutil.ErrNoEncoreApp) { cmdutil.Fatalf("no encore.app found, this command must be run from an Encore app directory") } if err != nil { cmdutil.Fatal(err) } // parse encore.app filePath := filepath.Join(root, "encore.app") encoreApp, err := appfile.ParseFile(filePath) if err != nil { cmdutil.Fatalf("couldn't parse encore.app: %s", err) } var lang cmdutil.Language switch encoreApp.Lang { case appfile.LangGo: lang = cmdutil.LanguageGo case appfile.LangTS: lang = cmdutil.LanguageTS } if err := SetupLLMRules(tool, lang, root, encoreApp.ID); err != nil { cmdutil.Fatal(err) } PrintLLMRulesInfo(tool) return nil } type toolSelectorModel struct { toolModel ToolSelectModel aborted bool } func (t toolSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmds []tea.Cmd c tea.Cmd ) switch msg := msg.(type) { case tea.WindowSizeMsg: t.SetSize(msg.Width, msg.Height) return t, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc", "q": t.aborted = true return t, tea.Quit } t.toolModel, c = t.toolModel.Update(msg) cmds = append(cmds, c) return t, tea.Batch(cmds...) case ToolSelectDone: cmds = append(cmds, tea.Quit) } t.toolModel, c = t.toolModel.Update(msg) cmds = append(cmds, c) return t, tea.Batch(cmds...) } func (t toolSelectorModel) Init() tea.Cmd { return nil } func (t toolSelectorModel) View() string { var b strings.Builder b.WriteString(t.toolModel.View()) return cmdutil.DocStyle.Render(b.String()) } func (t *toolSelectorModel) SetSize(width, height int) { t.toolModel.SetSize(width, height) } ================================================ FILE: cli/cmd/encore/llm_rules/llm_rules.go ================================================ package llm_rules import ( "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/root" ) var llmRulesCmd = &cobra.Command{ Use: "llm-rules", Short: "Commands to create LLM rules for apps", } func init() { root.Cmd.AddCommand(llmRulesCmd) } ================================================ FILE: cli/cmd/encore/llm_rules/tool.go ================================================ package llm_rules import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "time" "encr.dev/cli/cmd/encore/cmdutil" "github.com/briandowns/spinner" "github.com/fatih/color" ) const mdcTemplate string = `--- description: Encore %s rules globs: alwaysApply: true --- %s ` type Tool string // NOTE: changes to these values should also be reflected in userconfig const ( LLMRulesToolNone Tool = "" LLMRulesToolCursor Tool = "cursor" LLMRulesToolClaudCode Tool = "claudecode" LLMRulesToolVSCode Tool = "vscode" LLMRulesToolAgentsMD Tool = "agentsmd" LLMRulesToolZed Tool = "zed" ) // all available options exept for None var AllLLMRules = []Tool{ LLMRulesToolCursor, LLMRulesToolClaudCode, LLMRulesToolVSCode, LLMRulesToolAgentsMD, LLMRulesToolZed, } func LLMRulesFlagValues() []string { result := make([]string, 0, len(AllLLMRules)) for _, r := range AllLLMRules { result = append(result, string(r)) } return result } func (e Tool) Display() string { switch e { case LLMRulesToolCursor: return "Cursor" case LLMRulesToolClaudCode: return "Claude Code" case LLMRulesToolVSCode: return "VS Code" case LLMRulesToolAgentsMD: return "AGENTS.md" case LLMRulesToolZed: return "Zed" default: return "None" } } func (e Tool) SelectPrompt() string { return "Select a tool to generate LLM rules for" } type ToolItem struct { tool Tool } func NewLLMRulesItem(tool Tool) ToolItem { return ToolItem{tool: tool} } func (i ToolItem) FilterValue() string { return i.tool.Display() } func (i ToolItem) Title() string { return i.FilterValue() } func (i ToolItem) Description() string { return "" } func (i ToolItem) SelectedID() Tool { return i.tool } type ToolSelectModel = cmdutil.SimpleSelectModel[Tool, ToolItem] type ToolSelectDone = cmdutil.SimpleSelectDone[Tool] func SetupLLMRules(llmRules Tool, lang cmdutil.Language, appRootRelpath string, appSlug string) error { llmInstructions, err := downloadLLMInstructions(lang) if err != nil { return err } switch llmRules { case LLMRulesToolCursor: cursorDir := filepath.Join(appRootRelpath, ".cursor") rulesDir := filepath.Join(cursorDir, "rules") err := os.MkdirAll(rulesDir, 0755) if err != nil { return err } if appSlug != "" { // https://cursor.com/docs/context/mcp#using-mcpjson mcpPath := filepath.Join(cursorDir, "mcp.json") err = updateJsonFile(mcpPath, "mcpServers", func(mcpServers map[string]any) { // Add encore-mcp configuration mcpServers["encore-mcp"] = map[string]any{ "command": "encore", "args": []string{"mcp", "run", "--app=" + appSlug}, } }) if err != nil { return err } } // https://cursor.com/docs/context/rules // always overwrite as we have a dedicated encore config file err = os.WriteFile(filepath.Join(rulesDir, "encore.mdc"), fmt.Appendf(nil, mdcTemplate, lang, string(llmInstructions)), 0644) if err != nil { return err } case LLMRulesToolClaudCode: if appSlug != "" { // https://code.claude.com/docs/en/mcp#project-scope mcpPath := filepath.Join(appRootRelpath, ".mcp.json") err = updateJsonFile(mcpPath, "mcpServers", func(mcpServers map[string]any) { // Add encore-mcp configuration mcpServers["encore-mcp"] = map[string]any{ "command": "encore", "args": []string{"mcp", "run", "--app=" + appSlug}, } }) if err != nil { return err } } // https://code.claude.com/docs/en/settings#key-points-about-the-configuration-system claudeDir := filepath.Join(appRootRelpath, ".claude") if err := os.MkdirAll(claudeDir, 0755); err != nil { return err } err = writeNewFileOrSkip(filepath.Join(claudeDir, "CLAUDE.md"), []byte(llmInstructions)) if err != nil { return err } case LLMRulesToolVSCode: githubDir := filepath.Join(appRootRelpath, ".github") if err := os.MkdirAll(githubDir, 0755); err != nil { return err } // https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions#writing-your-own-copilot-instructionsmd-file err = writeNewFileOrSkip(filepath.Join(githubDir, "copilot-instructions.md"), []byte(llmInstructions)) if err != nil { return err } vscodePath := filepath.Join(appRootRelpath, ".vscode") if err := os.MkdirAll(vscodePath, 0755); err != nil { return err } // https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_configuration-format mcpPath := filepath.Join(vscodePath, "mcp.json") err = updateJsonFile(mcpPath, "servers", func(servers map[string]any) { // Add encore-mcp configuration servers["encore-mcp"] = map[string]any{ "command": "encore", "args": []string{"mcp", "run", "--app=" + appSlug}, } }) if err != nil { return err } case LLMRulesToolAgentsMD: // https://agents.md/ err = writeNewFileOrSkip(filepath.Join(appRootRelpath, "AGENTS.md"), []byte(llmInstructions)) if err != nil { return err } case LLMRulesToolZed: // https://zed.dev/docs/ai/rules#rules-files rulesPath := filepath.Join(appRootRelpath, ".rules") err = writeNewFileOrSkip(rulesPath, []byte(llmInstructions)) if err != nil { return err } if appSlug != "" { zedDir := filepath.Join(appRootRelpath, ".zed") err := os.MkdirAll(zedDir, 0755) if err != nil { return err } // https://zed.dev/docs/ai/mcp#as-custom-servers settingsPath := filepath.Join(zedDir, "settings.json") err = updateJsonFile(settingsPath, "context_servers", func(contextServers map[string]any) { // Add encore-mcp configuration contextServers["encore-mcp"] = map[string]any{ "command": "encore", "args": []string{"mcp", "run", "--app=" + appSlug}, "env": map[string]any{}, "source": "custom", } }) if err != nil { return err } } } return nil } func PrintLLMRulesInfo(tool Tool) { if tool == LLMRulesToolNone { return } cyan := color.New(color.FgCyan) cyanf := cyan.SprintfFunc() switch tool { case LLMRulesToolCursor, LLMRulesToolClaudCode, LLMRulesToolVSCode, LLMRulesToolZed: fmt.Printf("MCP: %s\n", cyanf("Configured in %s", tool.Display())) fmt.Println() } fmt.Printf("Try these prompts in %s:\n", tool.Display()) fmt.Println("→ \"add image uploads to my hello world app\"") fmt.Println("→ \"add a SQL database for storing user profiles\"") fmt.Println("→ \"add a pub/sub topic for sending notifications\"") fmt.Println() } func updateJsonFile(path, parent string, updateFn func(field map[string]any)) error { var conf map[string]any // Read existing mcp.json if it exists if existingData, err := os.ReadFile(path); err == nil { if err := json.Unmarshal(existingData, &conf); err != nil { return fmt.Errorf("failed to parse existing %s: %w", path, err) } } else { conf = make(map[string]any) } // Get or create mcpServers mcpServers, ok := conf[parent].(map[string]any) if !ok { mcpServers = make(map[string]any) conf[parent] = mcpServers } updateFn(mcpServers) // Write back the config data, err := json.MarshalIndent(conf, "", " ") if err != nil { return fmt.Errorf("failed to marshal mcp.json: %w", err) } err = os.WriteFile(path, data, 0644) if err != nil { return err } return nil } // write to file if it doesnt exist, and emits a warning and skips writing if the file exist func writeNewFileOrSkip(filePath string, data []byte) error { if _, err := os.Stat(filePath); err == nil { // File already exists, skip writing yellow := color.New(color.FgYellow) yellow.Printf("Warning: %s file already exists, skipping\n", filePath) } else { err = os.WriteFile(filePath, data, 0644) if err != nil { return err } } return nil } func downloadLLMInstructions(lang cmdutil.Language) (string, error) { fmt.Println("Downloading LLM Instructions...") var url string switch lang { case cmdutil.LanguageGo: url = "https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/go_llm_instructions.txt" case cmdutil.LanguageTS: url = "https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/ts_llm_instructions.txt" default: return "", fmt.Errorf("unsupported language") } s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Downloading LLM instructions..." s.Start() defer s.Stop() resp, err := http.Get(url) if err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error()) return "", err } return string(body), nil } ================================================ FILE: cli/cmd/encore/logs.go ================================================ package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "os" "os/signal" "time" "github.com/gorilla/websocket" "github.com/logrusorgru/aurora/v3" "github.com/rs/zerolog" "github.com/spf13/cobra" "encr.dev/cli/internal/platform" "encr.dev/pkg/appfile" ) var ( logsEnv string logsJSON bool logsQuiet bool ) var logsCmd = &cobra.Command{ Use: "logs [--env=prod] [--json]", Short: "Streams logs from your application", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { appRoot, _ := determineAppRoot() streamLogs(appRoot, logsEnv) }, } func streamLogs(appRoot, envName string) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() appSlug, err := appfile.Slug(appRoot) if err != nil { fatal(err) } else if appSlug == "" { fatal("app is not linked with Encore Cloud") } if envName == "" { envName = "@primary" } logs, err := platform.EnvLogs(ctx, appSlug, envName) if err != nil { var e platform.Error if errors.As(err, &e) { switch e.Code { case "env_not_found": fatalf("environment %q not found", envName) } } fatal(err) } go func() { <-ctx.Done() logs.Close() }() // Use the same configuration as the runtime zerolog.TimeFieldFormat = time.RFC3339Nano if !logsQuiet { fmt.Println(aurora.Gray(12, "Connected, waiting for logs...")) } cw := zerolog.NewConsoleWriter() for { _, message, err := logs.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { fatal("the server closed the connection unexpectedly.") } return } lines := bytes.Split(message, []byte("\n")) for _, line := range lines { // Pretty-print logs if requested and it looks like a JSON log line if !logsJSON && bytes.HasPrefix(line, []byte{'{'}) { if _, err := cw.Write(mapCloudFieldNamesToExpected(line)); err != nil { // Fall back to regular stdout in case of error os.Stdout.Write(line) os.Stdout.Write([]byte("\n")) } } else { os.Stdout.Write(line) os.Stdout.Write([]byte("\n")) } } } } // mapCloudFieldNamesToExpected detects if we're logging with GCP style logging and then swaps // the field names to what is expected by zerolog func mapCloudFieldNamesToExpected(jsonBytes []byte) []byte { unmarshaled := map[string]any{} err := json.Unmarshal(jsonBytes, &unmarshaled) if err != nil { return jsonBytes } _, hasSeverity := unmarshaled["severity"] _, hasExpectedLevelField := unmarshaled[zerolog.LevelFieldName] _, hasTimestamp := unmarshaled["timestamp"] _, hasExpectedTimeField := unmarshaled[zerolog.TimestampFieldName] // GCP logs have a severity field and a timestamp field and not the default level and timestamp if hasSeverity && !hasExpectedLevelField && hasTimestamp && !hasExpectedTimeField { unmarshaled[zerolog.LevelFieldName] = unmarshaled["severity"] delete(unmarshaled, "severity") unmarshaled[zerolog.TimestampFieldName] = unmarshaled["timestamp"] delete(unmarshaled, "timestamp") } else { // No changes, return the original bytes unmodified return jsonBytes } newBytes, err := json.Marshal(unmarshaled) if err != nil { return jsonBytes } return newBytes } func init() { rootCmd.AddCommand(logsCmd) logsCmd.Flags().StringVarP(&logsEnv, "env", "e", "", "Environment name to stream logs from (defaults to the primary environment)") logsCmd.Flags().BoolVar(&logsJSON, "json", false, "Whether to print logs in raw JSON format") logsCmd.Flags().BoolVarP(&logsQuiet, "quiet", "q", false, "Whether to print initial message when the command is waiting for logs") } ================================================ FILE: cli/cmd/encore/main.go ================================================ package main import ( "fmt" "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/tools/go/packages" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" // Register commands _ "encr.dev/cli/cmd/encore/app" _ "encr.dev/cli/cmd/encore/config" _ "encr.dev/cli/cmd/encore/k8s" _ "encr.dev/cli/cmd/encore/namespace" _ "encr.dev/cli/cmd/encore/secrets" ) // for backwards compatibility, for now var rootCmd = root.Cmd func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) if err := root.Cmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // determineAppRoot determines the app root by looking for the "encore.app" file, // initially in the current directory and then recursively in parent directories // up to the filesystem root. // It reports the absolute path to the app root, and the // relative path from the app root to the working directory. // On errors it prints an error message and exits. func determineAppRoot() (appRoot, relPath string) { return cmdutil.AppRoot() } func determineWorkspaceRoot(appRoot string) string { return cmdutil.WorkspaceRoot(appRoot) } func resolvePackages(dir string, patterns ...string) ([]string, error) { cfg := &packages.Config{ Mode: packages.NeedName, Dir: dir, } pkgs, err := packages.Load(cfg, patterns...) if err != nil { return nil, err } paths := make([]string, 0, len(pkgs)) for _, pkg := range pkgs { paths = append(paths, pkg.PkgPath) } return paths, nil } func displayError(out *os.File, err []byte) { cmdutil.DisplayError(out, err) } func fatal(args ...interface{}) { cmdutil.Fatal(args...) } func fatalf(format string, args ...interface{}) { cmdutil.Fatalf(format, args...) } func nonZeroPtr[T comparable](v T) *T { var zero T if v == zero { return nil } return &v } ================================================ FILE: cli/cmd/encore/mcp.go ================================================ package main import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "maps" "net/http" "os" "strings" "sync" "time" "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/cli/internal/jsonrpc2" ) var mcpCmd = &cobra.Command{ Use: "mcp", Short: "MCP (Message Context Provider) commands", } var ( appID string mcpPort int = 9900 ) var startCmd = &cobra.Command{ Use: "start", Short: "Starts an SSE based MCP session and prints the SSE URL", Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() if appID == "" { appID = cmdutil.AppSlugOrLocalID() } setupDaemon(ctx) _, _ = fmt.Fprintf(os.Stderr, " MCP Service is running!\n\n") _, _ = fmt.Fprintf(os.Stderr, " MCP SSE URL: %s\n", aurora.Cyan(fmt.Sprintf( "http://localhost:%d/sse?app=%s", mcpPort, appID))) _, _ = fmt.Fprintf(os.Stderr, " MCP stdio Command: %s\n", aurora.Cyan(fmt.Sprintf( "encore mcp run --app=%s", appID))) }, } type sseConnection struct { read func() (typ, data string, err error) close func() error appID string connected bool path string client *http.Client // Track outstanding request IDs mu sync.Mutex requestIDs map[jsonrpc2.ID]struct{} } func (c *sseConnection) Read() (typ, data string, err error) { typ, data, err = c.read() if err != nil { c.connected = false return "", "", err } return typ, data, nil } func (c *sseConnection) Close() error { if c.close != nil { c.connected = false return c.close() } return nil } func (c *sseConnection) reconnect(ctx context.Context) error { // Close the existing connection if there is one if c.close != nil { _ = c.close() } c.connected = false // Initial backoff duration backoff := 1000 * time.Millisecond maxBackoff := 10 * time.Second for { // Check if context is canceled select { case <-ctx.Done(): return ctx.Err() default: } if root.Verbosity > 0 { fmt.Fprintf(os.Stderr, "Reconnecting to MCP: %v\n", backoff) } // Try to connect err := c.connect(ctx) if err == nil { c.connected = true return nil } // If connection failed, wait and retry with exponential backoff if root.Verbosity > 0 { fmt.Fprintf(os.Stderr, "Failed to connect to MCP: %v, retrying in %v\n", err, backoff) } select { case <-time.After(backoff): // Double the backoff for next attempt, but cap at maxBackoff backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } case <-ctx.Done(): return ctx.Err() } } } func (c *sseConnection) connect(ctx context.Context) error { setupDaemon(ctx) if c.client == nil { c.client = &http.Client{} } // Initialize the request IDs map c.mu.Lock() c.requestIDs = make(map[jsonrpc2.ID]struct{}) c.mu.Unlock() resp, err := c.client.Get(fmt.Sprintf("http://localhost:%d/sse?app=%s", mcpPort, c.appID)) if err != nil { return err } if resp.StatusCode != 200 { resp.Body.Close() return fmt.Errorf("error getting session ID: %v", resp.Status) } c.read = eventReader(startLineReader(ctx, bufio.NewReader(resp.Body).ReadString)) c.close = resp.Body.Close c.connected = true // Read the endpoint path event, path, err := c.Read() if err != nil { return fmt.Errorf("error reading endpoint path: %v", err) } if event != "endpoint" { return fmt.Errorf("expected endpoint event, got %q", event) } c.path = path return nil } func (c *sseConnection) SendMessage(data []byte) error { if !c.connected { return fmt.Errorf("not connected to MCP") } if c.client == nil { c.client = &http.Client{} } // Track the request ID if it's a Call msg, err := jsonrpc2.DecodeMessage(data) if err == nil { if call, ok := msg.(*jsonrpc2.Call); ok { c.mu.Lock() c.requestIDs[call.ID()] = struct{}{} c.mu.Unlock() } } resp, err := c.client.Post(fmt.Sprintf("http://localhost:%d%s", mcpPort, c.path), "application/json", bytes.NewReader(data)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 202 { return fmt.Errorf("error forwarding request: %v", resp.Status) } return nil } // CreateErrorResponse creates a JSON-RPC error response with the correct ID if available func (c *sseConnection) CreateErrorResponse(id *jsonrpc2.ID, code int, message string) string { // Build the error response response := map[string]interface{}{ "jsonrpc": "2.0", "error": map[string]interface{}{ "code": code, "message": message, }, } // Include ID if available if id != nil { response["id"] = id // Remove from tracking as we're responding to it c.mu.Lock() delete(c.requestIDs, *id) c.mu.Unlock() } else { response["id"] = nil } // Marshal to JSON jsonData, err := json.Marshal(response) if err != nil { // Fallback if marshaling fails return fmt.Sprintf(`{"jsonrpc":"2.0","id":null,"error":{"code":%d,"message":"%s"}}`, code, message) } return string(jsonData) } // RemoveRequestID removes a request ID from tracking once a response is received func (c *sseConnection) RemoveRequestID(id jsonrpc2.ID) { c.mu.Lock() delete(c.requestIDs, id) c.mu.Unlock() } var runCmd = &cobra.Command{ Use: "run", Short: "Runs an stdio-based MCP session", Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() if appID == "" { appID = cmdutil.AppSlugOrLocalID() } if root.Verbosity > 0 { _, _ = fmt.Fprintf(os.Stderr, "Starting an MCP session for app %s\n", appID) } conn := &sseConnection{appID: appID} if err := conn.connect(ctx); err != nil { fmt.Fprintf(os.Stderr, "Error connecting to MCP: %v\n", err) os.Exit(1) } defer conn.Close() go func() { for { event, data, err := conn.Read() if err != nil { fmt.Fprintf(os.Stderr, "Error reading event: %v\n", err) conn.mu.Lock() requestIDs := maps.Clone(conn.requestIDs) conn.mu.Unlock() for id := range requestIDs { fmt.Println(conn.CreateErrorResponse(&id, -32700, "error")) } if err := conn.reconnect(ctx); err != nil { fmt.Fprintf(os.Stderr, "Error reconnecting to MCP: %v\n", err) os.Exit(1) } continue } if root.Verbosity > 0 { fmt.Fprintf(os.Stderr, "Received event: %s: %s\n", event, data) } if event == "message" { // If it's a response message, remove the ID from tracking responseMsg := struct { JSONRPC string `json:"jsonrpc"` ID *jsonrpc2.ID `json:"id"` Result interface{} `json:"result,omitempty"` Error interface{} `json:"error,omitempty"` }{} if err := json.Unmarshal([]byte(data), &responseMsg); err == nil && responseMsg.ID != nil { conn.RemoveRequestID(*responseMsg.ID) } fmt.Println(data) } } }() stdinReader := startLineReader(ctx, bufio.NewReader(os.Stdin).ReadBytes) if root.Verbosity > 0 { _, _ = fmt.Fprintf(os.Stderr, "Listening on stdin for MCP requests\n\n") } for { line, err := stdinReader() if err != nil { if err == io.EOF || err == context.Canceled { break } fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) os.Exit(1) } if strings.TrimSpace(string(line)) == "" { continue } msg, err := jsonrpc2.DecodeMessage(line) if err != nil { fmt.Fprintf(os.Stderr, "Error decoding request: %v\n", err) fmt.Println(conn.CreateErrorResponse(nil, -32700, "parse error")) continue } if root.Verbosity > 0 { fmt.Fprintf(os.Stderr, "Sending request: %s\n", line) } err = conn.SendMessage(line) if err != nil { fmt.Fprintf(os.Stderr, "Error sending message: %v\n", err) // Create error response with the request ID if available var requestID *jsonrpc2.ID if call, ok := msg.(*jsonrpc2.Call); ok { id := call.ID() requestID = &id } fmt.Println(conn.CreateErrorResponse(requestID, -32700, "error sending message")) continue } } }, } type lineResult[T any] struct { res T err error } func startLineReader[T any](ctx context.Context, rd func(byte) (T, error)) func() (T, error) { channel := make(chan lineResult[T]) go func() { for { line, err := rd('\n') // wait for Enter key if err != nil { channel <- lineResult[T]{err: err} return } channel <- lineResult[T]{res: line} } }() return func() (T, error) { var t T select { case <-ctx.Done(): return t, ctx.Err() case result := <-channel: if result.err != nil { return t, result.err } return result.res, nil } } } func eventReader(reader func() (string, error)) func() (typ, data string, err error) { return func() (typ, data string, err error) { var line string for { line, err = reader() if err != nil { return "", "", err } if strings.HasPrefix(line, "event:") { break } } typ = strings.TrimSpace(strings.TrimPrefix(line, "event:")) line, err = reader() if err != nil { return "", "", err } if !strings.HasPrefix(line, "data:") { return "", "", fmt.Errorf("expected data: prefix, got %q", line) } data = strings.TrimSpace(strings.TrimPrefix(line, "data:")) return typ, data, nil } } func init() { mcpCmd.AddCommand(runCmd) runCmd.Flags().StringVar(&appID, "app", "", "The app ID to use for the MCP session") mcpCmd.AddCommand(startCmd) startCmd.Flags().StringVar(&appID, "app", "", "The app ID to use for the MCP session") root.Cmd.AddCommand(mcpCmd) } ================================================ FILE: cli/cmd/encore/namespace/namespace.go ================================================ package namespace import ( "bytes" "cmp" "context" "encoding/json" "fmt" "os" "slices" "text/tabwriter" "time" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" daemonpb "encr.dev/proto/encore/daemon" ) var nsCmd = &cobra.Command{ Use: "namespace", Short: "Manage infrastructure namespaces", Aliases: []string{"ns"}, } func init() { output := cmdutil.Oneof{Value: "columns", Allowed: []string{"columns", "json"}} listCmd := &cobra.Command{ Use: "list", Short: "List infrastructure namespaces", Aliases: []string{"ls"}, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() daemon := cmdutil.ConnectDaemon(ctx) resp, err := daemon.ListNamespaces(ctx, &daemonpb.ListNamespacesRequest{AppRoot: appRoot}) if err != nil { cmdutil.Fatal(err) } nss := resp.Namespaces // Sort by active first, then name second. slices.SortFunc(nss, func(a, b *daemonpb.Namespace) int { if a.Active != b.Active { if a.Active { return -1 } else { return 1 } } return cmp.Compare(a.Name, b.Name) }) if output.Value == "json" { var buf bytes.Buffer buf.WriteByte('[') for i, ns := range nss { data, err := protojson.MarshalOptions{ UseProtoNames: true, EmitUnpopulated: true, }.Marshal(ns) if err != nil { cmdutil.Fatal(err) } if i > 0 { buf.WriteByte(',') } buf.Write(data) } buf.WriteByte(']') var dst bytes.Buffer if err := json.Indent(&dst, buf.Bytes(), "", " "); err != nil { cmdutil.Fatal(err) } _, _ = fmt.Fprintln(os.Stdout, dst.String()) return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.StripEscape) _, _ = fmt.Fprint(w, "NAME\tID\tACTIVE\n") for _, ns := range nss { active := "" if ns.Active { active = "yes" } _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", ns.Name, ns.Id, active) } _ = w.Flush() }, } output.AddFlag(listCmd) nsCmd.AddCommand(listCmd) } var createCmd = &cobra.Command{ Use: "create NAME", Short: "Create a new infrastructure namespace", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() daemon := cmdutil.ConnectDaemon(ctx) ns, err := daemon.CreateNamespace(ctx, &daemonpb.CreateNamespaceRequest{ AppRoot: appRoot, Name: args[0], }) if err != nil { cmdutil.Fatal(err) } _, _ = fmt.Fprintf(os.Stdout, "created namespace %s\n", ns.Name) }, } var deleteCmd = &cobra.Command{ Use: "delete NAME", Short: "Delete an infrastructure namespace", Aliases: []string{"del"}, Args: cobra.ExactArgs(1), ValidArgsFunction: namespaceListCompletion, Run: func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() daemon := cmdutil.ConnectDaemon(ctx) name := args[0] _, err := daemon.DeleteNamespace(ctx, &daemonpb.DeleteNamespaceRequest{ AppRoot: appRoot, Name: name, }) if err != nil { cmdutil.Fatal(err) } _, _ = fmt.Fprintf(os.Stdout, "deleted namespace %s\n", name) }, } func init() { var create bool switchCmd := &cobra.Command{ Use: "switch [--create] NAME", Short: "Switch to a different infrastructure namespace", Long: `Switch to a specified infrastructure namespace. Subsequent commands will use the given namespace by default. If -c is specified, the namespace will first be created before switching to it. You can use '-' as the namespace name to switch back to the previously active namespace. `, Args: cobra.ExactArgs(1), ValidArgsFunction: namespaceListCompletion, Run: func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() daemon := cmdutil.ConnectDaemon(ctx) ns, err := daemon.SwitchNamespace(ctx, &daemonpb.SwitchNamespaceRequest{ AppRoot: appRoot, Name: args[0], Create: create, }) if err != nil { cmdutil.Fatal(err) } _, _ = fmt.Fprintf(os.Stdout, "switched to namespace %s\n", ns.Name) }, } switchCmd.Flags().BoolVarP(&create, "create", "c", false, "create the namespace before switching") nsCmd.AddCommand(switchCmd) } func init() { nsCmd.AddCommand(createCmd) nsCmd.AddCommand(deleteCmd) root.Cmd.AddCommand(nsCmd) } func namespaceListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // List namespaces from the daemon for completion. ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() daemon := cmdutil.ConnectDaemon(ctx) resp, err := daemon.ListNamespaces(ctx, &daemonpb.ListNamespacesRequest{AppRoot: appRoot}) if err != nil { return nil, cobra.ShellCompDirectiveError } namespaces := make([]string, len(resp.Namespaces)) for i, ns := range resp.Namespaces { namespaces[i] = ns.Name } return namespaces, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cli/cmd/encore/rand.go ================================================ package main import ( cryptorand "crypto/rand" "encoding/base32" "encoding/base64" "encoding/hex" "fmt" "os" "strconv" "strings" "github.com/gofrs/uuid" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/pkg/words" ) var randCmd = &cobra.Command{ Use: "rand", Short: "Utilities for generating cryptographically secure random data", } func init() { rootCmd.AddCommand(randCmd) } // UUID command func init() { var v1, v4, v6, v7 bool uuidCmd := &cobra.Command{ Use: "uuid [-1|-4|-6|-7]", Short: "Generates a random UUID (defaults to version 4)", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { versions := map[bool]func() (uuid.UUID, error){ v1: uuid.NewV1, v4: uuid.NewV4, v6: uuid.NewV6, v7: uuid.NewV7, } fn, ok := versions[true] if !ok { fatalf("unsupported UUID version") } u, err := fn() if err != nil { fatalf("failed to generate UUID: %v", err) } _, _ = fmt.Println(u.String()) }, } uuidCmd.Flags().BoolVarP(&v1, "v1", "1", false, "Generate a version 1 UUID") uuidCmd.Flags().BoolVarP(&v4, "v4", "4", true, "Generate a version 4 UUID") uuidCmd.Flags().BoolVarP(&v6, "v6", "6", false, "Generate a version 6 UUID") uuidCmd.Flags().BoolVarP(&v7, "v7", "7", false, "Generate a version 7 UUID") uuidCmd.MarkFlagsMutuallyExclusive("v1", "v4", "v6", "v7") randCmd.AddCommand(uuidCmd) } // Bytes command func init() { format := cmdutil.Oneof{ Value: "hex", Allowed: []string{"hex", "base32", "base32hex", "base32crockford", "base64", "base64url", "raw"}, Flag: "format", FlagShort: "f", Desc: "Output format", } noPadding := false doFormat := func(data []byte) string { switch format.Value { case "hex": return hex.EncodeToString(data) case "base32": enc := base32.StdEncoding if noPadding { enc = enc.WithPadding(base32.NoPadding) } return enc.EncodeToString(data) case "base32hex": enc := base32.HexEncoding if noPadding { enc = enc.WithPadding(base32.NoPadding) } return enc.EncodeToString(data) case "base32crockford": enc := base32.NewEncoding("0123456789ABCDEFGHJKMNPQRSTVWXYZ") if noPadding { enc = enc.WithPadding(base32.NoPadding) } return enc.EncodeToString(data) case "base64": enc := base64.StdEncoding if noPadding { enc = enc.WithPadding(base64.NoPadding) } return enc.EncodeToString(data) case "base64url": enc := base64.URLEncoding if noPadding { enc = enc.WithPadding(base64.NoPadding) } return enc.EncodeToString(data) default: fatalf("unsupported output format: %s", format.Value) panic("unreachable") } } bytesCmd := &cobra.Command{ Use: "bytes BYTES [-f " + format.Alternatives() + "]", Short: "Generates random bytes and outputs them in the specified format", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { num, err := strconv.ParseInt(args[0], 10, 64) if err != nil { fatalf("invalid number of bytes: %v", err) } else if num < 1 { fatalf("number of bytes must be positive") } else if num > 1024*1024 { fatalf("too many bytes requested") } data := make([]byte, num) _, err = cryptorand.Read(data) if err != nil { fatalf("failed to generate random bytes: %v", err) } if format.Value == "raw" { _, err = os.Stdout.Write(data) if err != nil { fatalf("failed to write: %v", err) } } else { formatted := doFormat(data) if _, err := os.Stdout.WriteString(formatted); err != nil { fatalf("failed to write: %v", err) } _, _ = os.Stdout.Write([]byte{'\n'}) } }, } format.AddFlag(bytesCmd) bytesCmd.Flags().BoolVar(&noPadding, "no-padding", false, "omit padding characters from base32/base64 output") randCmd.AddCommand(bytesCmd) } // Words command func init() { var sep string wordsCmd := &cobra.Command{ Use: "words [--sep=SEPARATOR] NUM", Short: "Generates random 4-5 letter words for memorable passphrases", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { num, err := strconv.ParseInt(args[0], 10, 64) if err != nil { fatalf("invalid number of words: %v", err) } else if num < 1 { fatalf("number of words must be positive") } else if num > 1024 { fatalf("too many words requested") } selected, err := words.Select(int(num)) if err != nil { fatalf("failed to select words: %v", err) } formatted := strings.Join(selected, sep) if _, err := os.Stdout.WriteString(formatted); err != nil { fatalf("failed to write: %v", err) } _, _ = os.Stdout.Write([]byte{'\n'}) }, } wordsCmd.Flags().StringVarP(&sep, "sep", "s", " ", "separator between words") randCmd.AddCommand(wordsCmd) } ================================================ FILE: cli/cmd/encore/root/rootcmd.go ================================================ package root import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "encr.dev/pkg/errlist" ) var ( Verbosity int traceFile string // TraceFile is the file to write trace logs to. // If nil (the default), trace logs are not written. TraceFile *string ) var preRuns []func(cmd *cobra.Command, args []string) // AddPreRun adds a function to be executed before the command runs. func AddPreRun(f func(cmd *cobra.Command, args []string)) { preRuns = append(preRuns, f) } var Cmd = &cobra.Command{ Use: "encore", Short: "encore is the fastest way of developing backend applications", SilenceErrors: true, // We'll handle displaying an error in our main func CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, // Hide the "completion" command from help (used for generating auto-completions for the shell) }, PersistentPreRun: func(cmd *cobra.Command, args []string) { if traceFile != "" { TraceFile = &traceFile } level := zerolog.InfoLevel if Verbosity == 1 { level = zerolog.DebugLevel } else if Verbosity >= 2 { level = zerolog.TraceLevel } if Verbosity >= 1 { errlist.Verbose = true } log.Logger = log.Logger.Level(level) for _, f := range preRuns { f(cmd, args) } }, } func init() { Cmd.PersistentFlags().CountVarP(&Verbosity, "verbose", "v", "verbose output") Cmd.PersistentFlags().StringVar(&traceFile, "trace", "", "file to write execution trace data to") } ================================================ FILE: cli/cmd/encore/run.go ================================================ package main import ( "context" "fmt" "net" "os" "os/signal" "strconv" "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" "golang.org/x/term" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/cli/internal/onboarding" daemonpb "encr.dev/proto/encore/daemon" ) var ( color bool noColor bool // for "--no-color" compatibility debug = cmdutil.Oneof{ Value: "", NoOptDefVal: "enabled", Allowed: []string{"enabled", "break"}, Flag: "debug", FlagShort: "", // no short flag Desc: "Compile for debugging (disables some optimizations)", TypeDesc: "string", } watch bool listen string logLevel = cmdutil.Oneof{ Value: "", Allowed: []string{"trace", "debug", "info", "warn", "error"}, Flag: "level", FlagShort: "l", Desc: "Minimum log level to display", TypeDesc: "string", } port uint jsonLogs bool scrubSensitiveData bool browser = cmdutil.Oneof{ Value: "auto", Allowed: []string{"auto", "never", "always"}, Flag: "browser", FlagShort: "", // no short flag Desc: "Whether to open the local development dashboard in the browser on startup", TypeDesc: "string", } ) func init() { runCmd := &cobra.Command{ Use: "run [--debug] [--watch=true] [--level=TRACE] [--port=4000] [--listen=]", Short: "Runs your application", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { appRoot, wd := determineAppRoot() // If the user didn't explicitly set --watch and we're in debug mode, disable watching // as we typically don't want to swap the process when the user is debugging. if !cmd.Flag("watch").Changed && debug.Value != "" { watch = false } runApp(appRoot, wd) }, } isTerm := term.IsTerminal(int(os.Stdout.Fd())) rootCmd.AddCommand(runCmd) runCmd.Flags().BoolVarP(&watch, "watch", "w", true, "Watch for changes and live-reload") runCmd.Flags().StringVar(&listen, "listen", "", "Address to listen on (for example \"0.0.0.0:4000\")") runCmd.Flags().UintVarP(&port, "port", "p", 4000, "Port to listen on") runCmd.Flags().BoolVar(&jsonLogs, "json", false, "Display logs in JSON format") runCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)") runCmd.Flags().BoolVar(&color, "color", isTerm, "Whether to display colorized output") runCmd.Flags().BoolVar(&noColor, "no-color", false, "Equivalent to --color=false") runCmd.Flags().BoolVar(&scrubSensitiveData, "redact", false, "Redact sensitive data in traces when running locally") runCmd.Flags().MarkHidden("no-color") logLevel.AddFlag(runCmd) debug.AddFlag(runCmd) browser.AddFlag(runCmd) } // runApp runs the app. func runApp(appRoot, wd string) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() // Determine listen addr. var listenAddr string if listen == "" { // If we have no listen address at all, listen on localhost. // (we do this so MacOS's firewall doesn't ask for permission for the daemon to listen on all interfaces) listenAddr = fmt.Sprintf("127.0.0.1:%d", port) } else if _, _, err := net.SplitHostPort(listen); err == nil { // If --listen is given with a port, use that directly and ignore --port. listenAddr = listen } else { // Otherwise use --listen as the host and --port as the port. listenAddr = net.JoinHostPort(listen, strconv.Itoa(int(port))) } browserMode := daemonpb.RunRequest_BROWSER_AUTO switch browser.Value { case "auto": browserMode = daemonpb.RunRequest_BROWSER_AUTO case "never": browserMode = daemonpb.RunRequest_BROWSER_NEVER case "always": browserMode = daemonpb.RunRequest_BROWSER_ALWAYS } debugMode := daemonpb.RunRequest_DEBUG_DISABLED switch debug.Value { case "enabled": debugMode = daemonpb.RunRequest_DEBUG_ENABLED case "break": debugMode = daemonpb.RunRequest_DEBUG_BREAK } daemon := setupDaemon(ctx) stream, err := daemon.Run(ctx, &daemonpb.RunRequest{ AppRoot: appRoot, DebugMode: debugMode, Watch: watch, WorkingDir: wd, ListenAddr: listenAddr, Environ: os.Environ(), TraceFile: root.TraceFile, Namespace: nonZeroPtr(nsName), Browser: browserMode, LogLevel: nonZeroPtr(logLevel.Value), ScrubSensitiveData: scrubSensitiveData, }) if err != nil { fatal(err) } cmdutil.ClearTerminalExceptFirstNLines(1) var converter cmdutil.OutputConverter if !jsonLogs { converter = cmdutil.ConvertJSONLogs(cmdutil.Colorize(color && !noColor)) } code := cmdutil.StreamCommandOutput(stream, converter) if code == 0 { if state, err := onboarding.Load(); err == nil { if state.DeployHint.Set() { if err := state.Write(); err == nil { _, _ = fmt.Println(aurora.Sprintf("\nHint: deploy your app to the cloud by running: %s", aurora.Cyan("git push encore"))) } } } } os.Exit(code) } func init() { } ================================================ FILE: cli/cmd/encore/secrets/archive.go ================================================ package secrets import ( "strings" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" ) var archiveSecretCmd = &cobra.Command{ Deprecated: "Use the command 'encore secret delete ' to delete the secret group.\n", Use: "archive ", Short: "Archives a secret value", DisableFlagsInUseLine: true, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { doArchiveOrUnarchive(args[0], true) }, } var unarchiveSecretCmd = &cobra.Command{ Deprecated: "use the command 'encore secret delete ' to delete the secret group.\n", Use: "unarchive ", Short: "Unarchives a secret value", DisableFlagsInUseLine: true, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { doArchiveOrUnarchive(args[0], false) }, } func doArchiveOrUnarchive(groupID string, archive bool) { if !strings.HasPrefix(groupID, "secgrp") { cmdutil.Fatal("the id must begin with 'secgrp_'. Valid ids can be found with 'encore secret list '.") } // newer version // do nothing since we are providing the deprecated string from the cobra command } func init() { secretCmd.AddCommand(archiveSecretCmd) secretCmd.AddCommand(unarchiveSecretCmd) } ================================================ FILE: cli/cmd/encore/secrets/delete.go ================================================ package secrets import ( "context" "fmt" "strings" "time" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/platform" ) var forceFlag bool var deleteSecretCmd = &cobra.Command{ Use: "delete ", Short: "Deletes a secret value", DisableFlagsInUseLine: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Check if --yes / --force flag was passed to skip confirmation if !forceFlag { fmt.Printf("Are you sure you want to delete secret %q? [y/N]: ", args[0]) var response string _, _ = fmt.Scanln(&response) if response != "y" && response != "yes" { fmt.Println("Aborted.") return nil } } doDelete(args[0], true) return nil }, } func doDelete(groupID string, delete bool) { if !strings.HasPrefix(groupID, "secgrp") { cmdutil.Fatal("the id must begin with 'secgrp_'. Valid ids can be found with 'encore secret list '.") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := platform.UpdateSecretGroup(ctx, platform.UpdateSecretGroupParams{ ID: groupID, Delete: &delete, }) if err != nil { cmdutil.Fatal(err) } fmt.Printf("Successfully deleted secret group %s.\n", groupID) } func init() { deleteSecretCmd.Flags().BoolVarP(&forceFlag, "yes", "y", false, "Skip confirmation prompt") secretCmd.AddCommand(deleteSecretCmd) } ================================================ FILE: cli/cmd/encore/secrets/list.go ================================================ package secrets import ( "bytes" "cmp" "context" "fmt" "os" "slices" "strings" "text/tabwriter" "time" "github.com/fatih/color" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/platform" "encr.dev/cli/internal/platform/gql" ) var listSecretCmd = &cobra.Command{ Use: "list [keys...]", Short: "Lists secrets, optionally for a specific key", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { appSlug := cmdutil.AppSlug() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var keys []string if len(args) > 0 { keys = args } secrets, err := platform.ListSecretGroups(ctx, appSlug, keys) if err != nil { cmdutil.Fatal(err) } if keys == nil { // Print secrets overview var buf bytes.Buffer w := tabwriter.NewWriter(&buf, 0, 0, 3, ' ', tabwriter.StripEscape) _, _ = fmt.Fprint(w, "Secret Key\tProduction\tDevelopment\tLocal\tPreview\tSpecific Envs\t\n") const ( checkYes = "\u2713" checkNo = "\u2717" ) for _, s := range secrets { render := func(b bool) string { if b { return checkYes } else { return checkNo } } d := getSecretEnvDesc(s.Groups) if !d.hasAny { continue } _, _ = fmt.Fprintf(w, "%s\t%v\t%v\t%v\t%v\t", s.Key, render(d.prod), render(d.dev), render(d.local), render(d.preview)) // Render specific envs, if any for i, env := range d.specific { if i > 0 { _, _ = fmt.Fprintf(w, ",") } _, _ = fmt.Fprintf(w, "%s", env.Name) } _, _ = fmt.Fprint(w, "\t\n") } _ = w.Flush() // Add color to the checkmarks now that the table is correctly laid out. // We can't do it before since the tabwriter will get the alignment wrong // if we include a bunch of ANSI escape codes that it doesn't understand. r := strings.NewReplacer(checkYes, color.GreenString(checkYes), checkNo, color.RedString(checkNo)) _, _ = r.WriteString(os.Stdout, buf.String()) } else { // Specific secrets w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) _, _ = fmt.Fprint(w, "ID\tSecret Key\tEnvironment(s)\t\n") slices.SortFunc(secrets, func(a, b *gql.Secret) int { return cmp.Compare(a.Key, b.Key) }) for _, s := range secrets { // Sort the archived groups to the end slices.SortFunc(s.Groups, func(a, b *gql.SecretGroup) int { aa, ab := a.ArchivedAt != nil, b.ArchivedAt != nil if aa != ab { if aa { return 1 } else { return -1 } } else if aa { return a.ArchivedAt.Compare(*b.ArchivedAt) } else { return cmp.Compare(a.ID, b.ID) } }) for _, g := range s.Groups { var sel []string for _, s := range g.Selector { switch s := s.(type) { case *gql.SecretSelectorSpecificEnv: // If we have a specific environment, render the name // instead of the id (which is the default when using s.String()). sel = append(sel, "env:"+s.Env.Name) default: sel = append(sel, s.String()) } } s := fmt.Sprintf("%s\t%s\t%s\t", g.ID, s.Key, strings.Join(sel, ", ")) if g.DestroyedAt != nil { s += "(destroyed)\t" _, _ = color.New(color.CrossedOut).Fprintln(w, s) } else if g.ArchivedAt != nil { s += "(archived)\t" _, _ = color.New(color.Concealed).Fprintln(w, s) } else { _, _ = fmt.Fprintln(w, s) } } } _ = w.Flush() } }, } func init() { secretCmd.AddCommand(listSecretCmd) } type secretEnvDesc struct { hasAny bool // if there are any non-archived groups at all prod, dev, local, preview bool specific []*gql.Env } func getSecretEnvDesc(groups []*gql.SecretGroup) secretEnvDesc { var desc secretEnvDesc for _, g := range groups { if g.ArchivedAt != nil { continue } desc.hasAny = true for _, sel := range g.Selector { switch sel := sel.(type) { case *gql.SecretSelectorEnvType: switch sel.Kind { case "production": desc.prod = true case "development": desc.dev = true case "local": desc.local = true case "preview": desc.preview = true } case *gql.SecretSelectorSpecificEnv: desc.specific = append(desc.specific, sel.Env) } } } return desc } ================================================ FILE: cli/cmd/encore/secrets/secrets.go ================================================ package secrets import ( "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/root" ) var secretCmd = &cobra.Command{ Use: "secret", Short: "Secret management commands", Aliases: []string{"secrets"}, } func init() { root.Cmd.AddCommand(secretCmd) } ================================================ FILE: cli/cmd/encore/secrets/set.go ================================================ package secrets import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "slices" "sort" "strings" "syscall" "time" "github.com/cockroachdb/errors" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/platform" "encr.dev/cli/internal/platform/gql" daemonpb "encr.dev/proto/encore/daemon" ) var setSecretCmd = &cobra.Command{ Use: "set --type ", Short: "Sets a secret value", Long: ` Sets a secret value for one or more environment types. The valid environment types are 'prod', 'dev', 'pr' and 'local'. `, Example: ` Entering a secret directly in terminal: $ encore secret set --type dev,local MySecret Enter secret value: ... Successfully created secret value for MySecret. Piping a secret from a file: $ encore secret set --type dev,local,pr MySecret < my-secret.txt Successfully created secret value for MySecret. Note that this strips trailing newlines from the secret value.`, Args: cobra.ExactArgs(1), DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { setSecret(args[0]) }, } var secretEnvs secretEnvSelector type secretEnvSelector struct { devFlag bool prodFlag bool envTypes []string envNames []string } func init() { secretCmd.AddCommand(setSecretCmd) setSecretCmd.Flags().BoolVarP(&secretEnvs.devFlag, "dev", "d", false, "To set the secret for development use") setSecretCmd.Flags().BoolVarP(&secretEnvs.prodFlag, "prod", "p", false, "To set the secret for production use") setSecretCmd.Flags().StringSliceVarP(&secretEnvs.envTypes, "type", "t", nil, "environment type(s) to set for (comma-separated list)") setSecretCmd.Flags().StringSliceVarP(&secretEnvs.envNames, "env", "e", nil, "environment name(s) to set for (comma-separated list)") _ = setSecretCmd.Flags().MarkHidden("dev") _ = setSecretCmd.Flags().MarkHidden("prod") } func setSecret(key string) { plaintextValue := readSecretValue() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() appRoot, _ := cmdutil.AppRoot() appSlug := cmdutil.AppSlug() sel := secretEnvs.ParseSelector(ctx, appSlug) app, err := platform.GetApp(ctx, appSlug) if err != nil { cmdutil.Fatalf("unable to lookup app %s: %v", appSlug, err) } // Does a matching secret group already exist? secrets, err := platform.ListSecretGroups(ctx, app.Slug, []string{key}) if err != nil { cmdutil.Fatalf("unable to list secrets: %v", err) } if matching := findMatchingSecretGroup(secrets, key, sel); matching != nil { // We found a matching secret group. Update it. err := platform.CreateSecretVersion(ctx, platform.CreateSecretVersionParams{ GroupID: matching.ID, PlaintextValue: plaintextValue, Etag: matching.Etag, }) if err != nil { cmdutil.Fatalf("unable to update secret: %v", err) } fmt.Printf("Successfully updated secret value for %s.\n", key) return } // Otherwise create a new secret group. err = platform.CreateSecretGroup(ctx, platform.CreateSecretGroupParams{ AppID: app.ID, Key: key, PlaintextValue: plaintextValue, Selector: sel, Description: "", // not yet supported from CLI }) if err != nil { if ce, ok := getConflictError(err); ok { var errMsg strings.Builder fmt.Fprintln(&errMsg, "the environment selection conflicts with other secret values:") for _, c := range ce.Conflicts { fmt.Fprintf(&errMsg, "\t%s %s\n", c.GroupID, strings.Join(c.Conflicts, ", ")) } cmdutil.Fatal(errMsg.String()) } cmdutil.Fatalf("unable to create secret: %v", err) } daemon := cmdutil.ConnectDaemon(ctx) if _, err := daemon.SecretsRefresh(ctx, &daemonpb.SecretsRefreshRequest{AppRoot: appRoot}); err != nil { fmt.Fprintln(os.Stderr, "warning: failed to refresh secret secret, skipping:", err) } fmt.Printf("Successfully created secret value for %s.\n", key) } func (s secretEnvSelector) ParseSelector(ctx context.Context, appSlug string) []gql.SecretSelector { if s.devFlag && s.prodFlag { cmdutil.Fatal("cannot specify both --dev and --prod") } else if s.devFlag && (len(s.envTypes) > 0 || len(s.envNames) > 0) { cmdutil.Fatal("cannot combine --dev with --type/--env") } else if s.prodFlag && (len(s.envTypes) > 0 || len(s.envNames) > 0) { cmdutil.Fatal("cannot combine --prod with --type/--env") } // Look up the environments envMap := make(map[string]string) // name -> id envs, err := platform.ListEnvs(ctx, appSlug) if err != nil { cmdutil.Fatalf("unable to list environments: %v", err) } for _, env := range envs { envMap[env.Slug] = env.ID } var sel []gql.SecretSelector if s.devFlag { sel = append(sel, &gql.SecretSelectorEnvType{Kind: "development"}, &gql.SecretSelectorEnvType{Kind: "preview"}, &gql.SecretSelectorEnvType{Kind: "local"}, ) } else if s.prodFlag { sel = append(sel, &gql.SecretSelectorEnvType{Kind: "production"}) } else { // Parse env types and env names seenTypes := make(map[string]bool) validTypes := map[string]string{ // Actual names "development": "development", "production": "production", "preview": "preview", "local": "local", // Aliases "dev": "development", "prod": "production", "pr": "preview", "ephemeral": "preview", } for _, t := range s.envTypes { val, ok := validTypes[t] if !ok { cmdutil.Fatalf("invalid environment type %q", t) } if !seenTypes[val] { seenTypes[val] = true sel = append(sel, &gql.SecretSelectorEnvType{Kind: val}) } } for _, n := range s.envNames { envID, ok := envMap[n] if !ok { cmdutil.Fatalf("environment %q not found", n) } sel = append(sel, &gql.SecretSelectorSpecificEnv{Env: &gql.Env{ID: envID}}) } } if len(sel) == 0 { cmdutil.Fatal("must specify at least one environment with --type/--env (or --dev/--prod)") } return sel } // readSecretValue reads the secret value from the user. // If it's a terminal it becomes an interactive prompt, // otherwise it reads from stdin. func readSecretValue() string { var value string fd := syscall.Stdin if terminal.IsTerminal(int(fd)) { fmt.Fprint(os.Stderr, "Enter secret value: ") data, err := terminal.ReadPassword(int(fd)) if err != nil { cmdutil.Fatal(err) } value = string(data) fmt.Fprintln(os.Stderr) } else { data, err := io.ReadAll(os.Stdin) if err != nil { cmdutil.Fatal(err) } value = string(bytes.TrimRight(data, "\r\n")) } return value } // findMatchingSecretGroup find whether a matching secret group already exists // for the given secret key and selector. func findMatchingSecretGroup(secrets []*gql.Secret, key string, selector []gql.SecretSelector) *gql.SecretGroup { // canonicalize returns the secret selectors in canonical form canonicalize := func(sels []gql.SecretSelector) []string { var strs []string for _, s := range sels { strs = append(strs, s.String()) } sort.Strings(strs) return strs } want := canonicalize(selector) for _, s := range secrets { if s.Key == key { for _, g := range s.Groups { got := canonicalize(g.Selector) if slices.Equal(got, want) { return g } } } } return nil } func getConflictError(err error) (*gql.ConflictError, bool) { var gqlErr gql.ErrorList if !errors.As(err, &gqlErr) { return nil, false } for _, e := range gqlErr { if conflict := e.Extensions["conflict"]; len(conflict) > 0 { var cerr gql.ConflictError if err := json.Unmarshal(conflict, &cerr); err == nil { return &cerr, true } } } return nil, false } ================================================ FILE: cli/cmd/encore/sqlc.go ================================================ package main import ( "bufio" "encoding/json" "fmt" "io" "os" "path/filepath" "github.com/golang/protobuf/proto" "github.com/spf13/cobra" "github.com/sqlc-dev/sqlc/pkg/cli" "google.golang.org/protobuf/encoding/protojson" "encr.dev/proto/encore/daemon" ) type sqlcSQL struct { Schema string `json:"schema"` Queries string `json:"queries"` Engine string `json:"engine"` Codegen []sqlcCodegen `json:"codegen"` } type sqlcCodegen struct { Out string `json:"out"` Plugin string `json:"plugin"` } type sqlcPlugin struct { Name string `json:"name"` Process sqlcProcess `json:"process"` } type sqlcProcess struct { Cmd string `json:"cmd"` } type sqlcConfig struct { Version string `json:"version"` SQL []sqlcSQL `json:"sql"` Plugins []sqlcPlugin `json:"plugins"` } func init() { var useProto bool genCmd := &cobra.Command{ Use: "generate-sql-schema ", Short: "Plugin for SQLC: stores the parsed sqlc model in a protobuf file", Hidden: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { schemaPath, err := filepath.Abs(args[0]) if err != nil { return err } tmpDir, err := os.MkdirTemp("", "encore-sqlc") if err != nil { return err } defer func() { _ = os.RemoveAll(tmpDir) }() sqlcPath := filepath.Join(tmpDir, "sqlc.json") queryPath := filepath.Join(tmpDir, "query.sql") outPath := filepath.Join(tmpDir, "gen") // SQLC requires the schema path to be relative to the sqlc.json file schemaPath, err = filepath.Rel(tmpDir, schemaPath) if err != nil { return err } cfg := sqlcConfig{ Version: "2", SQL: []sqlcSQL{ { Schema: schemaPath, Queries: "query.sql", Engine: "postgresql", Codegen: []sqlcCodegen{ { Out: "gen", Plugin: "encore", }, }, }, }, Plugins: []sqlcPlugin{ { Name: "encore", Process: sqlcProcess{ Cmd: os.Args[0], }, }, }, } cfgData, err := json.Marshal(cfg) if err != nil { return err } err = os.WriteFile(sqlcPath, cfgData, 0644) if err != nil { return err } // SQLC requires at least one query to be present in the query file err = os.WriteFile(queryPath, []byte("-- name: Dummy :one\nSELECT 'dummy';"), 0644) if err != nil { return err } res := cli.Run([]string{"generate", "-f", sqlcPath}) if res != 0 { return fmt.Errorf("sqlc exited with code %d", res) } reqBlob, err := os.ReadFile(filepath.Join(outPath, "output.pb")) if !useProto { req := &daemon.SQLCPlugin_GenerateRequest{} if err := proto.Unmarshal(reqBlob, req); err != nil { return err } reqBlob, err = protojson.MarshalOptions{ EmitUnpopulated: true, Indent: " ", UseProtoNames: true, }.Marshal(req) if err != nil { return err } } w := bufio.NewWriter(os.Stdout) if _, err := w.Write(reqBlob); err != nil { return err } if err := w.Flush(); err != nil { return err } return nil }, } genCmd.Flags().BoolVar(&useProto, "proto", false, "Output the parsed schema as protobuf") pluginCmd := &cobra.Command{ Use: "/plugin.CodegenService/Generate", Short: "Plugin for SQLC: stores the parsed sqlc model in a protobuf file", Hidden: true, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { reqBlob, err := io.ReadAll(os.Stdin) if err != nil { return err } resp := &daemon.SQLCPlugin_GenerateResponse{ Files: []*daemon.SQLCPlugin_File{ { Name: "output.pb", Contents: reqBlob, }, }, } respBlob, err := proto.Marshal(resp) if err != nil { return err } w := bufio.NewWriter(os.Stdout) if _, err := w.Write(respBlob); err != nil { return err } if err := w.Flush(); err != nil { return err } return nil }, } rootCmd.AddCommand(genCmd) rootCmd.AddCommand(pluginCmd) } ================================================ FILE: cli/cmd/encore/telemetry.go ================================================ package main import ( "context" "fmt" "os" "slices" "strings" "github.com/logrusorgru/aurora/v3" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" "encr.dev/cli/internal/telemetry" "encr.dev/pkg/fns" daemonpb "encr.dev/proto/encore/daemon" ) var TelemetryDisabledByEnvVar = os.Getenv("DISABLE_ENCORE_TELEMETRY") == "1" var TelemetryDebugByEnvVar = os.Getenv("ENCORE_TELEMETRY_DEBUG") == "1" func printTelemetryStatus() { status := aurora.Green("Enabled").String() if !telemetry.IsEnabled() { status = aurora.Red("Disabled").String() } fmt.Println(aurora.Sprintf("%s\n", aurora.Bold("Encore Telemetry"))) items := [][2]string{ {"Status", status}, } if root.Verbosity > 0 { items = append(items, [2]string{"Install ID", telemetry.GetAnonID()}) } if telemetry.IsDebug() { items = append(items, [2]string{"Debug", aurora.Green("Enabled").String()}) } maxKeyLen := fns.Max(items, func(entry [2]string) int { return len(entry[0]) }) for _, item := range items { spacing := strings.Repeat(" ", maxKeyLen-len(item[0])) fmt.Printf("%s: %s%s\n", item[0], spacing, item[1]) } fmt.Println(aurora.Sprintf("\nLearn more: %s", aurora.Underline("https://encore.dev/docs/telemetry"))) } func updateTelemetry(ctx context.Context) { // Update the telemetry config on the daemon if it is running if cmdutil.IsDaemonRunning(ctx) { daemon := cmdutil.ConnectDaemon(ctx) _, err := daemon.Telemetry(ctx, &daemonpb.TelemetryConfig{ AnonId: telemetry.GetAnonID(), Enabled: telemetry.IsEnabled(), Debug: telemetry.IsDebug(), }) if err != nil { log.Debug().Err(err).Msgf("could not update daemon telemetry: %s", err) } } if err := telemetry.SaveConfig(); err != nil { log.Debug().Err(err).Msgf("could not save telemetry: %s", err) } } var telemetryCommand = &cobra.Command{ Use: "telemetry", Short: "Reports the current telemetry status", Run: func(cmd *cobra.Command, args []string) { printTelemetryStatus() }, } var telemetryEnableCommand = &cobra.Command{ Use: "enable", Short: "Enables telemetry reporting", Run: func(cmd *cobra.Command, args []string) { if telemetry.SetEnabled(true) { updateTelemetry(cmd.Context()) } printTelemetryStatus() }, } var telemetryDisableCommand = &cobra.Command{ Use: "disable", Short: "Disables telemetry reporting", Run: func(cmd *cobra.Command, args []string) { if telemetry.SetEnabled(false) { updateTelemetry(cmd.Context()) } printTelemetryStatus() }, } func isCommand(cmd *cobra.Command, name ...string) bool { for cmd != nil { if slices.Contains(name, cmd.Name()) { return true } cmd = cmd.Parent() } return false } func init() { telemetryCommand.AddCommand(telemetryEnableCommand, telemetryDisableCommand) rootCmd.AddCommand(telemetryCommand) root.AddPreRun(func(cmd *cobra.Command, args []string) { update := false if TelemetryDisabledByEnvVar { update = telemetry.SetEnabled(false) } if cmd.Use == "daemon" { return } update = update || telemetry.SetDebug(TelemetryDebugByEnvVar) if update { go updateTelemetry(cmd.Context()) } if telemetry.ShouldShowWarning() && !isCommand(cmd, "version", "completion") { fmt.Println() fmt.Println(aurora.Sprintf("%s: This CLI tool collects usage data to help us improve Encore.", aurora.Bold("Note"))) fmt.Println(aurora.Sprintf(" You can disable this by running '%s'.\n", aurora.Yellow("encore telemetry disable"))) telemetry.SetShownWarning() } }) } ================================================ FILE: cli/cmd/encore/test.go ================================================ package main import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "os/signal" "path/filepath" "slices" "strings" "time" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/cmd/encore/cmdutil" daemonpb "encr.dev/proto/encore/daemon" ) var testCmd = &cobra.Command{ Use: "test [go test flags]", Short: "Tests your application", Long: "Takes all the same flags as `go test`.", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { var ( traceFile string codegenDebug bool prepareOnly bool noColor bool ) // Support specific args but otherwise let all args be passed on to "go test" for i := 0; i < len(args); i++ { arg := args[i] if arg == "-h" || arg == "--help" { _ = cmd.Help() return } else if arg == "--trace" || strings.HasPrefix(arg, "--trace=") { // Drop this argument always. args = slices.Delete(args, i, i+1) i-- // We either have '--trace=file' or '--trace file'. // Handle both. if _, value, ok := strings.Cut(arg, "="); ok { traceFile = value } else { // Make sure there is a next argument. if i < len(args) { traceFile = args[i] args = slices.Delete(args, i, i+1) i-- } } } else if arg == "--codegen-debug" { codegenDebug = true args = slices.Delete(args, i, i+1) i-- } else if arg == "--prepare" { prepareOnly = true args = slices.Delete(args, i, i+1) i-- } else if arg == "--no-color" { noColor = true args = slices.Delete(args, i, i+1) i-- } } appRoot, relPath := determineAppRoot() exitCode, err := runTests(appRoot, relPath, args, traceFile, codegenDebug, prepareOnly, noColor) if err != nil { fatal(err) } os.Exit(exitCode) }, } func runTests(appRoot, testDir string, args []string, traceFile string, codegenDebug, prepareOnly, noColor bool) (int, error) { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-interrupt cancel() }() converter := cmdutil.ConvertJSONLogs(cmdutil.Colorize(!noColor)) if slices.Contains(args, "-json") { converter = convertTestEventOutputOnly(converter) } var tempDir string // only use temp dir if we are not compiling tests or only running prepare if !prepareOnly && !slices.Contains(args, "-o") && !slices.Contains(args, "-c") { var err error tempDir, err = os.MkdirTemp("", "encore-test") if err != nil { return 1, fmt.Errorf("couldn't create temp dir for test: %w", err) } defer func() { _ = os.RemoveAll(tempDir) }() } daemon := setupDaemon(ctx) // Is this a node package? packageJsonPath := filepath.Join(appRoot, "package.json") if _, err := os.Stat(packageJsonPath); err == nil || prepareOnly { spec, err := daemon.TestSpec(ctx, &daemonpb.TestSpecRequest{ AppRoot: appRoot, WorkingDir: testDir, Args: args, Environ: os.Environ(), TempDir: tempDir, }) if status.Code(err) == codes.NotFound { return 1, errors.New("application does not define any tests.\nNote: Add a 'test' script command to package.json to run tests.") } else if err != nil { return 1, err } if prepareOnly { for _, ln := range spec.Environ { fmt.Println(ln) } return 0, nil } cmd := exec.Command(spec.Command, spec.Args...) cmd.Env = spec.Environ cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { return exitErr.ExitCode(), nil } else { return 1, err } } return 0, nil } stream, err := daemon.Test(ctx, &daemonpb.TestRequest{ AppRoot: appRoot, WorkingDir: testDir, Args: args, Environ: os.Environ(), TraceFile: nonZeroPtr(traceFile), CodegenDebug: codegenDebug, TempDir: tempDir, }) if err != nil { return 1, err } return cmdutil.StreamCommandOutput(stream, converter), nil } func init() { testCmd.DisableFlagParsing = true rootCmd.AddCommand(testCmd) // Even though we've disabled flag parsing, we still need to define the flags // so that the help text is correct. testCmd.Flags().Bool("codegen-debug", false, "Dump generated code (for debugging Encore's code generation)") testCmd.Flags().Bool("prepare", false, "Prepare for running tests (without running them)") testCmd.Flags().String("trace", "", "Specifies a trace file to write trace information about the parse and compilation process to.") testCmd.Flags().Bool("no-color", false, "Disable colorized output") } func convertTestEventOutputOnly(converter cmdutil.OutputConverter) cmdutil.OutputConverter { return func(line []byte) []byte { // If this isn't a JSON log line, just return it as-is if len(line) == 0 || line[0] != '{' { return line } testEvent := &testJSONEvent{} if err := json.Unmarshal(line, testEvent); err == nil && testEvent.Action == "output" { if testEvent.Output != nil && (*(testEvent.Output))[0] == '{' { convertedLogs := textBytes(converter(*testEvent.Output)) testEvent.Output = &convertedLogs newLine, err := json.Marshal(testEvent) if err == nil { return append(newLine, '\n') } } } return line } } // testJSONEvent and textBytes taken from the Go source code type testJSONEvent struct { Time *time.Time `json:",omitempty"` Action string Package string `json:",omitempty"` Test string `json:",omitempty"` Elapsed *float64 `json:",omitempty"` Output *textBytes `json:",omitempty"` } // textBytes is a hack to get JSON to emit a []byte as a string // without actually copying it to a string. // It implements encoding.TextMarshaler, which returns its text form as a []byte, // and then json encodes that text form as a string (which was our goal). type textBytes []byte func (b *textBytes) MarshalText() ([]byte, error) { return *b, nil } func (b *textBytes) UnmarshalText(in []byte) error { *b = in return nil } ================================================ FILE: cli/cmd/encore/version.go ================================================ package main import ( "context" "fmt" "os" "strings" "time" "github.com/logrusorgru/aurora/v3" "github.com/spf13/cobra" "encr.dev/cli/internal/update" "encr.dev/internal/version" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Reports the current version of the encore application", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { var ( ver *update.LatestVersion err error ) if version.Version != "" { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() ver, err = update.Check(ctx) } // NOTE: This output format is relied on by the Encore IntelliJ plugin. // Don't change this without considering its impact on that plugin. fmt.Fprintln(os.Stdout, "encore version", version.Version) if err != nil { fatalf("could not check for update: %v", err) } else if ver.IsNewer(version.Version) { if ver.ForceUpgrade { fmt.Println(aurora.Red("An urgent security update for Encore is available.")) if ver.SecurityNotes != "" { fmt.Println(aurora.Sprintf(aurora.Yellow("%s"), ver.SecurityNotes)) } versionUpdateCmd.Run(cmd, args) } else { if ver.SecurityUpdate { fmt.Println(aurora.Sprintf(aurora.Red("A security update is update available: %s -> %s\nUpdate with: encore version update"), version.Version, ver.Version())) if ver.SecurityNotes != "" { fmt.Println(aurora.Sprintf(aurora.Yellow("%s"), ver.SecurityNotes)) } } else { fmt.Println(aurora.Sprintf(aurora.Yellow("Update available: %s -> %s\nUpdate with: encore version update"), version.Version, ver.Version())) } } } }, } var versionUpdateCmd = &cobra.Command{ Use: "update", Short: "Checks for an update of encore and, if one is available, runs the appropriate command to update it.", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { if version.Version == "" || strings.HasPrefix(version.Version, "devel") { fatal("cannot update development build, first install Encore from https://encore.dev/docs/install") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() ver, err := update.Check(ctx) if err != nil { fatalf("could not check for update: %v", err) } if ver.IsNewer(version.Version) { fmt.Printf("Upgrading Encore to %v...\n", ver.Version()) if err := ver.DoUpgrade(os.Stdout, os.Stderr); err != nil { fatalf("could not update: %v", err) os.Exit(1) } os.Exit(0) } else { fmt.Println("Encore already up to date.") } }, } func init() { versionCmd.AddCommand(versionUpdateCmd) rootCmd.AddCommand(versionCmd) } ================================================ FILE: cli/cmd/git-remote-encore/main.go ================================================ // Command git-remote-encore provides a gitremote helper for // interacting with Encore's git hosting without SSH keys, // by piggybacking on Encore's auth tokens. package main import ( "bufio" "fmt" "net/url" "os" "os/exec" "path/filepath" "strings" "encr.dev/internal/conf" ) func main() { if err := run(os.Args); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err) os.Exit(1) } } var isLocalTest = (func() bool { return filepath.Base(os.Args[0]) == "git-remote-encorelocal" })() // remoteScheme is the remote scheme we expect. // It's "encore" in general but "encorelocal" for local development. var remoteScheme = (func() string { if isLocalTest { return "encorelocal" } else { return "encore" } })() func run(args []string) error { stdin := bufio.NewReader(os.Stdin) stdout := os.Stdout // Read commands from stdin. for { cmd, err := stdin.ReadString('\n') if err != nil { return fmt.Errorf("unexpected error reading stdin: %v", err) } cmd = cmd[:len(cmd)-1] // skip trailing newline switch { case cmd == "capabilities": if _, err := stdout.Write([]byte("*connect\n\n")); err != nil { return err } case strings.HasPrefix(cmd, "connect "): service := cmd[len("connect "):] return connect(args, service) default: return fmt.Errorf("unsupported command: %s", cmd) } } } // connect implements the "connect" capability by copying data // to and from the remote end over gRPC. func connect(args []string, svc string) error { uri, err := url.Parse(args[2]) if err != nil { return fmt.Errorf("connect %s: invalid remote uri: %v", os.Args[2], err) } else if uri.Scheme != remoteScheme { return fmt.Errorf("connect %s: expected remote scheme %q, got %q", os.Args[2], remoteScheme, uri.Scheme) } appID := uri.Hostname() ts := conf.NewTokenSource() tok, err := ts.Token() if err != nil { return fmt.Errorf("could not get Encore auth token: %v", err) } f, err := os.CreateTemp("", "encore-token-auth-sentinel-key") if err != nil { return err } keyPath := f.Name() defer func() { _ = os.Remove(keyPath) }() if err := f.Chmod(0600); err != nil { _ = f.Close() return err } else if _, err := f.Write([]byte(SentinelPrivateKey)); err != nil { _ = f.Close() return err } else if err := f.Close(); err != nil { return err } // Create a dummy config file so that we can work around any host overrides // present on the system. cfg, err := os.CreateTemp("", "encore-dummy-ssh-config") if err != nil { return err } cfgPath := cfg.Name() defer func() { _ = os.Remove(cfgPath) }() // Communicate to Git that the connection is established. _, _ = os.Stdout.Write([]byte("\n")) sshServer, port := "git.encore.dev", "22" if isLocalTest { sshServer, port = "localhost", "9040" } // Set up an SSH tunnel with a sentinel key as a way to signal // Encore to use token-based authentication, and pass the token // as part of the command. cmd := exec.Command("ssh", "-x", "-T", "-F", cfgPath, "-o", "IdentitiesOnly=yes", "-i", keyPath, "-p", port, sshServer, fmt.Sprintf("token=%s %s '%s'", tok.AccessToken, svc, appID)) cmd.Env = []string{} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } // SentinelPrivateKey is a sentinel private key that Encore recognizes as // the key that communicates that the user wishes to do token-based authentication // instead of key-based authentication. // // NOTE: This is not a security problem. The key is meant to be public // and does not serve as a means of authentication. // nosemgrep const SentinelPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACCyj3F5Tp1eBIp7rMohszumYzlys/BFfmX/LVkXJS8magAAAJjsp3yz7Kd8 swAAAAtzc2gtZWQyNTUxOQAAACCyj3F5Tp1eBIp7rMohszumYzlys/BFfmX/LVkXJS8mag AAAEDMiwRrf5WET2mTKjKjX7z6vox3n6hKGKbP7V4MDtVre7KPcXlOnV4EinusyiGzO6Zj OXKz8EV+Zf8tWRclLyZqAAAAE2VuY29yZS1zZW50aW5lbC1rZXkBAg== -----END OPENSSH PRIVATE KEY----- ` ================================================ FILE: cli/cmd/tsbundler-encore/main.go ================================================ package main import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/evanw/esbuild/pkg/api" "encr.dev/internal/version" flag "github.com/spf13/pflag" ) var ( entryPoints []string specifiedEngines []string // replacementFile string outDir string bundle bool minify bool help bool logLevel int ) // main is the entry point for the tsbundler-encore command. // // It is responsible for parsing the command line flags, validating the input, and then triggering esbuild. // // Run with --help for more information. func main() { // Required flags // flag.StringVar(&replacementFile, "replacements", "", "Replacement file or json object (default read from stdin)") // Optional flags flag.StringVar(&outDir, "outdir", "dist", "Output directory") flag.BoolVar(&bundle, "bundle", true, "Bundle all dependencies") flag.BoolVar(&minify, "minify", false, "Minify output (default false)") flag.StringArrayVar(&specifiedEngines, "engine", []string{"node:21"}, "Target engine") flag.CountVarP(&logLevel, "verbose", "v", "Increase logging level (can be specified multiple times)") flag.BoolVarP(&help, "help", "h", false, "Print help") flag.Usage = printHelp flag.Parse() entryPoints = flag.Args() if help { printHelp() os.Exit(0) } // Validate input (note: these functions will exit on error) validateEntrypointParams() engines := readEngines() // replacements := readReplacementMapping() // Create our transformer plugin // rewritePlugin := api.Plugin{ // Name: "encore-codegen-transformer", // Setup: func(build api.PluginBuild) { // build.OnLoad( // api.OnLoadOptions{Filter: `\.(ts|js)(x?)$`}, // func(args api.OnLoadArgs) (api.OnLoadResult, error) { // replacement, found := replacements[args.Path] // if !found { // return api.OnLoadResult{}, nil // } // contentsBytes, err := os.ReadFile(replacement) // if err != nil { // return api.OnLoadResult{}, fmt.Errorf("error reading replacement file: %w", err) // } // content := string(contentsBytes) // return api.OnLoadResult{ // PluginName: "encore-codegen-transformer", // Contents: &content, // Loader: api.LoaderTS, // }, nil // }, // ) // }, // } banner := `// This file was bundled by Encore ` + version.Version + ` // // https://encore.dev` outBase := "" if len(entryPoints) == 1 { // If there's a single entrypoint, use its directory as the outbase // as otherwise esbuild won't include the "[dir]" token in the output. outBase = filepath.Dir(filepath.Dir(entryPoints[0])) } // Trigger esbuild result := api.Build(api.BuildOptions{ // Setup base settings LogLevel: api.LogLevelWarning - api.LogLevel(logLevel), Banner: map[string]string{"js": banner}, Charset: api.CharsetUTF8, Sourcemap: api.SourceMapLinked, Packages: api.PackagesExternal, Plugins: []api.Plugin{ // rewritePlugin, }, TreeShaking: api.TreeShakingTrue, // Set our build target Platform: api.PlatformNode, Format: api.FormatESModule, Target: api.ES2022, Engines: engines, // Minification settings MinifyWhitespace: minify, MinifySyntax: minify, MinifyIdentifiers: minify, // Pass in what we want to build EntryNames: "[dir]/[name]", EntryPoints: entryPoints, Bundle: bundle, Outdir: outDir, Outbase: outBase, Write: true, // Write to outdir OutExtension: map[string]string{ ".js": ".mjs", }, Define: map[string]string{ "ENCORE_DROP_TESTS": "true", }, }) if len(result.Errors) > 0 { os.Exit(1) } } func printHelp() { binary := filepath.Base(os.Args[0]) // Base usage help versionStr := fmt.Sprintf("tsbundler-encore (%s)", version.Version) _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", versionStr, strings.Repeat("=", len(versionStr))) _, _ = fmt.Fprintf(os.Stderr, "\nUsage: %s [options]\n", binary) flag.PrintDefaults() // Replacements help // _, _ = fmt.Fprintf(os.Stderr, "\nReplacements JSON Format:\n") // _, _ = fmt.Fprintf(os.Stderr, " {\n") // _, _ = fmt.Fprintf(os.Stderr, " \"/absolute/path/to/file.ts\": \"/path/to/replacement.ts\",\n") // _, _ = fmt.Fprintf(os.Stderr, " \"/absolute/path/to/file2.ts\": \"/path/to/replacement2.ts\"\n") // _, _ = fmt.Fprintf(os.Stderr, " }\n") // Engine help _, _ = fmt.Fprintf(os.Stderr, "\nEngines:\n\nEngines can be specified as a name, or a name and version separated by a colon,\nfor example \"node:21\" or \"node\". Multiple engines can be specified if required.\n\nThe supported engines are:\n") _, _ = fmt.Fprintf(os.Stderr, " - node\n") _, _ = fmt.Fprintf(os.Stderr, " - bun\n") _, _ = fmt.Fprintf(os.Stderr, " - deno\n") _, _ = fmt.Fprintf(os.Stderr, " - rhino\n") } // validateEntrypointParams validates that the entry points parameters was specified and that all entry points exist // and are readable on the file system. func validateEntrypointParams() { if len(entryPoints) == 0 { _, _ = fmt.Fprintf(os.Stderr, "Error: at least one entry point must be specified\n\n") printHelp() os.Exit(1) } for _, entryPoint := range entryPoints { if st, err := os.Stat(entryPoint); errors.Is(err, fs.ErrNotExist) { _, _ = fmt.Fprintf(os.Stderr, "Error: entry point %s does not exist\n", entryPoint) os.Exit(1) } else if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Error: error reading entry point %s: %s\n", entryPoint, err) os.Exit(1) } else if st.IsDir() { _, _ = fmt.Fprintf(os.Stderr, "Error: entry point %s is a directory\n", entryPoint) os.Exit(1) } } } // readReplacementMapping reads a replacement mapping from either a file or stdin depending // on if the replacementFile flag was specified. // // It then validates that all the keys are valid paths to files and the values are valid paths to files. // func readReplacementMapping() map[string]string { // out := make(map[string]string) // // If a replacement file was specified, read it // replacementFile = strings.TrimSpace(replacementFile) // if replacementFile != "" { // if replacementFile[0] == '{' { // err := json.Unmarshal([]byte(replacementFile), &out) // if err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error parsing replacement object: %s\n", err) // os.Exit(1) // } // } else { // data, err := os.ReadFile(replacementFile) // if err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error reading replacement file: %s\n", err) // os.Exit(1) // } // err = json.Unmarshal(data, &out) // if err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error parsing replacement file: %s\n", err) // os.Exit(1) // } // } // } else { // // Check something is being piped in // info, _ := os.Stdin.Stat() // if (info.Mode()&os.ModeCharDevice) != 0 || info.Size() <= 0 { // _, _ = fmt.Fprintf(os.Stderr, "Error: no replacement file specified and nothing piped in\n") // os.Exit(1) // } // // Otherwise, read from stdin // if err := json.NewDecoder(os.Stdin).Decode(&out); err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error reading replacement file from stdin: %s\n", err) // os.Exit(1) // } // } // // Validate that all the keys are valid paths to files and the values are valid paths to files // for key, value := range out { // // Validate key // if st, err := os.Stat(key); errors.Is(err, fs.ErrNotExist) { // _, _ = fmt.Fprintf(os.Stderr, "Error: replacement key %s does not exist\n", key) // os.Exit(1) // } else if err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error: error reading replacement key %s: %s\n", key, err) // os.Exit(1) // } else if st.IsDir() { // _, _ = fmt.Fprintf(os.Stderr, "Error: replacement key %s is a directory\n", key) // os.Exit(1) // } else if !filepath.IsAbs(key) { // _, _ = fmt.Fprintf(os.Stderr, "Error: replacement key %s is not an absolute path\n", key) // os.Exit(1) // } // // Validate value // if st, err := os.Stat(value); errors.Is(err, fs.ErrNotExist) { // _, _ = fmt.Fprintf(os.Stderr, "Error: replacement value %s does not exist\n", value) // os.Exit(1) // } else if err != nil { // _, _ = fmt.Fprintf(os.Stderr, "Error: error reading replacement value %s: %s\n", value, err) // os.Exit(1) // } else if st.IsDir() { // _, _ = fmt.Fprintf(os.Stderr, "Error: replacement value %s is a directory\n", value) // os.Exit(1) // } // } // return out // } // readEngines reads the engines from the specified flag and returns a list of engines. func readEngines() []api.Engine { if len(specifiedEngines) == 0 { _, _ = fmt.Fprintf(os.Stderr, "Error: at least one engine must be specified\n\n") printHelp() os.Exit(1) } var engines []api.Engine for _, engineName := range specifiedEngines { engineName = strings.ToLower(strings.TrimSpace(engineName)) engineName, engineVersion, _ := strings.Cut(engineName, ":") var eng api.Engine switch engineName { case "node", "bun": // Note: esbuild doesn't have a "bun" engine (yet), but to future proof we'll alias it to node eng = api.Engine{Name: api.EngineNode, Version: engineVersion} case "deno": eng = api.Engine{Name: api.EngineDeno, Version: engineVersion} case "rhino": eng = api.Engine{Name: api.EngineRhino, Version: engineVersion} default: _, _ = fmt.Fprintf(os.Stderr, "Error: unknown/unsupported engine %s\n\n", engineName) printHelp() os.Exit(1) } engines = append(engines, eng) } return engines } ================================================ FILE: cli/daemon/apps/apps.go ================================================ package apps import ( "database/sql" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/cockroachdb/errors" "github.com/golang/protobuf/proto" "github.com/rs/zerolog/log" "go4.org/syncutil" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/internal/manifest" "encr.dev/internal/conf" "encr.dev/internal/env" "encr.dev/internal/goldfish" "encr.dev/pkg/appfile" "encr.dev/pkg/fns" "encr.dev/pkg/watcher" "encr.dev/pkg/xos" meta "encr.dev/proto/encore/parser/meta/v1" ) var ErrNotFound = errors.New("app not found") func NewManager(db *sql.DB) *Manager { return &Manager{ db: db, instances: make(map[string]*Instance), } } // Manager keeps track of known apps and watches them for changes. type Manager struct { db *sql.DB setupWatch syncutil.Once appRegMu sync.Mutex appListeners []func(*Instance) watchMu sync.Mutex watchers []WatchFunc instanceMu sync.Mutex instances map[string]*Instance // root -> instance } type TrackOption func(*Instance) error func WithTutorial(tutorial string) TrackOption { return func(i *Instance) error { err := manifest.SetTutorial(i.root, tutorial) if err != nil { return errors.Wrap(err, "set tutorial") } i.tutorial = tutorial return nil } } // Track begins tracking an app, and marks it as updated // if the app is already tracked. func (mgr *Manager) Track(appRoot string, options ...TrackOption) (*Instance, error) { app, err := mgr.resolve(appRoot) for _, opt := range options { if err := opt(app); err != nil { return nil, err } } if err != nil { return nil, err } _, err = mgr.db.Exec(` INSERT OR REPLACE INTO app (root, local_id, platform_id, updated_at) VALUES (?, ?, ?, ?) `, app.root, app.localID, app.PlatformID(), time.Now()) if err != nil { return nil, errors.Wrap(err, "update app store") } log.Info().Str("app_id", app.PlatformOrLocalID()).Msg("tracking app") return app, nil } // FindLatestByPlatformID finds the most recently updated app instance with the given platformID. // If no such app is found it reports an error matching ErrNotFound. func (mgr *Manager) FindLatestByPlatformID(platformID string) (*Instance, error) { var root string err := mgr.db.QueryRow(` SELECT root FROM app WHERE platform_id = ? ORDER BY updated_at DESC LIMIT 1 `, platformID).Scan(&root) if errors.Is(err, sql.ErrNoRows) { return nil, errors.WithStack(ErrNotFound) } else if err != nil { return nil, errors.Wrap(err, "query app store") } return mgr.resolve(root) } func (mgr *Manager) FindLatestByPlatformOrLocalID(id string) (*Instance, error) { // Local ID do not contain hyphens, platform ID's always contain hyphens. if strings.Contains(id, "-") { return mgr.FindLatestByPlatformID(id) } var root string err := mgr.db.QueryRow(` SELECT root FROM app WHERE local_id = ? ORDER BY updated_at DESC LIMIT 1 `, id).Scan(&root) if errors.Is(err, sql.ErrNoRows) { return nil, errors.WithStack(ErrNotFound) } else if err != nil { return nil, errors.Wrap(err, "query app store") } return mgr.resolve(root) } // List lists all known apps. func (mgr *Manager) List() ([]*Instance, error) { roots, err := mgr.listRoots() if err != nil { return nil, err } var apps []*Instance for _, root := range roots { app, err := mgr.resolve(root) if errors.Is(err, fs.ErrNotExist) { log.Debug().Str("root", root).Msg("app no longer exists, skipping") // Delete the _, _ = mgr.db.Exec(`DELETE FROM app WHERE root = ?`, root) continue } else if err != nil { log.Error().Err(err).Str("root", root).Msg("unable to resolve app") continue } apps = append(apps, app) } return apps, nil } func (mgr *Manager) listRoots() ([]string, error) { rows, err := mgr.db.Query(`SELECT root FROM app`) if err != nil { return nil, errors.Wrap(err, "query app roots") } defer fns.CloseIgnore(rows) var roots []string for rows.Next() { var root string if err := rows.Scan(&root); err != nil { return nil, errors.Wrap(err, "scan row") } roots = append(roots, root) } err = errors.Wrap(rows.Err(), "iterate rows") return roots, err } // RegisterAppListener registers a callback that gets invoked every time // an app is tracked. func (mgr *Manager) RegisterAppListener(fn func(*Instance)) { mgr.instanceMu.Lock() defer mgr.instanceMu.Unlock() mgr.appRegMu.Lock() mgr.appListeners = append(mgr.appListeners, fn) mgr.appRegMu.Unlock() // Call the handler for all existing apps for _, inst := range mgr.instances { fn(inst) } } // WatchFunc is the signature of functions registered as app watchers. type WatchFunc func(*Instance, []watcher.Event) // WatchAll watches all apps for changes. func (mgr *Manager) WatchAll(fn WatchFunc) error { err := mgr.setupWatch.Do(func() error { // Begin tracking all known apps by calling List (since it calls resolve). _, err := mgr.List() return err }) if err != nil { return err } mgr.watchMu.Lock() mgr.watchers = append(mgr.watchers, fn) mgr.watchMu.Unlock() return nil } func (mgr *Manager) onWatchEvent(i *Instance, ev []watcher.Event) { mgr.watchMu.Lock() watchers := mgr.watchers mgr.watchMu.Unlock() for _, fn := range watchers { fn(i, ev) } } // resolve resolves the current information about the app located at appRoot. // If the app does not exist (either because appRoot does not exist, // or because encore.app does not exist within it), it reports an error // matching fs.ErrNotExist. func (mgr *Manager) resolve(appRoot string) (*Instance, error) { mgr.instanceMu.Lock() defer mgr.instanceMu.Unlock() if existing, ok := mgr.instances[appRoot]; ok { return existing, nil } platformID, err := readPlatformID(appRoot) if err != nil { return nil, err } // Parse the manifest file man, err := manifest.ReadOrCreate(appRoot) if err != nil { return nil, errors.Wrap(err, "parse manifest") } i := NewInstance(appRoot, man.LocalID, platformID) i.tutorial = man.Tutorial i.mgr = mgr if err := i.beginWatch(); err != nil && !errors.Is(err, fs.ErrNotExist) { log.Error().Err(err).Str("id", i.PlatformOrLocalID()).Msg("unable to begin watching app") } mgr.instances[appRoot] = i // Notify any listeners about the new app for _, fn := range mgr.appListeners { fn(i) } return i, nil } func (mgr *Manager) Close() error { mgr.instanceMu.Lock() defer mgr.instanceMu.Unlock() for _, inst := range mgr.instances { if err := inst.Close(); err != nil { log.Err(err).Str("id", inst.PlatformOrLocalID()).Msg("unable to close app instance") // do not return an error here as we want to close all instances } } return nil } // Instance describes an app instance known by the Encore daemon. type Instance struct { root string localID string platformID *goldfish.Cache[string] tutorial string // mgr is a reference to the manager that created it. // It may be nil if an instance was created without a manager. mgr *Manager watcher *watcher.Watcher setupWatch syncutil.Once watchMu sync.Mutex nextWatchID WatchSubscriptionID watchers map[WatchSubscriptionID]*watchSubscription mdMu sync.Mutex cachedMd *meta.Data } func NewInstance(root, localID, platformID string) *Instance { i := &Instance{ root: root, localID: localID, watchers: make(map[WatchSubscriptionID]*watchSubscription), } i.platformID = goldfish.New[string](1*time.Second, i.fetchPlatformID) if platformID != "" { i.platformID.Set(platformID) } return i } func (i *Instance) Tutorial() string { return i.tutorial } // Root returns the filesystem path for the app root. // It always returns a non-empty string. func (i *Instance) Root() string { return i.root } // LocalID reports a local, random id unique for this app, // as persisted in the .encore/manifest.json file. // It always returns a non-empty string. func (i *Instance) LocalID() string { return i.localID } // PlatformID reports the Encore Platform's ID for this app. // If the app is not linked it reports the empty string. func (i *Instance) PlatformID() string { val, _ := i.platformID.Get() return val } // PlatformOrLocalID reports PlatformID() if set and otherwise LocalID(). func (i *Instance) PlatformOrLocalID() string { if id := i.PlatformID(); id != "" { return id } return i.localID } // Name returns the platform ID for the app, or if there isn't one // it returns the folder name the app is in. func (i *Instance) Name() string { if id := i.PlatformID(); id != "" { return id } return filepath.Base(i.root) } func (i *Instance) fetchPlatformID() (string, error) { return readPlatformID(i.root) } func readPlatformID(appRoot string) (string, error) { // Parse the encore.app file path := filepath.Join(appRoot, appfile.Name) data, err := os.ReadFile(path) if err != nil { return "", err } encore, err := appfile.Parse(data) if err != nil { return "", errors.Wrap(err, "parse encore.app") } return encore.ID, nil } // Experiments returns the enabled experiments for this app. // // Note: we read the app file here instead of a cached value so we // can detect changes between runs of the compiler if we're in // watch mode. func (i *Instance) Experiments(environ []string) (*experiments.Set, error) { exp, err := appfile.Experiments(i.root) if err != nil { return nil, err } return experiments.FromAppFileAndEnviron(exp, environ) } func (i *Instance) Lang() appfile.Lang { appFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name)) if err != nil { return appfile.LangGo } return appFile.Lang } func (i *Instance) Hooks() (*appfile.Hooks, error) { appFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name)) if err != nil { return nil, err } return &appFile.Build.Hooks, nil } func (i *Instance) AppFile() (*appfile.File, error) { return appfile.ParseFile(filepath.Join(i.root, appfile.Name)) } func (i *Instance) BuildSettings() (appfile.Build, error) { appFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name)) if err != nil { return appfile.Build{}, err } return appFile.Build, nil } // GlobalCORS returns the CORS configuration for the app which // will be applied against all API gateways into the app func (i *Instance) GlobalCORS() (appfile.CORS, error) { cors, err := appfile.GlobalCORS(i.root) if err != nil { return appfile.CORS{}, err } // If there are no Global CORS return the default if cors == nil { return appfile.CORS{}, nil } return *cors, nil } func (i *Instance) Watch(fn WatchFunc) (WatchSubscriptionID, error) { if err := i.beginWatch(); err != nil { return 0, err } i.watchMu.Lock() i.nextWatchID++ id := i.nextWatchID i.watchers[id] = &watchSubscription{id, fn} i.watchMu.Unlock() return id, nil } func (i *Instance) Unwatch(id WatchSubscriptionID) { i.watchMu.Lock() delete(i.watchers, id) i.watchMu.Unlock() } func (i *Instance) beginWatch() error { return i.setupWatch.Do(func() error { watch, err := watcher.New(i.PlatformOrLocalID()) if err != nil { return errors.Wrap(err, "unable to create watcher") } i.watcher = watch if err := i.watcher.RecursivelyWatch(i.root); err != nil { return errors.Wrap(err, "unable to watch app") } // If we're in dev mode, we want to watch the runtime // too, so we can develop changes to the runtime without // needing to restart the application. if conf.DevDaemon { if err := i.watcher.RecursivelyWatch(env.EncoreRuntimesPath()); err != nil { return errors.Wrap(err, "unable to watch runtime") } } go func() { for { events, ok := i.watcher.WaitForEvents() if !ok { // We're done watching. return } if i.mgr != nil { i.mgr.onWatchEvent(i, events) } i.watchMu.Lock() watchers := i.watchers i.watchMu.Unlock() for _, sub := range watchers { sub.f(i, events) } } }() return nil }) } // CachePath returns the path to the cache directory for this app. // It creates the directory if it does not exist. func (i *Instance) CachePath() (string, error) { cacheDir, err := conf.CacheDir() if err != nil { return "", errors.Wrap(err, "unable to get encore cache dir") } // we use local ID to be stable if the app is linked to the platform later cacheDir = filepath.Join(cacheDir, i.localID) if err := os.MkdirAll(cacheDir, 0755); err != nil { return "", errors.Wrap(err, "unable to create app cache dir") } return cacheDir, nil } // CacheMetadata caches the metadata for this app onto the file system func (i *Instance) CacheMetadata(md *meta.Data) error { i.mdMu.Lock() defer i.mdMu.Unlock() i.cachedMd = md cacheDir, err := i.CachePath() if err != nil { return err } data, err := proto.Marshal(md) if err != nil { return errors.Wrap(err, "unable to marshal metadata") } err = xos.WriteFile(filepath.Join(cacheDir, "metadata.pb"), data, 0644) if err != nil { return errors.Wrap(err, "unable to write metadata") } return nil } // CachedMetadata returns the cached metadata for this app, if any func (i *Instance) CachedMetadata() (*meta.Data, error) { i.mdMu.Lock() defer i.mdMu.Unlock() if i.cachedMd != nil { return i.cachedMd, nil } cacheDir, err := i.CachePath() if err != nil { return nil, err } data, err := os.ReadFile(filepath.Join(cacheDir, "metadata.pb")) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, nil } return nil, errors.Wrap(err, "unable to read metadata") } md := &meta.Data{} err = proto.Unmarshal(data, md) if err != nil { return nil, errors.Wrap(err, "unable to unmarshal metadata") } i.cachedMd = md return md, nil } func (i *Instance) Close() error { if i.watcher != nil { return i.watcher.Close() } return nil } type WatchSubscriptionID int64 type watchSubscription struct { id WatchSubscriptionID f WatchFunc } ================================================ FILE: cli/daemon/check.go ================================================ package daemon import ( "encr.dev/cli/daemon/run" daemonpb "encr.dev/proto/encore/daemon" ) // Check checks the app for compilation errors. func (s *Server) Check(req *daemonpb.CheckRequest, stream daemonpb.Daemon_CheckServer) error { slog := &streamLog{stream: stream, buffered: false} log := newStreamLogger(slog) app, err := s.apps.Track(req.AppRoot) if err != nil { log.Error().Err(err).Msg("failed to resolve app") streamExit(stream, 1) return nil } buildDir, err := s.mgr.Check(stream.Context(), run.CheckParams{ App: app, WorkingDir: req.WorkingDir, CodegenDebug: req.CodegenDebug, Environ: req.Environ, Tests: req.ParseTests, }) exitCode := 0 if err != nil { exitCode = 1 log.Error().Msg(err.Error()) } if req.CodegenDebug && buildDir != "" { log.Info().Msgf("wrote generated code to: %s", buildDir) } streamExit(stream, exitCode) return nil } ================================================ FILE: cli/daemon/common.go ================================================ package daemon import ( "io" "net" "os" "runtime" "strconv" "strings" "syscall" "github.com/logrusorgru/aurora/v3" "encr.dev/cli/daemon/run" "encr.dev/cli/internal/onboarding" "encr.dev/pkg/errlist" meta "encr.dev/proto/encore/parser/meta/v1" ) // OnStart implements run.EventListener. func (s *Server) OnStart(r *run.Run) {} func (s *Server) OnCompileStart(r *run.Run) {} // OnReload implements run.EventListener. func (s *Server) OnReload(r *run.Run) {} // OnStop implements run.EventListener. func (s *Server) OnStop(r *run.Run) {} // OnStdout implements run.EventListener. func (s *Server) OnStdout(r *run.Run, line []byte) { s.mu.Lock() slog, ok := s.streams[r.ID] s.mu.Unlock() if ok { _, _ = slog.Stdout(true).Write(line) } } // OnStderr implements run.EventListener. func (s *Server) OnStderr(r *run.Run, line []byte) { s.mu.Lock() slog, ok := s.streams[r.ID] s.mu.Unlock() if ok { _, _ = slog.Stderr(true).Write(line) } } func (s *Server) OnError(r *run.Run, err *errlist.List) { s.mu.Lock() slog, ok := s.streams[r.ID] s.mu.Unlock() if ok { slog.Error(err) } } func showFirstRunExperience(run *run.Run, md *meta.Data, stdout io.Writer) { if state, err := onboarding.Load(); err == nil { if !state.FirstRun.IsSet() { // Is there a suitable endpoint to call? var rpc *meta.RPC var command string for _, svc := range md.Svcs { for _, r := range svc.Rpcs { if cmd := genCurlCommand(run, md, r); rpc == nil || len(command) < len(cmd) { rpc = r command = cmd } } } if rpc != nil { state.FirstRun.Set() if err := state.Write(); err == nil { _, _ = stdout.Write([]byte(aurora.Sprintf("\nHint: make an API call by running: %s\n", aurora.Cyan(command)))) } } } } } // findAvailableAddr attempts to find an available host:port that's near // the given startAddr. func findAvailableAddr(startAddr string) (host string, port int, ok bool) { host, portStr, err := net.SplitHostPort(startAddr) if err != nil { host = "localhost" portStr = "4000" } startPort, err := strconv.Atoi(portStr) if err != nil { startPort = 4000 } for p := startPort + 1; p <= startPort+10 && p <= 65535; p++ { addr := host + ":" + strconv.Itoa(p) ln, err := net.Listen("tcp", addr) if err == nil { _ = ln.Close() return host, p, true } } return "", 0, false } func genCurlCommand(run *run.Run, md *meta.Data, rpc *meta.RPC) string { var payload []byte method := rpc.HttpMethods[0] switch method { case "GET", "HEAD", "DELETE": // doesn't use HTTP body payloads default: payload = genSchema(md, rpc.RequestSchema) } var segments []string for _, seg := range rpc.Path.Segments { var v string switch seg.Type { default: v = "foo" case meta.PathSegment_LITERAL: v = seg.Value case meta.PathSegment_WILDCARD, meta.PathSegment_FALLBACK: v = "foo" case meta.PathSegment_PARAM: switch seg.ValueType { case meta.PathSegment_STRING: v = "foo" case meta.PathSegment_BOOL: v = "true" case meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64, meta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64: v = "1" case meta.PathSegment_UUID: v = "be23a21f-d12c-432c-91ec-fb8a52e23967" // some random UUID default: v = "foo" } } segments = append(segments, v) } parts := []string{"curl"} if (payload != nil && method != "POST") || (payload == nil && method != "GET") { parts = append(parts, " -X ", method) } // nosemgrep path := "/" + strings.Join(segments, "/") parts = append(parts, " http://", run.ListenAddr, path) if payload != nil { parts = append(parts, " -d '", string(payload), "'") } return strings.Join(parts, "") } // errIsAddrInUse reports whether the error is due to the address already being in use. func errIsAddrInUse(err error) bool { if opErr, ok := err.(*net.OpError); ok { if syscallErr, ok := opErr.Err.(*os.SyscallError); ok { if errno, ok := syscallErr.Err.(syscall.Errno); ok { const WSAEADDRINUSE = 10048 switch { case errno == syscall.EADDRINUSE: return true case runtime.GOOS == "windows" && errno == WSAEADDRINUSE: return true } } } } return false } ================================================ FILE: cli/daemon/create.go ================================================ package daemon import ( "context" "encr.dev/cli/daemon/apps" daemonpb "encr.dev/proto/encore/daemon" ) // CreateApp adds tracking for a new app func (s *Server) CreateApp(ctx context.Context, req *daemonpb.CreateAppRequest) (*daemonpb.CreateAppResponse, error) { var options []apps.TrackOption if req.Tutorial { options = append(options, apps.WithTutorial(req.Template)) } app, err := s.apps.Track(req.AppRoot, options...) if err != nil { return nil, err } return &daemonpb.CreateAppResponse{AppId: app.PlatformOrLocalID()}, nil } ================================================ FILE: cli/daemon/daemon.go ================================================ // Package daemon implements the Encore daemon gRPC server. package daemon import ( "bytes" "context" "errors" "io" "strings" "sync" "sync/atomic" "time" "github.com/golang/protobuf/ptypes/empty" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/mcp" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/secret" "encr.dev/cli/daemon/sqldb" "encr.dev/cli/internal/platform" "encr.dev/cli/internal/update" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/clientgen" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/errlist" "encr.dev/pkg/fns" daemonpb "encr.dev/proto/encore/daemon" meta "encr.dev/proto/encore/parser/meta/v1" ) var _ daemonpb.DaemonServer = (*Server)(nil) // Server implements daemonpb.DaemonServer. type Server struct { apps *apps.Manager mgr *run.Manager cm *sqldb.ClusterManager sm *secret.Manager ns *namespace.Manager mcp *mcp.Manager mu sync.Mutex streams map[string]*streamLog // run id -> stream availableVerInit sync.Once availableVer atomic.Value // string appDebounceMu sync.Mutex appDebouncers map[*apps.Instance]*regenerateCodeDebouncer daemonpb.UnimplementedDaemonServer } // New creates a new Server. func New(appsMgr *apps.Manager, mgr *run.Manager, cm *sqldb.ClusterManager, sm *secret.Manager, ns *namespace.Manager, mcp *mcp.Manager) *Server { srv := &Server{ apps: appsMgr, mgr: mgr, cm: cm, sm: sm, ns: ns, mcp: mcp, streams: make(map[string]*streamLog), appDebouncers: make(map[*apps.Instance]*regenerateCodeDebouncer), } mgr.AddListener(srv) // Check immediately for the latest version to avoid blocking 'encore run' go srv.availableUpdate() // Begin watching known apps for changes go srv.watchApps() return srv } // GenClient generates a client based on the app's API. func (s *Server) GenClient(ctx context.Context, params *daemonpb.GenClientRequest) (*daemonpb.GenClientResponse, error) { var md *meta.Data envName := params.EnvName if envName == "" { envName = "local" } if envName == "local" { var app *apps.Instance var err error // If the command was called with an app id, find the app instance by id. if params.AppRoot == "" { app, err = s.apps.FindLatestByPlatformOrLocalID(params.AppId) if errors.Is(err, apps.ErrNotFound) { return nil, status.Errorf(codes.FailedPrecondition, "the app %s must be run locally before generating a client for the 'local' environment.", params.AppId) } else if err != nil { return nil, status.Errorf(codes.Internal, "unable to query app info: %v", err) } } else { // Otherwise, track the app by its root directory. app, err = s.apps.Track(params.AppRoot) if err != nil { return nil, status.Errorf(codes.Internal, "unable to query app info: %v", err) } } // Get the app metadata expSet, err := app.Experiments(nil) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to parse app experiments: %v", err) } // Parse the app to figure out what infrastructure is needed. bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to prepare app: %v", err) } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to parse app metadata: %v", err) } md = parse.Meta if err := app.CacheMetadata(md); err != nil { return nil, status.Errorf(codes.Internal, "failed to cache app metadata: %v", err) } } else { meta, err := platform.GetEnvMeta(ctx, params.AppId, envName) if err != nil { if strings.Contains(err.Error(), "env_not_found") || strings.Contains(err.Error(), "env_not_deployed") { if envName == "@primary" { return nil, status.Error(codes.NotFound, "You have no deployments of this application.\n\nYou can generate the client for your local code by setting `--env=local`.") } return nil, status.Errorf(codes.NotFound, "A deployed environment called `%s` not found.\n\nYou can generate the client for your local code by setting `--env=local`.", envName) } return nil, status.Errorf(codes.Unavailable, "could not fetch API metadata: %v", err) } md = meta } lang := clientgen.Lang(params.Lang) servicesToGenerate := clientgentypes.NewServiceSet(md, params.Services, params.ExcludedServices) tagSet := clientgentypes.NewTagSet(params.EndpointTags, params.ExcludedEndpointTags) opts := clientgentypes.Options{} if params.OpenapiExcludePrivateEndpoints != nil { opts.OpenAPIExcludePrivateEndpoints = *params.OpenapiExcludePrivateEndpoints } if params.TsSharedTypes != nil { opts.TSSharedTypes = *params.TsSharedTypes } if params.TsClientTarget != nil { opts.TSClientTarget = *params.TsClientTarget } code, err := clientgen.Client(lang, params.AppId, md, servicesToGenerate, tagSet, opts) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } return &daemonpb.GenClientResponse{Code: code}, nil } func (s *Server) SecretsRefresh(ctx context.Context, req *daemonpb.SecretsRefreshRequest) (*daemonpb.SecretsRefreshResponse, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } s.sm.UpdateKey(app.PlatformID(), req.Key, req.Value) return &daemonpb.SecretsRefreshResponse{}, nil } // Version reports the daemon version. func (s *Server) Version(context.Context, *empty.Empty) (*daemonpb.VersionResponse, error) { configHash, err := version.ConfigHash() if err != nil { return nil, err } return &daemonpb.VersionResponse{ Version: version.Version, ConfigHash: configHash, }, nil } // availableUpdate checks for updates to Encore. // If there is a new version it returns it as a semver string. func (s *Server) availableUpdate() *update.LatestVersion { check := func() *update.LatestVersion { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() ver, err := update.Check(ctx) if err != nil { log.Error().Err(err).Msg("could not check for new encore release") } return ver } s.availableVerInit.Do(func() { ver := check() s.availableVer.Store(ver) go func() { for { time.Sleep(1 * time.Hour) if ver := check(); ver != nil { s.availableVer.Store(ver) } } }() }) curr := version.Version latest := s.availableVer.Load().(*update.LatestVersion) if latest.IsNewer(curr) { return latest } return nil } var errDatabaseNotFound = (func() error { st := status.New(codes.NotFound, "database not found") return st.Err() })() var errNotLinked = (func() error { st, err := status.New(codes.FailedPrecondition, "app not linked").WithDetails( &errdetails.PreconditionFailure{ Violations: []*errdetails.PreconditionFailure_Violation{ { Type: "NOT_LINKED", Description: "app is not linked with Encore Cloud", }, }, }, ) if err != nil { panic(err) } return st.Err() })() type commandStream interface { Send(msg *daemonpb.CommandMessage) error } func newStreamLogger(slog *streamLog) zerolog.Logger { return zerolog.New(zerolog.SyncWriter(slog.Stderr(false))).With().Timestamp().Logger() } type streamWriter struct { mu *sync.Mutex sl *streamLog stderr bool // if true write to stderr, otherwise stdout buffer bool } func (w streamWriter) Write(b []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.buffer && w.sl.buffered { if w.stderr { return w.sl.writeBuffered(&w.sl.stderr, b) } else { return w.sl.writeBuffered(&w.sl.stdout, b) } } return w.sl.writeStream(w.stderr, b) } func streamExit(stream commandStream, code int) { _ = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Exit{ Exit: &daemonpb.CommandExit{ Code: int32(code), }, }}) } type streamLog struct { stream commandStream mu sync.Mutex buffered bool stdout *bytes.Buffer // lazily allocated stderr *bytes.Buffer // lazily allocated } func (log *streamLog) Stdout(buffer bool) io.Writer { return streamWriter{mu: &log.mu, sl: log, stderr: false, buffer: buffer} } func (log *streamLog) Stderr(buffer bool) io.Writer { return streamWriter{mu: &log.mu, sl: log, stderr: true, buffer: buffer} } func (log *streamLog) Error(err *errlist.List) { log.mu.Lock() defer log.mu.Unlock() _ = err.SendToStream(log.stream) } func (log *streamLog) FlushBuffers() { var stdout, stderr []byte log.mu.Lock() defer log.mu.Unlock() if b := log.stdout; b != nil { stdout = b.Bytes() log.stdout = nil } if b := log.stderr; b != nil { stderr = b.Bytes() log.stderr = nil } _, _ = log.writeStream(false, stderr) _, _ = log.writeStream(true, stdout) log.buffered = false } func (log *streamLog) writeBuffered(b **bytes.Buffer, p []byte) (int, error) { if *b == nil { *b = &bytes.Buffer{} } return (*b).Write(p) } func (log *streamLog) writeStream(stderr bool, b []byte) (int, error) { out := &daemonpb.CommandOutput{} if stderr { out.Stderr = b } else { out.Stdout = b } err := log.stream.Send(&daemonpb.CommandMessage{ Msg: &daemonpb.CommandMessage_Output{ Output: out, }, }) if err != nil { return 0, err } return len(b), nil } ================================================ FILE: cli/daemon/dash/ai/assembler.go ================================================ package ai import ( "context" "slices" "strings" "encr.dev/pkg/fns" "encr.dev/pkg/idents" "encr.dev/v2/parser/apis/api/apienc" ) // partialEndpoint is a helper struct that is used to assemble the endpoint // from the incoming websocket updates. type partialEndpoint struct { service string endpoint *Endpoint } // notification generates a partially assembled endpoint structure to return to the client func (e *partialEndpoint) notification() LocalEndpointUpdate { e.endpoint.EndpointSource = e.endpoint.Render() e.endpoint.TypeSource = "" for i, s := range e.endpoint.Types { if i > 0 { e.endpoint.TypeSource += "\n\n" } e.endpoint.TypeSource += s.Render() } return LocalEndpointUpdate{ Service: e.service, Endpoint: e.endpoint, Type: "EndpointUpdate", } } func (e *partialEndpoint) upsertType(name, doc string) *Type { if name == "" { return nil } for _, s := range e.endpoint.Types { if s.Name == name { if doc != "" { s.Doc = wrapDoc(doc, 77) } return s } } si := &Type{Name: name, Doc: wrapDoc(doc, 77)} e.endpoint.Types = append(e.endpoint.Types, si) return si } func wrapDoc(doc string, width int) string { doc = strings.ReplaceAll(doc, "\n", " ") doc = strings.TrimSpace(doc) bytes := []byte(doc) i := 0 for { start := i if start+width >= len(bytes) { break } i += width for i > start && bytes[i] != ' ' { i-- } if i > start { bytes[i] = '\n' } else { for i < len(bytes) && bytes[i] != ' ' { i++ } if i < len(bytes) { bytes[i] = '\n' } } } return string(bytes) } func (e *partialEndpoint) upsertError(err ErrorUpdate) *Error { for _, s := range e.endpoint.Errors { if s.Code == err.Code { if err.Doc != "" { s.Doc = wrapDoc(err.Doc, 60) } return s } } si := &Error{Code: err.Code, Doc: wrapDoc(err.Doc, 60)} e.endpoint.Errors = append(e.endpoint.Errors, si) return si } func (e *partialEndpoint) upsertPathParam(up PathParamUpdate) PathSegment { for i, s := range e.endpoint.Path { if s.Value != nil && *s.Value == up.Param { if up.Doc != "" { e.endpoint.Path[i].Doc = wrapDoc(up.Doc, 73) } return s } } seg := PathSegment{ Type: SegmentTypeParam, ValueType: ptr[SegmentValueType]("string"), Value: &up.Param, Doc: wrapDoc(up.Doc, 73), } e.endpoint.Path = append(e.endpoint.Path, seg) return seg } func (e *partialEndpoint) upsertField(up TypeFieldUpdate) *Type { if up.Struct == "" { return nil } s := e.upsertType(up.Struct, "") for _, f := range s.Fields { if f.Name == up.Name { if up.Doc != "" { f.Doc = wrapDoc(up.Doc, 73) } if up.Type != "" { f.Type = up.Type } return s } } defaultLoc := apienc.Body isRequest := up.Struct == e.endpoint.RequestType if slices.Contains([]string{"GET", "HEAD", "DELETE"}, e.endpoint.Method) && isRequest { defaultLoc = apienc.Query } fi := &TypeField{ Name: up.Name, Doc: wrapDoc(up.Doc, 73), Type: up.Type, Location: defaultLoc, WireName: idents.Convert(up.Name, idents.CamelCase), } s.Fields = append(s.Fields, fi) return s } // The endpointsAssembler is a helper struct that is used to assemble the endpoint // from the incoming websocket updates. It keeps track of the existing endpoints and services // and updates them accordingly. type endpointsAssembler struct { eps map[string]*partialEndpoint } func newEndpointAssembler(existing []Service) *endpointsAssembler { eas := &endpointsAssembler{ eps: make(map[string]*partialEndpoint), } for _, svc := range existing { for _, ep := range svc.Endpoints { key := svc.Name + "." + ep.Name eas.eps[key] = &partialEndpoint{ service: svc.Name, endpoint: ep, } } } return eas } func (s *endpointsAssembler) upsertEndpoint(e EndpointUpdate) *partialEndpoint { for _, ep := range s.eps { if ep.service != e.Service || ep.endpoint.Name != e.Name { continue } if e.Doc != "" { ep.endpoint.Doc = wrapDoc(e.Doc, 77) } if e.Method != "" { ep.endpoint.Method = e.Method } if e.Visibility != "" { ep.endpoint.Visibility = e.Visibility } if len(e.Path) > 0 { ep.endpoint.Path = e.Path } if e.RequestType != "" { ep.endpoint.RequestType = e.RequestType ep.upsertType(e.RequestType, "") } if e.ResponseType != "" { ep.endpoint.ResponseType = e.ResponseType ep.upsertType(e.ResponseType, "") } if e.Errors != nil { ep.endpoint.Errors = fns.Map(e.Errors, func(e string) *Error { return &Error{Code: e} }) } return ep } ep := &partialEndpoint{ service: e.Service, endpoint: &Endpoint{ Name: e.Name, Doc: wrapDoc(e.Doc, 77), Method: e.Method, Visibility: e.Visibility, Path: e.Path, RequestType: e.RequestType, ResponseType: e.ResponseType, Errors: fns.Map(e.Errors, func(e string) *Error { return &Error{Code: e} }), Language: "GO", }, } s.eps[e.Service+"."+e.Name] = ep return ep } func (s *endpointsAssembler) endpoint(service, endpoint string) *partialEndpoint { key := service + "." + endpoint ep, ok := s.eps[key] if !ok { ep := &partialEndpoint{ service: service, endpoint: &Endpoint{Name: endpoint}, } s.eps[key] = ep } return ep } func newEndpointAssemblerHandler(existing []Service, notifier AINotifier, epComplete bool) AINotifier { epCache := newEndpointAssembler(existing) var lastEp *partialEndpoint return func(ctx context.Context, msg *AINotification) error { var ep *partialEndpoint msgVal := msg.Value switch val := msg.Value.(type) { case TypeUpdate: ep = epCache.endpoint(val.Service, val.Endpoint) ep.upsertType(val.Name, val.Doc) msgVal = ep.notification() case TypeFieldUpdate: ep = epCache.endpoint(val.Service, val.Endpoint) ep.upsertField(val) msgVal = ep.notification() case EndpointUpdate: ep = epCache.upsertEndpoint(val) msgVal = ep.notification() case ErrorUpdate: ep = epCache.endpoint(val.Service, val.Endpoint) ep.upsertError(val) msgVal = ep.notification() case PathParamUpdate: ep = epCache.endpoint(val.Service, val.Endpoint) ep.upsertPathParam(val) msgVal = ep.notification() } if epComplete && lastEp != ep { if lastEp != nil { msg.Value = struct { Type string `json:"type"` Service string `json:"service"` Endpoint string `json:"endpoint"` }{"EndpointComplete", lastEp.service, lastEp.endpoint.Name} if err := notifier(ctx, msg); err != nil || msg.Finished { return err } } lastEp = ep } msg.Value = msgVal return notifier(ctx, msg) } } ================================================ FILE: cli/daemon/dash/ai/client.go ================================================ package ai import ( "context" "fmt" "time" "github.com/cockroachdb/errors" "github.com/hasura/go-graphql-client" "github.com/hasura/go-graphql-client/pkg/jsonutil" "github.com/rs/zerolog/log" "encr.dev/internal/conf" ) type TaskMessage struct { Type string `graphql:"__typename"` ServiceUpdate `graphql:"... on ServiceUpdate"` TypeUpdate `graphql:"... on TypeUpdate"` TypeFieldUpdate `graphql:"... on TypeFieldUpdate"` ErrorUpdate `graphql:"... on ErrorUpdate"` EndpointUpdate `graphql:"... on EndpointUpdate"` SessionUpdate `graphql:"... on SessionUpdate"` TitleUpdate `graphql:"... on TitleUpdate"` PathParamUpdate `graphql:"... on PathParamUpdate"` } func (u *TaskMessage) GetValue() AIUpdateType { switch u.Type { case "ServiceUpdate": return u.ServiceUpdate case "TypeUpdate": return u.TypeUpdate case "TypeFieldUpdate": return u.TypeFieldUpdate case "ErrorUpdate": return u.ErrorUpdate case "EndpointUpdate": return u.EndpointUpdate case "SessionUpdate": return u.SessionUpdate case "TitleUpdate": return u.TitleUpdate case "PathParamUpdate": return u.PathParamUpdate } return nil } type AIStreamMessage struct { Value TaskMessage Error string Finished bool } type aiTask struct { Message *AIStreamMessage `graphql:"result"` } func getClient(errHandler func(err error)) *graphql.SubscriptionClient { client := graphql.NewSubscriptionClient(conf.WSBaseURL + "/graphql"). WithRetryTimeout(5 * time.Second). WithRetryDelay(2 * time.Second). WithRetryStatusCodes("500-599"). WithWebSocketOptions( graphql.WebsocketOptions{ HTTPClient: conf.AuthClient, }).WithSyncMode(true) go func() { log.Info().Msg("starting ai client") err := client.Run() log.Info().Msg("closed ai client") if err != nil { errHandler(err) } }() return client } type AITask struct { SubscriptionID string client *graphql.SubscriptionClient } func (t *AITask) Stop() error { return t.client.Unsubscribe(t.SubscriptionID) } // startAITask is a helper function to intitiate an AI query to the encore platform. The query // should be assembled to stream a 'result' graphql field that is a AIStreamMessage. func startAITask[Query any](ctx context.Context, params map[string]interface{}, notifier AINotifier) (*AITask, error) { var subId string var errStrReply = func(error string, code any) error { log.Error().Msgf("ai error: %s (%v)", error, code) _ = notifier(ctx, &AINotification{ SubscriptionID: subId, Error: &AIError{Message: error, Code: fmt.Sprintf("%v", code)}, Finished: true, }) return graphql.ErrSubscriptionStopped } var errReply = func(err error) error { var graphqlErr graphql.Errors if errors.As(err, &graphqlErr) { for _, e := range graphqlErr { _ = errStrReply(e.Message, e.Extensions["code"]) } return graphql.ErrSubscriptionStopped } return errStrReply(err.Error(), "") } var query Query client := getClient(func(err error) { _ = errReply(err) }) subId, err := client.Subscribe(&query, params, func(message []byte, err error) error { if err != nil { return errReply(err) } var result aiTask err = jsonutil.UnmarshalGraphQL(message, &result) if err != nil { return errReply(err) } if result.Message.Error != "" { return errStrReply(result.Message.Error, "") } err = notifier(ctx, &AINotification{ SubscriptionID: subId, Value: result.Message.Value.GetValue(), Finished: result.Message.Finished, }) if err != nil { return errReply(err) } return nil }) return &AITask{SubscriptionID: subId, client: client}, err } // AINotification is a wrapper around messages and errors from the encore platform ai service type AINotification struct { SubscriptionID string `json:"subscriptionId,omitempty"` Value any `json:"value,omitempty"` Error *AIError `json:"error,omitempty"` Finished bool `json:"finished,omitempty"` } type AIError struct { Message string `json:"message"` Code string `json:"code"` } type AINotifier func(context.Context, *AINotification) error ================================================ FILE: cli/daemon/dash/ai/codegen.go ================================================ package ai import ( "bytes" "context" "fmt" "go/ast" "go/parser" "go/token" "os" "path" "path/filepath" "runtime" "strings" "golang.org/x/exp/maps" "golang.org/x/tools/go/packages" "golang.org/x/tools/imports" "encr.dev/cli/daemon/apps" "encr.dev/internal/env" "encr.dev/pkg/fns" "encr.dev/pkg/paths" "encr.dev/v2/codegen/rewrite" "encr.dev/v2/internals/perr" "encr.dev/v2/internals/pkginfo" "encr.dev/v2/parser/apis/api/apienc" "encr.dev/v2/parser/apis/directive" ) const defAuthHandler = `package auth import ( "context" "encore.dev/beta/auth" ) type Data struct { Username string } //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) { panic("not yet implemented") }` const ( PathDocPrefix = "Path Parameters" ErrDocPrefix = "Errors" ) func (p PathSegments) Render() (docPath string, goParams []string) { var params []string return "/" + path.Join(fns.Map(p, func(s PathSegment) string { switch s.Type { case SegmentTypeLiteral: return *s.Value case SegmentTypeParam: params = append(params, fmt.Sprintf("%s %s", *s.Value, *s.ValueType)) return fmt.Sprintf(":%s", *s.Value) case SegmentTypeWildcard: params = append(params, fmt.Sprintf("%s %s", *s.Value, SegmentValueTypeString)) return fmt.Sprintf("*%s", *s.Value) case SegmentTypeFallback: params = append(params, fmt.Sprintf("%s %s", *s.Value, SegmentValueTypeString)) return fmt.Sprintf("!%s", *s.Value) default: panic(fmt.Sprintf("unknown path segment type: %s", s.Type)) } })...), params } func (s *Type) Render() string { rtn := strings.Builder{} if s.Doc != "" { rtn.WriteString(fmtComment(strings.TrimSpace(s.Doc), 0, 1)) } rtn.WriteString(fmt.Sprintf("type %s struct {\n", s.Name)) for i, f := range s.Fields { if i > 0 { rtn.WriteString("\n") } if f.Doc != "" { rtn.WriteString(fmtComment(strings.TrimSpace(f.Doc), 2, 1)) } tags := "" switch f.Location { case apienc.Body: tags = fmt.Sprintf(" `json:\"%s\"`", f.WireName) case apienc.Query: tags = fmt.Sprintf(" `query:\"%s\"`", f.WireName) case apienc.Header: tags = fmt.Sprintf(" `header:\"%s\"`", f.WireName) } rtn.WriteString(fmt.Sprintf(" %s %s%s\n", f.Name, f.Type, tags)) } rtn.WriteString("}") return rtn.String() } func (e *Endpoint) Render() string { buf := strings.Builder{} if e.Doc != "" { buf.WriteString(fmtComment(strings.TrimSpace(e.Doc)+"\n", 0, 1)) } buf.WriteString(renderDocList(PathDocPrefix, e.Path)) buf.WriteString(renderDocList(ErrDocPrefix, e.Errors)) pathStr, pathParams := e.Path.Render() params := []string{"ctx context.Context"} params = append(params, pathParams...) if e.RequestType != "" { params = append(params, "req *"+e.RequestType) } var rtnParams []string if e.ResponseType != "" { rtnParams = append(rtnParams, "*"+e.ResponseType) } rtnParams = append(rtnParams, "error") buf.WriteString(fmtComment("encore:api %s method=%s path=%s", 0, 0, e.Visibility, e.Method, pathStr)) paramsStr := strings.Join(params, ", ") rtnParamsStr := strings.Join(rtnParams, ", ") if len(rtnParams) > 1 { rtnParamsStr = fmt.Sprintf("(%s)", rtnParamsStr) } buf.WriteString(fmt.Sprintf("func %s(%s) %s", e.Name, paramsStr, rtnParamsStr)) return buf.String() } func indentItem(header, comment string) string { buf := strings.Builder{} buf.WriteString(header) for i, line := range strings.Split(strings.TrimSpace(comment), "\n") { indent := "" if i > 0 { indent = strings.Repeat(" ", len(header)) } buf.WriteString(fmt.Sprintf("%s%s\n", indent, line)) } return buf.String() } func renderDocList[T interface{ DocItem() (string, string) }](header string, items []T) string { maxLen := 0 items = fns.Filter(items, func(p T) bool { key, val := p.DocItem() if val == "" { return false } maxLen = max(maxLen, len(key)) return true }) buf := strings.Builder{} for i, item := range items { if i == 0 { buf.WriteString(header) buf.WriteString(":\n") } key, value := item.DocItem() spacing := strings.Repeat(" ", maxLen-len(key)) itemHeader := fmt.Sprintf(" - %s: %s", key, spacing) buf.WriteString(indentItem(itemHeader, value)) } return fmtComment(buf.String(), 0, 1) } // fmtComment prepends '//' to each line of the given comment and indents it with the given number of spaces. func fmtComment(comment string, before, after int, args ...any) string { if comment == "" { return "" } prefix := fmt.Sprintf("%s//%s", strings.Repeat(" ", before), strings.Repeat(" ", after)) result := prefix + strings.ReplaceAll(comment, "\n", "\n"+prefix) return fmt.Sprintf(result, args...) + "\n" } // generateSrcFiles generates source files for the given services. func generateSrcFiles(services []Service, app *apps.Instance) (map[paths.RelSlash]string, error) { svcPaths, err := newServicePaths(app) if err != nil { return nil, err } needAuth := fns.Any(fns.FlatMap(services, Service.GetEndpoints), (*Endpoint).Auth) files := map[paths.RelSlash]string{} if needAuth { md, err := app.CachedMetadata() if err != nil { return nil, err } if md.AuthHandler == nil { relFile, err := svcPaths.RelFileName("auth", "handler") if err != nil { return nil, err } file := paths.FS(app.Root()).JoinSlash(relFile) err = os.MkdirAll(file.Dir().ToIO(), 0755) if err != nil { return nil, err } files[relFile] = string(defAuthHandler) } } for _, s := range services { if svcPaths.IsNew(s.Name) { relFile, err := svcPaths.RelFileName(s.Name, s.Name) if err != nil { return nil, err } file := paths.FS(app.Root()).JoinSlash(relFile) err = os.MkdirAll(file.Dir().ToIO(), 0755) if err != nil { return nil, err } files[relFile] = fmt.Sprintf("%spackage %s\n", fmtComment(s.Doc, 0, 1), strings.ToLower(s.Name)) } for _, e := range s.Endpoints { relFile, err := svcPaths.RelFileName(s.Name, e.Name) if err != nil { return nil, err } filePath := paths.FS(app.Root()).JoinSlash(relFile) _, content := toSrcFile(filePath, s.Name, e.EndpointSource, e.TypeSource) files[relFile], err = addMissingFuncBodies(content) if err != nil { return nil, err } } } return files, nil } // addMissingFuncBodies adds a panic statement to functions that are missing a body. // This is used to generate a valid Go source file when the user has not implemented // the body of the endpoint functions. func addMissingFuncBodies(content []byte) (string, error) { set := token.NewFileSet() rewriter := rewrite.New(content, 0) file, err := parser.ParseFile(set, "", content, parser.ParseComments|parser.AllErrors) if err != nil { return "", err } ast.Inspect(file, func(n ast.Node) bool { switch n := n.(type) { case *ast.FuncDecl: if n.Body != nil { break } rewriter.Insert(n.End()-1, []byte(" {\n panic(\"not yet implemented\")\n}\n")) } return true }) return string(rewriter.Data()), err } // writeFiles writes the generated source files to disk. func writeFiles(services []Service, app *apps.Instance) ([]paths.RelSlash, error) { files, err := generateSrcFiles(services, app) if err != nil { return nil, err } for fileName, content := range files { root := paths.FS(app.Root()) err = os.WriteFile(root.JoinSlash(fileName).ToIO(), []byte(content), 0644) if err != nil { return nil, err } } return maps.Keys(files), nil } // toSrcFile wraps a code fragment in a package declaration and adds missing imports // using the goimports tool. func toSrcFile(filePath paths.FS, svc string, srcs ...string) (offset token.Position, data []byte) { const divider = "// @code-start\n" header := fmt.Sprintf("package %s\n\n", strings.ToLower(svc)) src := []byte(header + divider + strings.Join(srcs, "\n")) importedSrc, err := imports.Process(filePath.ToIO(), src, &imports.Options{ Comments: true, TabIndent: false, TabWidth: 4, }) // We don't need to handle the error here, as we'll catch parser/scanner errors in a later // phase. This is just a best effort to import missing packages. if err == nil { src = importedSrc } codeOffset := bytes.Index(src, []byte(divider)) // Remove the divider and any formatting made by the imports tool src = append(src[:codeOffset], strings.Join(srcs, "\n")...) // Compute offset of the user defined code lines := strings.Split(string(src[:codeOffset]), "\n") return token.Position{ Filename: filePath.ToIO(), Offset: codeOffset, Line: len(lines) - 1, Column: 0, }, src } // updateCode updates the source code fields of the EndpointInputs in the given services. // if overwrite is set, the code will be regenerated from scratch and replace the existing code, // otherwise, we'll modify the code in place func updateCode(ctx context.Context, services []Service, app *apps.Instance, overwrite bool) (rtn *SyncResult, err error) { overlays, err := newOverlays(app, overwrite, services...) fset := token.NewFileSet() perrs := perr.NewList(ctx, fset, overlays.ReadFile) defer func() { perr.CatchBailout(recover()) if rtn == nil { rtn = &SyncResult{ Services: services, } } rtn.Errors = overlays.validationErrors(perrs) }() for p, olay := range overlays.items { astFile, err := parser.ParseFile(fset, p.ToIO(), olay.content, parser.ParseComments|parser.AllErrors) if err != nil { perrs.AddStd(err) } rewriter := rewrite.New(olay.content, int(astFile.FileStart)) typeByName := map[string]*ast.GenDecl{} funcByName := map[string]*ast.FuncDecl{} for _, decl := range astFile.Decls { switch decl := decl.(type) { case *ast.GenDecl: if decl.Tok != token.TYPE { continue } for _, spec := range decl.Specs { typeSpec := spec.(*ast.TypeSpec) typeByName[typeSpec.Name.Name] = decl } case *ast.FuncDecl: funcByName[decl.Name.Name] = decl } } if olay.codeType == CodeTypeEndpoint { funcDecl, ok := funcByName[olay.endpoint.Name] if !ok { for _, f := range funcByName { dir, _, _ := directive.Parse(perrs, f.Doc) if dir != nil && dir.Name == "api" { funcDecl = f break } } } if funcDecl != nil { start := funcDecl.Pos() if funcDecl.Doc != nil { start = funcDecl.Doc.Pos() } end := funcDecl.End() if funcDecl.Body != nil { end = funcDecl.Body.Lbrace } rewriter.Replace(start, end, []byte(olay.endpoint.Render())) } else { if len(funcByName) > 0 { rewriter.Append([]byte("\n")) } rewriter.Append([]byte(olay.endpoint.Render())) } olay.content = rewriter.Data() content := string(olay.content[olay.headerOffset.Offset:]) olay.endpoint.EndpointSource = strings.TrimSpace(content) } else { for _, typ := range olay.endpoint.Types { typeSpec := typeByName[typ.Name] code := typ.Render() if typeSpec != nil { start := typeSpec.Pos() if typeSpec.Doc != nil { start = typeSpec.Doc.Pos() } rewriter.Replace(start, typeSpec.End(), []byte(code)) } else { rewriter.Append([]byte("\n\n" + code)) } } olay.content = rewriter.Data() content := string(olay.content[olay.headerOffset.Offset:]) olay.endpoint.TypeSource = strings.TrimSpace(content) } } goRoot := paths.RootedFSPath(env.EncoreGoRoot(), ".") // Parse the end result to catch any syntax errors pkginfo.UpdateGoPath(goRoot) pkgs, err := packages.Load(&packages.Config{ Mode: packages.NeedTypes | packages.NeedSyntax, Dir: app.Root(), Env: append(os.Environ(), "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH, "GOROOT="+goRoot.ToIO(), "PATH="+goRoot.Join("bin").ToIO()+string(filepath.ListSeparator)+os.Getenv("PATH"), ), Fset: fset, Overlay: overlays.PkgOverlay(), }, fns.Map(overlays.pkgPaths(), paths.Pkg.String)...) if err != nil { return nil, err } for _, pkg := range pkgs { for _, err := range pkg.Errors { // ignore missing function bodies error (it's allowed) if strings.Contains(err.Error(), "missing function body") { continue } perrs.AddStd(err) } } return &SyncResult{ Services: services, }, nil } ================================================ FILE: cli/daemon/dash/ai/conv.go ================================================ package ai import ( "slices" "strings" "encr.dev/pkg/clientgen" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" "encr.dev/v2/internals/resourcepaths" ) func toPathSegments(p *resourcepaths.Path, docs map[string]string) []PathSegment { rtn := make([]PathSegment, 0, len(p.Segments)) for _, s := range p.Segments { switch s.Type { case resourcepaths.Literal: rtn = append(rtn, PathSegment{Type: SegmentTypeLiteral, Value: ptr(s.Value)}) case resourcepaths.Param: rtn = append(rtn, PathSegment{ Type: SegmentTypeParam, Value: ptr(s.Value), ValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))), Doc: docs[s.Value], }) case resourcepaths.Wildcard: rtn = append(rtn, PathSegment{ Type: SegmentTypeWildcard, Value: ptr(s.Value), ValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))), Doc: docs[s.Value], }) case resourcepaths.Fallback: rtn = append(rtn, PathSegment{ Type: SegmentTypeFallback, Value: ptr(s.Value), ValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))), Doc: docs[s.Value], }) } } return rtn } func metaPathToPathSegments(metaPath *meta.Path) []PathSegment { var segments []PathSegment for _, seg := range metaPath.Segments { segments = append(segments, PathSegment{ Type: toSegmentType(seg.Type), Value: ptr(seg.Value), ValueType: ptr(toSegmentValueType(seg.ValueType)), }) } return segments } func toSegmentValueType(valueType meta.PathSegment_ParamType) SegmentValueType { switch valueType { case meta.PathSegment_UUID: return "string" default: return SegmentValueType(strings.ToLower(valueType.String())) } } func toSegmentType(segmentType meta.PathSegment_SegmentType) SegmentType { switch segmentType { case meta.PathSegment_LITERAL: return SegmentTypeLiteral case meta.PathSegment_PARAM: return SegmentTypeParam case meta.PathSegment_WILDCARD: return SegmentTypeWildcard case meta.PathSegment_FALLBACK: return SegmentTypeFallback default: panic("unknown segment type") } } func toVisibility(accessType meta.RPC_AccessType) VisibilityType { switch accessType { case meta.RPC_PUBLIC: return VisibilityTypePublic case meta.RPC_PRIVATE: return VisibilityTypePrivate case meta.RPC_AUTH: return "" default: panic("unknown access type") } } func renderTypesFromMetadata(md *meta.Data, svcs ...string) string { var types []*schema.Decl for _, metaSvc := range md.Svcs { if len(svcs) > 0 && !slices.Contains(svcs, metaSvc.Name) { continue } for _, rpc := range metaSvc.Rpcs { if rpc.RequestSchema != nil { types = append(types, md.Decls[rpc.RequestSchema.GetNamed().Id]) } if rpc.ResponseSchema != nil { types = append(types, md.Decls[rpc.ResponseSchema.GetNamed().Id]) } } } src, _ := clientgen.GenTypes(md, types...) return string(src) } func parseServicesFromMetadata(md *meta.Data, svcs ...string) []ServiceInput { services := []ServiceInput{} for _, metaSvc := range md.Svcs { if len(svcs) > 0 && !slices.Contains(svcs, metaSvc.Name) { continue } svc := ServiceInput{ Name: metaSvc.Name, } for _, rpc := range metaSvc.Rpcs { ep := &Endpoint{ Name: rpc.Name, Method: rpc.HttpMethods[0], Visibility: toVisibility(rpc.AccessType), Path: metaPathToPathSegments(rpc.Path), } if rpc.RequestSchema != nil { decl := md.Decls[rpc.RequestSchema.GetNamed().Id] ep.RequestType = decl.Name } if rpc.ResponseSchema != nil { decl := md.Decls[rpc.ResponseSchema.GetNamed().Id] ep.ResponseType = decl.Name } svc.Endpoints = append(svc.Endpoints, ep) } services = append(services, svc) } return services } ================================================ FILE: cli/daemon/dash/ai/manager.go ================================================ package ai import ( "context" "encr.dev/cli/daemon/apps" "encr.dev/pkg/fns" "encr.dev/pkg/paths" meta "encr.dev/proto/encore/parser/meta/v1" ) var ErrorCodeMap = map[string]int64{ "ai_task_limit_reached": 100, } // Manager exposes the ai functionality to the local dashboard type Manager struct{} func NewAIManager() *Manager { return &Manager{} } func (m *Manager) DefineEndpoints(ctx context.Context, appSlug string, sessionID AISessionID, prompt string, md *meta.Data, proposed []Service, notifier AINotifier) (*AITask, error) { svcs := fns.Map(proposed, Service.GetName) return startAITask[struct { Message *AIStreamMessage `graphql:"result: defineEndpoints(appSlug: $appSlug, sessionID: $sessionID, prompt: $prompt, current: $current, proposedDesign: $proposedDesign, existingTypes: $existingTypes)"` }](ctx, map[string]interface{}{ "appSlug": appSlug, "prompt": prompt, "current": parseServicesFromMetadata(md, svcs...), "proposedDesign": fns.Map(proposed, Service.GraphQL), "sessionID": sessionID, "existingTypes": renderTypesFromMetadata(md, svcs...), }, newEndpointAssemblerHandler(proposed, notifier, true)) } func (m *Manager) ProposeSystemDesign(ctx context.Context, appSlug, prompt string, md *meta.Data, notifier AINotifier) (*AITask, error) { return startAITask[struct { Message *AIStreamMessage `graphql:"result: proposeSystemDesign(appSlug: $appSlug, prompt: $prompt, current: $current)"` }](ctx, map[string]interface{}{ "appSlug": appSlug, "prompt": prompt, "current": parseServicesFromMetadata(md), }, newEndpointAssemblerHandler(nil, notifier, false)) } func (m *Manager) ModifySystemDesign(ctx context.Context, appSlug string, sessionID AISessionID, originalPrompt string, proposed []Service, newPrompt string, md *meta.Data, notifier AINotifier) (*AITask, error) { return startAITask[struct { Message *AIStreamMessage `graphql:"result: modifySystemDesign(appSlug: $appSlug, sessionID: $sessionID, originalPrompt: $originalPrompt, proposedDesign: $proposedDesign, newPrompt: $newPrompt, current: $current)"` }](ctx, map[string]interface{}{ "appSlug": appSlug, "originalPrompt": originalPrompt, "proposedDesign": fns.Map(proposed, Service.GraphQL), "current": parseServicesFromMetadata(md), "newPrompt": newPrompt, "sessionID": sessionID, }, newEndpointAssemblerHandler(proposed, notifier, false)) } func (m *Manager) ParseCode(ctx context.Context, services []Service, app *apps.Instance) (*SyncResult, error) { return parseCode(ctx, app, services) } func (m *Manager) UpdateCode(ctx context.Context, services []Service, app *apps.Instance, overwrite bool) (*SyncResult, error) { return updateCode(ctx, services, app, overwrite) } type WriteFilesResponse struct { FilesPaths []paths.RelSlash `json:"paths"` } func (m *Manager) WriteFiles(ctx context.Context, services []Service, app *apps.Instance) (*WriteFilesResponse, error) { files, err := writeFiles(services, app) return &WriteFilesResponse{FilesPaths: files}, err } type PreviewFile struct { Path paths.RelSlash `json:"path"` Content string `json:"content"` } type PreviewFilesResponse struct { Files []PreviewFile `json:"files"` } func (m *Manager) PreviewFiles(ctx context.Context, services []Service, app *apps.Instance) (*PreviewFilesResponse, error) { files, err := generateSrcFiles(services, app) return &PreviewFilesResponse{Files: fns.TransformMapToSlice(files, func(k paths.RelSlash, v string) PreviewFile { return PreviewFile{Path: k, Content: v} })}, err } ================================================ FILE: cli/daemon/dash/ai/overlay.go ================================================ package ai import ( "bytes" "fmt" "go/token" "io" "os" "strings" "time" "golang.org/x/exp/maps" "encr.dev/cli/daemon/apps" "encr.dev/pkg/errinsrc" "encr.dev/pkg/fns" "encr.dev/pkg/idents" "encr.dev/pkg/paths" meta "encr.dev/proto/encore/parser/meta/v1" "encr.dev/v2/internals/parsectx" "encr.dev/v2/internals/perr" ) // servicePaths is a helper struct to manage mapping between service names, pkg paths and filepaths // It's created by parsing the metadata of the app type servicePaths struct { relPaths map[string]paths.RelSlash root paths.FS module paths.Mod } func (s *servicePaths) IsNew(svc string) bool { _, ok := s.relPaths[svc] return !ok } func (s *servicePaths) Add(svc string, path paths.RelSlash) *servicePaths { s.relPaths[svc] = path return s } func (s *servicePaths) PkgPath(svc string) paths.Pkg { rel := s.RelPath(svc) return s.module.Pkg(rel) } func (s *servicePaths) FullPath(svc string) paths.FS { rel := s.RelPath(svc) return s.root.JoinSlash(rel) } func (s *servicePaths) RelPath(svc string) paths.RelSlash { pkgName, ok := s.relPaths[svc] if !ok { pkgName = paths.RelSlash(strings.ToLower(svc)) } return pkgName } func (s *servicePaths) FileName(svc, name string) (paths.FS, error) { relPath, err := s.RelFileName(svc, name) if err != nil { return "", err } return s.root.JoinSlash(relPath), nil } func (s *servicePaths) RelFileName(svc, name string) (paths.RelSlash, error) { pkgPath := s.FullPath(svc) name = idents.Convert(name, idents.SnakeCase) fileName := name + ".go" var i int for { fspath := pkgPath.Join(fileName) if _, err := os.Stat(fspath.ToIO()); os.IsNotExist(err) { return s.RelPath(svc).Join(fileName), nil } else if err != nil { return "", err } i++ fileName = fmt.Sprintf("%s_%d.go", name, i) } } func newServicePaths(app *apps.Instance) (*servicePaths, error) { md, err := app.CachedMetadata() if err != nil { return nil, err } pkgRelPath := fns.ToMap(md.Pkgs, func(p *meta.Package) string { return p.RelPath }) svcPaths := &servicePaths{ relPaths: map[string]paths.RelSlash{}, root: paths.FS(app.Root()), module: paths.Mod(md.ModulePath), } for _, svc := range md.Svcs { if pkgRelPath[svc.RelPath] != nil { svcPaths.Add(svc.Name, paths.RelSlash(pkgRelPath[svc.RelPath].RelPath)) } } return svcPaths, nil } // An overlay is a virtual file that is used to store the source code of an endpoint or types // It automatically generates a header with pkg name and imports. // It implements os.FileInfo and os.DirEntry interfaces type overlay struct { path paths.FS endpoint *Endpoint service *Service codeType CodeType content []byte headerOffset token.Position } func (o *overlay) Type() os.FileMode { return o.Mode() } func (o *overlay) Info() (os.FileInfo, error) { return o, nil } func (o *overlay) Name() string { return o.path.Base() } func (o *overlay) Size() int64 { return int64(len(o.content)) } func (o *overlay) Mode() os.FileMode { return os.ModePerm } func (o *overlay) ModTime() time.Time { return time.Now() } func (o *overlay) IsDir() bool { return false } func (o *overlay) Sys() any { //TODO implement me panic("implement me") } func (o *overlay) Stat() (os.FileInfo, error) { return o, nil } func (o *overlay) Reader() io.ReadCloser { return &overlayReader{o, bytes.NewReader(o.content)} } // overlayReader is a wrapper around the overlay to implement io.ReadCloser type overlayReader struct { *overlay *bytes.Reader } func (o *overlayReader) Close() error { return nil } var ( _ os.FileInfo = (*overlay)(nil) _ os.DirEntry = (*overlay)(nil) ) func newOverlays(app *apps.Instance, overwrite bool, services ...Service) (*overlays, error) { svcPaths, err := newServicePaths(app) if err != nil { return nil, err } o := &overlays{ items: map[paths.FS]*overlay{}, paths: svcPaths, } for _, s := range services { for _, e := range s.Endpoints { if overwrite { e.TypeSource = "" e.EndpointSource = "" } if err := o.add(s, e); err != nil { return nil, err } } } return o, nil } // overlays is a collection of virtual files that are used to store the source code of endpoints and types // in memory. It's modelled as a replacement for the os package. type overlays struct { items map[paths.FS]*overlay paths *servicePaths } func (o *overlays) Stat(name string) (os.FileInfo, error) { f, ok := o.items[paths.FS(name)] if !ok { // else return the filesystem file return os.Stat(name) } return f, nil } func (o *overlays) ReadDir(name string) ([]os.DirEntry, error) { entries := map[string]os.DirEntry{} osFiles, err := os.ReadDir(name) for _, f := range osFiles { entries[f.Name()] = f } dir := paths.FS(name) for _, info := range o.items { if dir == info.path.Dir() { entries[info.path.Base()] = info } } if len(entries) == 0 && err != nil { return nil, err } return maps.Values(entries), nil } func (o *overlays) PkgOverlay() map[string][]byte { files := map[string][]byte{} for f, info := range o.items { files[f.ToIO()] = info.content } return files } func (o *overlays) ReadFile(name string) ([]byte, error) { f, ok := o.items[paths.FS(name)] if !ok { // else return the filesystem file return os.ReadFile(name) } return f.content, nil } func (o *overlays) Open(name string) (io.ReadCloser, error) { f, ok := o.items[paths.FS(name)] if !ok { // else return the filesystem file return os.Open(name) } return f.Reader(), nil } func (o *overlays) pkgPaths() []paths.Pkg { pkgs := map[paths.Pkg]struct{}{} for _, info := range o.items { pkgs[o.paths.PkgPath(info.service.Name)] = struct{}{} } return maps.Keys(pkgs) } func (o *overlays) get(p paths.FS) (*overlay, bool) { rtn, ok := o.items[p] return rtn, ok } // validationErrors converts a perr.List into a slice of ValidationErrors func (o *overlays) validationErrors(list *perr.List) []ValidationError { var rtn []ValidationError for i := 0; i < list.Len(); i++ { err := list.At(i) rtn = append(rtn, o.validationError(err)...) } return rtn } // validationError translates errinsrc.ErrInSrc into a ValidationError which is a simplified error // used for displaying errors in the dashboard func (o *overlays) validationError(err *errinsrc.ErrInSrc) []ValidationError { if err.Params.Locations == nil { return []ValidationError{{ Message: err.Params.Summary, }} } var rtn []ValidationError for _, loc := range err.Params.Locations { o, ok := o.get(paths.FS(loc.File.FullPath)) if !ok { rtn = append(rtn, ValidationError{ Message: err.Params.Summary, }) continue } rtn = append(rtn, ValidationError{ Service: o.service.ID, Endpoint: o.endpoint.ID, CodeType: o.codeType, Message: err.Params.Summary, Start: &Pos{ Line: loc.Start.Line - o.headerOffset.Line, Column: loc.Start.Col - o.headerOffset.Column, }, End: &Pos{ Line: loc.End.Line - o.headerOffset.Line, Column: loc.End.Col - o.headerOffset.Column, }, }) } return rtn } // add creates new overlays for an endpoint and its types. // We create separate overlays for each endpoint and its types to allow for easier parsing and code generation. func (o *overlays) add(s Service, e *Endpoint) error { p, err := o.paths.FileName(s.Name, e.Name+"_func") if err != nil { return err } offset, content := toSrcFile(p, s.Name, e.EndpointSource) e.EndpointSource = string(content[offset.Offset:]) o.items[p] = &overlay{ path: p, endpoint: e, service: &s, codeType: CodeTypeEndpoint, content: content, headerOffset: offset, } p, err = o.paths.FileName(s.Name, e.Name+"_types") if err != nil { return err } offset, content = toSrcFile(p, s.Name, e.TypeSource) e.TypeSource = string(content[offset.Offset:]) o.items[p] = &overlay{ path: p, endpoint: e, service: &s, codeType: CodeTypeTypes, content: content, headerOffset: offset, } return nil } var ( _ parsectx.OverlaidOSFS = (*overlays)(nil) ) ================================================ FILE: cli/daemon/dash/ai/parser.go ================================================ package ai import ( "context" "go/ast" "go/token" "runtime" "slices" "strings" "github.com/rs/zerolog" "encr.dev/cli/daemon/apps" "encr.dev/internal/env" "encr.dev/pkg/fns" "encr.dev/pkg/paths" "encr.dev/v2/internals/parsectx" "encr.dev/v2/internals/perr" "encr.dev/v2/internals/pkginfo" "encr.dev/v2/internals/schema" "encr.dev/v2/parser/apis" "encr.dev/v2/parser/apis/api" "encr.dev/v2/parser/apis/api/apienc" "encr.dev/v2/parser/resource/resourceparser" ) // parseErrorList parses a list of errors docs from a doc string. func parseErrorList(doc string) (string, []*Error) { doc, errs := parseDocList(doc, ErrDocPrefix) return doc, fns.Map(errs, func(e docListItem) *Error { return &Error{ Code: e.Key, Doc: e.Doc, } }) } // parsePathList parses a list of path docs from a doc string. func parsePathList(doc string) (string, map[string]string) { doc, docs := parseDocList(doc, PathDocPrefix) rtn := map[string]string{} for _, d := range docs { rtn[d.Key] = d.Doc } return doc, rtn } // parseDocList parses a list of key-value pairs from a doc string. // e.g. // // Errors: // - NotFound: The requested resource was not found. // - InvalidArgument: The request had invalid arguments. func parseDocList(doc, section string) (string, []docListItem) { var errs []docListItem lines := strings.Split(doc, "\n") start := -1 end := -1 for i, line := range lines { end = i if strings.HasPrefix(strings.TrimSpace(line), section+":") { start = i } else if start == -1 { continue } else if len(line) > 2 { switch strings.TrimSpace(line[:2]) { case "-", "": default: end = i - 1 break } } lines[i] = strings.TrimSpace(line) if line == "" && lines[i-1] == "" { break } } if start == -1 { return doc, errs } for _, line := range lines[start+1 : end+1] { key, doc, ok := strings.Cut(line, ":") key = strings.TrimPrefix(key, "-") key = strings.TrimSpace(key) if ok { errs = append(errs, docListItem{ Key: key, Doc: strings.TrimSpace(doc), }) } else if len(errs) > 0 && line != "" { errs[len(errs)-1].Doc += "\n" + line } } return strings.Join(lines[:start], "\n"), errs } // docListItem represents a key-value pair in a doc list. type docListItem struct { Key string Doc string } // deref returns the underlying type of a pointer type. func deref(p schema.Type) schema.Type { for { if pt, ok := p.(schema.PointerType); ok { p = pt.Elem } else { return p } } } // parseCode updates the structured EndpointInput data based on the code in // EndpointInput.TypeSource and EndpointInput.EndpointSource fields. func parseCode(ctx context.Context, app *apps.Instance, services []Service) (rtn *SyncResult, err error) { // assamble an overlay with all our newly defined endpoints overlays, err := newOverlays(app, false, services...) if err != nil { return nil, err } fs := token.NewFileSet() errs := perr.NewList(ctx, fs, overlays.ReadFile) rootDir := paths.RootedFSPath(app.Root(), ".") pc := &parsectx.Context{ Ctx: ctx, Log: zerolog.Logger{}, Build: parsectx.BuildInfo{ Experiments: nil, GOROOT: paths.RootedFSPath(env.EncoreGoRoot(), "."), GOARCH: runtime.GOARCH, GOOS: runtime.GOOS, }, MainModuleDir: rootDir, FS: fs, ParseTests: false, Errs: errs, Overlay: overlays, } // Catch parser bailouts and convert them to ValidationErrors defer func() { perr.CatchBailout(recover()) if rtn == nil { rtn = &SyncResult{ Services: services, } } rtn.Errors = overlays.validationErrors(errs) }() // Load overlay packages using the encore loader loader := pkginfo.New(pc) pkgs := map[paths.Pkg]*pkginfo.Package{} for _, pkgPath := range overlays.pkgPaths() { pkg, ok := loader.LoadPkg(token.NoPos, pkgPath) if ok { pkgs[pkgPath] = pkg } } // Create a schema parser to help us parse the types schemaParser := schema.NewParser(pc, loader) for _, pkg := range pkgs { // Use the API parser to parser the endpoints for each overlaid package pass := &resourceparser.Pass{ Context: pc, SchemaParser: schemaParser, Pkg: pkg, } apis.Parser.Run(pass) for _, r := range pass.Resources() { switch r := r.(type) { case *api.Endpoint: // We're only interested in endpoints that are in our overlays overlay, ok := overlays.get(r.File.FSPath) if !ok { continue } e := overlay.endpoint pathDocs := map[string]string{} e.Doc, e.Errors = parseErrorList(r.Doc) e.Doc, pathDocs = parsePathList(e.Doc) e.Name = r.Name e.Method = r.HTTPMethods[0] e.Visibility = VisibilityType(r.Access) e.Language = "GO" e.Path = toPathSegments(r.Path, pathDocs) // Clear the types as we will reparse them e.Types = []*Type{} if nr, ok := deref(r.Request).(schema.NamedType); ok { e.RequestType = nr.String() // If the request type is in the overlays, we should parse it and // add it to the endpoint associated with the overlay ov, ok := overlays.get(nr.DeclInfo.File.FSPath) if len(r.RequestEncoding()) > 0 && ok { e = ov.endpoint e.Types = append(e.Types, &Type{ Name: nr.String(), Doc: strings.TrimSpace(nr.DeclInfo.Doc), Fields: fns.Map(r.RequestEncoding()[0].AllParameters(), func(f *apienc.ParameterEncoding) *TypeField { return &TypeField{ Name: f.SrcName, WireName: f.WireName, Location: f.Location, Type: f.Type.String(), Doc: strings.TrimSpace(f.Doc), } }), }) } } if nr, ok := deref(r.Response).(schema.NamedType); ok { e.ResponseType = nr.String() // If the response type is in the overlays, we should parse it and // add it to the endpoint associated with the overlay ov, ok := overlays.get(nr.DeclInfo.File.FSPath) if r.ResponseEncoding() != nil && ok { e = ov.endpoint e.Types = append(e.Types, &Type{ Name: nr.String(), Doc: strings.TrimSpace(nr.DeclInfo.Doc), Fields: fns.Map(r.ResponseEncoding().AllParameters(), func(f *apienc.ParameterEncoding) *TypeField { return &TypeField{ Name: f.SrcName, WireName: f.WireName, Location: f.Location, Type: f.Type.String(), Doc: strings.TrimSpace(f.Doc), } }), }) } } } } // Parse types which are in the overlays but not used in request/response for _, file := range pkg.Files { ast.Inspect(file.AST(), func(node ast.Node) bool { switch node := node.(type) { case *ast.GenDecl: // We're only interested in type declarations if node.Tok != token.TYPE { return true } for _, spec := range node.Specs { d := spec.(*ast.TypeSpec) // If the type is not defined in our overlays, skip it. olay, ok := overlays.get(file.FSPath) if !ok { continue } // If it's not a struct type, skip it. s, ok := schemaParser.ParseType(file, d.Type).(schema.StructType) if !ok { continue } e := olay.endpoint // If the type has already been parsed, skip it. if slices.ContainsFunc(e.Types, func(t *Type) bool { return t.Name == d.Name.Name }) { continue } // Otherwise we should add it e.Types = append(e.Types, &Type{ Name: d.Name.Name, Doc: docText(node.Doc), Fields: fns.MapAndFilter(s.Fields, parseTypeField), }) } } return true }) } } return &SyncResult{ Services: services, }, nil } // parseTypeField is a helper function to parse a schema field into a TypeField. func parseTypeField(f schema.StructField) (*TypeField, bool) { name, ok := f.Name.Get() if !ok { return nil, false } // Fields which are parsed by this functions are not a request or response type, // so we can assume the wire name is the json tag name. wireName := name if tag, err := f.Tag.Get("json"); err == nil { wireName = tag.Name } return &TypeField{ Name: name, Type: f.Type.String(), Doc: f.Doc, WireName: wireName, }, true } // helper function to extract the text from a comment node or "" if nil func docText(c *ast.CommentGroup) string { if c == nil { return "" } return strings.TrimSpace(c.Text()) } ================================================ FILE: cli/daemon/dash/ai/sql.go ================================================ package ai import ( "os" "os/exec" "path/filepath" "github.com/golang/protobuf/proto" "encr.dev/cli/daemon/apps" "encr.dev/proto/encore/daemon" ) // ParseSQLSchema uses SQLC to parse the migration files for an encore database and returns // the parsed catalog func ParseSQLSchema(app *apps.Instance, schema string) (*daemon.SQLCPlugin_Catalog, error) { schemaPath := filepath.Join(app.Root(), schema) cmd := exec.Command(os.Args[0], "generate-sql-schema", "--proto", schemaPath) output, err := cmd.Output() if err != nil { return nil, err } var req daemon.SQLCPlugin_GenerateRequest if err := proto.Unmarshal(output, &req); err != nil { return nil, err } return req.Catalog, nil } ================================================ FILE: cli/daemon/dash/ai/types.go ================================================ package ai import ( "encr.dev/v2/parser/apis/api/apienc" ) type VisibilityType string const ( VisibilityTypePublic VisibilityType = "public" VisibilityTypePrivate VisibilityType = "private" VisibilityTypeAuth VisibilityType = "auth" ) type SegmentType string const ( SegmentTypeLiteral SegmentType = "literal" SegmentTypeParam SegmentType = "param" SegmentTypeWildcard SegmentType = "wildcard" SegmentTypeFallback SegmentType = "fallback" ) type SegmentValueType string const SegmentValueTypeString SegmentValueType = "string" type PathSegments []PathSegment type PathSegment struct { Type SegmentType `json:"type,omitempty"` Value *string `json:"value,omitempty"` ValueType *SegmentValueType `json:"valueType,omitempty"` Doc string `graphql:"-" json:"doc,omitempty"` } func (p PathSegment) DocItem() (string, string) { return *p.Value, p.Doc } type Endpoint struct { ID string `json:"id,omitempty"` Name string `json:"name"` Doc string `json:"doc"` Method string `json:"method"` Visibility VisibilityType `json:"visibility"` Path PathSegments `json:"path"` RequestType string `json:"requestType,omitempty"` ResponseType string `json:"responseType,omitempty"` Errors []*Error `json:"errors,omitempty"` Types []*Type `json:"types,omitempty"` Language string `json:"language,omitempty"` TypeSource string `json:"typeSource,omitempty"` EndpointSource string `json:"endpointSource,omitempty"` } func (s *Endpoint) Auth() bool { return s.Visibility == VisibilityTypeAuth } // GraphQL scrubs data that is not needed for the graphql client func (s *Endpoint) GraphQL() *Endpoint { s.ID = "" s.EndpointSource = "" s.TypeSource = "" s.Types = nil s.Language = "" for i, _ := range s.Path { s.Path[i].Doc = "" } return s } type Type struct { Name string `json:"name,omitempty"` Doc string `json:"doc,omitempty"` Fields []*TypeField `json:"fields,omitempty"` } type Service struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Doc string `json:"doc,omitempty"` Endpoints []*Endpoint `json:"endpoints,omitempty"` } func (s Service) GetName() string { return s.Name } func (s Service) GetEndpoints() []*Endpoint { return s.Endpoints } // ServiceInput is the graphql input type for our queries // the graphQL client we use requires the type name to match the // graphql type type ServiceInput Service // GraphQL scrubs data that is not needed for the graphql client func (s Service) GraphQL() ServiceInput { s.ID = "" for _, e := range s.Endpoints { e.GraphQL() } return ServiceInput(s) } type BaseAIUpdateType struct { Type string `graphql:"__typename" json:"type"` } func (b BaseAIUpdateType) IsAIUpdateType() {} type AIUpdateType interface { IsAIUpdateType() } type AIStreamUpdate = Result[AIUpdateType] func ptr[T any](val T) *T { return &val } type Result[T any] struct { Value T Finished *bool Error *string } type EndpointUpdate struct { BaseAIUpdateType Service string `json:"service,omitempty"` Name string `json:"name,omitempty"` Doc string `json:"doc,omitempty"` Method string `json:"method,omitempty"` Visibility VisibilityType `json:"visibility,omitempty"` Path []PathSegment `json:"path,omitempty"` RequestType string `json:"requestType,omitempty"` ResponseType string `json:"responseType,omitempty"` Errors []string `json:"errors,omitempty"` } type ServiceUpdate struct { BaseAIUpdateType Name string `json:"name,omitempty"` Doc string `json:"doc,omitempty"` } type TypeUpdate struct { BaseAIUpdateType Service string `json:"service,omitempty"` Endpoint string `json:"endpoint,omitempty"` Name string `json:"name,omitempty"` Doc string `graphql:"mdoc: doc" json:"doc,omitempty"` } type AISessionID string type SessionUpdate struct { BaseAIUpdateType Id AISessionID } type TitleUpdate struct { BaseAIUpdateType Title string } type LocalEndpointUpdate struct { Type string `json:"type,omitempty"` Service string `json:"service,omitempty"` Endpoint *Endpoint `json:"endpoint,omitempty"` } type TypeField struct { Name string `json:"name,omitempty"` WireName string `json:"wireName,omitempty"` Type string `json:"type,omitempty"` Location apienc.WireLoc `json:"location,omitempty"` Doc string `json:"doc,omitempty"` } type TypeFieldUpdate struct { BaseAIUpdateType Service string `json:"service,omitempty"` Endpoint string `json:"endpoint,omitempty"` Struct string `json:"struct,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Doc string `graphql:"mdoc: doc" json:"doc,omitempty"` } type Error struct { Code string `json:"code,omitempty"` Doc string `json:"doc,omitempty"` } func (e Error) DocItem() (string, string) { return e.Code, e.Doc } func (e Error) String() string { return e.Code } type ErrorUpdate struct { BaseAIUpdateType Code string `json:"code,omitempty"` Doc string `json:"doc,omitempty"` Service string `json:"service,omitempty"` Endpoint string `json:"endpoint,omitempty"` } type PathParamUpdate struct { BaseAIUpdateType Service string `json:"service,omitempty"` Endpoint string `json:"endpoint,omitempty"` Param string `json:"param,omitempty"` Doc string `json:"doc,omitempty"` } type SyncResult struct { Services []Service `json:"services"` Errors []ValidationError `json:"errors"` } // ValidationError is a simplified ErrInSrc to return to the dashboard type ValidationError struct { Service string `json:"service"` Endpoint string `json:"endpoint"` CodeType CodeType `json:"codeType"` Message string `json:"message"` Start *Pos `json:"start,omitempty"` End *Pos `json:"end,omitempty"` } type CodeType string const ( CodeTypeEndpoint CodeType = "endpoint" CodeTypeTypes CodeType = "types" ) type Pos struct { Line int `json:"line"` Column int `json:"column"` } ================================================ FILE: cli/daemon/dash/ai/types_test.go ================================================ package ai import ( "fmt" "strings" "testing" ) func TestWrapDoc(t *testing.T) { var wrapTests = []struct { width int string string }{ {1, "Lorem ipsum dolor sit amet"}, {80, "Lorem ipsum dolor sit amet"}, {80, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, {80, "Lorem Loremipsumdolorsitamet,consecteturadipiscingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagna"}, {30, "Loremipsumdolorsitamet,consecteturadipiscingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagna"}, {80, ""}, {80, "a\nb\nc\nd"}, } for _, test := range wrapTests { t.Run(fmt.Sprintf("WrapDoc(%d, %s)", test.width, test.string), func(t *testing.T) { result := wrapDoc(test.string, test.width) lines := strings.Split(result, "\n") for i, line := range lines { if len(line) > test.width && strings.Contains(line, " ") { t.Errorf("Line too long: %s", line) } if i+1 < len(lines) { nextWord, _, _ := strings.Cut(lines[i+1], " ") if len(line)+len(nextWord) < test.width { t.Errorf("Line too short: %s", line) } } } }) } } ================================================ FILE: cli/daemon/dash/apiproxy/apiproxy.go ================================================ package apiproxy import ( "net/http" "net/http/httputil" "net/url" "runtime" "github.com/cockroachdb/errors" "golang.org/x/oauth2" "encr.dev/internal/conf" "encr.dev/internal/version" ) func New(targetURL string) (*httputil.ReverseProxy, error) { target, err := url.Parse(targetURL) if err != nil { return nil, errors.Wrap(err, "parse target url") } proxy := &httputil.ReverseProxy{ Transport: &oauth2.Transport{ Base: http.DefaultTransport, Source: oauth2.ReuseTokenSource(nil, conf.DefaultTokenSource), }, ErrorHandler: func(writer http.ResponseWriter, request *http.Request, err error) { if errors.Is(err, conf.ErrNotLoggedIn) { writer.WriteHeader(http.StatusUnauthorized) return } writer.WriteHeader(http.StatusBadGateway) }, Rewrite: func(r *httputil.ProxyRequest) { r.Out.URL = target r.Out.Header.Set("User-Agent", "EncoreCLI/"+version.Version) r.Out.Header.Set("X-Encore-Dev-Dash", "true") r.Out.Header.Set("X-Encore-Version", version.Version) r.Out.Header.Set("X-Encore-GOOS", runtime.GOOS) r.Out.Header.Set("X-Encore-GOARCH", runtime.GOARCH) }, } return proxy, nil } ================================================ FILE: cli/daemon/dash/dash.go ================================================ // Package dash serves the Encore Developer Dashboard. package dash import ( "context" "encoding/json" "errors" "fmt" "path/filepath" "slices" "strings" "sync" "time" "github.com/golang/protobuf/jsonpb" "github.com/rs/zerolog/log" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/dash/ai" "encr.dev/cli/daemon/engine/trace2" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/sqldb" "encr.dev/cli/internal/browser" "encr.dev/cli/internal/jsonrpc2" "encr.dev/cli/internal/onboarding" "encr.dev/cli/internal/telemetry" "encr.dev/internal/version" "encr.dev/parser/encoding" "encr.dev/pkg/editors" "encr.dev/pkg/errlist" "encr.dev/pkg/jsonext" tracepb2 "encr.dev/proto/encore/engine/trace2" meta "encr.dev/proto/encore/parser/meta/v1" ) type handler struct { rpc jsonrpc2.Conn apps *apps.Manager run *run.Manager ns *namespace.Manager ai *ai.Manager tr trace2.Store } func (h *handler) GetMeta(appID string) (*meta.Data, error) { runInstance := h.run.FindRunByAppID(appID) var md *meta.Data if runInstance != nil && runInstance.ProcGroup() != nil { md = runInstance.ProcGroup().Meta } else { app, err := h.apps.FindLatestByPlatformOrLocalID(appID) if err != nil { return nil, err } md, err = app.CachedMetadata() if err != nil { return nil, err } else if md == nil { return nil, err } } return md, nil } func (h *handler) GetNamespace(ctx context.Context, appID string) (*namespace.Namespace, error) { runInstance := h.run.FindRunByAppID(appID) if runInstance != nil && runInstance.ProcGroup() != nil { return runInstance.NS, nil } else { app, err := h.apps.FindLatestByPlatformOrLocalID(appID) if err != nil { return nil, err } ns, err := h.ns.GetActive(ctx, app) if err != nil { return nil, err } return ns, nil } } func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error { reply = makeProtoReplier(reply) unmarshal := func(dst interface{}) error { if r.Params() == nil { return fmt.Errorf("missing params") } return json.Unmarshal([]byte(r.Params()), dst) } switch r.Method() { case "db/query": var p QueryRequest if err := unmarshal(&p); err != nil { return reply(ctx, nil, err) } res, err := h.Query(ctx, p) return reply(ctx, res, err) case "db/transaction": var p TransactionRequest if err := unmarshal(&p); err != nil { return reply(ctx, nil, err) } res, err := h.Transaction(ctx, p) return reply(ctx, res, err) case "onboarding/get": state, err := onboarding.Load() if err != nil { return reply(ctx, nil, err) } resp := map[string]time.Time{} for key, val := range state.EventMap { if val.IsSet() { resp[key] = val.UTC() } } return reply(ctx, resp, nil) case "onboarding/set": type params struct { Properties []string `json:"properties"` } var p params if err := unmarshal(&p); err != nil { return reply(ctx, nil, err) } state, err := onboarding.Load() if err != nil { return reply(ctx, nil, err) } for _, prop := range p.Properties { state.Property(prop).Set() } err = state.Write() if err != nil { return reply(ctx, nil, err) } return reply(ctx, nil, nil) case "telemetry": type params struct { Event string `json:"event"` Properties map[string]interface{} `json:"properties"` Once bool `json:"once,omitempty"` } var p params if err := unmarshal(&p); err != nil { return reply(ctx, nil, err) } if p.Once { telemetry.SendOnce(p.Event, p.Properties) } else { telemetry.Send(p.Event, p.Properties) } return reply(ctx, "ok", nil) case "version": type versionResp struct { Version string `json:"version"` Channel string `json:"channel"` } rtn := versionResp{ Version: version.Version, Channel: string(version.Channel), } return reply(ctx, rtn, nil) case "list-apps": type app struct { ID string `json:"id"` Name string `json:"name"` AppRoot string `json:"app_root"` Offline bool `json:"offline,omitempty"` } apps := []app{} // prevent marshalling as null // Load all the apps we know about allApp, err := h.apps.List() if err != nil { return reply(ctx, nil, err) } for _, instance := range allApp { data := app{ ID: instance.PlatformOrLocalID(), Name: instance.Name(), AppRoot: instance.Root(), Offline: true, } if run := h.run.FindRunByAppID(instance.PlatformOrLocalID()); run != nil { data.Offline = false } apps = append(apps, data) } // Sort the apps by offline status, then by name slices.SortStableFunc(apps, func(a, b app) int { if a.Offline == b.Offline { return strings.Compare(a.Name, b.Name) } if a.Offline { return 1 } return -1 }) return reply(ctx, apps, nil) case "traces/clear": telemetry.Send("traces.clear") var params struct { AppID string `json:"app_id"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } err := h.tr.Clear(ctx, params.AppID) return reply(ctx, "ok", err) case "traces/list": telemetry.Send("traces.list") var params struct { AppID string `json:"app_id"` MessageID string `json:"message_id"` TestTraces *bool `json:"test_traces,omitempty"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } query := &trace2.Query{ AppID: params.AppID, TestFilter: params.TestTraces, MessageID: params.MessageID, Limit: 100, } var list []*tracepb2.SpanSummary iter := func(s *tracepb2.SpanSummary) bool { list = append(list, s) return true } err := h.tr.List(ctx, query, iter) if err != nil { log.Error().Err(err).Msg("dash: could not list traces") } return reply(ctx, list, err) case "traces/get": telemetry.Send("traces.get") var params struct { AppID string `json:"app_id"` TraceID string `json:"trace_id"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } var events []*tracepb2.TraceEvent iter := func(ev *tracepb2.TraceEvent) bool { events = append(events, ev) return true } err := h.tr.Get(ctx, params.AppID, params.TraceID, iter) if err != nil { log.Error().Err(err).Msg("dash: could not list trace events") } return reply(ctx, events, err) case "traces/spans/summaries/list": telemetry.Send("traces.spans.summaries.list") var params struct { AppID string `json:"app_id"` TraceID string `json:"trace_id"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } spans, err := h.tr.GetSpanSummaries(ctx, params.AppID, params.TraceID) if err != nil { log.Error().Err(err).Msg("dash: could not list trace spans") return reply(ctx, nil, err) } return reply(ctx, spans, err) case "traces/spans/events/list": telemetry.Send("traces.spans.events.list") var params struct { AppID string `json:"app_id"` TraceID string `json:"trace_id"` SpanID string `json:"span_id"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } events, err := h.tr.GetEvents(ctx, params.AppID, params.TraceID, params.SpanID) if err != nil { log.Error().Err(err).Msg("dash: could not get span events") } return reply(ctx, events, err) case "status": var params struct { AppID string } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } // Find the latest app by platform ID or local ID. app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { if errors.Is(err, apps.ErrNotFound) { return reply(ctx, map[string]interface{}{"running": false}, nil) } else { return reply(ctx, nil, err) } } // Now find the running instance(s) runInstance := h.run.FindRunByAppID(params.AppID) status, err := buildAppStatus(app, runInstance) if err != nil { log.Error().Err(err).Msg("dash: could not build app status") return reply(ctx, nil, err) } return reply(ctx, status, nil) case "db-migration-status": var params struct { AppID string } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } // Find the latest app by platform ID or local ID. app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } appMeta, err := h.GetMeta(params.AppID) if err != nil { return reply(ctx, nil, err) } namespace, err := h.GetNamespace(ctx, params.AppID) if err != nil { return reply(ctx, nil, err) } clusterType := sqldb.Run cluster, ok := h.run.ClusterMgr.Get(sqldb.GetClusterID(app, clusterType, namespace)) if !ok { return reply(ctx, []dbMigrationHistory{}, nil) } status := buildDbMigrationStatus(ctx, appMeta, cluster) return reply(ctx, status, nil) case "api-call": telemetry.Send("api.call") var params run.ApiCallParams if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } res, err := run.CallAPI(ctx, h.run.FindRunByAppID(params.AppID), ¶ms) return reply(ctx, res, err) case "editors/list": var resp struct { Editors []string `json:"editors"` } found, err := editors.Resolve(ctx) if err != nil { log.Err(err).Msg("dash: could not list editors") return reply(ctx, nil, err) } for _, e := range found { resp.Editors = append(resp.Editors, string(e.Editor)) } return reply(ctx, resp, nil) case "ai/propose-system-design": telemetry.Send("ai.propose") log.Debug().Msg("dash: propose-system-design") var params struct { AppID string `json:"app_id"` Prompt string `json:"prompt"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } md, err := h.GetMeta(params.AppID) if err != nil { return reply(ctx, nil, err) } sessionCh := make(chan *ai.AINotification) defer close(sessionCh) idResp := sync.Once{} task, err := h.ai.ProposeSystemDesign(ctx, params.AppID, params.Prompt, md, func(ctx context.Context, msg *ai.AINotification) error { if _, ok := msg.Value.(ai.SessionUpdate); ok || msg.Error != nil { idResp.Do(func() { sessionCh <- msg }) if ok { return nil } } return h.rpc.Notify(ctx, r.Method()+"/stream", msg) }) if err != nil { return reply(ctx, nil, err) } select { case msg := <-sessionCh: su, ok := msg.Value.(ai.SessionUpdate) if !ok || msg.Error != nil { if msg.Error != nil { err = jsonrpc2.NewError(ai.ErrorCodeMap[msg.Error.Code], msg.Error.Message) } else { err = jsonrpc2.NewError(1, "missing session_id") } return reply(ctx, nil, err) } return reply(ctx, map[string]string{ "session_id": string(su.Id), "subscription_id": task.SubscriptionID, }, nil) case <-ctx.Done(): return reply(ctx, nil, ctx.Err()) case <-time.NewTimer(10 * time.Second).C: _ = task.Stop() return reply(ctx, nil, errors.New("timed out waiting for response")) } case "ai/modify-system-design": telemetry.Send("ai.modify") log.Debug().Msg("dash: modify-system-design") var params struct { AppID string `json:"app_id"` SessionID ai.AISessionID `json:"session_id"` OriginalPrompt string `json:"original_prompt"` Prompt string `json:"prompt"` Proposed []ai.Service `json:"proposed"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } md, err := h.GetMeta(params.AppID) if err != nil { return reply(ctx, nil, err) } task, err := h.ai.ModifySystemDesign(ctx, params.AppID, params.SessionID, params.OriginalPrompt, params.Proposed, params.Prompt, md, func(ctx context.Context, msg *ai.AINotification) error { return h.rpc.Notify(ctx, r.Method()+"/stream", msg) }) return reply(ctx, task.SubscriptionID, err) case "ai/define-endpoints": telemetry.Send("ai.details") log.Debug().Msg("dash: define-endpoints") log.Debug().Msg("dash: define-endpoints") var params struct { AppID string `json:"app_id"` SessionID ai.AISessionID `json:"session_id"` Prompt string `json:"prompt"` Proposed []ai.Service `json:"proposed"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } md, err := h.GetMeta(params.AppID) if err != nil { return reply(ctx, nil, err) } task, err := h.ai.DefineEndpoints(ctx, params.AppID, params.SessionID, params.Prompt, md, params.Proposed, func(ctx context.Context, msg *ai.AINotification) error { return h.rpc.Notify(ctx, r.Method()+"/stream", msg) }) return reply(ctx, task.SubscriptionID, err) case "ai/parse-code": log.Debug().Msg("dash: parse-code") var params struct { AppID string `json:"app_id"` Services []ai.Service `json:"services"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } results, err := h.ai.ParseCode(ctx, params.Services, app) return reply(ctx, results, err) case "ai/update-code": log.Debug().Msg("dash: update-code") var params struct { AppID string `json:"app_id"` Services []ai.Service `json:"services"` Overwrite bool `json:"overwrite"` // Ovwerwrite any existing endpoint code } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } results, err := h.ai.UpdateCode(ctx, params.Services, app, params.Overwrite) return reply(ctx, results, err) case "ai/preview-files": telemetry.Send("ai.preview") log.Debug().Msg("dash: preview-files") var params struct { AppID string `json:"app_id"` Services []ai.Service `json:"services"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } result, err := h.ai.PreviewFiles(ctx, params.Services, app) return reply(ctx, result, err) case "ai/write-files": telemetry.Send("ai.write") log.Debug().Msg("dash: write-files") var params struct { AppID string `json:"app_id"` Services []ai.Service `json:"services"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } result, err := h.ai.WriteFiles(ctx, params.Services, app) return reply(ctx, result, err) case "ai/parse-sql-schema": var params struct { AppID string `json:"app_id"` } if err := unmarshal(¶ms); err != nil { return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { return reply(ctx, nil, err) } md, err := h.GetMeta(params.AppID) if err != nil { return reply(ctx, nil, err) } for _, db := range md.SqlDatabases { _, err := ai.ParseSQLSchema(app, *db.MigrationRelPath) if err != nil { return reply(ctx, nil, err) } } return reply(ctx, true, err) case "editors/open": telemetry.Send("editors.open") var params struct { AppID string `json:"app_id"` Editor editors.EditorName `json:"editor"` File string `json:"file"` StartLine int `json:"start_line,omitempty"` StartCol int `json:"start_col,omitempty"` EndLine int `json:"end_line,omitempty"` EndCol int `json:"end_col,omitempty"` } if err := unmarshal(¶ms); err != nil { log.Warn().Err(err).Msg("dash: could not parse open command") return reply(ctx, nil, err) } editor, err := editors.Find(ctx, params.Editor) if err != nil { log.Err(err).Str("editor", string(params.Editor)).Msg("dash: could not find editor") return reply(ctx, nil, err) } app, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID) if err != nil { if errors.Is(err, apps.ErrNotFound) { return reply(ctx, nil, fmt.Errorf("app not found, try running encore run")) } log.Err(err).Str("app_id", params.AppID).Msg("dash: could not find app") return reply(ctx, nil, err) } if !filepath.IsLocal(params.File) { log.Warn().Str("file", params.File).Msg("dash: file was not local to the repo") return reply(ctx, nil, errors.New("file path must be local")) } params.File = filepath.Join(app.Root(), params.File) if err := editors.LaunchExternalEditor(params.File, params.StartLine, params.StartCol, editor); err != nil { log.Err(err).Str("editor", string(params.Editor)).Msg("dash: could not open file") return reply(ctx, nil, err) } type openResp struct{} return reply(ctx, openResp{}, nil) } return jsonrpc2.MethodNotFound(ctx, reply, r) } type sourceContextResponse struct { Lines []string `json:"lines"` Start int `json:"start"` } func (h *handler) listenNotify(ctx context.Context, ch <-chan *notification) { for { select { case <-ctx.Done(): return case r := <-ch: if err := h.rpc.Notify(ctx, r.Method, r.Params); err != nil { return } } } } func (s *Server) listenTraces() { for sp := range s.traceCh { // Only marshal the trace if someone's listening. s.mu.Lock() hasClients := len(s.clients) > 0 s.mu.Unlock() if !hasClients { continue } data, err := jsonext.ProtoEncoder.Marshal(sp.Span) if err != nil { log.Error().Err(err).Msg("dash: could not marshal trace") continue } s.notify(¬ification{ Method: "trace/new", Params: map[string]any{ "app_id": sp.AppID, "test_trace": sp.TestTrace, "span": json.RawMessage(data), }, }) } } var _ run.EventListener = (*Server)(nil) // OnStart notifies active websocket clients about the started run. func (s *Server) OnStart(r *run.Run) { status, err := buildAppStatus(r.App, r) if err != nil { log.Error().Err(err).Msg("dash: could not build app status") return } // Open the browser if needed. browserMode := r.Params.Browser if browserMode == run.BrowserModeAlways || (browserMode == run.BrowserModeAuto && !s.hasClients()) { u := fmt.Sprintf("http://localhost:%d/%s", s.dashPort, r.App.PlatformOrLocalID()) browser.Open(u) } s.notify(¬ification{ Method: "process/start", Params: status, }) } func (s *Server) OnCompileStart(r *run.Run) { status, err := buildAppStatus(r.App, r) if err != nil { log.Error().Err(err).Msg("dash: could not build app status") return } status.Compiling = true s.notify(¬ification{ Method: "process/compile-start", Params: status, }) } // OnReload notifies active websocket clients about the reloaded run. func (s *Server) OnReload(r *run.Run) { status, err := buildAppStatus(r.App, r) if err != nil { log.Error().Err(err).Msg("dash: could not build app status") return } s.notify(¬ification{ Method: "process/reload", Params: status, }) } // OnStop notifies active websocket clients about the stopped run. func (s *Server) OnStop(r *run.Run) { status, err := buildAppStatus(r.App, nil) if err != nil { log.Error().Err(err).Msg("dash: could not build app status") return } s.notify(¬ification{ Method: "process/stop", Params: status, }) } // OnStdout forwards the output to active websocket clients. func (s *Server) OnStdout(r *run.Run, out []byte) { s.onOutput(r, out) } // OnStderr forwards the output to active websocket clients. func (s *Server) OnStderr(r *run.Run, out []byte) { s.onOutput(r, out) } func (s *Server) OnError(r *run.Run, err *errlist.List) { if err == nil { return } status, statusErr := buildAppStatus(r.App, nil) if statusErr != nil { log.Error().Err(statusErr).Msg("dash: could not build app status") return } err.MakeRelative(r.App.Root(), "") status.CompileError = err.Error() s.notify(¬ification{ Method: "process/compile-error", Params: status, }) } func (s *Server) onOutput(r *run.Run, out []byte) { // Copy to a new slice since we cannot retain it after the call ends, and notify is async. out2 := make([]byte, len(out)) copy(out2, out) s.notify(¬ification{ Method: "process/output", Params: map[string]interface{}{ "appID": r.App.PlatformOrLocalID(), "pid": r.ID, "output": out2, }, }) } // protoReplier is a jsonrpc2.Replier that wraps another replier and serializes // any protobuf message with protojson. func makeProtoReplier(rep jsonrpc2.Replier) jsonrpc2.Replier { return func(ctx context.Context, result any, err error) error { if err != nil { return rep(ctx, nil, err) } jsonData, err := jsonext.ProtoEncoder.Marshal(result) return rep(ctx, json.RawMessage(jsonData), err) } } // appStatus is the the shared data structure to communicate app status to the client. // // It is mirrored in the frontend at src/lib/client/dev-dash-client.ts as `AppStatus`. type appStatus struct { Running bool `json:"running"` Tutorial string `json:"tutorial,omitempty"` AppID string `json:"appID"` PlatformID string `json:"platformID,omitempty"` AppRoot string `json:"appRoot"` PID string `json:"pid,omitempty"` Meta json.RawMessage `json:"meta,omitempty"` Addr string `json:"addr,omitempty"` APIEncoding *encoding.APIEncoding `json:"apiEncoding,omitempty"` Compiling bool `json:"compiling"` CompileError string `json:"compileError,omitempty"` } type dbMigrationHistory struct { DatabaseName string `json:"databaseName"` Migrations []dbMigration `json:"migrations"` } type dbMigration struct { Filename string `json:"filename"` Number uint64 `json:"number"` Description string `json:"description"` Applied bool `json:"applied"` } func buildAppStatus(app *apps.Instance, runInstance *run.Run) (s appStatus, err error) { // Now try and grab latest metadata for the app var md *meta.Data if runInstance != nil { proc := runInstance.ProcGroup() if proc != nil { md = proc.Meta } } if md == nil { md, err = app.CachedMetadata() if err != nil { return appStatus{}, err } } // Convert the metadata into a format we can send to the client mdStr := "null" var apiEnc *encoding.APIEncoding if md != nil { m := &jsonpb.Marshaler{OrigName: true, EmitDefaults: true} mdStr, err = m.MarshalToString(md) if err != nil { return appStatus{}, err } apiEnc = encoding.DescribeAPI(md) } // Build the response resp := appStatus{ Running: false, Tutorial: app.Tutorial(), AppID: app.PlatformOrLocalID(), PlatformID: app.PlatformID(), Meta: json.RawMessage(mdStr), AppRoot: app.Root(), APIEncoding: apiEnc, } if runInstance != nil { resp.Running = true resp.PID = runInstance.ID resp.Addr = runInstance.ListenAddr } return resp, nil } func buildDbMigrationStatus(ctx context.Context, appMeta *meta.Data, cluster *sqldb.Cluster) []dbMigrationHistory { var statuses []dbMigrationHistory for _, dbMeta := range appMeta.SqlDatabases { db, ok := cluster.GetDB(dbMeta.Name) if !ok { // Remote database migration status are not supported yet continue } appliedVersions, err := db.ListAppliedMigrations(ctx) if err != nil { log.Error().Msgf("failed to list applied migrations for database %s: %v", dbMeta.Name, err) continue } statuses = append(statuses, buildMigrationHistory(dbMeta, appliedVersions)) } return statuses } func buildMigrationHistory(dbMeta *meta.SQLDatabase, appliedVersions map[uint64]bool) dbMigrationHistory { history := dbMigrationHistory{ DatabaseName: dbMeta.Name, Migrations: []dbMigration{}, } // Go over migrations from latest to earliest sortedMigrations := make([]*meta.DBMigration, len(dbMeta.Migrations)) copy(sortedMigrations, dbMeta.Migrations) slices.SortStableFunc(sortedMigrations, func(a, b *meta.DBMigration) int { return int(b.Number - a.Number) }) implicitlyApplied := false for _, migration := range sortedMigrations { dirty, attempted := appliedVersions[migration.Number] applied := attempted && !dirty // If the database doesn't allow non-sequential migrations, // then any migrations before the last applied will also have // been applied even if we don't see them in the database. if !dbMeta.AllowNonSequentialMigrations && applied { implicitlyApplied = true } status := dbMigration{ Filename: migration.Filename, Number: migration.Number, Description: migration.Description, Applied: applied || implicitlyApplied, } history.Migrations = append(history.Migrations, status) } return history } ================================================ FILE: cli/daemon/dash/dash_test.go ================================================ package dash import ( "reflect" "testing" meta "encr.dev/proto/encore/parser/meta/v1" ) func TestBuildMigrationHistory(t *testing.T) { tests := []struct { name string dbMeta *meta.SQLDatabase appliedVersions map[uint64]bool want dbMigrationHistory }{ { name: "sequential migrations all applied cleanly", dbMeta: &meta.SQLDatabase{ Name: "test-db", Migrations: []*meta.DBMigration{ {Number: 1, Filename: "001.sql", Description: "first"}, {Number: 2, Filename: "002.sql", Description: "second"}, {Number: 3, Filename: "003.sql", Description: "third"}, }, AllowNonSequentialMigrations: false, }, appliedVersions: map[uint64]bool{ 1: false, // clean 2: false, // clean 3: false, // clean }, want: dbMigrationHistory{ DatabaseName: "test-db", Migrations: []dbMigration{ {Number: 3, Filename: "003.sql", Description: "third", Applied: true}, {Number: 2, Filename: "002.sql", Description: "second", Applied: true}, {Number: 1, Filename: "001.sql", Description: "first", Applied: true}, }, }, }, { name: "sequential migrations with dirty migration", dbMeta: &meta.SQLDatabase{ Name: "test-db", Migrations: []*meta.DBMigration{ {Number: 1, Filename: "001.sql", Description: "first"}, {Number: 2, Filename: "002.sql", Description: "second"}, {Number: 3, Filename: "003.sql", Description: "third"}, }, AllowNonSequentialMigrations: false, }, appliedVersions: map[uint64]bool{ 1: false, // clean 2: true, // dirty }, want: dbMigrationHistory{ DatabaseName: "test-db", Migrations: []dbMigration{ {Number: 3, Filename: "003.sql", Description: "third", Applied: false}, {Number: 2, Filename: "002.sql", Description: "second", Applied: false}, {Number: 1, Filename: "001.sql", Description: "first", Applied: true}, }, }, }, { name: "sequential migrations partially applied", dbMeta: &meta.SQLDatabase{ Name: "test-db", Migrations: []*meta.DBMigration{ {Number: 1, Filename: "001.sql", Description: "first"}, {Number: 2, Filename: "002.sql", Description: "second"}, {Number: 3, Filename: "003.sql", Description: "third"}, }, AllowNonSequentialMigrations: false, }, appliedVersions: map[uint64]bool{ 1: false, // clean 2: false, // clean }, want: dbMigrationHistory{ DatabaseName: "test-db", Migrations: []dbMigration{ {Number: 3, Filename: "003.sql", Description: "third", Applied: false}, {Number: 2, Filename: "002.sql", Description: "second", Applied: true}, {Number: 1, Filename: "001.sql", Description: "first", Applied: true}, }, }, }, { name: "non-sequential migrations with mix of clean and dirty", dbMeta: &meta.SQLDatabase{ Name: "test-db", Migrations: []*meta.DBMigration{ {Number: 1, Filename: "001.sql", Description: "first"}, {Number: 2, Filename: "002.sql", Description: "second"}, {Number: 3, Filename: "003.sql", Description: "third"}, }, AllowNonSequentialMigrations: true, }, appliedVersions: map[uint64]bool{ 1: false, // clean 2: true, // dirty 3: false, // clean }, want: dbMigrationHistory{ DatabaseName: "test-db", Migrations: []dbMigration{ {Number: 3, Filename: "003.sql", Description: "third", Applied: true}, {Number: 2, Filename: "002.sql", Description: "second", Applied: false}, {Number: 1, Filename: "001.sql", Description: "first", Applied: true}, }, }, }, { name: "empty migrations list", dbMeta: &meta.SQLDatabase{ Name: "test-db", Migrations: []*meta.DBMigration{}, AllowNonSequentialMigrations: false, }, appliedVersions: map[uint64]bool{}, want: dbMigrationHistory{ DatabaseName: "test-db", Migrations: []dbMigration{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := buildMigrationHistory(tt.dbMeta, tt.appliedVersions) if !reflect.DeepEqual(got, tt.want) { t.Errorf("buildMigrationHistory() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: cli/daemon/dash/dashproxy/dashproxy.go ================================================ // Package dashproxy proxies requests to the dash server, // caching them locally for offline access. package dashproxy import ( "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "github.com/cockroachdb/errors" "github.com/peterbourgon/diskv" "encr.dev/internal/conf" "encr.dev/internal/httpcache" "encr.dev/internal/httpcache/diskcache" "encr.dev/internal/version" ) func New(targetURL string) (*httputil.ReverseProxy, error) { target, err := url.Parse(targetURL) if err != nil { return nil, errors.Wrap(err, "parse target url") } var transport http.RoundTripper = &versionAddingTransport{version: version.Version} if conf.CacheDevDash { cacheDir, err := os.UserCacheDir() if err != nil { return nil, errors.Wrap(err, "get user cache dir") } cache := diskcache.NewWithDiskv(diskv.New(diskv.Options{ BasePath: filepath.Join(cacheDir, "encore", "dashcache"), CacheSizeMax: 1024 * 1024 * 1024, // 1GiB Compression: diskv.NewGzipCompression(), })) // Wrap the transport with a caching transport. cachingTransport := httpcache.NewTransport(cache) cachingTransport.Transport = transport transport = cachingTransport } proxy := &httputil.ReverseProxy{ Transport: transport, Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(target) // Configure cache headers so the cache behaves the way we want it to. r.Out.Header.Del("Cookie") r.Out.Header.Set("Cache-Control", "stale-if-error") r.Out.Header.Del("Vary") }, ModifyResponse: func(resp *http.Response) error { if resp.StatusCode < 300 { resp.Header.Del("Vary") resp.Header.Set("Cache-Control", "max-age=60,stale-if-error=86400") } return nil }, } return proxy, nil } type versionAddingTransport struct { version string } func (t *versionAddingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if t.version != "" { vals := req.URL.Query() vals.Set("cli_version", t.version) req.URL.RawQuery = vals.Encode() } return http.DefaultTransport.RoundTrip(req) } ================================================ FILE: cli/daemon/dash/dbbrowser.go ================================================ package dash import ( "context" "encr.dev/cli/daemon/sqldb" "encr.dev/pkg/fns" "github.com/cockroachdb/errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) // QueryRequest represents the request body for the /query endpoint type QueryRequest struct { Query string `json:"query"` Params []any `json:"params"` ArrayMode bool `json:"arrayMode"` DbID string `json:"dbId"` AppID string `json:"appId"` } // TransactionRequest represents the request body for the /transaction endpoint type TransactionRequest struct { Queries []struct { SQL string `json:"sql"` Params []any `json:"params"` } `json:"queries"` DbID string `json:"dbId"` AppID string `json:"appId"` } func (h *handler) Query(ctx context.Context, req QueryRequest) ([]any, error) { pgConn, err := h.browserConn(ctx, req.AppID, req.DbID) if err != nil { return nil, err } defer fns.CloseIgnoreCtx(ctx, pgConn.Close) rows, err := pgConn.Query(context.Background(), req.Query, req.Params...) if err != nil { return nil, err } defer rows.Close() results := []any{} if req.ArrayMode { // Return results as arrays for rows.Next() { values, err := rows.Values() if err != nil { return nil, err } results = append(results, values) } } else { // Return results as objects fieldDescriptions := rows.FieldDescriptions() for rows.Next() { values, err := rows.Values() if err != nil { return nil, err } row := make(map[string]any) for i, value := range values { row[fieldDescriptions[i].Name] = value } results = append(results, row) } } if err := rows.Err(); err != nil { return nil, err } return results, nil } // handleTransaction handles the /transaction endpoint func (h *handler) Transaction(ctx context.Context, req TransactionRequest) ([]any, error) { // Start a transaction conn, err := h.browserConn(ctx, req.AppID, req.DbID) if err != nil { return nil, err } defer fns.CloseIgnoreCtx(ctx, conn.Close) tx, err := conn.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(context.Background()) results := []any{} for _, query := range req.Queries { rows, err := tx.Query(context.Background(), query.SQL, query.Params...) if err != nil { return nil, err } var queryResults []map[string]any fieldDescriptions := rows.FieldDescriptions() for rows.Next() { values, err := rows.Values() if err != nil { rows.Close() return nil, err } row := make(map[string]any) for i, value := range values { row[fieldDescriptions[i].Name] = value } queryResults = append(queryResults, row) } rows.Close() if err := rows.Err(); err != nil { return nil, err } results = append(results, queryResults) } // Commit the transaction if err := tx.Commit(context.Background()); err != nil { return nil, err } return results, nil } func (s *handler) browserConn(ctx context.Context, appID string, dbID string) (*pgx.Conn, error) { // Find the latest app by platform ID or local ID. app, err := s.apps.FindLatestByPlatformOrLocalID(appID) if err != nil { return nil, errors.Wrap(err, "failed to find latest app") } namespace, err := s.GetNamespace(ctx, appID) if err != nil { return nil, errors.Wrap(err, "failed to get namespace") } clusterType := sqldb.Run cluster := s.run.ClusterMgr.Create(ctx, &sqldb.CreateParams{ ClusterID: sqldb.GetClusterID(app, clusterType, namespace), Memfs: false, }) appMeta, err := s.GetMeta(appID) if err != nil { return nil, err } if _, err = cluster.Start(ctx, nil); err != nil { return nil, errors.Wrap(err, "failed to start database cluster") } db, ok := cluster.GetDB(dbID) if !ok { if err := cluster.Setup(ctx, app.Root(), appMeta); err != nil { return nil, errors.Wrap(err, "failed to setup database cluster") } db, ok = cluster.GetDB(dbID) if !ok { return nil, errors.Newf("failed to get database %s", dbID) } } info, err := db.Cluster.Info(ctx) if err != nil { return nil, err } uri := info.ConnURI(db.ApplicationCloudName(), info.Config.Superuser) conn, err := pgx.Connect(ctx, uri) if err != nil { return nil, err } conn.TypeMap().RegisterType(&pgtype.Type{ Name: "char", OID: 18, Codec: pgtype.TextCodec{}, }) conn.TypeMap().RegisterType(&pgtype.Type{ Name: "uuid", OID: 2950, Codec: pgtype.TextCodec{}, }) return conn, nil } ================================================ FILE: cli/daemon/dash/server.go ================================================ package dash import ( "context" "encoding/json" "fmt" "net/http" "net/http/httputil" "sync" "github.com/gorilla/websocket" "github.com/rs/zerolog/log" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/dash/ai" "encr.dev/cli/daemon/dash/apiproxy" "encr.dev/cli/daemon/dash/dashproxy" "encr.dev/cli/daemon/engine/trace2" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run" "encr.dev/cli/internal/jsonrpc2" "encr.dev/internal/conf" "encr.dev/pkg/fns" ) var upgrader = websocket.Upgrader{ CheckOrigin: func(*http.Request) bool { return true }, } // NewServer starts a new server and returns it. func NewServer(appsMgr *apps.Manager, runMgr *run.Manager, nsMgr *namespace.Manager, tr trace2.Store, dashPort int) *Server { proxy, err := dashproxy.New(conf.DevDashURL) if err != nil { log.Fatal().Err(err).Msg("could not create dash proxy") } apiProxy, err := apiproxy.New(conf.APIBaseURL + "/graphql") if err != nil { log.Fatal().Err(err).Msg("could not create graphql proxy") } aiMgr := ai.NewAIManager() s := &Server{ proxy: proxy, apiProxy: apiProxy, apps: appsMgr, run: runMgr, ns: nsMgr, tr: tr, dashPort: dashPort, traceCh: make(chan trace2.NewSpanEvent, 10), clients: make(map[chan<- *notification]struct{}), ai: aiMgr, } runMgr.AddListener(s) tr.Listen(s.traceCh) go s.listenTraces() return s } // Server is the http.Handler for serving the developer dashboard. type Server struct { proxy *httputil.ReverseProxy apiProxy *httputil.ReverseProxy apps *apps.Manager run *run.Manager ns *namespace.Manager tr trace2.Store dashPort int traceCh chan trace2.NewSpanEvent ai *ai.Manager mu sync.Mutex clients map[chan<- *notification]struct{} } func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/__encore": s.WebSocket(w, req) case "/__graphql": s.apiProxy.ServeHTTP(w, req) default: s.proxy.ServeHTTP(w, req) } } // WebSocket serves the jsonrpc2 API over WebSocket. func (s *Server) WebSocket(w http.ResponseWriter, req *http.Request) { c, err := upgrader.Upgrade(w, req, nil) if err != nil { log.Error().Err(err).Msg("dash: could not upgrade websocket") return } defer fns.CloseIgnore(c) log.Info().Msg("dash: websocket connection established") stream := &wsStream{c: c} conn := jsonrpc2.NewConn(stream) handler := &handler{rpc: conn, apps: s.apps, run: s.run, ns: s.ns, tr: s.tr, ai: s.ai} conn.Go(req.Context(), handler.Handle) ch := make(chan *notification, 20) s.addClient(ch) defer s.removeClient(ch) // nosemgrep: tools.semgrep-rules.semgrep-go.http-request-go-context go handler.listenNotify(req.Context(), ch) <-conn.Done() if err := conn.Err(); err != nil { if ce, ok := err.(*websocket.CloseError); ok && ce.Code == websocket.CloseNormalClosure { log.Info().Msg("dash: websocket closed") } else { log.Info().Err(err).Msg("dash: websocket closed with error") } } } func (s *Server) addClient(ch chan *notification) { s.mu.Lock() defer s.mu.Unlock() s.clients[ch] = struct{}{} } func (s *Server) removeClient(ch chan *notification) { s.mu.Lock() defer s.mu.Unlock() delete(s.clients, ch) } // hasClients reports whether there are any active clients. func (s *Server) hasClients() bool { s.mu.Lock() defer s.mu.Unlock() return len(s.clients) > 0 } type notification struct { Method string Params interface{} } // notify notifies any active clients. func (s *Server) notify(n *notification) { var clients []chan<- *notification s.mu.Lock() for c := range s.clients { clients = append(clients, c) } s.mu.Unlock() for _, c := range clients { select { case c <- n: default: } } } // wsStream implements jsonrpc2.Stream over a websocket. type wsStream struct { writeMu sync.Mutex c *websocket.Conn } func (s *wsStream) Close() error { s.writeMu.Lock() defer s.writeMu.Unlock() return s.c.Close() } func (s *wsStream) Read(context.Context) (jsonrpc2.Message, int64, error) { typ, data, err := s.c.ReadMessage() if err != nil { return nil, 0, err } if typ != websocket.TextMessage { return nil, 0, fmt.Errorf("webedit.wsStream: got non-text message type %v", typ) } msg, err := jsonrpc2.DecodeMessage(data) if err != nil { return nil, 0, err } return msg, int64(len(data)), nil } func (s *wsStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) { s.writeMu.Lock() defer s.writeMu.Unlock() data, err := json.Marshal(msg) if err != nil { return 0, err } err = s.c.WriteMessage(websocket.TextMessage, data) if err != nil { return 0, err } return int64(len(data)), nil } ================================================ FILE: cli/daemon/db.go ================================================ package daemon import ( "context" "errors" "fmt" "net" "strconv" "time" "github.com/rs/zerolog/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/daemon/sqldb" "encr.dev/cli/internal/platform" "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/fns" "encr.dev/pkg/pgproxy" daemonpb "encr.dev/proto/encore/daemon" ) func toRoleType(role daemonpb.DBRole) sqldb.RoleType { switch role { case daemonpb.DBRole_DB_ROLE_READ: return sqldb.RoleRead case daemonpb.DBRole_DB_ROLE_WRITE: return sqldb.RoleWrite case daemonpb.DBRole_DB_ROLE_ADMIN: return sqldb.RoleAdmin case daemonpb.DBRole_DB_ROLE_SUPERUSER: return sqldb.RoleSuperuser default: return sqldb.RoleRead } } // DBConnect starts the database and returns the DSN for connecting to it. func (s *Server) DBConnect(ctx context.Context, req *daemonpb.DBConnectRequest) (*daemonpb.DBConnectResponse, error) { if req.EnvName == "local" { return s.dbConnectLocal(ctx, req) } appID, err := appfile.Slug(req.AppRoot) if err != nil { return nil, err } else if appID == "" { return nil, errNotLinked } port, passwd, err := sqldb.OneshotProxy(appID, req.EnvName, toRoleType(req.Role)) if err != nil { return nil, err } dsn := fmt.Sprintf("postgresql://encore:%s@127.0.0.1:%d/%s?sslmode=disable", passwd, port, req.DbName) return &daemonpb.DBConnectResponse{Dsn: dsn}, nil } func (s *Server) dbConnectLocal(ctx context.Context, req *daemonpb.DBConnectRequest) (*daemonpb.DBConnectResponse, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } expSet, err := app.Experiments(nil) if err != nil { return nil, err } // Parse the app to figure out what infrastructure is needed. bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) if err != nil { return nil, err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { return nil, err } // The Encore IDE plugins will request a connection to the database "_any_" // as they will be unaware of any database names ahead of time. // // We will use the first database name in the app's schema on the returned connection string if req.DbName == "_any_" { req.DbName = "" if len(parse.Meta.SqlDatabases) > 0 { req.DbName = parse.Meta.SqlDatabases[0].Name } // If no database has been found, return an error if req.DbName == "" { return nil, errDatabaseNotFound } } else { // Otherwise we need to check the requested service exists databaseExists := false for _, s := range parse.Meta.SqlDatabases { if s.Name == req.DbName { databaseExists = true break } } if !databaseExists { return nil, errDatabaseNotFound } } clusterNS, err := s.namespaceOrActive(ctx, app, req.Namespace) if err != nil { return nil, err } var passwd string clusterType := getClusterType(req) switch clusterType { case sqldb.Run: // If the user didn't specify a namespace, leave it out from the password // so it uses the active namespace. if req.Namespace != nil { passwd = "local-" + string(clusterNS.ID) } else { passwd = "local" } default: passwd = fmt.Sprintf("%s-%s", clusterType, clusterNS.ID) } clusterID := sqldb.GetClusterID(app, clusterType, clusterNS) log := log.With().Interface("cluster", clusterID).Logger() log.Info().Msg("setting up database cluster") cluster := s.cm.Create(ctx, &sqldb.CreateParams{ ClusterID: clusterID, Memfs: clusterType.Memfs(), }) if cluster.IsExternalDB(req.DbName) { return nil, errors.New("connecting to an external database is disabled") } // TODO would be nice to stream this to the CLI if _, err := cluster.Start(ctx, nil); err != nil { log.Error().Err(err).Msg("failed to start db cluster") return nil, err } else if err := cluster.Setup(ctx, req.AppRoot, parse.Meta); err != nil { log.Error().Err(err).Msg("failed to create databases") return nil, err } log.Info().Msg("created database cluster") dsn := fmt.Sprintf("postgresql://%s:%s@127.0.0.1:%d/%s?sslmode=disable", app.PlatformOrLocalID(), passwd, s.mgr.DBProxyPort, req.DbName) return &daemonpb.DBConnectResponse{Dsn: dsn}, nil } // DBProxy starts a local database proxy for connecting to remote databases // on the encore.dev platform. func (s *Server) DBProxy(params *daemonpb.DBProxyRequest, stream daemonpb.Daemon_DBProxyServer) (err error) { ctx := stream.Context() appID, err := appfile.Slug(params.AppRoot) if err != nil { return err } else if appID == "" && params.EnvName != "local" { return errNotLinked } ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(params.Port))) if err != nil { return status.Error(codes.FailedPrecondition, err.Error()) } port := ln.Addr().(*net.TCPAddr).Port go func() { <-ctx.Done() _ = ln.Close() }() log.Info().Msgf("dbproxy: listening on localhost:%d", port) defer log.Info().Msg("dbproxy: proxy closed") err = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Output{ Output: &daemonpb.CommandOutput{ Stdout: []byte(fmt.Sprintf("dbproxy: listening for TCP connections on localhost:%d\n", port)), }, }}) if err != nil { return err } var runProxy func() error if params.EnvName == "local" { app, err := s.apps.Track(params.AppRoot) if err != nil { return err } expSet, err := app.Experiments(nil) if err != nil { return err } // Parse the app to figure out what infrastructure is needed. bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) if err != nil { return err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { return err } clusterType := getClusterType(params) clusterNS, err := s.namespaceOrActive(stream.Context(), app, params.Namespace) if err != nil { return err } clusterID := sqldb.GetClusterID(app, clusterType, clusterNS) cluster := s.cm.Create(ctx, &sqldb.CreateParams{ ClusterID: clusterID, Memfs: clusterType.Memfs(), }) if _, err := cluster.Start(ctx, nil); err != nil { return err } else if err := cluster.Setup(ctx, params.AppRoot, parse.Meta); err != nil { return err } runProxy = func() error { return serveProxy(ctx, ln, func(ctx context.Context, client net.Conn) { _ = s.cm.PreauthProxyConn(client, clusterID) }) } } else { proxy := &pgproxy.SingleBackendProxy{ Log: log.Logger, RequirePassword: false, FrontendTLS: nil, DialBackend: func(ctx context.Context, startup *pgproxy.StartupData) (pgproxy.LogicalConn, error) { startupData, err := startup.Raw.Encode(nil) if err != nil { return nil, err } ws, err := platform.DBConnect(ctx, appID, params.EnvName, startup.Database, toRoleType(params.Role).String(), startupData) if err != nil { return nil, err } return &sqldb.WebsocketLogicalConn{Conn: ws}, nil }, } runProxy = func() error { return proxy.Serve(ctx, ln) } } msgs := make(chan string, 10) defer close(msgs) go func() { for msg := range msgs { _ = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Output{ Output: &daemonpb.CommandOutput{ Stdout: []byte(msg), }, }}) } }() return runProxy() } // DBReset resets the given databases, recreating them from scratch. func (s *Server) DBReset(req *daemonpb.DBResetRequest, stream daemonpb.Daemon_DBResetServer) error { sendErr := func(err error) { _ = stream.Send(&daemonpb.CommandMessage{ Msg: &daemonpb.CommandMessage_Output{Output: &daemonpb.CommandOutput{ Stderr: []byte(err.Error() + "\n"), }}, }) _ = stream.Send(&daemonpb.CommandMessage{ Msg: &daemonpb.CommandMessage_Exit{Exit: &daemonpb.CommandExit{ Code: 1, }}, }) } app, err := s.apps.Track(req.AppRoot) if err != nil { sendErr(err) return nil } expSet, err := app.Experiments(nil) if err != nil { sendErr(err) return nil } // Parse the app to figure out what infrastructure is needed. bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(stream.Context(), builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) if err != nil { sendErr(err) return nil } parse, err := bld.Parse(stream.Context(), builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { sendErr(err) return nil } clusterNS, err := s.namespaceOrActive(stream.Context(), app, req.Namespace) if err != nil { sendErr(err) return nil } clusterType := getClusterType(req) clusterID := sqldb.GetClusterID(app, clusterType, clusterNS) cluster, ok := s.cm.Get(clusterID) if !ok { cluster = s.cm.Create(stream.Context(), &sqldb.CreateParams{ ClusterID: clusterID, Memfs: clusterType.Memfs(), }) } if _, err := cluster.Start(stream.Context(), nil); err != nil { sendErr(err) return nil } err = cluster.Recreate(stream.Context(), req.AppRoot, req.DatabaseNames, parse.Meta) if err != nil { sendErr(err) } return nil } func serveProxy(ctx context.Context, ln net.Listener, handler func(context.Context, net.Conn)) error { var tempDelay time.Duration // how long to sleep on accept failure for { frontend, e := ln.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Printf("dbproxy: accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return fmt.Errorf("dbproxy: could not accept: %w", e) } tempDelay = 0 go handler(ctx, frontend) } } func getClusterType(req interface{ GetClusterType() daemonpb.DBClusterType }) sqldb.ClusterType { switch req.GetClusterType() { case daemonpb.DBClusterType_DB_CLUSTER_TYPE_RUN: return sqldb.Run case daemonpb.DBClusterType_DB_CLUSTER_TYPE_TEST: return sqldb.Test case daemonpb.DBClusterType_DB_CLUSTER_TYPE_SHADOW: return sqldb.Shadow default: return sqldb.Run } } ================================================ FILE: cli/daemon/debug.go ================================================ package daemon import ( "bytes" "context" "runtime" "github.com/golang/protobuf/jsonpb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/fns" "encr.dev/pkg/vcs" daemonpb "encr.dev/proto/encore/daemon" ) func (s *Server) DumpMeta(ctx context.Context, req *daemonpb.DumpMetaRequest) (*daemonpb.DumpMetaResponse, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } expSet, err := app.Experiments(req.Environ) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } // TODO: We should check that all secret keys are defined as well. vcsRevision := vcs.GetRevision(app.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: req.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: app, WorkingDir: req.WorkingDir, }) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: app, Experiments: expSet, WorkingDir: req.WorkingDir, ParseTests: req.ParseTests, Prepare: prepareResult, }) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } var out []byte switch req.Format { case daemonpb.DumpMetaRequest_FORMAT_PROTO: out, err = proto.Marshal(parse.Meta) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } case daemonpb.DumpMetaRequest_FORMAT_JSON: var buf bytes.Buffer m := &jsonpb.Marshaler{OrigName: true, EmitDefaults: true, Indent: " "} if err := m.Marshal(&buf, parse.Meta); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } out = buf.Bytes() default: return nil, status.Error(codes.InvalidArgument, "invalid format") } return &daemonpb.DumpMetaResponse{Meta: out}, nil } ================================================ FILE: cli/daemon/engine/runtime.go ================================================ package engine import ( "bufio" "fmt" "net/http" "strconv" "github.com/cockroachdb/errors" tracemodel "encore.dev/appruntime/exported/trace2" "encr.dev/cli/daemon/engine/trace2" "encr.dev/cli/daemon/run" ) type server struct { runMgr *run.Manager rec *trace2.Recorder } func NewServer(runMgr *run.Manager, rec *trace2.Recorder) http.Handler { s := &server{runMgr: runMgr, rec: rec} return s } // ServeHTTP implements http.Handler. func (s *server) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/trace": s.RecordTrace(w, req) default: http.Error(w, "Not Found", http.StatusNotFound) } } func (s *server) RecordTrace(w http.ResponseWriter, req *http.Request) { data, err := s.parseTraceData(req) if err != nil { http.Error(w, "unable to parse trace header: "+err.Error(), http.StatusBadRequest) return } err = s.rec.RecordTrace(data) if err != nil { http.Error(w, "unable to record trace: "+err.Error(), http.StatusInternalServerError) return } } func (s *server) parseTraceData(req *http.Request) (d trace2.RecordData, err error) { // Parse trace version traceVersion := req.Header.Get("X-Encore-Trace-Version") version, err := strconv.Atoi(traceVersion) if err != nil || version <= 0 { return d, fmt.Errorf("bad trace protocol version %q", traceVersion) } d.TraceVersion = tracemodel.Version(version) pid := req.Header.Get("X-Encore-Env-ID") if pid == "test" { appID := req.Header.Get("X-Encore-App-ID") if appID == "" { return d, errors.New("missing X-Encore-App-ID header") } d.Meta = &trace2.Meta{AppID: appID} } else { if pid == "" { return d, errors.New("missing X-Encore-Env-ID header") } proc := s.runMgr.FindProc(pid) if proc == nil { return d, errors.Newf("process %q is not running", pid) } d.Meta = &trace2.Meta{AppID: proc.Run.App.PlatformOrLocalID()} } // Parse time anchor timeAnchor := req.Header.Get("X-Encore-Trace-TimeAnchor") if timeAnchor == "" { return d, errors.New("missing X-Encore-Trace-TimeAnchor header") } if err := d.Anchor.UnmarshalText([]byte(timeAnchor)); err != nil { return d, errors.Wrap(err, "unable to parse X-Encore-Trace-TimeAnchor header") } d.Buf = bufio.NewReader(req.Body) return d, nil } ================================================ FILE: cli/daemon/engine/trace/parse_test.go ================================================ package trace import ( "net/http" "testing" "time" "github.com/rs/zerolog" "encore.dev/appruntime/exported/model" "encore.dev/appruntime/exported/trace" "encore.dev/beta/errs" ) type parseTest[T any] struct { name string val T emit func(l *trace.Log, val T) } func (pt parseTest[T]) Name() string { return pt.name } func (pt parseTest[T]) Data() []byte { log := &trace.Log{} pt.emit(log, pt.val) return log.GetAndClear() } func TestParse(t *testing.T) { type reqResp struct { Req *model.Request Resp *model.Response } tests := []interface { Name() string Data() []byte }{ parseTest[*model.Request]{ name: "basic", val: &model.Request{ Type: model.RPCCall, SpanID: model.SpanID{0, 0, 0, 0, 0, 0, 0, 1}, ParentSpanID: model.SpanID{}, Start: time.Now(), Traced: true, RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", Raw: false, }, HTTPMethod: "POST", Path: "/path/hello", PathParams: model.PathParams{{Name: "one", Value: "hello"}}, UserID: "", AuthData: nil, NonRawPayload: []byte(`{"Body":"foo"}`), RequestHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, }, emit: func(l *trace.Log, val *model.Request) { l.BeginRequest(val, 0) }, }, parseTest[reqResp]{ name: "raw_err", val: reqResp{ Req: &model.Request{ Type: model.RPCCall, SpanID: model.SpanID{0, 0, 0, 0, 0, 0, 0, 1}, ParentSpanID: model.SpanID{}, Start: time.Now(), Traced: true, RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", Raw: true, }, HTTPMethod: "POST", Path: "/path/hello", PathParams: model.PathParams{{Name: "one", Value: "hello"}}, RequestHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, }, Resp: &model.Response{ HTTPStatus: 500, Err: &errs.Error{Code: errs.Unavailable}, RawRequestPayload: []byte("foo"), RawResponsePayload: []byte("bar"), }, }, emit: func(l *trace.Log, val reqResp) { l.BeginRequest(val.Req, 0) l.FinishRequest(val.Req, val.Resp) }, }, } for _, tt := range tests { t.Run(tt.Name(), func(t *testing.T) { data := tt.Data() logger := zerolog.New(zerolog.NewTestWriter(t)) _, err := Parse(&logger, ID{}, data, trace.CurrentVersion, nil) if err != nil { t.Fatalf("failed to parse trace: %v", err) } }) } } ================================================ FILE: cli/daemon/engine/trace/trace.go ================================================ package trace import ( "context" "encoding/binary" "errors" "fmt" "math" "path/filepath" "strings" "sync" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/protobuf/types/known/timestamppb" "encore.dev/appruntime/exported/trace" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/internal/sym" "encr.dev/pkg/eerror" tracepb "encr.dev/proto/encore/engine/trace" metapb "encr.dev/proto/encore/parser/meta/v1" ) type ID [16]byte type TraceMeta struct { ID ID Reqs []*tracepb.Request App *apps.Instance EnvID string Date time.Time Meta *metapb.Data } // A Store stores traces received from running applications. type Store struct { trmu sync.Mutex traces map[string][]*TraceMeta requestIDMapping map[string]*tracepb.Request // Trace ID -> Request lnmu sync.Mutex ln map[chan<- *TraceMeta]struct{} } func NewStore() *Store { return &Store{ traces: make(map[string][]*TraceMeta), requestIDMapping: make(map[string]*tracepb.Request), ln: make(map[chan<- *TraceMeta]struct{}), } } func (st *Store) Listen(ch chan<- *TraceMeta) { st.lnmu.Lock() st.ln[ch] = struct{}{} st.lnmu.Unlock() } func (st *Store) Store(ctx context.Context, tr *TraceMeta) error { appID := tr.App.PlatformOrLocalID() st.trmu.Lock() st.traces[appID] = append(st.traces[appID], tr) const limit = 100 // Remove earlier traces if we exceed the limit. if n := len(st.traces[appID]); n > limit { st.traces[appID] = st.traces[appID][n-limit:] } for _, req := range tr.Reqs { st.requestIDMapping[req.TraceId.String()] = req } st.trmu.Unlock() st.lnmu.Lock() defer st.lnmu.Unlock() for ch := range st.ln { // Don't block trying to send select { case ch <- tr: default: } } return nil } func (st *Store) GetRootTrace(traceID *tracepb.TraceID) (rtn *tracepb.Request) { st.trmu.Lock() defer st.trmu.Unlock() next := st.requestIDMapping[traceID.String()] for next != nil { rtn = next next = st.requestIDMapping[rtn.ParentTraceId.String()] } return rtn } func (st *Store) List(appID string) []*TraceMeta { st.trmu.Lock() tr := st.traces[appID] st.trmu.Unlock() return tr } func Parse(log *zerolog.Logger, traceID ID, data []byte, version trace.Version, symTable SymTabler) ([]*tracepb.Request, error) { id := &tracepb.TraceID{ Low: bin.Uint64(traceID[:8]), High: bin.Uint64(traceID[8:]), } tp := &traceParser{ log: log, version: version, traceReader: traceReader{buf: data}, symTable: symTable, traceID: id, reqMap: make(map[uint64]*tracepb.Request), txMap: make(map[uint64]*tracepb.DBTransaction), queryMap: make(map[uint64]*tracepb.DBQuery), callMap: make(map[uint64]interface{}), goMap: make(map[goKey]*tracepb.Goroutine), httpMap: make(map[uint64]*tracepb.HTTPCall), publishMap: make(map[uint64]*tracepb.PubsubMsgPublished), serviceInits: make(map[uint64]*tracepb.ServiceInit), cacheMap: make(map[uint64]*tracepb.CacheOp), } if err := tp.Parse(); err != nil { return nil, err } return tp.reqs, nil } type goKey struct { spanID uint64 goid uint32 } type SymTabler interface { SymTable(ctx context.Context) (*sym.Table, error) } type traceParser struct { traceReader log *zerolog.Logger version trace.Version symTable SymTabler traceID *tracepb.TraceID reqs []*tracepb.Request reqMap map[uint64]*tracepb.Request txMap map[uint64]*tracepb.DBTransaction queryMap map[uint64]*tracepb.DBQuery callMap map[uint64]interface{} // *RPCCall or *AuthCall httpMap map[uint64]*tracepb.HTTPCall goMap map[goKey]*tracepb.Goroutine publishMap map[uint64]*tracepb.PubsubMsgPublished serviceInits map[uint64]*tracepb.ServiceInit cacheMap map[uint64]*tracepb.CacheOp } func (tp *traceParser) Parse() error { for i := 0; !tp.Done(); i++ { ev := trace.EventType(tp.Byte()) ts := tp.Uint64() size := int(tp.Uint32()) startOff := tp.Offset() var err error if tp.version >= 3 { err = tp.parseEventV3(ev, ts, size) } else { err = tp.parseEventV1(byte(ev), ts, size) } if errors.Is(err, errUnknownEvent) { tp.log.Info().Msgf("trace: event #%d: unknown event type %s, skipping", i, ev.String()) tp.Skip(size) err = nil } else if err != nil { return eerror.WithMeta(err, map[string]any{"event#": i, "event": ev.String()}) } if tp.Overflow() { return eerror.New("trace_parser", "invalid trace format: reader overflow parsing event", map[string]any{"event#": i, "event": ev}) } else if off, want := tp.Offset(), startOff+size; off < want { tp.log.Warn().Msgf("trace: event #%d: parsing event=%s ended before end of frame, skipping ahead %d bytes", i, ev, want-off) tp.Skip(want - off) } else if off > want { return eerror.New("trace_parser", "event exceed frame size", map[string]any{"event#": i, "event": ev.String(), "excess": off - want}) } } return nil } var errUnknownEvent = errors.New("unknown event") func (tp *traceParser) parseEventV3(ev trace.EventType, ts uint64, size int) error { switch ev { case trace.RequestStart: return tp.requestStart(ts) case trace.RequestEnd: return tp.requestEnd(ts) case trace.GoStart: return tp.goroutineStart(ts) case trace.GoEnd: return tp.goroutineEnd(ts) case trace.GoClear: return tp.goroutineClear(ts) case trace.TxStart: return tp.transactionStart(ts) case trace.TxEnd: return tp.transactionEnd(ts) case trace.QueryStart: return tp.queryStart(ts) case trace.QueryEnd: return tp.queryEnd(ts) case trace.CallStart: return tp.callStart(ts, size) case trace.CallEnd: return tp.callEnd(ts) case trace.AuthStart, trace.AuthEnd: // Skip these events for now tp.Skip(size) return nil case trace.HTTPCallStart: return tp.httpStart(ts) case trace.HTTPCallEnd: return tp.httpEnd(ts) case trace.HTTPCallBodyClosed: return tp.httpBodyClosed(ts) case trace.LogMessage: return tp.logMessage(ts) case trace.PublishStart: return tp.publishStart(ts) case trace.PublishEnd: return tp.publishEnd(ts) case trace.ServiceInitStart: return tp.serviceInitStart(ts) case trace.ServiceInitEnd: return tp.serviceInitEnd(ts) case trace.CacheOpStart: return tp.cacheOpStart(ts) case trace.CacheOpEnd: return tp.cacheOpEnd(ts) case trace.BodyStream: return tp.bodyStream(ts) default: return errUnknownEvent } } func (tp *traceParser) parseEventV1(ev byte, ts uint64, size int) error { switch ev { case 0x01: return tp.requestStart(ts) case 0x02: return tp.requestEnd(ts) case 0x03: return tp.goroutineStart(ts) case 0x04: return tp.goroutineEnd(ts) case 0x05: return tp.goroutineClear(ts) case 0x06: return tp.transactionStart(ts) case 0x07: return tp.transactionEnd(ts) case 0x08: return tp.queryStart(ts) case 0x09: return tp.queryEnd(ts) case 0x10: return tp.callStart(ts, size) case 0x11: return tp.callEnd(ts) case 0x12, 0x13: // Skip these events for now tp.Skip(size) return nil default: return errUnknownEvent } } func (tp *traceParser) requestStart(ts uint64) error { typ, err := tp.parseRequestType() if err != nil { return err } // Determine the absolute start time. var absStart time.Time if tp.version >= 6 { absStart = tp.Time() } else { // We don't have enough information to determine the exact start time, // but approximate it from the monotonic clock reading absStart = time.Unix(0, int64(ts)) } // Set the trace ID traceID := tp.traceID if tp.version >= 11 { parsedTraceID := tp.parseTraceID() if parsedTraceID.Low != 0 || parsedTraceID.High != 0 { traceID = parsedTraceID } } var parentTraceID *tracepb.TraceID if tp.version >= 12 { parentTraceID = tp.parseTraceID() } spanID := tp.Uint64() parentSpanID := tp.Uint64() var service, endpoint string if tp.version < 6 { service, endpoint = "unknown", "Unknown" } else if tp.version < 9 { service = tp.String() endpoint = tp.String() } goid := uint32(tp.UVarint()) if tp.version < 9 { _ = tp.UVarint() // skip CallLoc: no longer used } defLoc := int32(tp.UVarint()) req := &tracepb.Request{ TraceId: traceID, ParentTraceId: parentTraceID, SpanId: spanID, ParentSpanId: parentSpanID, StartTime: ts, ServiceName: service, EndpointName: endpoint, AbsStartTime: uint64(absStart.UnixNano()), // EndTime not set yet DefLoc: defLoc, Goid: goid, Type: typ, } if tp.version < 9 { req.Uid = tp.String() for n, i := tp.UVarint(), uint64(0); i < n; i++ { size := tp.UVarint() if size > (10 << 20) { return eerror.New("trace_parser", "input too large", map[string]any{"size": size}) } input := make([]byte, size) tp.Bytes(input) req.Inputs = append(req.Inputs, input) } } switch typ { case tracepb.Request_RPC: if tp.version >= 9 { isRaw := tp.Bool() req.ServiceName = tp.String() req.EndpointName = tp.String() req.HttpMethod = tp.String() req.Path = tp.String() numParams := tp.UVarint() req.PathParams = make([]string, numParams) for i := uint64(0); i < numParams; i++ { req.PathParams[i] = tp.String() } req.Uid = tp.String() if tp.version >= 11 { req.ExternalRequestId = tp.String() if tp.version >= 12 { req.ExternalCorrelationId = tp.String() } } if isRaw { req.RawRequestHeaders = tp.parseHTTPHeaders() } else { req.RequestPayload = tp.ByteString() } } case tracepb.Request_AUTH: if tp.version >= 9 { req.ServiceName = tp.String() req.EndpointName = tp.String() req.RequestPayload = tp.ByteString() } case tracepb.Request_PUBSUB_MSG: if tp.version >= 9 { req.ServiceName = tp.String() } req.TopicName = tp.String() req.SubscriptionName = tp.String() req.MessageId = tp.String() req.Attempt = tp.Uint32() req.PublishTime = uint64(tp.Time().UnixMilli()) if tp.version >= 10 { req.RequestPayload = tp.ByteString() } } tp.reqs = append(tp.reqs, req) tp.reqMap[req.SpanId] = req return nil } func (tp *traceParser) bodyStream(ts uint64) error { spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } flags := tp.Byte() data := tp.ByteString() isResponse := (flags & 1) == 1 overflowed := (flags & 2) == 2 req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_BodyStream{ BodyStream: &tracepb.BodyStream{ IsResponse: isResponse, Overflowed: overflowed, Data: data, }, }, }) return nil } func (tp *traceParser) requestEnd(ts uint64) error { var typ tracepb.Request_Type if tp.version >= 9 { var err error typ, err = tp.parseRequestType() if err != nil { return err } } spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } if tp.version < 9 { // Not captured by the protocol for old versions, // so grab it from the request. typ = req.Type } // dur := ts - rd.startTs req.EndTime = ts if tp.version >= 9 { errMsg := tp.ByteString() if len(errMsg) > 0 { req.Err = errMsg req.ErrStack = tp.stack(filterNone) if tp.version >= 13 { req.PanicStack = tp.formattedStack() } } switch typ { case tracepb.Request_RPC: if isRaw := tp.Bool(); isRaw { req.RawResponseHeaders = tp.parseHTTPHeaders() } else { req.ResponsePayload = tp.ByteString() } case tracepb.Request_AUTH: req.Uid = tp.String() req.ResponsePayload = tp.ByteString() case tracepb.Request_PUBSUB_MSG: req.ResponsePayload = tp.ByteString() } } else { isErr := tp.Bool() if isErr { msg := tp.ByteString() if len(msg) == 0 { msg = []byte("unknown error") } if tp.version >= 5 { req.ErrStack = tp.stack(filterNone) } } else { for n, i := tp.UVarint(), uint64(0); i < n; i++ { size := tp.UVarint() if size > (10 << 20) { return eerror.New("trace_parser", "input too large", map[string]any{"size": size}) } output := make([]byte, size) tp.Bytes(output) req.Outputs = append(req.Outputs, output) } } } return nil } func (tp *traceParser) goroutineStart(ts uint64) error { spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { // This is an expected error in certain situations like goroutines // living past the request end that then spawn additional goroutines. // Treat it as a warning but don't fail the parse. tp.log.Warn().Uint64("span_id", spanID).Msg("unknown request span") return nil } goid := tp.Uint32() g := &tracepb.Goroutine{ Goid: goid, StartTime: ts, } k := goKey{spanID: spanID, goid: goid} req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Goroutine{Goroutine: g}, }) tp.goMap[k] = g return nil } func (tp *traceParser) goroutineEnd(ts uint64) error { spanID := tp.Uint64() goid := tp.Uint32() k := goKey{spanID: spanID, goid: goid} g, ok := tp.goMap[k] if !ok { return eerror.New("trace_parser", "unknown goroutine id", map[string]any{"goid": goid}) } g.EndTime = ts delete(tp.goMap, k) return nil } func (tp *traceParser) goroutineClear(ts uint64) error { spanID := tp.Uint64() goid := tp.Uint32() k := goKey{spanID: spanID, goid: goid} g, ok := tp.goMap[k] if !ok { return eerror.New("trace_parser", "unknown goroutine id", map[string]any{"spanID": spanID, "goid": goid}) } g.EndTime = ts delete(tp.goMap, k) return nil } func (tp *traceParser) transactionStart(ts uint64) error { txid := tp.UVarint() spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } goid := uint32(tp.UVarint()) if tp.version < 4 { _ = tp.UVarint() // StartLoc; no longer used } tx := &tracepb.DBTransaction{ Goid: goid, StartTime: ts, } if tp.version >= 5 { tx.BeginStack = tp.stack(filterDB) } tp.txMap[txid] = tx req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Tx{Tx: tx}, }) return nil } func (tp *traceParser) transactionEnd(ts uint64) error { txid := tp.UVarint() _ = tp.Uint64() // spanID tx, ok := tp.txMap[txid] if !ok { return eerror.New("trace_parser", "unknown transaction id", map[string]any{"txid": txid}) } _ = uint32(tp.UVarint()) // goid compl := tp.Byte() if tp.version < 4 { _ = int32(tp.UVarint()) // EndLoc; no longer used } errMsg := tp.ByteString() var stack *tracepb.StackTrace if tp.version >= 5 { stack = tp.stack(filterDB) } // It's possible to get multiple transaction end events. // Ignore them for now; we will expose this information later. if tx.EndTime == 0 { tx.EndTime = ts tx.Err = errMsg tx.EndStack = stack switch compl { case 0: tx.Completion = tracepb.DBTransaction_ROLLBACK case 1: tx.Completion = tracepb.DBTransaction_COMMIT default: return eerror.New("trace_parser", "unknown completion type", map[string]any{"compl": compl}) } } return nil } func (tp *traceParser) queryStart(ts uint64) error { qid := tp.UVarint() spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } txid := tp.UVarint() goid := uint32(tp.UVarint()) if tp.version < 4 { _ = tp.UVarint() // CallLoc; no longer used } q := &tracepb.DBQuery{ Goid: goid, StartTime: ts, Query: tp.ByteString(), } if tp.version >= 5 { q.Stack = tp.stack(filterDB) } tp.queryMap[qid] = q if txid != 0 { tx, ok := tp.txMap[txid] if !ok { return eerror.New("trace_parser", "unknown transaction id", map[string]any{"txid": txid}) } tx.Queries = append(tx.Queries, q) } else { req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Query{Query: q}, }) } return nil } func (tp *traceParser) queryEnd(ts uint64) error { qid := tp.UVarint() q, ok := tp.queryMap[qid] if !ok { return eerror.New("trace_parser", "unknown query id", map[string]any{"qid": qid}) } q.EndTime = ts q.Err = tp.ByteString() return nil } func (tp *traceParser) callStart(ts uint64, size int) error { callID := tp.UVarint() spanID := tp.Uint64() // TODO(eandre) We currently (Dec 2, 2020) have an old format // that leaves out the child span id. Detect this based on the size // and provide a workaround that doesn't crash. var childSpanID uint64 if size == 12 { childSpanID = spanID } else { childSpanID = tp.Uint64() } req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } goid := uint32(tp.UVarint()) _ = tp.UVarint() // CallLoc: no longer used defLoc := int32(tp.UVarint()) c := &tracepb.RPCCall{ SpanId: childSpanID, Goid: goid, DefLoc: defLoc, StartTime: ts, } if tp.version >= 5 { c.Stack = tp.stack(filterNone) } tp.callMap[callID] = c req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Rpc{Rpc: c}, }) return nil } func (tp *traceParser) callEnd(ts uint64) error { callID := tp.UVarint() errMsg := tp.ByteString() c, ok := tp.callMap[callID].(*tracepb.RPCCall) if !ok { return eerror.New("trace_parser", "unknown call ", map[string]any{"callID": callID}) } c.EndTime = ts c.Err = errMsg delete(tp.callMap, callID) return nil } func (tp *traceParser) httpStart(ts uint64) error { callID := tp.UVarint() spanID := tp.Uint64() childSpanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } c := &tracepb.HTTPCall{ SpanId: childSpanID, Goid: uint32(tp.UVarint()), Method: tp.String(), Url: tp.String(), StartTime: ts, } tp.httpMap[callID] = c req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Http{Http: c}, }) return nil } func (tp *traceParser) httpEnd(ts uint64) error { callID := tp.UVarint() errMsg := tp.ByteString() status := tp.UVarint() c, ok := tp.httpMap[callID] if !ok { return eerror.New("trace_parser", "unknown call ", map[string]any{"callID": callID}) } c.EndTime = ts c.Err = errMsg c.StatusCode = uint32(status) numEvents := tp.UVarint() c.Events = make([]*tracepb.HTTPTraceEvent, 0, numEvents) for i := 0; i < int(numEvents); i++ { ev, err := tp.httpEvent() if err != nil { return err } c.Events = append(c.Events, ev) } return nil } func (tp *traceParser) httpBodyClosed(ts uint64) error { callID := tp.UVarint() _ = tp.ByteString() // close error c, ok := tp.httpMap[callID] if !ok { return eerror.New("trace_parser", "unknown call ", map[string]any{"callID": callID}) } c.BodyClosedTime = ts delete(tp.httpMap, callID) return nil } func (tp *traceParser) httpEvent() (*tracepb.HTTPTraceEvent, error) { code := tracepb.HTTPTraceEventCode(tp.Byte()) ts := tp.Int64() ev := &tracepb.HTTPTraceEvent{ Code: code, Time: uint64(ts), } switch code { case tracepb.HTTPTraceEventCode_GET_CONN: ev.Data = &tracepb.HTTPTraceEvent_GetConn{ GetConn: &tracepb.HTTPGetConnData{ HostPort: tp.String(), }, } case tracepb.HTTPTraceEventCode_GOT_CONN: ev.Data = &tracepb.HTTPTraceEvent_GotConn{ GotConn: &tracepb.HTTPGotConnData{ Reused: tp.Bool(), WasIdle: tp.Bool(), IdleDurationNs: tp.Int64(), }, } case tracepb.HTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE: // no data case tracepb.HTTPTraceEventCode_GOT_1XX_RESPONSE: ev.Data = &tracepb.HTTPTraceEvent_Got_1XxResponse{ Got_1XxResponse: &tracepb.HTTPGot1XxResponseData{ Code: int32(tp.Varint()), }, } case tracepb.HTTPTraceEventCode_DNS_START: ev.Data = &tracepb.HTTPTraceEvent_DnsStart{ DnsStart: &tracepb.HTTPDNSStartData{ Host: tp.String(), }, } case tracepb.HTTPTraceEventCode_DNS_DONE: data := &tracepb.HTTPDNSDoneData{ Err: tp.ByteString(), } addrs := int(tp.UVarint()) for j := 0; j < addrs; j++ { data.Addrs = append(data.Addrs, &tracepb.DNSAddr{ Ip: tp.ByteString(), }) } ev.Data = &tracepb.HTTPTraceEvent_DnsDone{DnsDone: data} case tracepb.HTTPTraceEventCode_CONNECT_START: ev.Data = &tracepb.HTTPTraceEvent_ConnectStart{ ConnectStart: &tracepb.HTTPConnectStartData{ Network: tp.String(), Addr: tp.String(), }, } case tracepb.HTTPTraceEventCode_CONNECT_DONE: ev.Data = &tracepb.HTTPTraceEvent_ConnectDone{ ConnectDone: &tracepb.HTTPConnectDoneData{ Network: tp.String(), Addr: tp.String(), Err: tp.ByteString(), }, } case tracepb.HTTPTraceEventCode_TLS_HANDSHAKE_START: // no data case tracepb.HTTPTraceEventCode_TLS_HANDSHAKE_DONE: ev.Data = &tracepb.HTTPTraceEvent_TlsHandshakeDone{ TlsHandshakeDone: &tracepb.HTTPTLSHandshakeDoneData{ Err: tp.ByteString(), TlsVersion: tp.Uint32(), CipherSuite: tp.Uint32(), ServerName: tp.String(), NegotiatedProtocol: tp.String(), }, } case tracepb.HTTPTraceEventCode_WROTE_HEADERS: // no data case tracepb.HTTPTraceEventCode_WROTE_REQUEST: ev.Data = &tracepb.HTTPTraceEvent_WroteRequest{ WroteRequest: &tracepb.HTTPWroteRequestData{ Err: tp.ByteString(), }, } case tracepb.HTTPTraceEventCode_WAIT_100_CONTINUE: // no data default: return nil, eerror.New("trace_parser", "unknown http event", map[string]any{"code": code}) } return ev, nil } func (tp *traceParser) logMessage(ts uint64) error { spanID := tp.Uint64() goid := uint32(tp.UVarint()) level := tp.Byte() msg := tp.String() fields := int(tp.UVarint()) req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request", map[string]any{"spanID": spanID}) } else if fields > 64 { return eerror.New("trace_parser", "too many fields", map[string]any{"fields": fields}) } log := &tracepb.LogMessage{ SpanId: spanID, Goid: goid, Time: ts, Msg: msg, } // We introduced more log levels in trace version 8. if tp.version >= 8 { switch level { case 0: log.Level = tracepb.LogMessage_TRACE case 1: log.Level = tracepb.LogMessage_DEBUG case 2: log.Level = tracepb.LogMessage_INFO case 3: log.Level = tracepb.LogMessage_WARN case 4: log.Level = tracepb.LogMessage_ERROR default: return eerror.New("trace_parser", "unknown log message level", map[string]any{"level": level}) } } else { switch level { case 0: log.Level = tracepb.LogMessage_DEBUG case 1: log.Level = tracepb.LogMessage_INFO case 2: log.Level = tracepb.LogMessage_ERROR default: return eerror.New("trace_parser", "unknown log message level", map[string]any{"level": level}) } } for i := 0; i < fields; i++ { f, err := tp.logField() if err != nil { return eerror.Wrap(err, "trace_parser", "error parsing field", map[string]any{"field#": i}) } log.Fields = append(log.Fields, f) } if tp.version >= 5 { log.Stack = tp.stack(filterNone) } req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Log{Log: log}, }) return nil } func (tp *traceParser) logField() (*tracepb.LogField, error) { typ := tp.Byte() key := tp.String() f := &tracepb.LogField{ Key: key, } switch typ { case 1: if tp.version >= 7 { // We only added stack's to error log fields with version 7 (it was missing from the internal runtime before that) f.Value = &tracepb.LogField_ErrorWithStack{ErrorWithStack: &tracepb.ErrWithStack{ Error: tp.String(), Stack: tp.stack(filterNone), }} } else { f.Value = &tracepb.LogField_ErrorWithoutStack{ErrorWithoutStack: tp.String()} } case 2: f.Value = &tracepb.LogField_Str{Str: tp.String()} case 3: f.Value = &tracepb.LogField_Bool{Bool: tp.Bool()} case 4: f.Value = &tracepb.LogField_Time{Time: timestamppb.New(tp.Time())} case 5: f.Value = &tracepb.LogField_Dur{Dur: tp.Int64()} case 6: b := make([]byte, 16) tp.Bytes(b) f.Value = &tracepb.LogField_Uuid{Uuid: b} case 7: val := tp.ByteString() err := tp.String() if err != "" { f.Value = &tracepb.LogField_ErrorWithoutStack{ErrorWithoutStack: err} } else { f.Value = &tracepb.LogField_Json{Json: val} } case 8: f.Value = &tracepb.LogField_Int{Int: tp.Varint()} case 9: f.Value = &tracepb.LogField_Uint{Uint: tp.UVarint()} case 10: f.Value = &tracepb.LogField_Float32{Float32: tp.Float32()} case 11: f.Value = &tracepb.LogField_Float64{Float64: tp.Float64()} default: return nil, eerror.New("trace_parser", "unknown field type", map[string]any{"typ": typ}) } return f, nil } func (tp *traceParser) publishStart(ts uint64) error { publishID := tp.UVarint() spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } publish := &tracepb.PubsubMsgPublished{ Goid: tp.UVarint(), StartTime: ts, Topic: tp.String(), Message: tp.ByteString(), Stack: tp.stack(filterNone), } tp.publishMap[publishID] = publish req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_PublishedMsg{PublishedMsg: publish}, }) return nil } func (tp *traceParser) publishEnd(ts uint64) error { publishID := tp.UVarint() publish, ok := tp.publishMap[publishID] if !ok { return eerror.New("trace_parser", "unknown publish", map[string]any{"publishID": publishID}) } publish.EndTime = ts publish.MessageId = tp.String() publish.Err = tp.ByteString() delete(tp.publishMap, publishID) return nil } func (tp *traceParser) serviceInitStart(ts uint64) error { spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } initID := tp.UVarint() svcInit := &tracepb.ServiceInit{ Goid: tp.UVarint(), DefLoc: int32(tp.UVarint()), StartTime: ts, Service: tp.String(), } tp.serviceInits[initID] = svcInit req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_ServiceInit{ServiceInit: svcInit}, }) return nil } func (tp *traceParser) serviceInitEnd(ts uint64) error { initID := tp.UVarint() svcInit, ok := tp.serviceInits[initID] if !ok { return eerror.New("trace_parser", "unknown service init", map[string]any{"initID": initID}) } svcInit.EndTime = ts svcInit.Err = tp.ByteString() if len(svcInit.Err) > 0 { svcInit.ErrStack = tp.stack(filterNone) } delete(tp.serviceInits, initID) return nil } func (tp *traceParser) cacheOpStart(ts uint64) error { opID := tp.UVarint() spanID := tp.Uint64() req, ok := tp.reqMap[spanID] if !ok { return eerror.New("trace_parser", "unknown request span", map[string]any{"spanID": spanID}) } op := &tracepb.CacheOp{ Goid: uint32(tp.UVarint()), DefLoc: int32(tp.UVarint()), StartTime: ts, Operation: tp.String(), Write: tp.Bool(), Result: tracepb.CacheOp_UNKNOWN, Stack: tp.stack(filterNone), } numKeys := tp.UVarint() op.Keys = make([]string, numKeys) for i := 0; i < int(numKeys); i++ { op.Keys[i] = tp.String() } numInputs := tp.UVarint() op.Inputs = make([][]byte, numInputs) for i := 0; i < int(numInputs); i++ { op.Inputs[i] = tp.ByteString() } tp.cacheMap[opID] = op req.Events = append(req.Events, &tracepb.Event{ Data: &tracepb.Event_Cache{Cache: op}, }) return nil } func (tp *traceParser) cacheOpEnd(ts uint64) error { opID := tp.UVarint() op, ok := tp.cacheMap[opID] if !ok { return eerror.New("trace_parser", "unknown cache", map[string]any{"opID": opID}) } op.EndTime = ts res := trace.CacheOpResult(tp.Byte()) switch res { case trace.CacheOK: op.Result = tracepb.CacheOp_OK case trace.CacheNoSuchKey: op.Result = tracepb.CacheOp_NO_SUCH_KEY case trace.CacheConflict: op.Result = tracepb.CacheOp_CONFLICT case trace.CacheErr: op.Result = tracepb.CacheOp_ERR op.Err = tp.ByteString() } numOutputs := tp.UVarint() op.Outputs = make([][]byte, numOutputs) for i := 0; i < int(numOutputs); i++ { op.Outputs[i] = tp.ByteString() } delete(tp.cacheMap, opID) return nil } type stackFilter int const ( filterNone stackFilter = iota filterDB ) func (tp *traceParser) stack(filterMode stackFilter) *tracepb.StackTrace { n := int(tp.Byte()) tr := &tracepb.StackTrace{} if n == 0 { return tr } diffs := make([]int64, n) for i := 0; i < n; i++ { diff := tp.Varint() diffs[i] = diff } tr.Pcs = diffs if tp.symTable == nil { return tr } // If we have a symTable, we can extract the full set of frames from the trace sym, err := tp.symTable.SymTable(context.Background()) if err != nil { log.Error().Err(err).Msg("could not parse sym table") return tr } prev := int64(0) pcs := make([]uint64, n) for i := 0; i < n; i++ { x := prev + diffs[i] prev = x pcs[i] = uint64(x) + sym.BaseOffset } tr.Frames = make([]*tracepb.StackFrame, 0, n) PCLoop: for _, pc := range pcs { file, line, fn := sym.PCToLine(pc) if fn != nil { if filterMode == filterDB && strings.Contains(filepath.ToSlash(file), "/src/database/sql/") { continue PCLoop } tr.Frames = append(tr.Frames, &tracepb.StackFrame{ Func: fn.Name, Filename: file, Line: int32(line), }) } } return tr } func (tp *traceParser) formattedStack() *tracepb.StackTrace { n := int(tp.Byte()) tr := &tracepb.StackTrace{} if n == 0 { return tr } tr.Frames = make([]*tracepb.StackFrame, 0, n) for i := 0; i < n; i++ { tr.Frames = append(tr.Frames, &tracepb.StackFrame{ Filename: tp.String(), Line: int32(tp.UVarint()), Func: tp.String(), }) } return tr } func (tp *traceParser) parseRequestType() (tracepb.Request_Type, error) { switch b := tp.Byte(); b { case 0x01: return tracepb.Request_RPC, nil case 0x02: return tracepb.Request_AUTH, nil case 0x03: return tracepb.Request_PUBSUB_MSG, nil default: return -1, eerror.New("trace_parser", "unknown request type", map[string]any{"type": fmt.Sprintf("%x", b)}) } } func (tp *traceParser) parseTraceID() *tracepb.TraceID { var traceID [16]byte tp.Bytes(traceID[:]) return &tracepb.TraceID{ Low: bin.Uint64(traceID[:8]), High: bin.Uint64(traceID[8:]), } } func (tp *traceParser) parseHTTPHeaders() map[string]string { numHeaders := tp.UVarint() h := make(map[string]string, numHeaders) for i := uint64(0); i < numHeaders; i++ { h[tp.String()] = tp.String() } return h } var bin = binary.LittleEndian type traceReader struct { buf []byte off int err bool } func (tr *traceReader) Offset() int { return tr.off } func (tr *traceReader) Done() bool { return tr.off >= len(tr.buf) } func (tr *traceReader) Overflow() bool { return tr.err } func (tr *traceReader) Bytes(b []byte) { n := copy(b, tr.buf[tr.off:]) tr.off += n if len(b) > n { tr.err = true } } func (tr *traceReader) Skip(n int) { tr.off += n if tr.off > len(tr.buf) { tr.off = len(tr.buf) tr.err = true } } func (tr *traceReader) Byte() byte { var buf [1]byte tr.Bytes(buf[:]) return buf[0] } func (tr *traceReader) Bool() bool { return tr.Byte() != 0 } func (tr *traceReader) String() string { return string(tr.ByteString()) } func (tr *traceReader) ByteString() []byte { size := tr.UVarint() if (size) == 0 { return nil } b := make([]byte, int(size)) tr.Bytes(b) return b } func (tr *traceReader) Time() time.Time { sec := tr.Int64() nsec := tr.Int32() return time.Unix(sec, int64(nsec)).UTC() } func (tr *traceReader) Int32() int32 { u := tr.Uint32() var v int32 if u&1 == 0 { v = int32(u >> 1) } else { v = ^int32(u >> 1) } return v } func (tr *traceReader) Uint32() uint32 { var buf [4]byte tr.Bytes(buf[:]) return bin.Uint32(buf[:]) } func (tr *traceReader) Int64() int64 { u := tr.Uint64() var v int64 if u&1 == 0 { v = int64(u >> 1) } else { v = ^int64(u >> 1) } return v } func (tr *traceReader) Uint64() uint64 { var buf [8]byte tr.Bytes(buf[:]) return bin.Uint64(buf[:]) } func (tr *traceReader) Varint() int64 { u := tr.UVarint() var v int64 if u&1 == 0 { v = int64(u >> 1) } else { v = ^int64(u >> 1) } return v } func (tr *traceReader) UVarint() uint64 { var u uint64 for i := 0; tr.off < len(tr.buf); i += 7 { b := tr.buf[tr.off] u |= uint64(b&^0x80) << i tr.off++ if b&0x80 == 0 { break } } return u } func (tr *traceReader) Float32() float32 { b := tr.Uint32() return math.Float32frombits(b) } func (tr *traceReader) Float64() float64 { b := tr.Uint64() return math.Float64frombits(b) } ================================================ FILE: cli/daemon/engine/trace2/recorder.go ================================================ package trace2 import ( "bufio" "context" "io" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "encore.dev/appruntime/exported/trace2" "encr.dev/pkg/traceparser" tracepb2 "encr.dev/proto/encore/engine/trace2" ) type Recorder struct { s Store } func NewRecorder(s Store) *Recorder { return &Recorder{s} } type RecordData struct { Meta *Meta TraceVersion trace2.Version Buf *bufio.Reader Anchor trace2.TimeAnchor } func (h *Recorder) RecordTrace(data RecordData) error { eventCh := make(chan *tracepb2.TraceEvent, 100) go func() { defer close(eventCh) for { ev, err := traceparser.ParseEvent(data.Buf, data.Anchor, data.TraceVersion) if ev != nil { eventCh <- ev } if err == nil { continue } // We have an error. if !errors.Is(err, io.EOF) { log.Error().Err(err).Msg("unable to parse trace") } return } }() writeEvents := func(ctx context.Context, ev []*tracepb2.TraceEvent) error { if len(ev) == 0 { return nil } return h.s.WriteEvents(ctx, data.Meta, ev) } // pendingWrites are the accumulated events that we have parsed so far // that have not yet been written to the store. pendingWrites := make([]*tracepb2.TraceEvent, 0, 100) flushWrites := func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := writeEvents(ctx, pendingWrites); err != nil { log.Error().Err(err).Msg("unable to write trace events") return } // Garbage collect the slice if it's too big. if cap(pendingWrites) > 1000 { pendingWrites = make([]*tracepb2.TraceEvent, 0, 100) } else { pendingWrites = pendingWrites[:0] } } debounce := time.NewTicker(500 * time.Millisecond) defer debounce.Stop() for { select { case ev, ok := <-eventCh: if !ok { // No more events. flushWrites() return nil } debounce.Reset(500 * time.Millisecond) pendingWrites = append(pendingWrites, ev) // Flush immediately if we've accumulated a bunch of events // since the debounce may never run in a high throughput scenario. if len(pendingWrites) >= 100 { flushWrites() } case <-debounce.C: flushWrites() } } } ================================================ FILE: cli/daemon/engine/trace2/sqlite/read.go ================================================ package sqlite import ( "context" "database/sql" "strconv" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" "encr.dev/cli/daemon/engine/trace2" "encr.dev/pkg/fns" tracepb2 "encr.dev/proto/encore/engine/trace2" ) func (s *Store) List(ctx context.Context, q *trace2.Query, iter trace2.ListEntryIterator) error { limit := q.Limit if limit <= 0 { limit = 100 } args := []any{ q.AppID, tracepb2.SpanSummary_AUTH, /* ignore auth spans */ } extraWhereClause := "" if q.MessageID != "" { args = append(args, q.MessageID) extraWhereClause += " AND message_id = $" + strconv.Itoa(len(args)) } // If we're filter for tests / not tests, add the extra where clause if q.TestFilter != nil { args = append(args, tracepb2.SpanSummary_TEST) if *q.TestFilter { extraWhereClause += " AND span_type = $" + strconv.Itoa(len(args)) } else { extraWhereClause += " AND span_type != $" + strconv.Itoa(len(args)) } } rows, err := s.db.QueryContext(ctx, ` SELECT trace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name, topic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos, src_file, src_line, parent_span_id, caller_event_id FROM trace_span_index WHERE app_id = $1 AND has_response AND is_root AND span_type != $2 `+extraWhereClause+` ORDER BY started_at DESC LIMIT `+strconv.Itoa(limit)+` `, args...) if err != nil { return errors.Wrap(err, "query traces") } defer fns.CloseIgnore(rows) n := 0 for rows.Next() { if n >= limit { break } n++ var t tracepb2.SpanSummary var startedAt int64 err := rows.Scan( &t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName, &t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped, &t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId) if err != nil { return errors.Wrap(err, "scan trace") } ts := time.Unix(0, startedAt) t.StartedAt = timestamppb.New(ts) if !iter(&t) { return nil } } return errors.Wrap(rows.Err(), "iterate traces") } // emitCompleteSpanToListeners emits the given trace/span to all listeners // if it's a complete root span (meaning it has a response and is not an auth span). func (s *Store) emitCompleteSpanToListeners(ctx context.Context, appID, traceID, spanID string) { var t tracepb2.SpanSummary var startedAt int64 err := s.db.QueryRowContext(ctx, ` SELECT trace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name, topic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos, src_file, src_line, parent_span_id, caller_event_id FROM trace_span_index WHERE app_id = ? AND trace_id = ? AND span_id = ? AND has_response AND is_root AND span_type != ? ORDER BY started_at DESC `, appID, traceID, spanID, tracepb2.SpanSummary_AUTH).Scan( &t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName, &t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped, &t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId) if errors.Is(err, sql.ErrNoRows) { return } else if err != nil { log.Error().Err(err).Msg("unable to query trace span") return } ts := time.Unix(0, startedAt) t.StartedAt = timestamppb.New(ts) for _, ln := range s.listeners { ln <- trace2.NewSpanEvent{ AppID: appID, TestTrace: t.Type == tracepb2.SpanSummary_TEST, Span: &t, } } } func (s *Store) Get(ctx context.Context, appID, traceID string, iter trace2.EventIterator) error { rows, err := s.db.QueryContext(ctx, ` SELECT event_data FROM trace_event WHERE app_id = ? AND trace_id = ? `, appID, traceID) if err != nil { return errors.Wrap(err, "get trace") } defer fns.CloseIgnore(rows) hasRows := false for rows.Next() { hasRows = true var data []byte err := rows.Scan(&data) if err != nil { return errors.Wrap(err, "scan trace data") } var ev tracepb2.TraceEvent if err := protojson.Unmarshal(data, &ev); err != nil { return errors.Wrap(err, "unmarshal trace event") } if !iter(&ev) { return nil } } if err := rows.Err(); err != nil { return errors.Wrap(err, "iterate events") } else if !hasRows { return trace2.ErrNotFound } return nil } func (s *Store) GetSpanSummaries(ctx context.Context, appID, traceID string) ([]*tracepb2.SpanSummary, error) { rows, err := s.db.QueryContext(ctx, ` SELECT trace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name, topic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos, src_file, src_line, parent_span_id, caller_event_id FROM trace_span_index WHERE app_id = ? AND trace_id = ? ORDER BY started_at ASC `, appID, traceID) if err != nil { return nil, errors.Wrap(err, "query span summaries") } defer fns.CloseIgnore(rows) var summaries []*tracepb2.SpanSummary for rows.Next() { var t tracepb2.SpanSummary var startedAt int64 err := rows.Scan( &t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName, &t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped, &t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId) if err != nil { return nil, errors.Wrap(err, "scan span summary") } ts := time.Unix(0, startedAt) t.StartedAt = timestamppb.New(ts) summaries = append(summaries, &t) } if err := rows.Err(); err != nil { return nil, errors.Wrap(err, "iterate span summaries") } return summaries, nil } func (s *Store) GetEvents(ctx context.Context, appID, traceID, spanID string) ([]*tracepb2.TraceEvent, error) { rows, err := s.db.QueryContext(ctx, ` SELECT event_data FROM trace_event WHERE app_id = ? AND trace_id = ? AND span_id = ? `, appID, traceID, spanID) if err != nil { return nil, errors.Wrap(err, "get span events") } defer fns.CloseIgnore(rows) var events []*tracepb2.TraceEvent for rows.Next() { var data []byte if err := rows.Scan(&data); err != nil { return nil, errors.Wrap(err, "scan event data") } var ev tracepb2.TraceEvent if err := protojson.Unmarshal(data, &ev); err != nil { return nil, errors.Wrap(err, "unmarshal trace event") } events = append(events, &ev) } if err := rows.Err(); err != nil { return nil, errors.Wrap(err, "iterate events") } return events, nil } ================================================ FILE: cli/daemon/engine/trace2/sqlite/write.go ================================================ package sqlite import ( "context" "database/sql" "encoding/base32" "encoding/binary" "net/http" "strings" "time" "github.com/cockroachdb/errors" "github.com/lib/pq" "github.com/rs/zerolog/log" "google.golang.org/protobuf/encoding/protojson" "encr.dev/cli/daemon/engine/trace2" "encr.dev/pkg/fns" tracepbcli "encr.dev/proto/encore/engine/trace2" ) // New creates a new store backed by the given db. func New(db *sql.DB) *Store { return &Store{ db: db, } } type Store struct { db *sql.DB listeners []chan<- trace2.NewSpanEvent } var _ trace2.Store = (*Store)(nil) func scanRows[T any](rows *sql.Rows) ([]T, error) { defer rows.Close() var out []T for rows.Next() { var v T err := rows.Scan(&v) if err != nil { return nil, err } out = append(out, v) } return out, nil } func (s *Store) CleanEvery(ctx context.Context, freq time.Duration, triggerAt, eventsToKeep, batchSize int) { for { timer := time.NewTimer(freq) select { case <-ctx.Done(): return case <-timer.C: if err := s.DoClean(ctx, triggerAt, eventsToKeep, batchSize); err != nil { log.Error().Err(err).Msg("trace cleanup failed") } } } } func (s *Store) DoClean(ctx context.Context, triggerAt, eventsToKeep, batchSize int) error { log.Info().Msg("initiating trace event cleanup sweep") rows, err := s.db.QueryContext(ctx, "SELECT app_id FROM trace_event GROUP BY app_id HAVING COUNT(distinct trace_id) > ?", triggerAt) if err != nil { return errors.Wrap(err, "query app ids") } appIDs, err := scanRows[string](rows) if err != nil { return errors.Wrap(err, "scan app ids") } for _, appID := range appIDs { row := s.db.QueryRowContext(ctx, ` WITH latest_events AS ( SELECT trace_id, min(id) as id FROM trace_event WHERE app_id = ? GROUP BY 1 ORDER BY 2 DESC LIMIT ? ) SELECT min(id) FROM latest_events; `, appID, eventsToKeep) var traceID int64 err := row.Scan(&traceID) if err != nil { log.Error().Err(err).Msg("failed to get trace id") continue } rows, err := s.db.QueryContext(ctx, "SELECT DISTINCT trace_id FROM trace_event WHERE app_id = ? AND id < ? ORDER BY id DESC LIMIT ?", appID, traceID, batchSize) if err != nil { log.Error().Err(err).Msg("failed to get old trace ids") continue } traceIDs, err := scanRows[string](rows) if len(traceIDs) == 0 { continue } idArgs := strings.Join(fns.Map(traceIDs, pq.QuoteLiteral), ",") res, err := s.db.ExecContext(ctx, "DELETE FROM trace_event WHERE app_id = ? AND trace_id IN ("+idArgs+")", appID) if err != nil { log.Error().Err(err).Msg("failed to delete old trace events") continue } rowCount, err := res.RowsAffected() if err != nil { log.Error().Err(err).Msg("failed to get rows affected") continue } log.Info().Str("app_id", appID).Int64("deleted", rowCount).Msg("cleaned up old trace events") res, err = s.db.ExecContext(ctx, "DELETE FROM trace_span_index WHERE app_id = ? AND trace_id IN ("+idArgs+")", appID) if err != nil { log.Error().Err(err).Msg("failed to delete old trace spans") continue } rowCount, err = res.RowsAffected() if err != nil { log.Error().Err(err).Msg("failed to get rows affected") continue } log.Info().Str("app_id", appID).Int64("deleted", rowCount).Msg("cleaned up old trace spans") } return nil } func (s *Store) Listen(ch chan<- trace2.NewSpanEvent) { s.listeners = append(s.listeners, ch) } func (s *Store) Clear(ctx context.Context, appID string) error { _, err := s.db.ExecContext(ctx, "DELETE FROM trace_event WHERE app_id = ?", appID) if err != nil { return errors.Wrap(err, "failed to clear trace events") } _, err = s.db.ExecContext(ctx, "DELETE FROM trace_span_index WHERE app_id = ?", appID) return errors.Wrap(err, "failed to clear trace spans") } func (s *Store) WriteEvents(ctx context.Context, meta *trace2.Meta, events []*tracepbcli.TraceEvent) error { for _, ev := range events { if err := s.insertEvent(ctx, meta, ev); err != nil { log.Error().Err(err).Msg("unable to insert trace span event") continue } } return nil } func (s *Store) insertEvent(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent) error { data, err := protojson.Marshal(ev) if err != nil { return errors.Wrap(err, "marshal trace event") } _, err = s.db.ExecContext(ctx, ` INSERT INTO trace_event ( app_id, trace_id, span_id, event_data) VALUES (?, ?, ?, ?) `, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), data) if err != nil { return errors.Wrap(err, "insert trace span event") } if start := ev.GetSpanStart(); start != nil { if err := s.updateSpanStartIndex(ctx, meta, ev, start); err != nil { return errors.Wrap(err, "update span start index") } } else if end := ev.GetSpanEnd(); end != nil { if err := s.updateSpanEndIndex(ctx, meta, ev, end); err != nil { return errors.Wrap(err, "update span end index") } } return nil } func (s *Store) updateSpanStartIndex(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent, start *tracepbcli.SpanStart) error { isRoot := start.ParentSpanId == nil if req := start.GetRequest(); req != nil { extRequestID := req.RequestHeaders[http.CanonicalHeaderKey("X-Request-ID")] var parentSpanID *string if start.ParentSpanId != nil { encodedParentSpanID := encodeSpanID(*start.ParentSpanId) parentSpanID = &encodedParentSpanID } _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, external_request_id, parent_span_id, caller_event_id, has_response, test_skipped ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false) ON CONFLICT (trace_id, span_id) DO UPDATE SET is_root = excluded.is_root, service_name = excluded.service_name, endpoint_name = excluded.endpoint_name, external_request_id = excluded.external_request_id, parent_span_id = excluded.parent_span_id, caller_event_id = excluded.caller_event_id `, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), tracepbcli.SpanSummary_REQUEST, ev.EventTime.AsTime().UnixNano(), isRoot, req.ServiceName, req.EndpointName, extRequestID, parentSpanID, start.CallerEventId) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if auth := start.GetAuth(); auth != nil { var parentSpanID *string if start.ParentSpanId != nil { encodedParentSpanID := encodeSpanID(*start.ParentSpanId) parentSpanID = &encodedParentSpanID } _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, parent_span_id, caller_event_id, has_response, test_skipped ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false) ON CONFLICT (trace_id, span_id) DO UPDATE SET is_root = excluded.is_root, service_name = excluded.service_name, endpoint_name = excluded.endpoint_name, parent_span_id = excluded.parent_span_id, caller_event_id = excluded.caller_event_id `, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), tracepbcli.SpanSummary_AUTH, ev.EventTime.AsTime().UnixNano(), isRoot, auth.ServiceName, auth.EndpointName, parentSpanID, start.CallerEventId) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if msg := start.GetPubsubMessage(); msg != nil { var parentSpanID *string if start.ParentSpanId != nil { encodedParentSpanID := encodeSpanID(*start.ParentSpanId) parentSpanID = &encodedParentSpanID } _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, started_at, is_root, service_name, topic_name, subscription_name, message_id, parent_span_id, caller_event_id, has_response, test_skipped ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false) ON CONFLICT (trace_id, span_id) DO UPDATE SET is_root = excluded.is_root, service_name = excluded.service_name, topic_name = excluded.topic_name, subscription_name = excluded.subscription_name, message_id = excluded.message_id, parent_span_id = excluded.parent_span_id, caller_event_id = excluded.caller_event_id `, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), tracepbcli.SpanSummary_PUBSUB_MESSAGE, ev.EventTime.AsTime().UnixNano(), isRoot, msg.ServiceName, msg.TopicName, msg.SubscriptionName, msg.MessageId, parentSpanID, start.CallerEventId) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if msg := start.GetTest(); msg != nil { var parentSpanID *string if start.ParentSpanId != nil { encodedParentSpanID := encodeSpanID(*start.ParentSpanId) parentSpanID = &encodedParentSpanID } _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, user_id, src_file, src_line, parent_span_id, caller_event_id, has_response, test_skipped ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false) ON CONFLICT (trace_id, span_id) DO UPDATE SET is_root = excluded.is_root, service_name = excluded.service_name, endpoint_name = excluded.endpoint_name, parent_span_id = excluded.parent_span_id, caller_event_id = excluded.caller_event_id `, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), tracepbcli.SpanSummary_TEST, ev.EventTime.AsTime().UnixNano(), isRoot, msg.ServiceName, msg.TestName, msg.Uid, msg.TestFile, msg.TestLine, parentSpanID, start.CallerEventId) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } return nil } func (s *Store) updateSpanEndIndex(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent, end *tracepbcli.SpanEnd) (err error) { traceID := encodeTraceID(ev.TraceId) spanID := encodeSpanID(ev.SpanId) defer func() { if err == nil { // If the span is complete, emit it to listeners. s.emitCompleteSpanToListeners(ctx, meta.AppID, traceID, spanID) } }() if req := end.GetRequest(); req != nil { _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos, caller_event_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (trace_id, span_id) DO UPDATE SET has_response = excluded.has_response, is_error = excluded.is_error, duration_nanos = excluded.duration_nanos, caller_event_id = excluded.caller_event_id `, meta.AppID, traceID, spanID, tracepbcli.SpanSummary_REQUEST, true, end.Error != nil, end.DurationNanos, req.CallerEventId, ) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if auth := end.GetAuth(); auth != nil { _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos, user_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (trace_id, span_id) DO UPDATE SET has_response = excluded.has_response, is_error = excluded.is_error, duration_nanos = excluded.duration_nanos, user_id = excluded.user_id `, meta.AppID, traceID, spanID, tracepbcli.SpanSummary_AUTH, true, end.Error != nil, end.DurationNanos, auth.Uid) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if msg := end.GetPubsubMessage(); msg != nil { _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos ) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (trace_id, span_id) DO UPDATE SET has_response = excluded.has_response, is_error = excluded.is_error, duration_nanos = excluded.duration_nanos `, meta.AppID, traceID, spanID, tracepbcli.SpanSummary_PUBSUB_MESSAGE, true, end.Error != nil, end.DurationNanos) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } if msg := end.GetTest(); msg != nil { _, err := s.db.ExecContext(ctx, ` INSERT INTO trace_span_index ( app_id, trace_id, span_id, span_type, has_response, is_error, test_skipped, duration_nanos ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (trace_id, span_id) DO UPDATE SET has_response = excluded.has_response, is_error = excluded.is_error, test_skipped = excluded.test_skipped, duration_nanos = excluded.duration_nanos `, meta.AppID, traceID, spanID, tracepbcli.SpanSummary_TEST, true, msg.Failed, msg.Skipped, end.DurationNanos) if err != nil { return errors.Wrap(err, "insert trace span event") } return nil } return nil } var ( binBE = binary.BigEndian binLE = binary.LittleEndian ) // encodeTraceID encodes the trace id as a human-readable string. func encodeTraceID(id *tracepbcli.TraceID) string { var b [16]byte binLE.PutUint64(b[0:8], id.Low) binLE.PutUint64(b[8:16], id.High) return base32hex.EncodeToString(b[:]) } // encodeSpanID encodes the span id as a human-readable string. func encodeSpanID(id uint64) string { var b [8]byte binLE.PutUint64(b[:], id) return base32hex.EncodeToString(b[:]) } var ( // base32hex is a lowercase base32 hex encoding without padding // that preserves lexicographic sort order. base32hex = base32.NewEncoding("0123456789abcdefghijklmnopqrstuv").WithPadding(base32.NoPadding) ) ================================================ FILE: cli/daemon/engine/trace2/store.go ================================================ package trace2 import ( "context" "errors" "time" tracepb2 "encr.dev/proto/encore/engine/trace2" ) type Meta struct { AppID string } type Query struct { AppID string Service string Endpoint string Topic string Subscription string TraceID string MessageID string TestFilter *bool // nil means both test and non-test traces are returned Tags []Tag // StartTime and EndTime specify the time range to query. // If zero values they are not bounded. StartTime, EndTime time.Time IsError *bool // nil means both successes and failures are returned // Minimum and maximum duration (in nanoseconds) to filter requests for. // If MaxDurMicros is 0 it defaults to no limit. MinDurNanos, MaxDurNanos uint64 Limit int // if 0 defaults to 100. } type Tag struct { Key string Value string } // ErrNotFound is reported by Store.Get when a trace is not found. var ErrNotFound = errors.New("trace not found") // A ListEntryIterator is called once for each trace matching the query string, // sequentially and in streaming fashion as traces are read from the store. // // If it returns false the listing operation is stopped and the function is // not called again. type ListEntryIterator func(*tracepb2.SpanSummary) bool // An EventIterator is called once for each event in a trace, // sequentially and in streaming fashion as events are read from the store. // // If it returns false the stream is aborted and the function is // not called again. type EventIterator func(*tracepb2.TraceEvent) bool // Store is the interface for storing and retrieving traces. type Store interface { // WriteEvents persists requests in the store. WriteEvents(ctx context.Context, meta *Meta, events []*tracepb2.TraceEvent) error // List lists traces that match the query. // It calls fn for each trace read; see ListEntryIterator. List(ctx context.Context, q *Query, iter ListEntryIterator) error // Get streams events matching the given trace id. // fn may be called with events out of order. // If the trace is not found it reports an error matching ErrNotFound. Get(ctx context.Context, appID, traceID string, iter EventIterator) error // GetSpanSummaries returns all span summaries for a trace. GetSpanSummaries(ctx context.Context, appID, traceID string) ([]*tracepb2.SpanSummary, error) // GetEvents returns events for a specific span. GetEvents(ctx context.Context, appID, traceID, spanID string) ([]*tracepb2.TraceEvent, error) // Listen listens for new spans. Listen(ch chan<- NewSpanEvent) // Clear removes all traces for an app Clear(ctx context.Context, appID string) error } type NewSpanEvent struct { AppID string TestTrace bool Span *tracepb2.SpanSummary } ================================================ FILE: cli/daemon/exec_script.go ================================================ package daemon import ( "fmt" "os" "path/filepath" "runtime/debug" "strings" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "golang.org/x/mod/modfile" "encr.dev/cli/daemon/run" "encr.dev/internal/optracker" "encr.dev/pkg/appfile" "encr.dev/pkg/fns" "encr.dev/pkg/paths" daemonpb "encr.dev/proto/encore/daemon" ) // ExecScript executes a one-off script. func (s *Server) ExecScript(req *daemonpb.ExecScriptRequest, stream daemonpb.Daemon_ExecScriptServer) error { ctx := stream.Context() slog := &streamLog{stream: stream, buffered: true} stderr := slog.Stderr(false) sendErr := func(err error) { if list := run.AsErrorList(err); list != nil { _ = list.SendToStream(stream) } else { errStr := err.Error() if !strings.HasSuffix(errStr, "\n") { errStr += "\n" } slog.Stderr(false).Write([]byte(errStr)) } streamExit(stream, 1) } ctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile) if err != nil { sendErr(err) return nil } defer tracer.Close() app, err := s.apps.Track(req.AppRoot) if err != nil { sendErr(err) return nil } ns, err := s.namespaceOrActive(ctx, app, req.Namespace) if err != nil { sendErr(err) return nil } ops := optracker.New(stderr, stream) defer ops.AllDone() // Kill the tracker when we exit this function testResults := make(chan error, 1) defer func() { if recovered := recover(); recovered != nil { var err error switch recovered := recovered.(type) { case error: err = recovered default: err = fmt.Errorf("%v", recovered) } log.Err(err).Msg("panic during script execution") testResults <- fmt.Errorf("panic occured within Encore during script execution: %v\n", recovered) } }() // Note: TypeScript apps use the ExecSpec RPC instead, which allows // the CLI to run the command locally with stdin support. if app.Lang() != appfile.LangGo { sendErr(fmt.Errorf("unsupported language for ExecScript: %s", app.Lang())) return nil } modPath := filepath.Join(app.Root(), "go.mod") modData, err := os.ReadFile(modPath) if err != nil { sendErr(err) return nil } mod, err := modfile.Parse(modPath, modData, nil) if err != nil { sendErr(err) return nil } commandRelPath := filepath.ToSlash(filepath.Join(req.WorkingDir, req.ScriptArgs[0])) scriptArgs := req.ScriptArgs[1:] commandPkg := paths.Pkg(mod.Module.Mod.Path).JoinSlash(paths.RelSlash(commandRelPath)) p := run.ExecScriptParams{ App: app, NS: ns, WorkingDir: req.WorkingDir, Environ: req.Environ, MainPkg: commandPkg, ScriptArgs: scriptArgs, Stdout: slog.Stdout(false), Stderr: slog.Stderr(false), OpTracker: ops, } if err := s.mgr.ExecScript(stream.Context(), p); err != nil { sendErr(err) } else { streamExit(stream, 0) } return nil } // ExecSpec returns the specification for how to run an exec command, // allowing the CLI to execute it directly with stdin support. // It streams progress messages during setup, then sends the spec as the final message. func (s *Server) ExecSpec(req *daemonpb.ExecSpecRequest, stream daemonpb.Daemon_ExecSpecServer) error { ctx := stream.Context() // Wrap the ExecSpec stream so it can be used with streamLog and optracker, // which expect a commandStream (Send(*CommandMessage)). adapter := &execSpecStreamAdapter{stream: stream} slog := &streamLog{stream: adapter, buffered: true} stderr := slog.Stderr(false) sendErr := func(err error) { if list := run.AsErrorList(err); list != nil { _ = list.SendToStream(adapter) } else { errStr := err.Error() if !strings.HasSuffix(errStr, "\n") { errStr += "\n" } slog.Stderr(false).Write([]byte(errStr)) } } ctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, nil) if err != nil { sendErr(err) return nil } defer fns.CloseIgnore(tracer) app, err := s.apps.Track(req.AppRoot) if err != nil { sendErr(err) return nil } if app.Lang() != appfile.LangTS { sendErr(errors.New("exec spec is only supported for TypeScript apps")) return nil } ns, err := s.namespaceOrActive(ctx, app, req.Namespace) if err != nil { sendErr(err) return nil } ops := optracker.New(stderr, adapter) defer ops.AllDone() defer func() { if recovered := recover(); recovered != nil { var panicErr error switch recovered := recovered.(type) { case error: panicErr = recovered default: panicErr = fmt.Errorf("%+v", recovered) } stack := debug.Stack() log.Err(panicErr).Msgf("panic during exec spec:\n%s", stack) sendErr(fmt.Errorf("panic during exec spec: %v", panicErr)) } }() spec, err := s.mgr.ExecSpec(ctx, run.ExecSpecParams{ App: app, NS: ns, WorkingDir: req.WorkingDir, Environ: req.Environ, Command: req.ScriptArgs[0], ScriptArgs: req.ScriptArgs[1:], TempDir: req.TempDir, OpTracker: ops, }) if err != nil { sendErr(err) return nil } // Send the spec as the final message. return stream.Send(&daemonpb.ExecSpecMessage{ Msg: &daemonpb.ExecSpecMessage_Spec{ Spec: &daemonpb.ExecSpecResponse{ Command: spec.Command, Args: spec.Args, Environ: spec.Environ, }, }, }) } // execSpecStreamAdapter adapts a Daemon_ExecSpecServer stream to the // commandStream interface, wrapping CommandOutput in ExecSpecMessage. type execSpecStreamAdapter struct { stream daemonpb.Daemon_ExecSpecServer } func (a *execSpecStreamAdapter) Send(msg *daemonpb.CommandMessage) error { switch m := msg.Msg.(type) { case *daemonpb.CommandMessage_Output: return a.stream.Send(&daemonpb.ExecSpecMessage{ Msg: &daemonpb.ExecSpecMessage_Output{Output: m.Output}, }) case *daemonpb.CommandMessage_Errors: // Send structured errors as stderr output so the client can display them. return a.stream.Send(&daemonpb.ExecSpecMessage{ Msg: &daemonpb.ExecSpecMessage_Output{Output: &daemonpb.CommandOutput{ Stderr: m.Errors.Errinsrc, }}, }) default: return nil } } ================================================ FILE: cli/daemon/export/download.go ================================================ package export import ( "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/internal/conf" "encr.dev/internal/env" "encr.dev/internal/version" "encr.dev/pkg/dockerbuild" ) const ( DOWNLOAD_BASE_URL = "https://storage.googleapis.com/encore-optional/encore" ) func downloadBinary(platform, arch string, binary string, log zerolog.Logger) (dockerbuild.HostPath, error) { if version.Channel == version.DevBuild { suffix := "" if platform != runtime.GOOS || arch != runtime.GOARCH { suffix = "-" + platform + "-" + arch } if binary == "encore-runtime.node" { binary = "js/" + binary } path := filepath.Join(env.EncoreRuntimesPath(), binary+suffix) if _, err := os.Stat(path); err == nil { return dockerbuild.HostPath(path), nil } return "", fmt.Errorf("development build of %s/%s %s not found at %s. Build it with `go run ./pkg/encorebuild/cmd/build-local-binary %[3]s --os=%[1]s --arch=%[2]s`", platform, arch, binary, path) } cacheDir, err := conf.CacheDir() if err != nil { return "", err } binDir := dockerbuild.HostPath(cacheDir).Join("bin") archDir := binDir.Join(version.Version, platform, arch) binaryPath := archDir.Join(binary) if _, err := os.Stat(binaryPath.String()); err == nil { return binaryPath, nil } if err := os.MkdirAll(archDir.String(), 0755); err != nil { return "", err } // Download the binaries archURL := fmt.Sprintf("%s/%s/%s-%s", DOWNLOAD_BASE_URL, version.Version, platform, arch) url := fmt.Sprintf("%s/%s", archURL, binary) log.Info().Msgf("Downloading %s/%s %s", platform, arch, binary) if err := downloadFile(url, binaryPath.String()); err != nil { return "", err } tryCleanupPreviousVersions(binDir) return binaryPath, nil } func tryCleanupPreviousVersions(binDir dockerbuild.HostPath) { // Clean up binaries for other versions entries, err := os.ReadDir(binDir.String()) if err != nil { log.Warn().Msgf("failed to read directory %s: %v", binDir, err) return } for _, entry := range entries { if entry.IsDir() && entry.Name() != version.Version { oldVersionPath := filepath.Join(binDir.String(), entry.Name()) if err := os.RemoveAll(oldVersionPath); err != nil { log.Warn().Msgf("failed to remove old version directory %s: %v", oldVersionPath, err) } } } return } func downloadFile(url, dest string) error { // Download the file to a temporary destination resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to download %s: %s", url, resp.Status) } tmpDest := dest + ".tmp" out, err := os.OpenFile(tmpDest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err } defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { return err } out.Close() // Download the checksum sha256url := url + ".sha256" resp, err = http.Get(sha256url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to download %s: %s", sha256url, resp.Status) } hash, err := io.ReadAll(resp.Body) if err != nil { return err } // Validate the checksum if err := validateHash(tmpDest, string(hash)); err != nil { return err } // Move the file if err := os.Rename(tmpDest, dest); err != nil { return err } return nil } func validateHash(file, hash string) error { f, err := os.Open(file) if err != nil { return err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return err } if fileHash := hex.EncodeToString(h.Sum(nil)); hash != fileHash { return fmt.Errorf("file checksum failed. Expected %s, got %s", hash, fileHash) } return nil } ================================================ FILE: cli/daemon/export/export.go ================================================ package export import ( "context" "encoding/base64" "encoding/json" "fmt" "path/filepath" "strings" "time" "github.com/cockroachdb/errors" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/rs/zerolog" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/internal/runlog" "encr.dev/internal/env" "encr.dev/internal/version" "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/dockerbuild" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/vcs" daemonpb "encr.dev/proto/encore/daemon" ) // Docker exports the app as a docker image. func Docker(ctx context.Context, app *apps.Instance, req *daemonpb.ExportRequest, log zerolog.Logger, streamLog runlog.Log) (success bool, err error) { params := req.GetDocker() if params == nil { return false, errors.Newf("unsupported format: %T", req.Format) } expSet, err := app.Experiments(req.Environ) if err != nil { return false, errors.Wrap(err, "get experimental features") } vcsRevision := vcs.GetRevision(app.Root()) buildInfo := builder.BuildInfo{ BuildTags: []string{"timetzdata"}, CgoEnabled: req.CgoEnabled, StaticLink: true, DebugMode: builder.DebugModeDisabled, Environ: req.Environ, GOOS: req.Goos, GOARCH: req.Goarch, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } appLang := app.Lang() bld := builderimpl.Resolve(appLang, expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: app, WorkingDir: ".", }) if err != nil { return false, err } hooks, err := app.Hooks() if err != nil { return false, err } if hooks.PreBuild.IsSet() { if err := executeHook(ctx, hooks.PreBuild, app.Root(), streamLog); err != nil { return false, err } } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { return false, err } if err := app.CacheMetadata(parse.Meta); err != nil { log.Info().Err(err).Msg("failed to cache metadata") return false, errors.Wrap(err, "cache metadata") } log.Info().Msgf("compiling Encore application for %s/%s", req.Goos, req.Goarch) result, err := bld.Compile(ctx, builder.CompileParams{ Build: buildInfo, App: app, Parse: parse, OpTracker: nil, // TODO Experiments: expSet, WorkingDir: ".", }) if err != nil { log.Info().Err(err).Msg("compilation failed") return false, errors.Wrap(err, "compilation failed") } if hooks.PostBuild.IsSet() { if err := executeHook(ctx, hooks.PostBuild, app.Root(), streamLog); err != nil { return false, err } } var crossNodeRuntime option.Option[dockerbuild.HostPath] if appLang == appfile.LangTS && buildInfo.IsCrossBuild() { binary, err := downloadBinary(req.Goos, req.Goarch, "encore-runtime.node", log) if err != nil { return false, errors.Wrap(err, "download runtime binaries") } crossNodeRuntime = option.Some(binary) } buildSettings, err := app.BuildSettings() if err != nil { return false, errors.Wrap(err, "get build settings") } describeCfg := dockerbuild.DescribeConfig{ Meta: parse.Meta, Compile: result, BundleSource: option.Option[dockerbuild.BundleSourceSpec]{}, DockerBaseImage: option.AsOptional(params.BaseImageTag), Runtimes: dockerbuild.HostPath(env.EncoreRuntimesPath()), NodeRuntime: crossNodeRuntime, ProcessPerService: buildSettings.Docker.ProcessPerService, } if buildSettings.Docker.BundleSource || appLang == appfile.LangTS { workspaceRoot := req.WorkspaceRoot appRoot := app.Root() relPath, err := filepath.Rel(workspaceRoot, appRoot) if err != nil { return false, errors.Wrap(err, "relative path from workspace root to app root") } relPath = filepath.ToSlash(relPath) includedPaths, err := dockerbuild.DetermineIncludes(appLang, buildSettings.Docker.BundleSource, workspaceRoot, appRoot) if err != nil { return false, errors.Wrap(err, "determine extra includes") } imageAppRoot := dockerbuild.ImagePath("/workspace").Join(relPath) describeCfg.BundleSource = option.Some(dockerbuild.BundleSourceSpec{ Source: dockerbuild.HostPath(workspaceRoot), Dest: "/workspace", AppRootRelpath: dockerbuild.RelPath(relPath), IncludeSource: includedPaths, ExcludeSource: []dockerbuild.RelPath{ ".git", }, }) if describeCfg.WorkingDir.Empty() { // Set the working directory to app root by default. describeCfg.WorkingDir = option.Some(imageAppRoot) } } spec, err := dockerbuild.Describe(describeCfg) if err != nil { return false, errors.Wrap(err, "describe docker image") } cors, err := app.GlobalCORS() if err != nil { return false, errors.Wrap(err, "get global CORS") } var logResponse string if !req.SkipInfraConf { cfg, infraCfgOutput, err := buildAndValidateInfraConfig(EmbeddedInfraConfigParams{ File: dockerbuild.HostPath(req.InfraConfPath), Services: req.Services, Gateways: req.Gateways, GlobalCORS: cors, Meta: parse.Meta, }) logResponse = infraCfgOutput if err != nil { return false, errors.Wrap(err, "build infra config") } data, err := json.Marshal(cfg) if err != nil { return false, errors.Wrap(err, "marshal infra config") } spec.WriteFiles[defaultInfraConfigPath] = data spec.Env = append(spec.Env, fmt.Sprintf("ENCORE_INFRA_CONFIG_PATH=%s", defaultInfraConfigPath)) // Validate the service configs. cfgs, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: cfg.Metadata.BaseURL, EnvName: cfg.Metadata.EnvName, EnvType: orDefault(cueutil.EnvType(cfg.Metadata.EnvType), "development"), CloudType: orDefault(cueutil.CloudType(cfg.Metadata.Cloud), "local"), }, }) if err != nil { return false, err } for svcName, cfgStr := range cfgs.Configs { spec.Env = append(spec.Env, fmt.Sprintf( "%s%s=%s", "ENCORE_CFG_", strings.ToUpper(svcName), base64.RawURLEncoding.EncodeToString([]byte(cfgStr)), )) } } var baseImgOverride option.Option[v1.Image] if params.BaseImageTag != "" { baseImg, err := resolveBaseImage(ctx, log, params, spec) if err != nil { return false, errors.Wrap(err, "resolve base image") } baseImgOverride = option.Some(baseImg) } var supervisorPath option.Option[dockerbuild.HostPath] if spec.Supervisor.Present() { binary, err := downloadBinary(req.Goos, req.Goarch, "supervisor-encore", log) if err != nil { return false, errors.Wrap(err, "download supervisor binaries") } supervisorPath = option.Some(binary) } img, err := dockerbuild.BuildImage(ctx, spec, dockerbuild.ImageBuildConfig{ BuildTime: time.Now(), BaseImageOverride: baseImgOverride, AddCACerts: option.Some[dockerbuild.ImagePath](""), SupervisorPath: supervisorPath, }) if err != nil { return false, errors.Wrap(err, "build docker image") } if params.LocalDaemonTag != "" { tag, err := name.NewTag(params.LocalDaemonTag, name.WeakValidation) if err != nil { log.Error().Err(err).Msg("invalid image tag") return false, nil } log.Info().Msg("saving image to local docker daemon") _, err = daemon.Write(tag, img, daemon.WithUnbufferedOpener()) if err != nil { log.Error().Err(err).Msg("unable to save docker image") return false, nil } log.Info().Msg("successfully saved local docker image") } if params.PushDestinationTag != "" { tag, err := name.NewTag(params.PushDestinationTag, name.WeakValidation) if err != nil { log.Error().Err(err).Msg("invalid image tag") return false, nil } log.Info().Msg("pushing image to docker registry") if err := pushDockerImage(ctx, log, img, tag); err != nil { log.Error().Err(err).Msg("unable to push docker image") return false, nil } } log.Info().Msgf("successfully exported app as docker image\n%s", logResponse) return true, nil } func orDefault[T comparable](value T, defaultValue T) T { var zero T if value == zero { return defaultValue } return value } func resolveBaseImage(ctx context.Context, log zerolog.Logger, p *daemonpb.DockerExportParams, spec *dockerbuild.ImageSpec) (v1.Image, error) { baseImgTag := p.BaseImageTag if baseImgTag == "" || baseImgTag == "scratch" { return empty.Image, nil } // Try to get it from the daemon if it exists. log.Info().Msgf("resolving base image %s", baseImgTag) baseImgRef, err := name.ParseReference(baseImgTag) if err != nil { return nil, errors.Wrap(err, "parse base image") } fetchRemote := true img, err := daemon.Image(baseImgRef, daemon.WithUnbufferedOpener()) if err == nil { file, err := img.ConfigFile() if err == nil { fetchRemote = file.OS != spec.OS || file.Architecture != spec.Arch } } if fetchRemote { log.Info().Msg("could not get image from local daemon, fetching it remotely") keychain := authn.DefaultKeychain img, err = remote.Image(baseImgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx), remote.WithPlatform(v1.Platform{ OS: spec.OS, Architecture: spec.Arch, })) if err != nil { return nil, errors.Wrap(err, "unable to fetch image") } // If the user requested to push the image locally, save the remote image locally as well. if p.LocalDaemonTag != "" { if tag, err := name.NewTag(baseImgTag, name.WeakValidation); err == nil { log.Info().Msgf("saving remote image %s to local docker daemon", baseImgTag) if _, err = daemon.Write(tag, img); err != nil { log.Warn().Err(err).Msg("unable to save remote image to local docker daemon, skipping") } else { log.Info().Msgf("saved remote image to local docker daemon") } } } } return img, nil } func pushDockerImage(ctx context.Context, log zerolog.Logger, img v1.Image, destination name.Tag) error { log.Info().Msg("pushing docker image to container registry") keychain := authn.DefaultKeychain if err := remote.Write(destination, img, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx)); err != nil { return errors.WithStack(err) } log.Info().Msg("successfully pushed docker image") return nil } func executeHook(ctx context.Context, hook appfile.Hook, workingDir string, streamLog runlog.Log) error { if err := hook.Run(ctx, workingDir, streamLog.Stdout(false), streamLog.Stderr(false)); err != nil { return errors.Wrap(err, "execute hook") } return nil } ================================================ FILE: cli/daemon/export/infra_config.go ================================================ package export import ( "encoding/json" "fmt" "os" "slices" "strings" "github.com/cockroachdb/errors" "github.com/logrusorgru/aurora" "github.com/tailscale/hujson" "golang.org/x/exp/maps" "encore.dev/appruntime/exported/config/infra" "encr.dev/pkg/appfile" "encr.dev/pkg/dockerbuild" "encr.dev/pkg/fns" meta "encr.dev/proto/encore/parser/meta/v1" ) var ( LEARN_MORE = aurora.Italic("Learn More: https://encore.dev/docs/how-to/self-host").String() ) // defaultInfraConfigPath is the path in the image where the environment configuration is mounted. const defaultInfraConfigPath dockerbuild.ImagePath = "/encore/infra.config.json" type EmbeddedInfraConfigParams struct { // The path to the infra config file. File dockerbuild.HostPath // Services to include in the image. Services []string // Gateways to include in the image. Gateways []string // CORS config to include in the image. GlobalCORS appfile.CORS Meta *meta.Data } func buildAndValidateInfraConfig(params EmbeddedInfraConfigParams) (*infra.InfraConfig, string, error) { missing := map[string][]string{} md := params.Meta services := params.Services gateways := params.Gateways if len(services)+len(gateways) == 0 { services = fns.Map(md.Svcs, (*meta.Service).GetName) gateways = fns.Map(md.Gateways, (*meta.Gateway).GetEncoreName) } unknownServices := fns.Filter(services, func(s string) bool { return !fns.Any(md.Svcs, func(svc *meta.Service) bool { return svc.Name == s }) }) if len(unknownServices) > 0 { return nil, "", errors.Newf("unknown services: %v", unknownServices) } unknownGateways := fns.Filter(gateways, func(s string) bool { return !fns.Any(md.Gateways, func(gw *meta.Gateway) bool { return gw.EncoreName == s }) }) if len(unknownGateways) > 0 { return nil, "", errors.Newf("unknown gateways: %v", unknownGateways) } var infraCfg infra.InfraConfig if params.File != "" { data, err := os.ReadFile(params.File.String()) if err != nil { return nil, "", errors.Wrap(err, "infra config not found") } data, err = hujson.Standardize(data) if err != nil { return nil, "", errors.Wrap(err, "could not standardize infra config") } err = json.Unmarshal(data, &infraCfg) if err != nil { return nil, "", errors.Wrap(err, "could not decode infra config") } } infraCfg.HostedGateways = gateways infraCfg.HostedServices = services envVars, validationErrors := infra.Validate(&infraCfg) hostedSvcs := fns.ToMap(fns.Filter(md.Svcs, func(svc *meta.Service) bool { return fns.Any(services, func(s string) bool { return svc.Name == s }) }), (*meta.Service).GetName) var secrets []string // Find all service dependencies for our hosted services. var svcDeps = map[string]struct{}{} pkgs := fns.ToMap(md.Pkgs, (*meta.Package).GetRelPath) // Add dependencies for all outbound RPCs for our hosted services // and collect all required secrets. for _, p := range md.Pkgs { if p.ServiceName == "" { secrets = append(secrets, p.Secrets...) continue } else if _, ok := hostedSvcs[p.ServiceName]; !ok { continue } secrets = append(secrets, p.Secrets...) for _, r := range p.RpcCalls { svcDeps[pkgs[r.Pkg].ServiceName] = struct{}{} } } // Add auth handler to service discovery if we host any auth RPCs. if md.AuthHandler != nil { requiresAuth := fns.Any(md.Svcs, func(svc *meta.Service) bool { return fns.Any(svc.Rpcs, func(rpc *meta.RPC) bool { return rpc.AccessType == meta.RPC_AUTH }) }) if requiresAuth { svcDeps[md.AuthHandler.ServiceName] = struct{}{} } } // Make sure we have service discovery for all services if we are hosting gateways. if len(gateways) > 0 { for _, svc := range md.Svcs { if _, ok := hostedSvcs[svc.Name]; ok { continue } svcDeps[svc.Name] = struct{}{} } } // Remove any services that we host from our service dependencies. for _, svc := range hostedSvcs { delete(svcDeps, svc.Name) } // Remove any service discovery entries for services that we don't host. for svc := range infraCfg.ServiceDiscovery { if _, ok := svcDeps[svc]; !ok { delete(infraCfg.ServiceDiscovery, svc) } else { delete(svcDeps, svc) } } // Make sure all our service dependencies are accounted for. if len(svcDeps) > 0 { missing["Service Discovery"] = maps.Keys(svcDeps) } // Remove secrets we don't need for our hosted services. slices.Sort(secrets) secrets = slices.Compact(secrets) var ok bool if infraCfg.Secrets.EnvRef == nil { for secret := range infraCfg.Secrets.SecretsMap { secrets, ok = fns.Delete(secrets, secret) if !ok { delete(infraCfg.Secrets.SecretsMap, secret) } } // Make sure all our secrets are accounted for. if len(secrets) > 0 { missing["Secrets"] = secrets } } else { // Print that you need to define a secrets map in the infra config. } // Find all databases for our hosted services. databases := fns.FlatMap(maps.Values(hostedSvcs), func(db *meta.Service) []string { return db.Databases }) slices.Sort(databases) databases = slices.Compact(databases) for i, sqlServer := range append([]*infra.SQLServer{}, infraCfg.SQLServers...) { for name := range sqlServer.Databases { databases, ok = fns.Delete(databases, name) if !ok { delete(sqlServer.Databases, name) } } if len(sqlServer.Databases) == 0 { infraCfg.SQLServers = append(infraCfg.SQLServers[:i], infraCfg.SQLServers[i+1:]...) } } if len(databases) > 0 { missing["Databases"] = databases } caches := fns.MapAndFilter(md.CacheClusters, func(cache *meta.CacheCluster) (string, bool) { return cache.Name, fns.Any(cache.Keyspaces, func(ks *meta.CacheCluster_Keyspace) bool { return fns.Any(services, func(s string) bool { return ks.Service == s }) }) }) for name := range infraCfg.Redis { caches, ok = fns.Delete(caches, name) if !ok { delete(infraCfg.Redis, name) } } if len(caches) > 0 { missing["Redis"] = caches } subscriptions := fns.FlatMap(md.PubsubTopics, func(topic *meta.PubSubTopic) [][2]string { return fns.MapAndFilter(topic.Subscriptions, func(s *meta.PubSubTopic_Subscription) ([2]string, bool) { return [2]string{topic.Name, s.Name}, fns.Any(services, func(svc string) bool { return s.ServiceName == svc }) }) }) for _, pubsub := range infraCfg.PubSub { for topicName, topic := range pubsub.GetTopics() { for subName := range topic.GetSubscriptions() { found := false for i, sub := range subscriptions { if sub[0] == topicName && sub[1] == subName { subscriptions = append(subscriptions[:i], subscriptions[i+1:]...) found = true break } } if !found { topic.DeleteSubscription(subName) } } } } if len(subscriptions) > 0 { missing["Subscriptions"] = fns.Map(subscriptions, func(sub [2]string) string { return sub[0] + "/" + sub[1] }) } topics := fns.MapAndFilter(md.PubsubTopics, func(topic *meta.PubSubTopic) (string, bool) { return topic.Name, fns.Any(topic.Publishers, func(p *meta.PubSubTopic_Publisher) bool { return fns.Any(services, func(s string) bool { return p.ServiceName == s }) }) }) for i, pubsub := range infraCfg.PubSub { for topicName, topic := range pubsub.GetTopics() { i := slices.Index(topics, topicName) if i != -1 { topics = append(topics[:i], topics[i+1:]...) } else if len(topic.GetSubscriptions()) == 0 { pubsub.DeleteTopic(topicName) } } if len(pubsub.GetTopics()) == 0 { infraCfg.PubSub = append(infraCfg.PubSub[:i], infraCfg.PubSub[i+1:]...) } } if len(topics) > 0 { missing["Topics"] = topics } // Validate bucket config buckets := fns.FlatMap(maps.Values(hostedSvcs), func(svc *meta.Service) []string { return fns.Map(svc.Buckets, (*meta.BucketUsage).GetBucket) }) slices.Sort(buckets) buckets = slices.Compact(buckets) for _, storage := range infraCfg.ObjectStorage { for name, infraCfg := range storage.GetBuckets() { metaBkt, ok := fns.Find(md.Buckets, func(b *meta.Bucket) bool { return b.Name == name }) if ok { if metaBkt.Public && infraCfg.PublicBaseURL == "" { path := infra.JSONPath("buckets").Append(infra.JSONPath(name)).Append("public_base_url") validationErrors[path] = errors.New("Bucket is public but no public base URL is set") return nil, "", configError(missing, validationErrors) } } buckets, ok = fns.Delete(buckets, name) if !ok { storage.DeleteBucket(name) } } } if len(buckets) > 0 { missing["Buckets"] = buckets } // Copy CORS config cors := infra.CORS(params.GlobalCORS) infraCfg.CORS = &cors if len(missing) > 0 || len(validationErrors) > 0 { return nil, "", configError(missing, validationErrors) } cronJobStr, err := formatCronJobInstructions(services, md) if err != nil { return nil, "", err } envStr := formatEnvVars(envVars) var resp strings.Builder if len(cronJobStr)+len(envStr) > 0 { resp.WriteString(aurora.Bold("Before you deploy, you may need to configure the following:\n").String()) resp.WriteString(cronJobStr) resp.WriteString(envStr) } resp.WriteString(LEARN_MORE) return &infraCfg, resp.String(), nil } func formatCronJobInstructions(services []string, md *meta.Data) (string, error) { if len(md.CronJobs) == 0 { return "", nil } svcByRelPath := fns.ToMap(md.Svcs, func(p *meta.Service) string { return p.RelPath }) cronsTable := [][]string{ {"ID", "Endpoint Path", "Schedule"}, } for _, cronJob := range md.CronJobs { svc, ok := svcByRelPath[cronJob.Endpoint.Pkg] if !ok { return "", errors.Newf("could not find service for cron job %s", cronJob.Id) } if !slices.Contains(services, svc.Name) { continue } rpc, ok := fns.Find(svc.Rpcs, func(r *meta.RPC) bool { return r.Name == cronJob.Endpoint.Name }) if !ok { return "", errors.Newf("could not find rpc for cron job %s", cronJob.Id) } cronsTable = append(cronsTable, []string{cronJob.Id, pathToString(rpc.Path), cronJob.Schedule}) } if len(cronsTable) == 1 { return "", nil } return aurora.Sprintf("\n%s\n%s\n", aurora.Bold("Cron Jobs:"), generateTable(cronsTable)), nil } func generateTable(rows [][]string) string { au := aurora.NewAurora(true) var sb strings.Builder // Calculate column widths colWidths := make([]int, len(rows[0])) for _, row := range rows { for i, cell := range row { colWidths[i] = max(colWidths[i], len(cell)) } } // Helper function to create a horizontal line createLine := func() string { line := "+" for _, width := range colWidths { line += strings.Repeat("-", width+2) + "+" } return line + "\n" } // Write top border sb.WriteString(au.Cyan(createLine()).String()) // Write header sb.WriteString(au.Cyan("| ").String()) for i, header := range rows[0] { sb.WriteString(au.Bold(fmt.Sprintf("%-*s", colWidths[i], header)).String()) sb.WriteString(au.Cyan(" | ").String()) } sb.WriteString("\n") // Write header-content separator sb.WriteString(au.Cyan(createLine()).String()) // Write content rows for _, row := range rows[1:] { sb.WriteString(au.Cyan("| ").String()) for i, cell := range row { sb.WriteString(fmt.Sprintf("%-*s", colWidths[i], cell)) sb.WriteString(au.Cyan(" | ").String()) } sb.WriteString("\n") } // Write bottom border sb.WriteString(au.Cyan(createLine()).String()) return sb.String() } func pathToString(path *meta.Path) string { b := strings.Builder{} for _, s := range path.Segments { b.WriteByte('/') switch s.Type { case meta.PathSegment_PARAM: b.WriteByte(':') case meta.PathSegment_WILDCARD: b.WriteByte('*') case meta.PathSegment_FALLBACK: b.WriteByte('!') } b.WriteString(s.Value) } return b.String() } func formatEnvVars(envVars map[infra.JSONPath]infra.EnvDesc) string { if len(envVars) == 0 { return "" } envByName := map[string]infra.EnvDesc{} for _, envVar := range envVars { envByName[envVar.Name] = envVar } envTable := [][]string{ {"Name", "Description"}, } for _, envVar := range envByName { envTable = append(envTable, []string{envVar.Name, envVar.Description}) } return aurora.Sprintf("%s\n%s\n", aurora.Bold("Environment Variables:"), generateTable(envTable)) } func configError(missing map[string][]string, validation map[infra.JSONPath]error) error { au := aurora.NewAurora(true) var errorMsg strings.Builder errorMsg.WriteString("\n") errorMsg.WriteString(au.Red("\nYour infra configuration is incomplete\n").String()) errorMsg.WriteString("\n") if len(missing) > 0 { errorMsg.WriteString(au.Red("Missing Resource Configurations:\n").String()) maxTypeLen := 0 for dataType := range missing { if len(dataType) > maxTypeLen { maxTypeLen = len(dataType) } } for dataType, values := range missing { paddedType := fmt.Sprintf("%-*s", maxTypeLen, dataType) errorMsg.WriteString(fmt.Sprintf(" %s: %s\n", au.Bold(paddedType), strings.Join(values, ", "))) } errorMsg.WriteString(" \n ") } if len(validation) > 0 { errorMsg.WriteString(au.Red("Validation Errors:\n").String()) for dataType, err := range validation { errorMsg.WriteString(fmt.Sprintf(" %s: %s\n", au.Bold(dataType), err.Error())) } errorMsg.WriteString(" \n ") } errorMsg.WriteString(LEARN_MORE) return errors.Newf(errorMsg.String()) } ================================================ FILE: cli/daemon/export.go ================================================ package daemon import ( "go/scanner" "encr.dev/cli/daemon/export" daemonpb "encr.dev/proto/encore/daemon" ) // Export exports the app. func (s *Server) Export(req *daemonpb.ExportRequest, stream daemonpb.Daemon_ExportServer) error { slog := &streamLog{stream: stream, buffered: false} log := newStreamLogger(slog) app, err := s.apps.Track(req.AppRoot) if err != nil { log.Error().Err(err).Msg("failed to resolve app") streamExit(stream, 1) return nil } exitCode := 0 success, err := export.Docker(stream.Context(), app, req, log, slog) if err != nil { exitCode = 1 if list, ok := err.(scanner.ErrorList); ok { for _, e := range list { log.Error().Msg(e.Error()) } } else { log.Error().Msg(err.Error()) } } else if !success { exitCode = 1 } streamExit(stream, exitCode) return nil } ================================================ FILE: cli/daemon/internal/runlog/runlog.go ================================================ package runlog import ( "io" "os" ) type Log interface { Stdout(buffered bool) io.Writer Stderr(buffered bool) io.Writer } type oslog struct{} func (oslog) Stdout(buffered bool) io.Writer { return os.Stdout } func (oslog) Stderr(buffered bool) io.Writer { return os.Stderr } func OS() Log { return oslog{} } ================================================ FILE: cli/daemon/internal/sym/sym.go ================================================ // Package sym parses symbol tables from Go binaries. package sym import ( "fmt" "io" "encr.dev/cli/internal/gosym" ) type Table struct { *gosym.Table BaseOffset uint64 } func Load(r io.ReaderAt) (*Table, error) { tbl, err := load(r) if err != nil { return nil, fmt.Errorf("sym.Load: %v", err) } return tbl, nil } ================================================ FILE: cli/daemon/internal/sym/sym_darwin.go ================================================ package sym import ( "debug/macho" "fmt" "io" "encr.dev/cli/internal/gosym" ) func load(r io.ReaderAt) (*Table, error) { exe, err := macho.NewFile(r) if err != nil { return nil, err } defer exe.Close() text := exe.Section("__text") if text == nil { return nil, fmt.Errorf("cannot find __text section") } textAddr := text.Addr pctbl := exe.Section("__gopclntab") if pctbl == nil { return nil, fmt.Errorf("cannot find __gopclntab section") } pctblData, err := pctbl.Data() if err != nil { return nil, fmt.Errorf("cannot read __gopclntab: %v", err) } symtab := exe.Section("__gosymtab") if symtab == nil { return nil, fmt.Errorf("cannot find __gosymtab section") } symtabData, err := symtab.Data() if err != nil { return nil, fmt.Errorf("cannot read __gosymtab: %v", err) } lntbl := gosym.NewLineTable(pctblData, textAddr) tbl, err := gosym.NewTable(symtabData, lntbl) if err != nil { return nil, err } return &Table{Table: tbl, BaseOffset: textAddr}, nil } ================================================ FILE: cli/daemon/internal/sym/sym_elf.go ================================================ //go:build !windows && !darwin // +build !windows,!darwin package sym import ( "debug/elf" "fmt" "io" "encr.dev/cli/internal/gosym" ) func load(r io.ReaderAt) (*Table, error) { exe, err := elf.NewFile(r) if err != nil { return nil, err } defer exe.Close() text := exe.Section(".text") if text == nil { return nil, fmt.Errorf("cannot find .text section") } textAddr := text.Addr pctbl := exe.Section(".gopclntab") if pctbl == nil { return nil, fmt.Errorf("cannot find .gopclntab section") } pctblData, err := pctbl.Data() if err != nil { return nil, fmt.Errorf("cannot read .gopclntab: %v", err) } symtab := exe.Section(".gosymtab") if symtab == nil { return nil, fmt.Errorf("cannot find .gosymtab section") } symtabData, err := symtab.Data() if err != nil { return nil, fmt.Errorf("cannot read .gosymtab: %v", err) } lntbl := gosym.NewLineTable(pctblData, textAddr) tbl, err := gosym.NewTable(symtabData, lntbl) if err != nil { return nil, err } return &Table{Table: tbl, BaseOffset: textAddr}, nil } ================================================ FILE: cli/daemon/internal/sym/sym_windows.go ================================================ package sym import ( "debug/pe" "fmt" "io" "encr.dev/cli/internal/gosym" ) // This code is a simplified version of $GOROOT/src/cmd/internal/objfile/pe.go. func load(r io.ReaderAt) (*Table, error) { exe, err := pe.NewFile(r) if err != nil { return nil, err } defer exe.Close() var imageBase, textStart uint64 switch oh := exe.OptionalHeader.(type) { case *pe.OptionalHeader32: imageBase = uint64(oh.ImageBase) case *pe.OptionalHeader64: imageBase = oh.ImageBase default: return nil, fmt.Errorf("pe file format not recognized") } if sect := exe.Section(".text"); sect != nil { textStart = imageBase + uint64(sect.VirtualAddress) } pclntab, err := loadPETable(exe, "runtime.pclntab", "runtime.epclntab") if err != nil { return nil, err } symtab, err := loadPETable(exe, "runtime.symtab", "runtime.esymtab") if err != nil { return nil, err } lntbl := gosym.NewLineTable(pclntab, textStart) tbl, err := gosym.NewTable(symtab, lntbl) if err != nil { return nil, err } return &Table{Table: tbl, BaseOffset: textStart}, nil } func findPESymbol(f *pe.File, name string) (*pe.Symbol, error) { for _, s := range f.Symbols { if s.Name != name { continue } if s.SectionNumber <= 0 { return nil, fmt.Errorf("symbol %s: invalid section number %d", name, s.SectionNumber) } if len(f.Sections) < int(s.SectionNumber) { return nil, fmt.Errorf("symbol %s: section number %d is larger than max %d", name, s.SectionNumber, len(f.Sections)) } return s, nil } return nil, fmt.Errorf("no %s symbol found", name) } func loadPETable(f *pe.File, sname, ename string) ([]byte, error) { ssym, err := findPESymbol(f, sname) if err != nil { return nil, err } esym, err := findPESymbol(f, ename) if err != nil { return nil, err } if ssym.SectionNumber != esym.SectionNumber { return nil, fmt.Errorf("%s and %s symbols must be in the same section", sname, ename) } sect := f.Sections[ssym.SectionNumber-1] data, err := sect.Data() if err != nil { return nil, err } return data[ssym.Value:esym.Value], nil } ================================================ FILE: cli/daemon/mcp/api_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "net" "net/http" "os" "sort" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "google.golang.org/protobuf/encoding/protojson" "encr.dev/cli/daemon/run" "encr.dev/pkg/builder" metav1 "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) func (m *Manager) registerAPITools() { // Add tool for calling an API endpoint m.server.AddTool(mcp.NewTool("call_endpoint", mcp.WithDescription("Make HTTP requests to any API endpoint in the currently open Encore. Always use this tool to make API calls and do not use curl. This tool will automatically start the application if it's not already running. This tool allows testing and interacting with the application's API endpoints, including authentication and custom payloads."), mcp.WithString("service", mcp.Description("The name of the service containing the endpoint to call. This must match a service defined in the currently open Encore.")), mcp.WithString("endpoint", mcp.Description("The name of the endpoint to call within the specified service. This must match an endpoint defined in the service.")), mcp.WithString("method", mcp.Description("The HTTP method to use for the request (GET, POST, PUT, DELETE, etc.). Must be a valid HTTP method.")), mcp.WithString("path", mcp.Description("The API request path, including any path parameters. This should match the endpoint's defined path pattern.")), mcp.WithString("payload", mcp.Description("JSON payload for the request containing all endpoint parameters. This includes path parameters, query parameters, headers, and request body as key-value pairs.")), mcp.WithString("auth_token", mcp.Description("Optional authentication token to include in the request. This is used for endpoints that require authentication.")), mcp.WithString("auth_payload", mcp.Description("Optional authentication payload in JSON format. This is used for custom authentication schemes.")), mcp.WithString("correlation_id", mcp.Description("Optional correlation ID to track the request through the system. Useful for debugging and tracing.")), ), m.callEndpoint) // Add tool for getting all services and endpoints m.server.AddTool(mcp.NewTool("get_services", mcp.WithDescription("Retrieve comprehensive information about all services and their endpoints in the currently open Encore. This includes endpoint schemas, documentation, and service-level metadata."), mcp.WithArray("services", mcp.Items(map[string]any{ "type": "string", "description": "Optional list of specific service names to retrieve information for. If not provided, returns information for all services in the currently open Encore.", })), mcp.WithArray("endpoints", mcp.Items(map[string]any{ "type": "string", "description": "Optional list of specific endpoint names to filter by. If not provided, returns all endpoints for the specified services.", })), mcp.WithBoolean("include_schemas", mcp.Description("When true, includes detailed request and response schemas for each endpoint. This is useful for understanding the data structures used by the API.")), mcp.WithBoolean("include_service_details", mcp.Description("When true, includes additional service-level information such as middleware, dependencies, and configuration.")), mcp.WithBoolean("include_endpoints", mcp.Description("When true, includes endpoint information in the response. Set to false to get only service-level information.")), ), m.getEndpoints) // Add tool for getting middleware metadata m.server.AddTool(mcp.NewTool("get_middleware", mcp.WithDescription("Retrieve detailed information about all middleware components in the currently open Encore, including their configuration, order of execution, and which services/endpoints they affect."), ), m.getMiddleware) // Add tool for getting auth handler metadata m.server.AddTool(mcp.NewTool("get_auth_handlers", mcp.WithDescription("Retrieve information about all authentication handlers in the currently open Encore, including their configuration, supported authentication methods, and which services/endpoints they protect."), ), m.getAuthHandlers) } func (m *Manager) callEndpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } // Extract and validate required arguments serviceStr, ok := request.Params.Arguments["service"].(string) if !ok { return nil, fmt.Errorf("missing or invalid service argument") } endpointStr, ok := request.Params.Arguments["endpoint"].(string) if !ok { return nil, fmt.Errorf("missing or invalid endpoint argument") } methodStr, ok := request.Params.Arguments["method"].(string) if !ok { return nil, fmt.Errorf("missing or invalid method argument") } pathStr, ok := request.Params.Arguments["path"].(string) if !ok { return nil, fmt.Errorf("missing or invalid path argument") } // Build API call parameters params := &run.ApiCallParams{ AppID: inst.PlatformOrLocalID(), Service: serviceStr, Endpoint: endpointStr, Path: pathStr, Method: methodStr, CorrelationID: "", } if !strings.HasPrefix(params.Path, "/") { params.Path = "/" + params.Path } // Add optional parameters if payload, ok := request.Params.Arguments["payload"].(string); ok && payload != "" { params.Payload = []byte(payload) } if authToken, ok := request.Params.Arguments["auth_token"].(string); ok && authToken != "" { params.AuthToken = authToken } if authPayload, ok := request.Params.Arguments["auth_payload"].(string); ok && authPayload != "" { params.AuthPayload = []byte(authPayload) } if correlationID, ok := request.Params.Arguments["correlation_id"].(string); ok && correlationID != "" { params.CorrelationID = correlationID } ns, err := m.ns.GetActive(ctx, inst) if err != nil { return nil, fmt.Errorf("failed to get active namespace: %w", err) } // Get the app's run instance appRun := m.run.FindRunByAppID(inst.PlatformOrLocalID()) if appRun == nil { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("failed to create listener: %w", err) } port := ln.Addr().(*net.TCPAddr).Port appRun, err = m.run.Start(ctx, run.StartParams{ App: inst, NS: ns, WorkingDir: "/", Watch: true, Listener: ln, ListenAddr: "127.0.0.1:" + fmt.Sprint(port), Environ: os.Environ(), OpsTracker: nil, Browser: run.BrowserModeNever, Debug: builder.DebugModeDisabled, }) if err != nil { return nil, fmt.Errorf("failed to start app run: %w", err) } } started := false for !started { select { case <-appRun.Done(): return nil, fmt.Errorf("app run failed to start") case <-time.After(100 * time.Millisecond): // Check if the app is ready by polling the health endpoint resp, err := http.Get("http://" + appRun.ListenAddr + "/__encore/healthz") if err != nil { continue } resp.Body.Close() started = resp.StatusCode == 200 } } // Call the API result, err := run.CallAPI(ctx, appRun, params) if err != nil { return nil, fmt.Errorf("API call failed: %w", err) } // Convert body to string if body, ok := result["body"]; ok { switch v := body.(type) { case []byte: result["body"] = string(v) } } // Serialize the response jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getEndpoints(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Get the list of services to retrieve endpoints for var serviceNames []string if services, ok := request.Params.Arguments["services"].([]interface{}); ok { for _, svc := range services { if svcName, ok := svc.(string); ok && svcName != "" { serviceNames = append(serviceNames, svcName) } } } // If no services specified, get all services if len(serviceNames) == 0 { for _, svc := range md.Svcs { serviceNames = append(serviceNames, svc.Name) } } // Parse request parameters includeEndpoints := true if include, ok := request.Params.Arguments["include_endpoints"].(bool); ok { includeEndpoints = include } var endpointNames []string var endpointFilter map[string]bool var hasEndpointFilter bool // Only process endpoint filters if we're including endpoints if includeEndpoints { // Get the list of endpoint names to filter by if endpoints, ok := request.Params.Arguments["endpoints"].([]interface{}); ok { for _, ep := range endpoints { if epName, ok := ep.(string); ok && epName != "" { endpointNames = append(endpointNames, epName) } } } // Create a map for faster lookups when filtering endpoints endpointFilter = make(map[string]bool) for _, name := range endpointNames { endpointFilter[name] = true } hasEndpointFilter = len(endpointFilter) > 0 } includeSchemas := false if include, ok := request.Params.Arguments["include_schemas"].(bool); ok { includeSchemas = include } includeServiceDetails := false if include, ok := request.Params.Arguments["include_service_details"].(bool); ok { includeServiceDetails = include } // Set up decl map for schema info if needed var declByID map[uint32]*schema.Decl if includeEndpoints && includeSchemas { declByID = map[uint32]*schema.Decl{} for _, decl := range md.Decls { declByID[decl.Id] = decl } } // Create a map to store services with their endpoints serviceMap := make(map[string]map[string]interface{}) // Process each requested service for _, serviceName := range serviceNames { // Find the service in metadata var targetService *metav1.Service for _, svc := range md.Svcs { if svc.Name == serviceName { targetService = svc break } } if targetService == nil { // Skip services that don't exist instead of returning an error continue } // Initialize service data serviceData := map[string]interface{}{} // Add service details if requested if includeServiceDetails { serviceData["name"] = targetService.Name serviceData["rel_path"] = targetService.RelPath serviceData["has_config"] = targetService.HasConfig serviceData["databases"] = targetService.Databases serviceData["rpc_count"] = len(targetService.Rpcs) } // Process endpoints if requested if includeEndpoints { // Initialize an empty array for this service's endpoints endpoints := make([]map[string]interface{}, 0) // Process all RPCs for this service for _, rpc := range targetService.Rpcs { // Skip this endpoint if it's not in the filter list (when filter is provided) if hasEndpointFilter && !endpointFilter[rpc.Name] { continue } endpoint := map[string]interface{}{ "name": rpc.Name, "access_type": rpc.AccessType.String(), "http_methods": rpc.HttpMethods, } // Add path if available if rpc.Path != nil { pathSegments := make([]string, 0) for _, segment := range rpc.Path.Segments { pathSegments = append(pathSegments, segment.Value) } endpoint["path"] = strings.Join(pathSegments, "/") } // Add documentation if available if rpc.Doc != nil { endpoint["doc"] = *rpc.Doc } // Include schema information if requested if includeSchemas { schemas := map[string]interface{}{} // For request and response schemas if rpc.RequestSchema != nil { str, _ := NamedOrInlineStruct(declByID, rpc.RequestSchema) qry, headers, cookies, body := StructBits(str, rpc.HttpMethods[0], false, false, true) schemas["request_schema"] = strings.Join([]string{"{", qry, headers, cookies, body, "}"}, "") } if rpc.ResponseSchema != nil { str, _ := NamedOrInlineStruct(declByID, rpc.ResponseSchema) qry, headers, cookies, body := StructBits(str, rpc.HttpMethods[0], true, false, true) schemas["response_schema"] = strings.Join([]string{"{", qry, headers, cookies, body, "}"}, "") } if len(schemas) > 0 { endpoint["schemas"] = schemas } } endpoints = append(endpoints, endpoint) } // Add endpoints to the service data if any were found if len(endpoints) > 0 { serviceData["endpoints"] = endpoints } } // Add service to the result map if it has data or endpoints if len(serviceData) > 0 { serviceMap[serviceName] = serviceData } } // Create the result object with services and summary result := map[string]interface{}{ "services": serviceMap, "summary": map[string]interface{}{ "total_services": len(serviceMap), }, } // Add endpoint count to summary if we're including endpoints if includeEndpoints { totalEndpoints := 0 for _, serviceData := range serviceMap { if endpoints, ok := serviceData["endpoints"].([]map[string]interface{}); ok { totalEndpoints += len(endpoints) } } result["summary"].(map[string]interface{})["total_endpoints"] = totalEndpoints } // Add filter information to summary if filters were applied if len(serviceNames) < len(md.Svcs) || (includeEndpoints && hasEndpointFilter) { filters := map[string]interface{}{} if len(serviceNames) < len(md.Svcs) { filters["services"] = serviceNames } if includeEndpoints && hasEndpointFilter { filters["endpoints"] = endpointNames } result["summary"].(map[string]interface{})["filters_applied"] = filters } jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal services and endpoints: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getMiddleware(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Find middleware definition locations from trace nodes middlewareDefLocations := make(map[string]map[string]interface{}) // Scan through all packages to find trace nodes related to middleware for _, pkg := range md.Pkgs { for _, node := range pkg.TraceNodes { // Check for middleware definitions if node.GetMiddlewareDef() != nil { middlewareDef := node.GetMiddlewareDef() middlewareName := middlewareDef.Name // Use package path + name as a unique identifier middlewareID := fmt.Sprintf("%s/%s", middlewareDef.PkgRelPath, middlewareName) middlewareDefLocations[middlewareID] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, "package_path": middlewareDef.PkgRelPath, } } } } // Group middleware by type (global vs service-specific) globalMiddleware := make([]map[string]interface{}, 0) serviceMiddleware := make(map[string][]map[string]interface{}) // Process all middleware for _, middleware := range md.Middleware { middlewareInfo := map[string]interface{}{ "doc": middleware.Doc, "global": middleware.Global, } // Add qualified name information if available if middleware.Name != nil { name := map[string]interface{}{ "package": middleware.Name.Pkg, "name": middleware.Name.Name, } middlewareInfo["name"] = name // Add definition location if available middlewareID := fmt.Sprintf("%s/%s", middleware.Name.Pkg, middleware.Name.Name) if location, exists := middlewareDefLocations[middlewareID]; exists { middlewareInfo["definition"] = map[string]interface{}{ "filepath": location["filepath"], "line_start": location["line_start"], "line_end": location["line_end"], "column_start": location["column_start"], "column_end": location["column_end"], } } } // Add target information if available if len(middleware.Target) > 0 { targets := make([]map[string]interface{}, 0, len(middleware.Target)) for _, target := range middleware.Target { targetInfo := map[string]interface{}{ "type": target.Type.String(), "value": target.Value, } targets = append(targets, targetInfo) } middlewareInfo["targets"] = targets } // Add to the appropriate group if middleware.Global { globalMiddleware = append(globalMiddleware, middlewareInfo) } else if middleware.ServiceName != nil { serviceName := *middleware.ServiceName if _, exists := serviceMiddleware[serviceName]; !exists { serviceMiddleware[serviceName] = make([]map[string]interface{}, 0) } serviceMiddleware[serviceName] = append(serviceMiddleware[serviceName], middlewareInfo) } } // Build the final result result := map[string]interface{}{ "global": globalMiddleware, "services": serviceMiddleware, "summary": map[string]interface{}{ "total_middleware": len(md.Middleware), "global_middleware": len(globalMiddleware), "service_middleware": make(map[string]int), "service_count": len(serviceMiddleware), }, } // Add counts by service summary := result["summary"].(map[string]interface{}) for service, middleware := range serviceMiddleware { summary["service_middleware"].(map[string]int)[service] = len(middleware) } // Sort middleware by name for consistent output sort.Slice(globalMiddleware, func(i, j int) bool { nameI := "" nameJ := "" if name, ok := globalMiddleware[i]["name"].(map[string]interface{}); ok { nameI = name["name"].(string) } if name, ok := globalMiddleware[j]["name"].(map[string]interface{}); ok { nameJ = name["name"].(string) } return nameI < nameJ }) // Sort service middleware as well for service, middleware := range serviceMiddleware { sort.Slice(middleware, func(i, j int) bool { nameI := "" nameJ := "" if name, ok := middleware[i]["name"].(map[string]interface{}); ok { nameI = name["name"].(string) } if name, ok := middleware[j]["name"].(map[string]interface{}); ok { nameJ = name["name"].(string) } return nameI < nameJ }) serviceMiddleware[service] = middleware } jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal middleware information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getAuthHandlers(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Find auth handler definition locations from trace nodes authHandlerDefLocations := make(map[string]map[string]interface{}) // Scan through all packages to find trace nodes related to auth handlers for _, pkg := range md.Pkgs { for _, node := range pkg.TraceNodes { // Check for auth handler definitions if node.GetAuthHandlerDef() != nil { authHandlerDef := node.GetAuthHandlerDef() serviceName := authHandlerDef.ServiceName handlerName := authHandlerDef.Name // Use service name + handler name as a unique identifier handlerID := fmt.Sprintf("%s/%s", serviceName, handlerName) authHandlerDefLocations[handlerID] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, "service_name": serviceName, } } } } // Process the main auth handler if it exists var mainAuthHandler map[string]interface{} if md.AuthHandler != nil { auth := md.AuthHandler authData := map[string]interface{}{ "name": auth.Name, "doc": auth.Doc, "service_name": auth.ServiceName, "pkg_path": auth.PkgPath, "pkg_name": auth.PkgName, } // Add parameter and auth data type information if auth.Params != nil { paramsData, err := protojson.Marshal(auth.Params) if err == nil { var paramsJson interface{} if err := json.Unmarshal(paramsData, ¶msJson); err == nil { authData["params"] = paramsJson } } } if auth.AuthData != nil { authDataTypeData, err := protojson.Marshal(auth.AuthData) if err == nil { var authDataJson interface{} if err := json.Unmarshal(authDataTypeData, &authDataJson); err == nil { authData["auth_data"] = authDataJson } } } // Add location information if available handlerID := fmt.Sprintf("%s/%s", auth.ServiceName, auth.Name) if location, exists := authHandlerDefLocations[handlerID]; exists { authData["definition"] = map[string]interface{}{ "filepath": location["filepath"], "line_start": location["line_start"], "line_end": location["line_end"], "column_start": location["column_start"], "column_end": location["column_end"], } } mainAuthHandler = authData } // Process gateway auth handlers gatewayAuthHandlers := make(map[string]map[string]interface{}) for _, gateway := range md.Gateways { if gateway.Explicit != nil && gateway.Explicit.AuthHandler != nil { auth := gateway.Explicit.AuthHandler authData := map[string]interface{}{ "name": auth.Name, "doc": auth.Doc, "service_name": auth.ServiceName, "pkg_path": auth.PkgPath, "pkg_name": auth.PkgName, "gateway_name": gateway.EncoreName, } // Add parameter and auth data type information if auth.Params != nil { paramsData, err := protojson.Marshal(auth.Params) if err == nil { var paramsJson interface{} if err := json.Unmarshal(paramsData, ¶msJson); err == nil { authData["params"] = paramsJson } } } if auth.AuthData != nil { authDataTypeData, err := protojson.Marshal(auth.AuthData) if err == nil { var authDataJson interface{} if err := json.Unmarshal(authDataTypeData, &authDataJson); err == nil { authData["auth_data"] = authDataJson } } } // Add location information if available handlerID := fmt.Sprintf("%s/%s", auth.ServiceName, auth.Name) if location, exists := authHandlerDefLocations[handlerID]; exists { authData["definition"] = map[string]interface{}{ "filepath": location["filepath"], "line_start": location["line_start"], "line_end": location["line_end"], "column_start": location["column_start"], "column_end": location["column_end"], } } gatewayAuthHandlers[gateway.EncoreName] = authData } } // Build the final result result := map[string]interface{}{ "main_auth_handler": mainAuthHandler, "gateway_auth_handlers": gatewayAuthHandlers, "summary": map[string]interface{}{ "has_main_auth": mainAuthHandler != nil, "gateway_count": len(md.Gateways), "auth_gateway_count": len(gatewayAuthHandlers), }, } jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal auth handler information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/bucket_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "os" "github.com/mark3labs/mcp-go/mcp" "encr.dev/pkg/emulators/storage/gcsemu" ) func (m *Manager) registerBucketTools() { m.server.AddTool(mcp.NewTool("get_storage_buckets", mcp.WithDescription("Retrieve comprehensive information about all storage buckets in the currently open Encore, including their configurations, access patterns, and the services that interact with them. This tool helps understand the application's storage architecture and data management strategy."), ), m.getStorageBuckets) m.server.AddTool(mcp.NewTool("get_objects", mcp.WithDescription("List and retrieve metadata about objects stored in one or more storage buckets. This tool helps inspect the contents of storage buckets and understand the data stored in them."), mcp.WithArray("buckets", mcp.Items(map[string]any{ "type": "string", "description": "List of bucket names to list objects from. Each bucket must be defined in the currently open Encore's storage configuration.", })), ), m.listObjects) } func (m *Manager) listObjects(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { app, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } clusterNS, err := m.ns.GetActive(ctx, app) if err != nil { return nil, fmt.Errorf("failed to get active namespace: %w", err) } dir, err := m.objects.BaseDir(clusterNS.ID) if err != nil { return nil, fmt.Errorf("failed to get base directory: %w", err) } store := gcsemu.NewFileStore(dir) buckets, ok := request.Params.Arguments["buckets"].([]any) if !ok { return nil, fmt.Errorf("buckets is not an array") } objects := map[string][]map[string]interface{}{} for _, bucket := range buckets { bucketName := bucket.(string) var bucketObjects []map[string]interface{} err = store.Walk(ctx, bucketName, func(ctx context.Context, filename string, fInfo os.FileInfo) error { objectInfo := map[string]interface{}{ "name": filename, "size": fInfo.Size(), "last_modified": fInfo.ModTime(), "is_directory": fInfo.IsDir(), } bucketObjects = append(bucketObjects, objectInfo) return nil }) if err != nil { return nil, fmt.Errorf("failed to walk bucket objects: %w", err) } objects[bucketName] = bucketObjects } jsonData, err := json.Marshal(objects) if err != nil { return nil, fmt.Errorf("failed to marshal object information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getStorageBuckets(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Build map of services that use each bucket with their operations bucketUsageByService := make(map[string][]map[string]interface{}) for _, svc := range md.Svcs { for _, bucketUsage := range svc.Buckets { bucketName := bucketUsage.Bucket // Convert operations to strings operations := make([]string, 0, len(bucketUsage.Operations)) for _, op := range bucketUsage.Operations { operations = append(operations, op.String()) } // Create usage info usageInfo := map[string]interface{}{ "service_name": svc.Name, "operations": operations, } // Add to map if _, exists := bucketUsageByService[bucketName]; !exists { bucketUsageByService[bucketName] = make([]map[string]interface{}, 0) } bucketUsageByService[bucketName] = append(bucketUsageByService[bucketName], usageInfo) } } // Collect bucket definition locations from trace nodes bucketDefLocations := make(map[string]map[string]interface{}) // Find bucket definitions in trace nodes if possible // Currently no specific bucket definition node type in the TraceNode, // so we leave this empty for now. This could be expanded in the future // if the metadata provides better tracking. // Process all buckets buckets := make([]map[string]interface{}, 0) for _, bucket := range md.Buckets { bucketInfo := map[string]interface{}{ "name": bucket.Name, "versioned": bucket.Versioned, "public": bucket.Public, } // Add documentation if available if bucket.Doc != nil { bucketInfo["doc"] = *bucket.Doc } // Add location information if available if location, exists := bucketDefLocations[bucket.Name]; exists { bucketInfo["definition"] = location } // Add service usage information if usages, exists := bucketUsageByService[bucket.Name]; exists { bucketInfo["service_usage"] = usages } else { bucketInfo["service_usage"] = []map[string]interface{}{} } buckets = append(buckets, bucketInfo) } jsonData, err := json.Marshal(buckets) if err != nil { return nil, fmt.Errorf("failed to marshal storage buckets information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/cache_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "sort" "strings" "github.com/mark3labs/mcp-go/mcp" "google.golang.org/protobuf/encoding/protojson" ) func (m *Manager) registerCacheTools() { m.server.AddTool(mcp.NewTool("get_cache_keyspaces", mcp.WithDescription("Retrieve comprehensive information about all cache keyspaces in the currently open Encore, including their configurations, usage patterns, and the services that interact with them. This tool helps understand the application's caching strategy and data access patterns."), ), m.getCacheKeyspaces) } func (m *Manager) getCacheKeyspaces(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Find keyspace definition locations from trace nodes keyspaceDefLocations := make(map[string]map[string]map[string]interface{}) // Scan through all packages to find trace nodes related to cache keyspaces for _, pkg := range md.Pkgs { for _, node := range pkg.TraceNodes { // Check for cache keyspace definitions if node.GetCacheKeyspace() != nil { keyspaceDef := node.GetCacheKeyspace() clusterName := keyspaceDef.ClusterName keyspaceName := keyspaceDef.VarName // Initialize maps if needed if _, exists := keyspaceDefLocations[clusterName]; !exists { keyspaceDefLocations[clusterName] = make(map[string]map[string]interface{}) } if _, exists := keyspaceDefLocations[clusterName][keyspaceName]; !exists { keyspaceDefLocations[clusterName][keyspaceName] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, "package_path": keyspaceDef.PkgRelPath, } } } } } // Build the result result := make([]map[string]interface{}, 0) // Process all cache clusters for _, cluster := range md.CacheClusters { clusterInfo := map[string]interface{}{ "name": cluster.Name, "eviction_policy": cluster.EvictionPolicy, "doc": cluster.Doc, } // Process keyspaces for this cluster keyspaces := make([]map[string]interface{}, 0) for _, keyspace := range cluster.Keyspaces { keyspaceInfo := map[string]interface{}{ "service": keyspace.Service, "doc": keyspace.Doc, } // Add key and value type information from protojson if keyspace.KeyType != nil { keyTypeData, err := protojson.Marshal(keyspace.KeyType) if err == nil { var keyTypeJson interface{} if err := json.Unmarshal(keyTypeData, &keyTypeJson); err == nil { keyspaceInfo["key_type"] = keyTypeJson } } } if keyspace.ValueType != nil { valueTypeData, err := protojson.Marshal(keyspace.ValueType) if err == nil { var valueTypeJson interface{} if err := json.Unmarshal(valueTypeData, &valueTypeJson); err == nil { keyspaceInfo["value_type"] = valueTypeJson } } } // Add path pattern if available if keyspace.PathPattern != nil { pathPattern := make([]string, 0) for _, segment := range keyspace.PathPattern.Segments { pathPattern = append(pathPattern, segment.Value) } keyspaceInfo["path_pattern"] = strings.Join(pathPattern, "/") } // Add definition location if available // We need to find the keyspace variable name from the definition data // This is approximate as we don't have a direct mapping in the metadata if locations, ok := keyspaceDefLocations[cluster.Name]; ok { for keyspaceName, location := range locations { // Try to match by service if location["package_path"] != "" && keyspace.Service != "" { // If this location is for a keyspace in this service, add it if packageService := findServiceNameForPackage(md, location["package_path"].(string)); packageService == keyspace.Service { keyspaceInfo["name"] = keyspaceName keyspaceInfo["definition"] = map[string]interface{}{ "filepath": location["filepath"], "line_start": location["line_start"], "line_end": location["line_end"], "column_start": location["column_start"], "column_end": location["column_end"], } break } } } } keyspaces = append(keyspaces, keyspaceInfo) } clusterInfo["keyspaces"] = keyspaces result = append(result, clusterInfo) } // Sort by cluster name for consistent output sort.Slice(result, func(i, j int) bool { return result[i]["name"].(string) < result[j]["name"].(string) }) jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal cache keyspaces information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/cron_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "github.com/mark3labs/mcp-go/mcp" ) func (m *Manager) registerCronTools() { m.server.AddTool(mcp.NewTool("get_cronjobs", mcp.WithDescription("Retrieve detailed information about all scheduled cron jobs in the currently open Encore, including their schedules, endpoints they trigger, and execution history. This tool helps understand the application's background task scheduling and automation capabilities."), ), m.getCronJobs) } func (m *Manager) getCronJobs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Create a map to find service and endpoint locations endpointLocations := make(map[string]map[string]map[string]interface{}) // Scan through all packages to find trace nodes related to RPC definitions for _, pkg := range md.Pkgs { for _, node := range pkg.TraceNodes { // Check for RPC definitions if node.GetRpcDef() != nil { rpcDef := node.GetRpcDef() serviceName := rpcDef.ServiceName rpcName := rpcDef.RpcName // Initialize maps if needed if _, exists := endpointLocations[serviceName]; !exists { endpointLocations[serviceName] = make(map[string]map[string]interface{}) } if _, exists := endpointLocations[serviceName][rpcName]; !exists { endpointLocations[serviceName][rpcName] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, } } } } } // Process cron jobs with location information cronjobs := make([]map[string]interface{}, 0) for _, job := range md.CronJobs { jobInfo := map[string]interface{}{ "id": job.Id, "title": job.Title, "schedule": job.Schedule, } // Add documentation if available if job.Doc != nil { jobInfo["doc"] = *job.Doc } // Add endpoint information if job.Endpoint != nil { endpoint := map[string]interface{}{ "package": job.Endpoint.Pkg, "name": job.Endpoint.Name, } // If we can find the service for this endpoint, add location info for _, svc := range md.Svcs { for _, rpc := range svc.Rpcs { if rpc.Name == job.Endpoint.Name && (svc.RelPath == job.Endpoint.Pkg || svc.Name == findServiceNameForPackage(md, job.Endpoint.Pkg)) { endpoint["service_name"] = svc.Name // Add location if we found it if locations, ok := endpointLocations[svc.Name]; ok { if loc, ok := locations[rpc.Name]; ok { endpoint["definition"] = loc } } break } } } jobInfo["endpoint"] = endpoint } cronjobs = append(cronjobs, jobInfo) } jsonData, err := json.Marshal(cronjobs) if err != nil { return nil, fmt.Errorf("failed to marshal cron jobs information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/db_tools.go ================================================ package mcp import ( "context" "database/sql" "encoding/json" "errors" "fmt" "github.com/lib/pq" "github.com/mark3labs/mcp-go/mcp" "encr.dev/cli/daemon/sqldb" "encr.dev/pkg/fns" ) func (m *Manager) registerDatabaseTools() { // Add tool for getting all databases and optionally their tables m.server.AddTool(mcp.NewTool("get_databases", mcp.WithDescription("Retrieve metadata about all SQL databases defined in the currently open Encore, including their schema, tables, and relationships. This tool helps understand the database structure and can optionally include detailed table information."), mcp.WithBoolean("include_tables", mcp.Description("When true, includes detailed information about each table in the database, including column names, types, and constraints. This is useful for understanding the complete database schema.")), mcp.WithArray("databases", mcp.Items(map[string]any{ "type": "string", "description": "Optional list of specific database names to retrieve information for. If not provided, returns information for all databases in the currently open Encore.", })), ), m.getDatabases) // Add tool for querying a database m.server.AddTool(mcp.NewTool("query_database", mcp.WithDescription("Execute SQL queries against one or more databases in the currently open Encore. This tool allows running custom SQL queries to inspect or manipulate data while respecting the application's database access patterns."), mcp.WithArray("queries", mcp.Items(map[string]any{ "type": "object", "description": "Array of query objects, where each object must contain 'database' (the database name to query) and 'query' (the SQL query to execute) fields. Multiple queries can be executed in a single call.", "properties": map[string]any{ "database": map[string]any{ "type": "string", "description": "The database name to query", }, "query": map[string]any{ "type": "string", "description": "The SQL query to execute", }, }, "required": []string{"database", "query"}, })), ), m.runQuery) } func (m *Manager) getDatabases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } includeTables := false if includeTablesParam, ok := request.Params.Arguments["include_tables"]; ok { includeTables, _ = includeTablesParam.(bool) } // Parse databases parameter if provided var filterDBs map[string]bool if dbsParam, ok := request.Params.Arguments["databases"]; ok && dbsParam != nil { dbsArray, ok := dbsParam.([]interface{}) if ok && len(dbsArray) > 0 { filterDBs = make(map[string]bool) for _, db := range dbsArray { if dbName, ok := db.(string); ok { filterDBs[dbName] = true } } } } // Build database list databases := make([]map[string]interface{}, 0) for _, db := range md.SqlDatabases { // Skip if we have a filter and this database isn't in it if filterDBs != nil && !filterDBs[db.Name] { continue } dbInfo := map[string]interface{}{ "name": db.Name, "doc": db.Doc, } // If we should include tables, get table information if includeTables { tables, err := m.getTablesForDatabase(ctx, db.Name) if err != nil { // Don't fail the whole request if one database fails dbInfo["tables_error"] = err.Error() } else { dbInfo["tables"] = tables } } databases = append(databases, dbInfo) } jsonData, err := json.Marshal(databases) if err != nil { return nil, fmt.Errorf("failed to marshal database list: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getTablesForDatabase(ctx context.Context, dbName string) ([]map[string]interface{}, error) { var tables []map[string]interface{} err := m.withConn(ctx, dbName, func(db *sql.DB) error { // Query to get tables and their columns from PostgreSQL query := ` SELECT t.table_name, ARRAY_AGG(c.column_name ORDER BY c.ordinal_position) as columns, ARRAY_AGG(c.data_type ORDER BY c.ordinal_position) as column_types FROM information_schema.tables t JOIN information_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema WHERE t.table_schema = 'public' GROUP BY t.table_name ORDER BY t.table_name; ` rows, err := db.QueryContext(ctx, query) if err != nil { return fmt.Errorf("failed to query tables: %w", err) } defer rows.Close() tables = []map[string]interface{}{} for rows.Next() { var tableName string var columns pq.StringArray var columnTypes pq.StringArray if err := rows.Scan(&tableName, &columns, &columnTypes); err != nil { return fmt.Errorf("failed to scan row: %w", err) } // Create structured column information columnInfo := make([]map[string]string, len(columns)) for i := range columns { columnInfo[i] = map[string]string{ "name": columns[i], "type": columnTypes[i], } } tables = append(tables, map[string]interface{}{ "table_name": tableName, "columns": columnInfo, }) } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating rows: %w", err) } return nil }) return tables, err } func (m *Manager) withConn(ctx context.Context, dbName string, fn func(db *sql.DB) error) error { app, err := m.getApp(ctx) if err != nil { return fmt.Errorf("failed to get app: %w", err) } clusterNS, err := m.ns.GetActive(ctx, app) if err != nil { return fmt.Errorf("failed to get active namespace: %w", err) } md, err := app.CachedMetadata() if err != nil { return fmt.Errorf("failed to get metadata: %w", err) } clusterID := sqldb.GetClusterID(app, sqldb.Run, clusterNS) cluster := m.cluster.Create(ctx, &sqldb.CreateParams{ ClusterID: clusterID, Memfs: sqldb.Run.Memfs(), }) if _, err := cluster.Start(ctx, nil); err != nil { return err } else if err := cluster.Setup(ctx, app.Root(), md); err != nil { return err } info, err := cluster.Info(ctx) if err != nil { return fmt.Errorf("failed to get cluster info: %w", err) } else if info.Status != sqldb.Running { return errors.New("cluster not running") } admin, ok := info.Encore.First(sqldb.RoleRead) if !ok { return errors.New("unable to find superuser or admin roles") } uri := info.ConnURI(dbName, admin) pool, err := sql.Open("pgx", uri) if err != nil { return err } defer fns.CloseIgnore(pool) return fn(pool) } func (m *Manager) runQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { queriesParam, ok := request.Params.Arguments["queries"].([]interface{}) if !ok || len(queriesParam) == 0 { return nil, fmt.Errorf("missing or invalid 'queries' parameter") } results := make(map[string][]map[string]interface{}) for _, queryObj := range queriesParam { queryMap, ok := queryObj.(map[string]interface{}) if !ok { continue } dbName, ok := queryMap["database"].(string) if !ok || dbName == "" { continue } sqlQuery, ok := queryMap["query"].(string) if !ok || sqlQuery == "" { continue } // Execute the query for this database var queryResults []map[string]interface{} err := m.withConn(ctx, dbName, func(db *sql.DB) error { rows, err := db.QueryContext(ctx, sqlQuery) if err != nil { return fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() // Serialize rows to JSON columns, err := rows.Columns() if err != nil { return fmt.Errorf("failed to get columns: %w", err) } queryResults = make([]map[string]interface{}, 0) for rows.Next() { values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range values { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { return fmt.Errorf("failed to scan row: %w", err) } row := make(map[string]interface{}) for i, col := range columns { row[col] = values[i] } queryResults = append(queryResults, row) } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating rows: %w", err) } return nil }) // Store results for this query key := fmt.Sprintf("%s: %s", dbName, sqlQuery) if err != nil { results[key] = []map[string]interface{}{ {"error": err.Error()}, } } else { results[key] = queryResults } } jsonData, err := json.Marshal(results) if err != nil { return nil, fmt.Errorf("failed to marshal results: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/docs_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "net/http" "regexp" "strings" "time" "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/mark3labs/mcp-go/mcp" "golang.org/x/net/html" ) func (m *Manager) registerDocsTools() { // Add tool for searching Encore documentation using Algolia m.server.AddTool(mcp.NewTool("search_docs", mcp.WithDescription("Search the Encore documentation using Algolia's search engine. This tool helps find relevant documentation about Encore features, best practices, and examples."), mcp.WithString("query", mcp.Description("The search query to find relevant documentation. Can include keywords, feature names, or specific topics you're looking for.")), mcp.WithNumber("page", mcp.Description("Page number for pagination, starting from 0. Use this to navigate through large result sets.")), mcp.WithNumber("hits_per_page", mcp.Description("Number of results to return per page. Default is 10. Adjust this to control the size of the result set.")), mcp.WithArray("facet_filters", mcp.Items(map[string]any{ "type": "string", "description": "Optional array of facet filters to narrow down search results. These can include categories, tags, or other metadata to refine the search.", })), ), m.searchDocs) // Add tool for fetching Encore documentation content m.server.AddTool(mcp.NewTool("get_docs", mcp.WithDescription("Retrieve the full content of specific documentation pages. This tool is useful for getting detailed information about specific topics after finding them with search_docs."), mcp.WithArray("paths", mcp.Items(map[string]any{ "type": "string", "description": "List of documentation paths to fetch (e.g. ['/docs/concepts', '/docs/services']). These paths should be valid documentation URLs without the domain.", })), ), m.getDocs) } func (m *Manager) searchDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract parameters from the request query, ok := request.Params.Arguments["query"].(string) if !ok || query == "" { return nil, fmt.Errorf("invalid or missing query parameter") } // Default pagination settings page := 0 if p, ok := request.Params.Arguments["page"].(float64); ok { page = int(p) } hitsPerPage := 10 if hpp, ok := request.Params.Arguments["hits_per_page"].(float64); ok { hitsPerPage = int(hpp) } // Process facet filters if provided var facetFilters []string if filters, ok := request.Params.Arguments["facet_filters"].([]interface{}); ok { for _, filter := range filters { if filterStr, ok := filter.(string); ok && filterStr != "" { facetFilters = append(facetFilters, filterStr) } } } // Set context timeout ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() // Perform the actual search with Algolia result, err := performAlgoliaSearch(ctx, query, page, hitsPerPage, facetFilters) if err != nil { return nil, fmt.Errorf("failed to search docs: %w", err) } // Marshal the response jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal search results: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } // performAlgoliaSearch performs the actual search against Algolia func performAlgoliaSearch(ctx context.Context, query string, page, hitsPerPage int, facetFilters []string) (map[string]interface{}, error) { // Initialize Algolia client with configurable app ID and API key // In a production environment, these should be loaded from configuration appID := "R7DAHI8GEL" apiKey := "85bf0533142cccdbbc6b9deb92b19fdf" client := search.NewClient(appID, apiKey) index := client.InitIndex("encore_docs") // Build search parameters params := []interface{}{ opt.Page(page), opt.HitsPerPage(hitsPerPage), } // Add facet filters if any if len(facetFilters) > 0 { // For a simple AND of all filters - need to convert []string to variadic arguments if len(facetFilters) == 1 { params = append(params, opt.FacetFilter(facetFilters[0])) } else { // Convert []string to []interface{} for compatibility facetFilterInterfaces := make([]interface{}, len(facetFilters)) for i, filter := range facetFilters { facetFilterInterfaces[i] = filter } params = append(params, opt.FacetFilterAnd(facetFilterInterfaces...)) } } // Perform the search res, err := index.Search(query, params...) if err != nil { return nil, fmt.Errorf("algolia search failed: %w", err) } // Convert the Algolia response to our expected format result := map[string]interface{}{ "hits": res.Hits, "page": res.Page, "nbHits": res.NbHits, "nbPages": res.NbPages, "hitsPerPage": res.HitsPerPage, "processingTimeMS": res.ProcessingTimeMS, "query": query, "params": res.Params, } return result, nil } func (m *Manager) getDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract paths parameter from the request var docPaths []string if paths, ok := request.Params.Arguments["paths"].([]interface{}); ok { for _, path := range paths { if pathStr, ok := path.(string); ok && pathStr != "" { docPaths = append(docPaths, pathStr) } } } if len(docPaths) == 0 { return nil, fmt.Errorf("no valid documentation paths provided") } // Set context timeout ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Fetch content for each path result := make(map[string]interface{}) docs := make(map[string]interface{}) for _, path := range docPaths { // Ensure path starts with a slash if !strings.HasPrefix(path, "/") { path = "/" + path } url := "https://encore.dev" + path content, err := fetchDocContent(ctx, url) if err != nil { docs[path] = map[string]interface{}{ "error": err.Error(), "success": false, } } else { docs[path] = map[string]interface{}{ "content": content, "url": url, "success": true, } } } result["docs"] = docs result["summary"] = map[string]interface{}{ "total": len(docPaths), "base_url": "https://encore.dev", "requested_at": time.Now().UTC().Format(time.RFC3339), } // Marshal the response jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal document results: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } // fetchDocContent fetches content from a URL and returns only the text content from the
tag func fetchDocContent(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } // Add appropriate headers to mimic a browser request req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("failed to fetch URL: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("received non-OK status code: %d", resp.StatusCode) } // Parse the HTML document doc, err := html.Parse(resp.Body) if err != nil { return "", fmt.Errorf("failed to parse HTML: %w", err) } // Find the main tag mainNode := findMainElement(doc) if mainNode == nil { return "", fmt.Errorf("no
tag found in the document") } // Extract text content from the main tag var textContent strings.Builder extractText(mainNode, &textContent) // Clean up the text content cleanedText := cleanText(textContent.String()) return cleanedText, nil } // findMainElement finds the
element in the HTML document func findMainElement(n *html.Node) *html.Node { if n.Type == html.ElementNode && strings.ToLower(n.Data) == "main" { return n } for c := n.FirstChild; c != nil; c = c.NextSibling { if result := findMainElement(c); result != nil { return result } } return nil } // extractText recursively extracts text nodes from an HTML node func extractText(n *html.Node, sb *strings.Builder) { // Skip script, style, and non-visible elements if n.Type == html.ElementNode { nodeName := strings.ToLower(n.Data) if nodeName == "script" || nodeName == "style" || nodeName == "noscript" || nodeName == "meta" || nodeName == "link" || nodeName == "iframe" { return } } // Process text nodes if n.Type == html.TextNode { text := strings.TrimSpace(n.Data) if text != "" { sb.WriteString(text) sb.WriteString(" ") } } // Recursively process all child nodes for c := n.FirstChild; c != nil; c = c.NextSibling { extractText(c, sb) } // Add line breaks for certain block elements if n.Type == html.ElementNode { nodeName := strings.ToLower(n.Data) if nodeName == "p" || nodeName == "div" || nodeName == "h1" || nodeName == "h2" || nodeName == "h3" || nodeName == "h4" || nodeName == "h5" || nodeName == "h6" || nodeName == "li" || nodeName == "br" || nodeName == "tr" { sb.WriteString("\n") } // Add extra line break for more significant sections if nodeName == "section" || nodeName == "article" || nodeName == "header" || nodeName == "footer" { sb.WriteString("\n\n") } } } // cleanText removes excessive whitespace and normalizes line breaks func cleanText(text string) string { // Replace multiple spaces with a single space text = regexp.MustCompile(`\s+`).ReplaceAllString(text, " ") // Replace multiple newlines with a maximum of two text = regexp.MustCompile(`\n{3,}`).ReplaceAllString(text, "\n\n") // Trim leading/trailing whitespace text = strings.TrimSpace(text) return text } ================================================ FILE: cli/daemon/mcp/mcp.go ================================================ package mcp import ( "context" "fmt" "net" "net/http" "github.com/mark3labs/mcp-go/server" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/engine/trace2" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/objects" "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/sqldb" ) type Manager struct { server *server.MCPServer sse *server.SSEServer cluster *sqldb.ClusterManager ns *namespace.Manager traces trace2.Store run *run.Manager objects *objects.ClusterManager apps *apps.Manager BaseURL string } type appContextKey struct{} type appContext struct { AppID string } func WithAppID(ctx context.Context, appID string) context.Context { return context.WithValue(ctx, appContextKey{}, &appContext{AppID: appID}) } func GetAppID(ctx context.Context) (string, bool) { if appCtx, ok := ctx.Value(appContextKey{}).(*appContext); ok { return appCtx.AppID, true } return "", false } func NewManager(apps *apps.Manager, cluster *sqldb.ClusterManager, ns *namespace.Manager, traces trace2.Store, runMgr *run.Manager, baseURL string) *Manager { // Create hooks for handling session registration hooks := &server.Hooks{} // Create a new MCP server s := server.NewMCPServer( "Encore MCP Server", "1.0.0", server.WithToolCapabilities(false), server.WithHooks(hooks), ) m := &Manager{ server: s, sse: server.NewSSEServer(s, server.WithAppendQueryToMessageEndpoint(), server.WithKeepAlive(true), server.WithHTTPContextFunc(addAppToContext)), apps: apps, ns: ns, cluster: cluster, traces: traces, run: runMgr, BaseURL: baseURL, } m.registerDatabaseTools() m.registerTraceTools() m.registerAPITools() m.registerPubSubTools() m.registerSrcTools() m.registerBucketTools() m.registerCacheTools() m.registerMetricsTools() m.registerCronTools() m.registerSecretTools() m.registerDocsTools() m.registerTraceResources() return m } func addAppToContext(ctx context.Context, r *http.Request) context.Context { if appID := r.URL.Query().Get("app"); appID != "" { return WithAppID(ctx, appID) } return ctx } func (m *Manager) Serve(listener net.Listener) error { return http.Serve(listener, m.sse) } func (m *Manager) getApp(ctx context.Context) (*apps.Instance, error) { appID, ok := GetAppID(ctx) if !ok { return nil, fmt.Errorf("app not found in context") } inst, err := m.apps.FindLatestByPlatformOrLocalID(appID) if err != nil { return nil, fmt.Errorf("failed to find app: %w", err) } return inst, nil } ================================================ FILE: cli/daemon/mcp/metrics_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "sort" "github.com/mark3labs/mcp-go/mcp" ) func (m *Manager) registerMetricsTools() { m.server.AddTool(mcp.NewTool("get_metrics", mcp.WithDescription("Retrieve comprehensive information about all metrics defined in the currently open Encore, including their types, labels, documentation, and usage across services. This tool helps understand the application's observability and monitoring capabilities."), ), m.getMetrics) } func (m *Manager) getMetrics(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Group metrics by service for better organization metricsByService := make(map[string][]map[string]interface{}) globalMetrics := make([]map[string]interface{}, 0) // Process all metrics for _, metric := range md.Metrics { metricInfo := map[string]interface{}{ "name": metric.Name, "kind": metric.Kind.String(), "value_type": metric.ValueType.String(), "doc": metric.Doc, } // Add labels if any if len(metric.Labels) > 0 { labels := make([]map[string]interface{}, 0, len(metric.Labels)) for _, label := range metric.Labels { labelInfo := map[string]interface{}{ "key": label.Key, "type": label.Type.String(), "doc": label.Doc, } labels = append(labels, labelInfo) } metricInfo["labels"] = labels } // Add to appropriate group (service-specific or global) if metric.ServiceName != nil { serviceName := *metric.ServiceName if _, exists := metricsByService[serviceName]; !exists { metricsByService[serviceName] = make([]map[string]interface{}, 0) } metricsByService[serviceName] = append(metricsByService[serviceName], metricInfo) } else { globalMetrics = append(globalMetrics, metricInfo) } } // Build the final result result := map[string]interface{}{ "services": make(map[string]interface{}), "global": globalMetrics, } // Add each service's metrics servicesMap := result["services"].(map[string]interface{}) for serviceName, metrics := range metricsByService { // Sort metrics by name within each service sort.Slice(metrics, func(i, j int) bool { return metrics[i]["name"].(string) < metrics[j]["name"].(string) }) servicesMap[serviceName] = metrics } // Also sort global metrics sort.Slice(globalMetrics, func(i, j int) bool { return globalMetrics[i]["name"].(string) < globalMetrics[j]["name"].(string) }) // Add summary counts summary := map[string]interface{}{ "total_metrics": len(md.Metrics), "global_metrics": len(globalMetrics), "service_count": len(metricsByService), "metrics_by_service": make(map[string]int), "metrics_by_kind": make(map[string]int), "metrics_by_type": make(map[string]int), } // Count metrics by service for service, metrics := range metricsByService { summary["metrics_by_service"].(map[string]int)[service] = len(metrics) } // Count metrics by kind and type kindCounts := make(map[string]int) typeCounts := make(map[string]int) for _, metric := range md.Metrics { kindStr := metric.Kind.String() kindCounts[kindStr] = kindCounts[kindStr] + 1 typeStr := metric.ValueType.String() typeCounts[typeStr] = typeCounts[typeStr] + 1 } summary["metrics_by_kind"] = kindCounts summary["metrics_by_type"] = typeCounts result["summary"] = summary jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal metrics information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/pubsub_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "github.com/mark3labs/mcp-go/mcp" "google.golang.org/protobuf/encoding/protojson" ) func (m *Manager) registerPubSubTools() { m.server.AddTool(mcp.NewTool("get_pubsub", mcp.WithDescription("Retrieve detailed information about all PubSub topics and their subscriptions in the currently open Encore. This includes topic configurations, subscription patterns, message schemas, and the services that publish to or subscribe to each topic."), ), m.getPubSub) } func (m *Manager) getPubSub(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Create a map to find topic and subscription definitions from trace nodes topicDefLocations := make(map[string]map[string]interface{}) subscriptionDefLocations := make(map[string]map[string]map[string]interface{}) // Scan through all packages to find trace nodes related to pubsub for _, pkg := range md.Pkgs { for _, node := range pkg.TraceNodes { // Check for topic definition nodes if node.GetPubsubTopicDef() != nil { topicDef := node.GetPubsubTopicDef() if _, exists := topicDefLocations[topicDef.TopicName]; !exists { topicDefLocations[topicDef.TopicName] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, } } } // Check for subscription definition nodes if node.GetPubsubSubscriber() != nil { subDef := node.GetPubsubSubscriber() if _, exists := subscriptionDefLocations[subDef.TopicName]; !exists { subscriptionDefLocations[subDef.TopicName] = make(map[string]map[string]interface{}) } if _, exists := subscriptionDefLocations[subDef.TopicName][subDef.SubscriberName]; !exists { subscriptionDefLocations[subDef.TopicName][subDef.SubscriberName] = map[string]interface{}{ "filepath": node.Filepath, "line_start": node.SrcLineStart, "line_end": node.SrcLineEnd, "column_start": node.SrcColStart, "column_end": node.SrcColEnd, } } } } } // Now build the response with locations topics := make([]map[string]interface{}, 0) for _, topic := range md.PubsubTopics { // Extract publishers publishers := make([]map[string]interface{}, 0) for _, publisher := range topic.Publishers { publishers = append(publishers, map[string]interface{}{ "service_name": publisher.ServiceName, }) } // Extract subscriptions subscriptions := make([]map[string]interface{}, 0) for _, subscription := range topic.Subscriptions { subscriptionInfo := map[string]interface{}{ "name": subscription.Name, "service_name": subscription.ServiceName, } // Add location information for subscription if available if subLocations, topicExists := subscriptionDefLocations[topic.Name]; topicExists { if subLocation, subExists := subLocations[subscription.Name]; subExists { subscriptionInfo["definition"] = subLocation } } // Add optional fields if they're set if subscription.AckDeadline > 0 { subscriptionInfo["ack_deadline"] = formatDuration(subscription.AckDeadline) } if subscription.MessageRetention > 0 { subscriptionInfo["message_retention"] = formatDuration(subscription.MessageRetention) } if subscription.MaxConcurrency != nil { subscriptionInfo["max_concurrency"] = *subscription.MaxConcurrency } // Add retry policy if available if subscription.RetryPolicy != nil { retryPolicy := map[string]interface{}{} if subscription.RetryPolicy.MinBackoff > 0 { retryPolicy["min_backoff"] = formatDuration(subscription.RetryPolicy.MinBackoff) } if subscription.RetryPolicy.MaxBackoff > 0 { retryPolicy["max_backoff"] = formatDuration(subscription.RetryPolicy.MaxBackoff) } if subscription.RetryPolicy.MaxRetries > 0 { retryPolicy["max_retries"] = subscription.RetryPolicy.MaxRetries } subscriptionInfo["retry_policy"] = retryPolicy } subscriptions = append(subscriptions, subscriptionInfo) } // Build topic info topicInfo := map[string]interface{}{ "name": topic.Name, "publishers": publishers, "subscriptions": subscriptions, "delivery_guarantee": topic.DeliveryGuarantee.String(), } // Add location information for topic if available if location, exists := topicDefLocations[topic.Name]; exists { topicInfo["definition"] = location } // Add documentation if available if topic.Doc != nil { topicInfo["doc"] = *topic.Doc } // Add ordering key if available if topic.OrderingKey != "" { topicInfo["ordering_key"] = topic.OrderingKey } // Add message type if available if topic.MessageType != nil { messageTypeData, err := protojson.Marshal(topic.MessageType) if err != nil { return nil, fmt.Errorf("failed to marshal message type: %w", err) } var messageTypeJson interface{} if err := json.Unmarshal(messageTypeData, &messageTypeJson); err != nil { return nil, fmt.Errorf("failed to unmarshal message type JSON: %w", err) } topicInfo["message_type"] = messageTypeJson } topics = append(topics, topicInfo) } jsonData, err := json.Marshal(topics) if err != nil { return nil, fmt.Errorf("failed to marshal PubSub information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/schema_json.go ================================================ package mcp import ( "bytes" "encoding/json" "net/url" "strconv" "strings" schema "encr.dev/proto/encore/parser/schema/v1" ) // FieldLocation represents where a field is located in the API request/response type FieldLocation int const ( FieldLocationBody FieldLocation = 0 FieldLocationQuery FieldLocation = 1 FieldLocationHeader FieldLocation = 2 FieldLocationCookie FieldLocation = 3 FieldLocationUnused FieldLocation = 4 ) // DescribedField is a field with additional metadata type DescribedField struct { *schema.Field SrcName string Name string Location FieldLocation } // StructBits generates JSON representations of a struct's fields separated by location // It returns query, headers, cookies, and JSON body as strings func StructBits(s *schema.Struct, method string, asResponse bool, asGoStruct bool, queryParamsAsObject bool) (query, headers, cookies, jsonBody string) { // Split the fields by location fieldsByLocation := splitFieldsByLocation(s, method, asResponse) // Generate query string if len(fieldsByLocation[FieldLocationQuery]) > 0 { if asGoStruct || queryParamsAsObject { query = writeFieldsAsJSON(fieldsByLocation[FieldLocationQuery], asGoStruct) } else { var queryParams []string for _, field := range fieldsByLocation[FieldLocationQuery] { fieldName := field.Name fieldValue := renderFieldValueAsQueryParam(field.Typ) queryParams = append(queryParams, url.QueryEscape(fieldName)+"="+fieldValue) // If it's a list, add a second parameter to show it's a list if field.Typ.GetList() != nil { queryParams = append(queryParams, url.QueryEscape(fieldName)+"="+fieldValue) } } query = "?" + strings.Join(queryParams, "&") } } // Generate headers if len(fieldsByLocation[FieldLocationHeader]) > 0 { headers = writeFieldsAsJSON(fieldsByLocation[FieldLocationHeader], asGoStruct) } // Generate cookies if len(fieldsByLocation[FieldLocationCookie]) > 0 { cookies = writeCookiesAsJSON(fieldsByLocation[FieldLocationCookie], asGoStruct) } // Generate JSON body if len(fieldsByLocation[FieldLocationBody]) > 0 { jsonBody = writeFieldsAsJSON(fieldsByLocation[FieldLocationBody], asGoStruct) } return } // writeFieldsAsJSON renders a list of fields as a JSON object func writeFieldsAsJSON(fields []DescribedField, asGoStruct bool) string { var buf bytes.Buffer buf.WriteString("\n") for i, f := range fields { fieldName := f.SrcName if !asGoStruct { fieldName = f.Name } buf.WriteString(" \"") buf.WriteString(fieldName) buf.WriteString("\": ") renderTypeValue(&buf, f.Typ) if i < len(fields)-1 { buf.WriteString(",") } buf.WriteString("\n") } return buf.String() } // writeCookiesAsJSON renders cookie fields as JSON func writeCookiesAsJSON(fields []DescribedField, asGoStruct bool) string { var buf bytes.Buffer buf.WriteString("\n") for i, f := range fields { fieldName := f.SrcName if !asGoStruct { fieldName = f.Name } buf.WriteString(" \"") buf.WriteString(fieldName) buf.WriteString("\": ") // If it's a builtin, render it normally, otherwise render as an empty string if f.Typ.GetBuiltin() != schema.Builtin_ANY { renderTypeValue(&buf, f.Typ) } else { buf.WriteString("\"\"") } if i < len(fields)-1 { buf.WriteString(",") } buf.WriteString("\n") } return buf.String() } // renderTypeValue renders a type value to the buffer func renderTypeValue(buf *bytes.Buffer, typ *schema.Type) { switch { case typ.GetBuiltin() != schema.Builtin_ANY: renderBuiltinValue(buf, typ.GetBuiltin(), false) case typ.GetList() != nil: buf.WriteString("[") renderTypeValue(buf, typ.GetList().Elem) buf.WriteString("]") case typ.GetStruct() != nil: buf.WriteString("{") for i, f := range typ.GetStruct().Fields { if f.JsonName == "-" { continue } jsonName := f.JsonName if jsonName == "" { jsonName = f.Name } buf.WriteString("\"") buf.WriteString(jsonName) buf.WriteString("\": ") renderTypeValue(buf, f.Typ) if i < len(typ.GetStruct().Fields)-1 { buf.WriteString(", ") } } buf.WriteString("}") case typ.GetMap() != nil: buf.WriteString("{") renderTypeValue(buf, typ.GetMap().Key) buf.WriteString(": ") renderTypeValue(buf, typ.GetMap().Value) buf.WriteString("}") case typ.GetNamed() != nil: // Just render as null for simplicity buf.WriteString("null") case typ.GetPointer() != nil: renderTypeValue(buf, typ.GetPointer().Base) case typ.GetUnion() != nil && len(typ.GetUnion().Types) > 0: // Just render the first type of the union renderTypeValue(buf, typ.GetUnion().Types[0]) case typ.GetLiteral() != nil: renderLiteralValue(buf, typ.GetLiteral()) default: buf.WriteString("") } } // renderBuiltinValue renders a builtin type value func renderBuiltinValue(buf *bytes.Buffer, b schema.Builtin, urlEncode bool) { var value string switch b { case schema.Builtin_ANY: value = "" case schema.Builtin_BOOL: value = "false" case schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64, schema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64: value = "0" case schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: value = "0.0" case schema.Builtin_STRING: value = "\"\"" case schema.Builtin_BYTES: value = "\"\" /* base64 */" case schema.Builtin_TIME: value = "\"2009-11-10T23:00:00Z\"" case schema.Builtin_UUID: value = "\"7d42f515-3517-4e76-be13-30880443546f\"" case schema.Builtin_JSON: value = "{}" case schema.Builtin_USER_ID: value = "\"userID\"" case schema.Builtin_DECIMAL: value = "\"0.0\"" default: value = "" } if urlEncode { // Remove quotes for URL encoding if they exist if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { value = value[1 : len(value)-1] } buf.WriteString(url.QueryEscape(value)) } else { buf.WriteString(value) } } // renderLiteralValue renders a literal value func renderLiteralValue(buf *bytes.Buffer, lit *schema.Literal) { switch v := lit.Value.(type) { case *schema.Literal_Boolean: if v.Boolean { buf.WriteString("true") } else { buf.WriteString("false") } case *schema.Literal_Int: buf.WriteString(strconv.FormatInt(v.Int, 10)) case *schema.Literal_Float: buf.WriteString(strconv.FormatFloat(v.Float, 'f', -1, 64)) case *schema.Literal_Str: jsonStr, _ := json.Marshal(v.Str) buf.Write(jsonStr) case *schema.Literal_Null: buf.WriteString("null") default: buf.WriteString("") } } // renderFieldValueAsQueryParam returns a URL-encoded string representation of a field's value func renderFieldValueAsQueryParam(typ *schema.Type) string { var buf bytes.Buffer if typ.GetBuiltin() != schema.Builtin_ANY { renderBuiltinValue(&buf, typ.GetBuiltin(), true) } else if typ.GetList() != nil { renderTypeValue(&buf, typ.GetList().Elem) } else { buf.WriteString("") } return buf.String() } // splitFieldsByLocation categorizes struct fields by their HTTP location func splitFieldsByLocation(s *schema.Struct, method string, asResponse bool) map[FieldLocation][]DescribedField { result := make(map[FieldLocation][]DescribedField) for _, f := range s.Fields { name, location := fieldNameAndLocation(f, method, asResponse) // Skip unused fields if location == FieldLocationUnused { continue } result[location] = append(result[location], DescribedField{ Field: f, SrcName: f.Name, Name: name, Location: location, }) } return result } // fieldNameAndLocation determines the name and location of a field based on HTTP method and tags func fieldNameAndLocation(f *schema.Field, method string, asResponse bool) (string, FieldLocation) { // For response, all fields go in the body unless explicitly tagged if asResponse { // Check for explicit wire location if f.Wire != nil { if f.Wire.GetHeader() != nil { name := f.Wire.GetHeader().GetName() if name == "" { name = f.Name } return name, FieldLocationHeader } else if f.Wire.GetQuery() != nil { name := f.Wire.GetQuery().GetName() if name == "" { name = f.Name } return name, FieldLocationQuery } } // Default response location is body jsonName := f.JsonName if jsonName == "" { jsonName = f.Name } return jsonName, FieldLocationBody } // For request, location depends on method and tags isGetLike := method == "GET" || method == "HEAD" || method == "DELETE" // Check for explicit wire location if f.Wire != nil { if f.Wire.GetHeader() != nil { name := f.Wire.GetHeader().GetName() if name == "" { name = f.Name } return name, FieldLocationHeader } else if f.Wire.GetQuery() != nil { name := f.Wire.GetQuery().GetName() if name == "" { name = f.Name } return name, FieldLocationQuery } } // Check for Cookie for _, tag := range f.Tags { if tag.Key == "cookie" { name := tag.Name if name == "" { name = f.Name } return name, FieldLocationCookie } } // For GET-like methods, fields go in query by default if isGetLike { name := f.QueryStringName if name == "-" { return f.Name, FieldLocationUnused } else if name == "" { name = f.Name } return name, FieldLocationQuery } // Default request location for POST/PUT/PATCH is body jsonName := f.JsonName if jsonName == "-" { return f.Name, FieldLocationUnused } else if jsonName == "" { jsonName = f.Name } return jsonName, FieldLocationBody } // NamedOrInlineStruct returns the struct type and type arguments for a named or inline struct. // Returns nil if the type is neither a named struct nor an inline struct. func NamedOrInlineStruct(meta map[uint32]*schema.Decl, t *schema.Type) (*schema.Struct, []*schema.Type) { if t == nil { return nil, nil } if named := t.GetNamed(); named != nil { st := meta[named.Id] if st != nil && st.GetType() != nil { if structType := st.GetType().GetStruct(); structType != nil { return structType, named.GetTypeArguments() } } } else if structType := t.GetStruct(); structType != nil { return structType, []*schema.Type{} } return nil, nil } ================================================ FILE: cli/daemon/mcp/secret_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "sort" "github.com/mark3labs/mcp-go/mcp" ) func (m *Manager) registerSecretTools() { m.server.AddTool(mcp.NewTool("get_secrets", mcp.WithDescription("Retrieve metadata about all secrets used in the currently open Encore, including their usage patterns, which services depend on them, and their configuration. This tool helps understand the application's security requirements and secret management strategy."), ), m.getSecrets) } func (m *Manager) getSecrets(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } // Build a map of all secrets and the services that use them secretUsageMap := make(map[string][]map[string]interface{}) // First go through all packages to find secrets for _, pkg := range md.Pkgs { if len(pkg.Secrets) > 0 && pkg.ServiceName != "" { // For each secret in this package for _, secretName := range pkg.Secrets { // Create usage info usageInfo := map[string]interface{}{ "service_name": pkg.ServiceName, "package_path": pkg.RelPath, } // Add to the map if _, exists := secretUsageMap[secretName]; !exists { secretUsageMap[secretName] = make([]map[string]interface{}, 0) } secretUsageMap[secretName] = append(secretUsageMap[secretName], usageInfo) } } } // Build the result secrets := make([]map[string]interface{}, 0) // Convert the map to an array for secretName, usages := range secretUsageMap { secretInfo := map[string]interface{}{ "name": secretName, "usages": usages, } // Count unique services serviceSet := make(map[string]bool) for _, usage := range usages { if svcName, ok := usage["service_name"].(string); ok { serviceSet[svcName] = true } } secretInfo["service_count"] = len(serviceSet) secrets = append(secrets, secretInfo) } // Sort by name for consistent output sort.Slice(secrets, func(i, j int) bool { return secrets[i]["name"].(string) < secrets[j]["name"].(string) }) jsonData, err := json.Marshal(secrets) if err != nil { return nil, fmt.Errorf("failed to marshal secrets information: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/src_tools.go ================================================ package mcp import ( "context" "encoding/json" "fmt" "os" "path/filepath" "github.com/mark3labs/mcp-go/mcp" "google.golang.org/protobuf/encoding/protojson" ) func (m *Manager) registerSrcTools() { // Add tool handlers m.server.AddTool(mcp.NewTool("get_metadata", mcp.WithDescription("Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components. This tool provides a comprehensive view of the application's architecture and configuration."), ), m.getMetadata) // Add tool handlers m.server.AddTool(mcp.NewTool("get_src_files", mcp.WithDescription("Retrieve the contents of one or more source files from the application. This tool is useful for examining specific parts of the codebase or understanding implementation details."), mcp.WithArray("files", mcp.Items(map[string]any{ "type": "string", "description": "List of file paths to retrieve, relative to the application root. Each path should point to a valid source file in the project.", })), ), m.getSrcFiles) } func (m *Manager) getMetadata(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } md, err := inst.CachedMetadata() if err != nil { return nil, fmt.Errorf("failed to get metadata: %w", err) } data, err := protojson.Marshal(md) return mcp.NewToolResultText(string(data)), nil } func (m *Manager) getSrcFiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } files, ok := request.Params.Arguments["files"].([]any) if !ok || len(files) == 0 { return nil, fmt.Errorf("no files provided") } rtn := map[string]string{} for _, file := range files { fileStr := file.(string) content, err := os.ReadFile(filepath.Join(inst.Root(), fileStr)) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } rtn[fileStr] = string(content) } jsonData, err := json.Marshal(rtn) if err != nil { return nil, fmt.Errorf("failed to marshal json: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } ================================================ FILE: cli/daemon/mcp/trace_tools.go ================================================ package mcp import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "encr.dev/cli/daemon/engine/trace2" tracepb2 "encr.dev/proto/encore/engine/trace2" ) func (m *Manager) registerTraceResources() { // Register the trace resources m.server.AddResourceTemplate(mcp.NewResourceTemplate( "trace://{id}", "API trace", mcp.WithTemplateDescription("Retrieve detailed information about a specific trace, including all spans, timing information, and associated metadata. This resource is useful for deep debugging of individual requests."), mcp.WithTemplateMIMEType("application/json"), ), m.getTraceResource) } func (m *Manager) registerTraceTools() { // Add tool for listing traces m.server.AddTool(mcp.NewTool("get_traces", mcp.WithDescription("Retrieve a list of request traces from the application, including their timing, status, and associated metadata. This tool helps understand the flow of requests through the system and diagnose issues."), mcp.WithString("service", mcp.Description("Optional service name to filter traces by. Only returns traces that involve the specified service.")), mcp.WithString("endpoint", mcp.Description("Optional endpoint name to filter traces by. Only returns traces that involve the specified endpoint.")), mcp.WithString("error", mcp.Description("Optional filter for traces with errors. Set to 'true' to see only failed traces, 'false' for successful traces, or omit to see all traces.")), mcp.WithString("limit", mcp.Description("Maximum number of traces to return. Helps manage response size when dealing with many traces.")), mcp.WithString("start_time", mcp.Description("ISO format timestamp to filter traces created after this time. Useful for focusing on recent activity.")), mcp.WithString("end_time", mcp.Description("ISO format timestamp to filter traces created before this time. Useful for focusing on a specific time period.")), ), m.listTraces) // Add tool for getting a single trace with all spans m.server.AddTool(mcp.NewTool("get_trace_spans", mcp.WithDescription("Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata. This tool is useful for deep debugging of individual requests."), mcp.WithArray("trace_ids", mcp.Items(map[string]any{ "type": "string", "description": "The unique identifiers of the traces to retrieve. These IDs are returned by the get_traces tool.", })), ), m.getTrace) } func (m *Manager) listTraces(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } // Build trace query query := &trace2.Query{ AppID: inst.PlatformOrLocalID(), Limit: 100, // Default limit } if service, ok := request.Params.Arguments["service"].(string); ok && service != "" { query.Service = service } if endpoint, ok := request.Params.Arguments["endpoint"].(string); ok && endpoint != "" { query.Endpoint = endpoint } if errorStr, ok := request.Params.Arguments["error"].(string); ok && errorStr != "" { if errorStr == "true" { isError := true query.IsError = &isError } else if errorStr == "false" { isError := false query.IsError = &isError } } if limitStr, ok := request.Params.Arguments["limit"].(string); ok && limitStr != "" { var limit int if _, err := fmt.Sscanf(limitStr, "%d", &limit); err == nil && limit > 0 { query.Limit = limit } } if startTime, ok := request.Params.Arguments["start_time"].(string); ok && startTime != "" { if t, err := time.Parse(time.RFC3339, startTime); err == nil { query.StartTime = t } } if endTime, ok := request.Params.Arguments["end_time"].(string); ok && endTime != "" { if t, err := time.Parse(time.RFC3339, endTime); err == nil { query.EndTime = t } } // Collect traces var traces []*tracepb2.SpanSummary err = m.traces.List(ctx, query, func(span *tracepb2.SpanSummary) bool { traces = append(traces, span) return true }) if err != nil { return nil, fmt.Errorf("failed to list traces: %w", err) } // Convert to JSON jsonData, err := json.Marshal(traces) if err != nil { return nil, fmt.Errorf("failed to marshal traces: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getTrace(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } traceIDs, ok := request.Params.Arguments["trace_ids"].([]interface{}) if !ok || len(traceIDs) == 0 { return nil, fmt.Errorf("trace_ids is required and must be a non-empty array") } result := make(map[string][]*tracepb2.TraceEvent) for _, traceIDVal := range traceIDs { traceID, ok := traceIDVal.(string) if !ok || traceID == "" { continue // Skip invalid IDs } // Collect all events for the trace var events []*tracepb2.TraceEvent err = m.traces.Get(ctx, inst.PlatformOrLocalID(), traceID, func(event *tracepb2.TraceEvent) bool { events = append(events, event) return true }) if err != nil { if errors.Is(err, trace2.ErrNotFound) { // Just skip not found traces continue } return nil, fmt.Errorf("failed to get trace %s: %w", traceID, err) } result[traceID] = events } // Convert to JSON jsonData, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal traces: %w", err) } return mcp.NewToolResultText(string(jsonData)), nil } func (m *Manager) getTraceResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { inst, err := m.getApp(ctx) if err != nil { return nil, fmt.Errorf("failed to get app: %w", err) } traceID := strings.TrimPrefix(request.Params.URI, "trace://") // Collect all events for the trace var events []*tracepb2.TraceEvent err = m.traces.Get(ctx, inst.PlatformOrLocalID(), traceID, func(event *tracepb2.TraceEvent) bool { events = append(events, event) return true }) if err != nil { if errors.Is(err, trace2.ErrNotFound) { return nil, fmt.Errorf("trace %s not found", traceID) } return nil, fmt.Errorf("failed to get trace %s: %w", traceID, err) } // Convert to JSON jsonData, err := json.Marshal(events) if err != nil { return nil, fmt.Errorf("failed to marshal events: %w", err) } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: "application/json", Text: string(jsonData), }, }, nil } ================================================ FILE: cli/daemon/mcp/util.go ================================================ package mcp import ( "time" metav1 "encr.dev/proto/encore/parser/meta/v1" ) // findServiceNameForPackage returns the service name for a given package path func findServiceNameForPackage(md *metav1.Data, pkgPath string) string { for _, pkg := range md.Pkgs { if pkg.RelPath == pkgPath && pkg.ServiceName != "" { return pkg.ServiceName } } return "" } // formatDuration formats a nanosecond duration into a human-readable string func formatDuration(nanos int64) string { duration := time.Duration(nanos) * time.Nanosecond return duration.String() } ================================================ FILE: cli/daemon/namespace/namespace.go ================================================ package namespace import ( "context" "database/sql" "time" "github.com/cockroachdb/errors" "github.com/rs/xid" "encr.dev/cli/daemon/apps" daemonpb "encr.dev/proto/encore/daemon" ) var ( ErrNotFound = errors.New("namespace not found") ErrActive = errors.New("namespace is active") ) type ( ID string Name string ) func (id ID) String() string { return string(id) } func ParseID(s string) (ID, bool) { id, err := xid.FromString(s) if err != nil { return "", false } return ID(id.String()), true } func NewManager(db *sql.DB) *Manager { return &Manager{db, nil} } // Manager manages namespaces. type Manager struct { db *sql.DB handlers []DeletionHandler } func (mgr *Manager) RegisterDeletionHandler(h DeletionHandler) { mgr.handlers = append(mgr.handlers, h) } type Namespace struct { ID ID App *apps.Instance Name Name Active bool CreatedAt time.Time LastActiveAt *time.Time } func (m *Manager) Create(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) { now := time.Now() id := ID(xid.NewWithTime(now).String()) tx, err := m.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer tx.Rollback() // committed explicitly on success _, err = tx.ExecContext(ctx, ` INSERT INTO namespace (id, app_id, name, active, created_at) VALUES (?, ?, ?, ?, ?) `, id, app.PlatformOrLocalID(), name, false, now) if err != nil { return nil, errors.Wrap(err, "create namespace") } ns := &Namespace{ ID: id, App: app, Name: name, CreatedAt: now, } // If there is no active namespace, make this one active. { var activeName string err = tx.QueryRowContext(ctx, ` SELECT name FROM namespace WHERE app_id = ? AND active = true `, app.PlatformOrLocalID()).Scan(&activeName) if err != nil { if errors.Is(err, sql.ErrNoRows) { // No active namespace; make this one active. _, err = tx.ExecContext(ctx, ` UPDATE namespace SET active = true, last_active_at = ? WHERE id = ? `, now, id) } if err != nil { return nil, errors.Wrap(err, "create namespace") } } ns.Active = true ns.LastActiveAt = &now } if err := tx.Commit(); err != nil { return nil, errors.Wrap(err, "create namespace") } return ns, nil } func (m *Manager) List(ctx context.Context, app *apps.Instance) ([]*Namespace, error) { rows, err := m.db.QueryContext(ctx, ` SELECT id, name, active, created_at, last_active_at FROM namespace WHERE app_id = ? ORDER BY name ASC `, app.PlatformOrLocalID()) if err != nil { return nil, errors.Wrap(err, "list namespaces") } defer rows.Close() var nss []*Namespace for rows.Next() { var ns Namespace if err := rows.Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt); err != nil { return nil, errors.Wrap(err, "scan namespace") } ns.App = app nss = append(nss, &ns) } if err := rows.Err(); err != nil { return nil, errors.Wrap(err, "list namespaces") } // If we have no namespaces at all, create a default one. if len(nss) == 0 { ns, err := m.Create(ctx, app, "default") if err != nil { return nil, err } nss = []*Namespace{ns} } return nss, nil } func (m *Manager) GetByName(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) { var ns Namespace err := m.db.QueryRowContext(ctx, ` SELECT id, name, active, created_at, last_active_at FROM namespace WHERE app_id = ? AND name = ? `, app.PlatformOrLocalID(), name).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, errors.Wrap(err, "get namespace") } ns.App = app return &ns, nil } func (m *Manager) GetByID(ctx context.Context, app *apps.Instance, id ID) (*Namespace, error) { var ns Namespace err := m.db.QueryRowContext(ctx, ` SELECT id, name, active, created_at, last_active_at FROM namespace WHERE app_id = ? AND id = ? `, app.PlatformOrLocalID(), id).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, errors.Wrap(err, "get namespace") } ns.App = app return &ns, nil } func (m *Manager) Delete(ctx context.Context, app *apps.Instance, name Name) error { tx, err := m.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // committed explicitly on success var ns Namespace err = tx.QueryRowContext(ctx, ` DELETE FROM namespace WHERE app_id = ? AND name = ? RETURNING id, name, active, created_at, last_active_at `, app.PlatformOrLocalID(), name).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt) if ns.Active { return ErrActive } ns.App = app // Check all the deletion handlers. for _, h := range m.handlers { if err := h.CanDeleteNamespace(ctx, app, &ns); err != nil { return errors.Newf("cannot delete namespace: %v", err) } } // Actually delete the namespace. for _, h := range m.handlers { if err := h.DeleteNamespace(ctx, app, &ns); err != nil { return errors.Newf("failed to delete namespace: %v", err) } } err = tx.Commit() return errors.Wrap(err, "delete namespace") } func (m *Manager) Switch(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) { // Resolve the namespace to switch to. var target *Namespace // If the name is "-", switch to the previous namespace. if name == "-" { nss, err := m.List(ctx, app) if err != nil { return nil, err } // Find the non-active namespace that was most recently active var lastActive *Namespace for _, ns := range nss { if !ns.Active && ns.LastActiveAt != nil { if lastActive == nil || ns.LastActiveAt.After(*lastActive.LastActiveAt) { lastActive = ns } } } if lastActive == nil { return nil, ErrNotFound } target = lastActive } else { var err error target, err = m.GetByName(ctx, app, name) if err != nil { return nil, err } } tx, err := m.db.BeginTx(ctx, nil) if err != nil { return nil, errors.WithStack(err) } defer tx.Rollback() // committed explicitly on success // Mark all namespaces as inactive. _, err = tx.ExecContext(ctx, ` UPDATE namespace SET active = false WHERE app_id = ? `, app.PlatformOrLocalID()) if err != nil { return nil, errors.Wrap(err, "switch namespace") } // Mark the selected namespace as active. _, err = tx.ExecContext(ctx, ` UPDATE namespace SET active = true, last_active_at = ? WHERE id = ? `, time.Now(), target.ID) if err != nil { return nil, errors.Wrap(err, "switch namespace") } if err := tx.Commit(); err != nil { return nil, errors.Wrap(err, "switch namespace") } target.Active = true return target, nil } // GetActive returns the active namespace for the given app. func (m *Manager) GetActive(ctx context.Context, app *apps.Instance) (*Namespace, error) { var ns Namespace err := m.db.QueryRowContext(ctx, ` SELECT id, name, active, created_at, last_active_at FROM namespace WHERE app_id = ? AND active = true `, app.PlatformOrLocalID()).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } else if err == nil { ns.App = app return &ns, nil } // No active namespace. // Do we have any namespaces at all? nss, err := m.List(ctx, app) if err != nil { return nil, err } else if len(nss) > 0 { return m.Switch(ctx, app, nss[0].Name) } else { // No namespaces. Create a new one. return m.Create(ctx, app, "default") } } func (ns *Namespace) ToProto() *daemonpb.Namespace { res := &daemonpb.Namespace{ Id: string(ns.ID), Name: string(ns.Name), Active: ns.Active, CreatedAt: ns.CreatedAt.String(), } if ns.LastActiveAt != nil { s := ns.LastActiveAt.String() res.LastActiveAt = &s } return res } // DeletionHandler is the interface for components that want to listen for // and handle namespace deletion events. type DeletionHandler interface { // CanDeleteNamespace is called to determine whether the namespace can be deleted // by the component. To signal the namespace cannot be deleted, return a non-nil error. CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *Namespace) error // DeleteNamespace is called when a namespace is deleted. // Due to the non-atomic nature of many components, failure to handle // the deletion cannot be fully rolled back. DeleteNamespace(ctx context.Context, app *apps.Instance, ns *Namespace) error } ================================================ FILE: cli/daemon/namespace.go ================================================ package daemon import ( "context" "github.com/golang/protobuf/ptypes/empty" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/pkg/fns" daemonpb "encr.dev/proto/encore/daemon" ) func (s *Server) CreateNamespace(ctx context.Context, req *daemonpb.CreateNamespaceRequest) (*daemonpb.Namespace, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } ns, err := s.ns.Create(ctx, app, namespace.Name(req.Name)) if err != nil { return nil, err } return ns.ToProto(), nil } func (s *Server) ListNamespaces(ctx context.Context, req *daemonpb.ListNamespacesRequest) (*daemonpb.ListNamespacesResponse, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } nss, err := s.ns.List(ctx, app) if err != nil { return nil, err } protos := fns.Map(nss, (*namespace.Namespace).ToProto) return &daemonpb.ListNamespacesResponse{Namespaces: protos}, nil } func (s *Server) DeleteNamespace(ctx context.Context, req *daemonpb.DeleteNamespaceRequest) (*empty.Empty, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } if err := s.ns.Delete(ctx, app, namespace.Name(req.Name)); err != nil { return nil, err } return &empty.Empty{}, nil } func (s *Server) SwitchNamespace(ctx context.Context, req *daemonpb.SwitchNamespaceRequest) (*daemonpb.Namespace, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, err } if req.Create { _, err := s.ns.Create(ctx, app, namespace.Name(req.Name)) if err != nil { return nil, err } } ns, err := s.ns.Switch(ctx, app, namespace.Name(req.Name)) if err != nil { return nil, err } return ns.ToProto(), nil } func (s *Server) namespaceOrActive(ctx context.Context, app *apps.Instance, ns *string) (*namespace.Namespace, error) { if ns == nil { return s.ns.GetActive(ctx, app) } return s.ns.GetByName(ctx, app, namespace.Name(*ns)) } ================================================ FILE: cli/daemon/objects/manager.go ================================================ package objects import ( "context" "os" "path/filepath" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/pkg/emulators/storage/gcsemu" ) // NewClusterManager creates a new ClusterManager. func NewClusterManager(ns *namespace.Manager) *ClusterManager { mgr := &ClusterManager{ ns: ns, } return mgr } type ClusterManager struct { ns *namespace.Manager } func (cm *ClusterManager) BaseDir(ns namespace.ID) (string, error) { cache, err := os.UserCacheDir() if err != nil { return "", err } return filepath.Join(cache, "encore", "objects", ns.String()), nil } // CanDeleteNamespace implements namespace.DeletionHandler. func (cm *ClusterManager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { return nil } // DeleteNamespace implements namespace.DeletionHandler. func (cm *ClusterManager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { baseDir, err := cm.BaseDir(ns.ID) if err == nil { err = os.RemoveAll(baseDir) } return err } // PersistentStoreFallback is a public server fallback handler // for resolving stores based on the cluster manager's base directory. func (cm *ClusterManager) PersistentStoreFallback(id string) (gcsemu.Store, bool) { if baseDir, err := cm.BaseDir(namespace.ID(id)); err == nil { if _, err := os.Stat(baseDir); err == nil { return gcsemu.NewFileStore(baseDir), true } } return nil, false } ================================================ FILE: cli/daemon/objects/objects.go ================================================ package objects import ( // nosemgrep "fmt" "net" "net/http" "encr.dev/cli/daemon/namespace" "encr.dev/pkg/emulators/storage/gcsemu" "github.com/cockroachdb/errors" "github.com/rs/xid" "github.com/rs/zerolog/log" "go4.org/syncutil" meta "encr.dev/proto/encore/parser/meta/v1" ) type Server struct { id string public *PublicBucketServer startOnce syncutil.Once cancel func() // set by Start store gcsemu.Store emu *gcsemu.GcsEmu ln net.Listener srv *http.Server inMemory bool } func NewInMemoryServer(public *PublicBucketServer) *Server { id := xid.New().String() store := gcsemu.NewMemStore() return newServer(public, id, store, true) } func NewDirServer(public *PublicBucketServer, nsID namespace.ID, baseDir string) *Server { store := gcsemu.NewFileStore(baseDir) return newServer(public, nsID.String(), store, false) } func newServer(public *PublicBucketServer, id string, store gcsemu.Store, isInMem bool) *Server { return &Server{ public: public, id: id, store: store, emu: gcsemu.NewGcsEmu(gcsemu.Options{Store: store}), inMemory: isInMem, } } func (s *Server) Initialize(md *meta.Data) error { for _, bucket := range md.Buckets { if err := s.emu.InitBucket(bucket.Name); err != nil { return errors.Wrap(err, "initialize object storage bucket") } } return nil } func (s *Server) Start() error { return s.startOnce.Do(func() error { if s.inMemory { s.public.Register(s.id, s.store) } mux := http.NewServeMux() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return errors.Wrap(err, "listen tcp") } s.emu.Register(mux) s.ln = ln s.srv = &http.Server{Handler: mux} go func() { if err := s.srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) { log.Error().Err(err).Msg("unable to listen to gcs server") } }() return nil }) } func (s *Server) Stop() { _ = s.srv.Close() if s.inMemory { s.public.Deregister(s.id) } } func (s *Server) Endpoint() string { // Ensure the server has been started if err := s.Start(); err != nil { panic(err) } port := s.ln.Addr().(*net.TCPAddr).Port return fmt.Sprintf("http://localhost:%d", port) } func (s *Server) PublicBaseURL() string { return fmt.Sprintf("%s/%s", s.public.BaseAddr(), s.id) } // IsUsed reports whether the application uses object storage at all. func IsUsed(md *meta.Data) bool { return len(md.Buckets) > 0 } ================================================ FILE: cli/daemon/objects/public.go ================================================ package objects import ( "bytes" "errors" "fmt" "io" "net" "net/http" "strconv" "strings" "sync" "time" "google.golang.org/api/storage/v1" "encr.dev/pkg/emulators/storage/gcsemu" ) // Fallback is a function that returns a store for a given namespace. // It is used for resolving namespace ids to stores, where // the store is not pre-registered by Register. type Fallback func(namespace string) (gcsemu.Store, bool) // NewPublicBucketServer creates a new PublicBucketServer. // If fallback is nil, no fallback will be used. func NewPublicBucketServer(baseAddr string, fallback Fallback) *PublicBucketServer { mux := http.NewServeMux() srv := &PublicBucketServer{ mux: mux, baseAddr: baseAddr, fallback: fallback, namespaces: make(map[string]gcsemu.Store), } mux.HandleFunc("/{namespace}/{bucket}/{object...}", srv.handler) return srv } type PublicBucketServer struct { mux *http.ServeMux baseAddr string fallback Fallback mu sync.RWMutex namespaces map[string]gcsemu.Store } func (s *PublicBucketServer) Serve(ln net.Listener) error { return http.Serve(ln, s) } func (s *PublicBucketServer) Register(namespace string, store gcsemu.Store) { s.mu.Lock() defer s.mu.Unlock() s.namespaces[namespace] = store } func (s *PublicBucketServer) Deregister(namespace string) { s.mu.Lock() defer s.mu.Unlock() delete(s.namespaces, namespace) } func (s *PublicBucketServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { s.mux.ServeHTTP(w, req) } func (s *PublicBucketServer) BaseAddr() string { return s.baseAddr } func (s *PublicBucketServer) handler(w http.ResponseWriter, req *http.Request) { nsID := req.PathValue("namespace") bucketName := req.PathValue("bucket") objName := req.PathValue("object") // Determine which store to use s.mu.RLock() store, ok := s.namespaces[nsID] s.mu.RUnlock() if !ok && s.fallback != nil { store, ok = s.fallback(nsID) } if !ok { http.Error(w, "unknown namespace", http.StatusNotFound) return } switch req.Method { case "OPTIONS": w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, HEAD") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration") w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration") case "GET", "HEAD": _, isSigned := (queryLowerCase(req))["x-goog-signature"] if isSigned { err := validateGcsSignedRequest(req, time.Now()) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } } obj, contents, err := store.Get("", bucketName, objName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } else if obj == nil { http.Error(w, "object not found", http.StatusNotFound) return } if obj.ContentType != "" { w.Header().Set("Content-Type", obj.ContentType) } if obj.Etag != "" { w.Header().Set("Etag", obj.Etag) } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration") w.Header().Set("Content-Length", strconv.Itoa(len(contents))) w.Header().Set("Accept-Ranges", "bytes") // Only write the body for GET requests, not HEAD if req.Method == "GET" { http.ServeContent(w, req, obj.Name, time.Time{}, bytes.NewReader(contents)) } case "PUT": err := validateGcsSignedRequest(req, time.Now()) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } buf, err := io.ReadAll(req.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } metaIn := parseObjectMeta(req) err = store.Add(bucketName, objName, buf, &metaIn) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Read back the object so we can add the etag value to the response. metaOut, _, err := store.Get("", bucketName, objName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration") w.Header().Set("Etag", metaOut.Etag) default: http.Error(w, "method not allowed", http.StatusBadRequest) } } // Only GCS is supported for local development func validateGcsSignedRequest(req *http.Request, now time.Time) error { const dateLayout = "20060102T150405Z" const gracePeriod = time.Duration(30) * time.Second query := queryLowerCase(req) // We don't try to actually verify the signature, we only check that it's non-empty. for _, s := range []string{ "x-goog-signature", "x-goog-credential", "x-goog-date", "x-goog-expires"} { if len(query[s]) <= 0 { return fmt.Errorf("missing or empty query param %q", s) } } t0, err := time.Parse(dateLayout, query["x-goog-date"]) if err != nil { return errors.New("failed to parse x-goog-date") } if t0.After(now.Add(gracePeriod)) { return errors.New("URL expiration base date is in the future") } td, err := strconv.Atoi(query["x-goog-expires"]) if err != nil { return errors.New("failed to parse x-goog-expires value into an integer") } t := t0.Add(time.Duration(td) * time.Second) if t.Before(now.Add(-gracePeriod)) { return errors.New("URL is expired") } return nil } func queryLowerCase(req *http.Request) map[string]string { query := map[string]string{} for k, vs := range req.URL.Query() { query[strings.ToLower(k)] = vs[0] } return query } func parseObjectMeta(req *http.Request) storage.Object { return storage.Object{ContentType: req.Header.Get("Content-Type")} } ================================================ FILE: cli/daemon/pubsub/nsq.go ================================================ package pubsub import ( "os" "strings" "github.com/cockroachdb/errors" "github.com/nsqio/go-nsq" "github.com/nsqio/nsq/nsqd" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "go4.org/syncutil" ) type NSQDaemon struct { nsqd *nsqd.NSQD startOnce syncutil.Once Opts *nsqd.Options } func (n *NSQDaemon) Stats() (*nsqd.Stats, error) { if n.nsqd == nil { return nil, errors.New("nsqd not started") } stats := n.nsqd.GetStats("", "", true) return &stats, nil } func (n *NSQDaemon) isReady() error { p, err := nsq.NewProducer(n.Addr(), nsq.NewConfig()) p.SetLogger(&logAdapter{"nsq producer"}, nsq.LogLevelWarning) if err != nil { return err } err = p.Ping() p.Stop() n.nsqd.GetError() return err } func (n *NSQDaemon) Addr() string { return n.nsqd.RealTCPAddr().String() } func (n *NSQDaemon) Start() error { return n.startOnce.Do(func() error { if n.Opts == nil { n.Opts = nsqd.NewOptions() tmpDir, err := os.MkdirTemp("", "encore-nsqd") if err != nil { return errors.Wrap(err, "failed to create tmp nsqd datapath") } n.Opts.DataPath = tmpDir n.Opts.LogLevel = nsqd.LOG_WARN n.Opts.Logger = &logAdapter{"nsqd"} // Take the default address options and scope down to localhost (to prevent firewall warnings / permission requests) // then set the port to 0 to allow any port to be used which is free n.Opts.TCPAddress = "127.0.0.1:0" n.Opts.HTTPAddress = "127.0.0.1:0" n.Opts.HTTPSAddress = "127.0.0.1:0" n.Opts.MaxMsgSize = 10 * 1024 * 1024 // 10MB } nsq, err := nsqd.New(n.Opts) if err != nil { return errors.Wrap(err, "failed to create new nsqd") } n.nsqd = nsq go func() { err = nsq.Main() if err != nil { log.Err(err).Msg("failed to start nsqd") } }() // Ping the daemon to make sure it has started correctly return n.isReady() }) } func (n *NSQDaemon) Stop() { if n.nsqd != nil { n.nsqd.Exit() } } type logAdapter struct{ serviceName string } var _ nsqd.Logger = (*logAdapter)(nil) func (l *logAdapter) Output(maxdepth int, s string) error { // Attempt to extract the level, start with cutting on ":" lvl, logMsg, found := strings.Cut(s, ":") if !found || strings.Contains(lvl, " ") { // then if that fails or we have a space in that cut, try cutting on the first space newLvl, suffix, _ := strings.Cut(lvl, " ") lvl = newLvl if found { logMsg = suffix + ":" + logMsg } } // Attempt to convert the level string to a zerolog level logLevel := l.OutputLevel(lvl) if logLevel == zerolog.NoLevel { // and if that fails, then just log the message logMsg = s } log.WithLevel(logLevel).Str("service", l.serviceName).Msg(strings.TrimSpace(logMsg)) return nil } func (l *logAdapter) OutputLevel(lvl string) zerolog.Level { switch strings.ToLower(lvl) { case "debug", "dbg": return zerolog.DebugLevel case "info", "inf": return zerolog.InfoLevel case "warn", "wrn": return zerolog.WarnLevel case "error", "err": return zerolog.ErrorLevel case "fatal": return zerolog.FatalLevel default: log.Warn().Msg("unknown level: " + lvl) return zerolog.NoLevel } } ================================================ FILE: cli/daemon/pubsub/utils.go ================================================ package pubsub import ( meta "encr.dev/proto/encore/parser/meta/v1" ) // IsUsed reports whether the application uses pubsub at all. func IsUsed(md *meta.Data) bool { return len(md.PubsubTopics) > 0 } ================================================ FILE: cli/daemon/redis/redis.go ================================================ package redis import ( mathrand "math/rand" // nosemgrep "time" "github.com/alicebob/miniredis/v2" "github.com/cockroachdb/errors" "go4.org/syncutil" meta "encr.dev/proto/encore/parser/meta/v1" ) type Server struct { startOnce syncutil.Once mini *miniredis.Miniredis cleanup *time.Ticker quit chan struct{} addr string } const tickInterval = 1 * time.Second func New() *Server { return &Server{ mini: miniredis.NewMiniRedis(), quit: make(chan struct{}), } } func (s *Server) Start() error { return s.startOnce.Do(func() error { if err := s.mini.Start(); err != nil { return errors.Wrap(err, "failed to start redis server") } s.addr = s.mini.Addr() s.cleanup = time.NewTicker(tickInterval) go s.doCleanup() return nil }) } func (s *Server) Stop() { s.mini.Close() s.cleanup.Stop() close(s.quit) } func (s *Server) Miniredis() *miniredis.Miniredis { return s.mini } func (s *Server) Addr() string { // Ensure the server has been started if err := s.Start(); err != nil { panic(err) } return s.addr } func (s *Server) doCleanup() { var acc time.Duration const cleanupInterval = 15 * time.Second for { select { case <-s.quit: return case <-s.cleanup.C: } s.mini.FastForward(tickInterval) // Clean up keys every so often acc += tickInterval if acc > cleanupInterval { acc -= cleanupInterval s.clearKeys() } } } // clearKeys clears random keys to get the redis server // down to 100 persisted keys, as a simple way to bound // the max memory usage. func (s *Server) clearKeys() { const maxKeys = 100 keys := s.mini.Keys() if n := len(keys); n > maxKeys { toDelete := n - maxKeys deleted := 0 for deleted < toDelete { id := mathrand.Intn(len(keys)) if keys[id] != "" { s.mini.Del(keys[id]) keys[id] = "" // mark it as deleted deleted++ } } } } // IsUsed reports whether the application uses redis at all. func IsUsed(md *meta.Data) bool { return len(md.CacheClusters) > 0 } ================================================ FILE: cli/daemon/run/call.go ================================================ package run import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "github.com/rs/zerolog/log" "github.com/tailscale/hujson" "encr.dev/parser/encoding" v1 "encr.dev/proto/encore/parser/meta/v1" ) type ApiCallParams struct { AppID string Service string Endpoint string Path string Method string Payload []byte AuthPayload []byte `json:"auth_payload,omitempty"` AuthToken string `json:"auth_token,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` } func CallAPI(ctx context.Context, run *Run, p *ApiCallParams) (map[string]any, error) { log := log.With().Str("app_id", p.AppID).Str("path", p.Path).Str("service", p.Service).Str("endpoint", p.Endpoint).Logger() if run == nil { log.Error().Str("app_id", p.AppID).Msg("dash: cannot make api call: app not running") return nil, fmt.Errorf("app not running") } proc := run.ProcGroup() if proc == nil { log.Error().Str("app_id", p.AppID).Msg("dash: cannot make api call: app not running") return nil, fmt.Errorf("app not running") } baseURL := "http://" + run.ListenAddr req, err := prepareRequest(ctx, baseURL, proc.Meta, p) if err != nil { log.Error().Err(err).Msg("dash: unable to prepare request") return nil, err } if p.CorrelationID != "" { req.Header.Set("X-Correlation-ID", p.CorrelationID) } resp, err := http.DefaultClient.Do(req) if err != nil { log.Error().Err(err).Msg("dash: api call failed") return nil, err } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() // Encode the body back into a Go style struct if resp.StatusCode >= 200 && resp.StatusCode < 300 { body = handleResponse(proc.Meta, p, resp.Header, body) } log.Info().Int("status", resp.StatusCode).Msg("dash: api call completed") return map[string]interface{}{ "status": resp.Status, "status_code": resp.StatusCode, "body": body, "trace_id": resp.Header.Get("X-Encore-Trace-Id"), }, nil } // findRPC finds the RPC with the given service and endpoint name. // If it cannot be found it reports nil. func findRPC(md *v1.Data, service, endpoint string) *v1.RPC { for _, svc := range md.Svcs { if svc.Name == service { for _, rpc := range svc.Rpcs { if rpc.Name == endpoint { return rpc } } break } } return nil } // prepareRequest prepares a request for sending based on the given ApiCallParams. func prepareRequest(ctx context.Context, baseURL string, md *v1.Data, p *ApiCallParams) (*http.Request, error) { reqSpec := newHTTPRequestSpec() rpc := findRPC(md, p.Service, p.Endpoint) if rpc == nil { return nil, fmt.Errorf("unknown service/endpoint: %s/%s", p.Service, p.Endpoint) } rpcEncoding, err := encoding.DescribeRPC(md, rpc, nil) if err != nil { return nil, fmt.Errorf("describe rpc: %v", err) } // Add request encoding { reqEnc := rpcEncoding.RequestEncodingForMethod(p.Method) if reqEnc == nil { return nil, fmt.Errorf("unsupported method: %s (supports: %s)", p.Method, strings.Join(rpc.HttpMethods, ",")) } if len(p.Payload) > 0 { if err := addToRequest(reqSpec, p.Payload, reqEnc.ParameterEncodingMapByName()); err != nil { return nil, fmt.Errorf("encode request params: %v", err) } } } // Add auth encoding, if any if h := md.AuthHandler; h != nil { auth, err := encoding.DescribeAuth(md, h.Params, nil) if err != nil { return nil, fmt.Errorf("describe auth: %v", err) } if auth.LegacyTokenFormat { reqSpec.Header.Set("Authorization", "Bearer "+p.AuthToken) } else { if err := addToRequest(reqSpec, p.AuthPayload, auth.ParameterEncodingMapByName()); err != nil { return nil, fmt.Errorf("encode auth params: %v", err) } } } var body io.Reader = nil if reqSpec.Body != nil { data, _ := json.Marshal(reqSpec.Body) body = bytes.NewReader(data) if reqSpec.Header["Content-Type"] == nil { reqSpec.Header.Set("Content-Type", "application/json") } } reqURL := baseURL + p.Path if len(reqSpec.Query) > 0 { reqURL += "?" + reqSpec.Query.Encode() } req, err := http.NewRequestWithContext(ctx, p.Method, reqURL, body) if err != nil { return nil, err } for k, v := range reqSpec.Header { req.Header[k] = v } for _, c := range reqSpec.Cookies { req.AddCookie(c) } return req, nil } func handleResponse(md *v1.Data, p *ApiCallParams, headers http.Header, body []byte) []byte { rpc := findRPC(md, p.Service, p.Endpoint) if rpc == nil { return body } encodingOptions := &encoding.Options{} rpcEncoding, err := encoding.DescribeRPC(md, rpc, encodingOptions) if err != nil { return body } decoded := map[string]json.RawMessage{} if err := json.Unmarshal(body, &decoded); err != nil { return body } members := make([]hujson.ObjectMember, 0) if rpcEncoding.ResponseEncoding != nil { for i, m := range rpcEncoding.ResponseEncoding.HeaderParameters { values := headers.Values(m.Name) var beforeExtra []byte if i == 0 { beforeExtra = []byte("\n // HTTP Headers\n ") } var val hujson.Value if len(values) == 1 { val = hujson.Value{Value: hujson.String(values[0])} } else { arr := &hujson.Array{} for _, v := range values { arr.Elements = append(arr.Elements, hujson.Value{Value: hujson.String(v)}) } val = hujson.Value{Value: arr} } members = append(members, hujson.ObjectMember{ Name: hujson.Value{Value: hujson.String(m.Name), BeforeExtra: beforeExtra}, Value: val, }) } for i, m := range rpcEncoding.ResponseEncoding.BodyParameters { value, ok := decoded[m.Name] if !ok { value = []byte("null") } var beforeExtra []byte if i == 0 { if len(rpcEncoding.ResponseEncoding.HeaderParameters) > 0 { beforeExtra = []byte("\n\n // JSON Payload\n ") } else { beforeExtra = []byte("\n ") } } // nosemgrep: trailofbits.go.invalid-usage-of-modified-variable.invalid-usage-of-modified-variable hValue, err := hujson.Parse(value) if err != nil { hValue = hujson.Value{Value: hujson.Literal(value)} } members = append(members, hujson.ObjectMember{ Name: hujson.Value{Value: hujson.String(m.Name), BeforeExtra: beforeExtra}, Value: hValue, }) } } value := hujson.Value{Value: &hujson.Object{Members: members}} value.Format() return value.Pack() } // httpRequestSpec specifies how the HTTP request should be generated. type httpRequestSpec struct { // Body are the fields to encode as the JSON body. // If nil, no body is added. Body map[string]json.RawMessage // Header are the HTTP headers to set in the request. Header http.Header // Query are the query string fields to set. Query url.Values // Cookies are the cookies to send. Cookies []*http.Cookie } func newHTTPRequestSpec() *httpRequestSpec { return &httpRequestSpec{ Body: nil, // to distinguish between no body and "{}". Header: make(http.Header), Query: make(url.Values), } } // addToRequest decodes rawPayload and adds it to the request according to the given parameter encodings. // The body argument is where body parameters are added; other parameter locations are added // directly to the request object itself. func addToRequest(req *httpRequestSpec, rawPayload []byte, params map[string][]*encoding.ParameterEncoding) error { payload, err := hujson.Parse(rawPayload) if err != nil { return fmt.Errorf("invalid payload: %v", err) } vals, ok := payload.Value.(*hujson.Object) if !ok { return fmt.Errorf("invalid payload: expected JSON object, got %s", payload.Pack()) } seenKeys := make(map[string]int) for _, kv := range vals.Members { lit, _ := kv.Name.Value.(hujson.Literal) key := lit.String() val := kv.Value val.Standardize() if matches := params[key]; len(matches) > 0 { // Get the index of this particular match, in case we have conflicts. idx := seenKeys[key] seenKeys[key]++ if idx < len(matches) { param := matches[idx] switch param.Location { case encoding.Body: if req.Body == nil { req.Body = make(map[string]json.RawMessage) } req.Body[param.WireFormat] = val.Pack() case encoding.Query: switch v := val.Value.(type) { case hujson.Literal: req.Query.Add(param.WireFormat, v.String()) case *hujson.Array: for _, elem := range v.Elements { if lit, ok := elem.Value.(hujson.Literal); ok { req.Query.Add(param.WireFormat, lit.String()) } else { return fmt.Errorf("unsupported value type for query string array element: %T", elem.Value) } } default: return fmt.Errorf("unsupported value type for query string: %T", v) } case encoding.Header: switch v := val.Value.(type) { case hujson.Literal: req.Header.Add(param.WireFormat, v.String()) default: return fmt.Errorf("unsupported value type for query string: %T", v) } case encoding.Cookie: switch v := val.Value.(type) { case hujson.Literal: // nosemgrep req.Cookies = append(req.Cookies, &http.Cookie{ Name: param.WireFormat, Value: v.String(), }) default: return fmt.Errorf("unsupported value type for cookie: %T", v) } default: return fmt.Errorf("unsupported parameter location %v", param.Location) } } } } return nil } ================================================ FILE: cli/daemon/run/check.go ================================================ package run import ( "context" "runtime" "github.com/cockroachdb/errors" "encr.dev/cli/daemon/apps" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/vcs" ) type CheckParams struct { // App is the app to start. App *apps.Instance // WorkingDir is the working dir, for formatting // error messages with relative paths. WorkingDir string // CodegenDebug, if true, specifies to keep the output // around for codegen debugging purposes. CodegenDebug bool // Environ are the environment variables to set, // in the same format as os.Environ(). Environ []string // Tests specifies whether to parse and codegen for tests as well. Tests bool } // Check checks the app for errors. // It reports a buildDir (if available) when codegenDebug is true. func (mgr *Manager) Check(ctx context.Context, p CheckParams) (buildDir string, err error) { expSet, err := p.App.Experiments(p.Environ) if err != nil { return "", err } // TODO: We should check that all secret keys are defined as well. vcsRevision := vcs.GetRevision(p.App.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: p.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: p.CodegenDebug, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } bld := builderimpl.Resolve(p.App.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: p.App, WorkingDir: p.WorkingDir, }) if err != nil { return "", err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: p.App, Experiments: expSet, WorkingDir: p.WorkingDir, ParseTests: p.Tests, Prepare: prepareResult, }) if err != nil { return "", err } if err := p.App.CacheMetadata(parse.Meta); err != nil { return "", errors.Wrap(err, "cache metadata") } // Validate the service configs. _, err = bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ // Dummy data to satisfy config validation. APIBaseURL: "http://localhost:0", EnvName: "encore-check", EnvType: cueutil.EnvType_Development, CloudType: cueutil.CloudType_Local, }, }) if err != nil { return "", err } result, err := bld.Compile(ctx, builder.CompileParams{ Build: buildInfo, App: p.App, Parse: parse, OpTracker: nil, // TODO Experiments: expSet, WorkingDir: p.WorkingDir, }) if result != nil && len(result.Outputs) > 0 { buildDir = result.Outputs[0].GetArtifactDir().ToIO() } return buildDir, err } ================================================ FILE: cli/daemon/run/errors.go ================================================ package run import ( "errors" "encr.dev/pkg/errlist" "encr.dev/v2/internals/perr" ) func AsErrorList(err error) *errlist.List { if errList := errlist.Convert(err); errList != nil { return errList } list := &perr.ListAsErr{} if errors.As(err, &list) { return &errlist.List{List: list.ErrorList()} } return nil } ================================================ FILE: cli/daemon/run/exec_command.go ================================================ package run import ( "context" "fmt" "path/filepath" "runtime" "time" "github.com/cockroachdb/errors" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run/infra" "encr.dev/internal/optracker" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/promise" "encr.dev/pkg/vcs" ) // ExecSpecParams groups the parameters for the ExecSpec method. type ExecSpecParams struct { // App is the app to execute the script for. App *apps.Instance // NS is the namespace to use. NS *namespace.Namespace // Command to execute Command string // ScriptArgs are the arguments to pass to the script binary. ScriptArgs []string // WorkingDir is the working dir to execute the script from. // It's relative to the app root. WorkingDir string // Environ are the environment variables to set when running the command, // in the same format as os.Environ(). Environ []string // TempDir is a path to a temp dir that will be cleaned up by the CLI. TempDir string OpTracker *optracker.OpTracker } // ExecSpecResponse contains the specification for how to run an exec command. type ExecSpecResponse struct { Command string Args []string Environ []string } // ExecSpec returns the specification for how to run an exec command, // without actually executing it. This allows the CLI to run the command // directly with stdin attached for interactive support. func (mgr *Manager) ExecSpec(ctx context.Context, p ExecSpecParams) (*ExecSpecResponse, error) { expSet, err := p.App.Experiments(p.Environ) if err != nil { return nil, err } rm := infra.NewResourceManager(p.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, p.NS, p.Environ, mgr.DBProxyPort, false) tracker := p.OpTracker jobs := optracker.NewAsyncBuildJobs(ctx, p.App.PlatformOrLocalID(), tracker) // Parse the app to figure out what infrastructure is needed. start := time.Now() parseOp := tracker.Add("Building Encore application graph", start) topoOp := tracker.Add("Analyzing service topology", start) bld := builderimpl.Resolve(p.App.Lang(), expSet) defer fns.CloseIgnore(bld) vcsRevision := vcs.GetRevision(p.App.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: p.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: p.App, WorkingDir: p.WorkingDir, }) if err != nil { tracker.Fail(parseOp, errors.New("prepare error")) return nil, err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: p.App, Experiments: expSet, WorkingDir: p.WorkingDir, ParseTests: false, Prepare: prepareResult, }) if err != nil { tracker.Fail(parseOp, errors.New("parse error")) return nil, err } if err := p.App.CacheMetadata(parse.Meta); err != nil { return nil, errors.Wrap(err, "cache metadata") } tracker.Done(parseOp, 500*time.Millisecond) tracker.Done(topoOp, 300*time.Millisecond) rm.StartRequiredServices(jobs, parse.Meta) var secrets map[string]string if usesSecrets(parse.Meta) { jobs.Go("Fetching application secrets", true, 150*time.Millisecond, func(ctx context.Context) error { data, err := mgr.Secret.Load(p.App).Get(ctx, expSet) if err != nil { return err } secrets = data.Values return nil }) } apiBaseURL := fmt.Sprintf("http://localhost:%d", mgr.RuntimePort) configProm := promise.New(func() (*builder.ServiceConfigsResult, error) { return bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: apiBaseURL, EnvName: "local", EnvType: cueutil.EnvType_Development, CloudType: cueutil.CloudType_Local, }, }) }) if err := jobs.Wait(); err != nil { return nil, err } gateways := make(map[string]GatewayConfig) for _, gw := range parse.Meta.Gateways { gateways[gw.EncoreName] = GatewayConfig{ BaseURL: apiBaseURL, Hostnames: []string{"localhost"}, } } cfg, err := configProm.Get(ctx) if err != nil { return nil, err } authKey := genAuthKey() configGen := &RuntimeConfigGenerator{ app: p.App, infraManager: rm, md: parse.Meta, AppID: option.Some(GenID()), EnvID: option.Some(GenID()), TraceEndpoint: option.Some(fmt.Sprintf("http://localhost:%d/trace", mgr.RuntimePort)), AuthKey: authKey, Gateways: gateways, DefinedSecrets: secrets, SvcConfigs: cfg.Configs, IncludeMeta: bld.NeedsMeta(), MetaPath: option.Some(filepath.Join(p.TempDir, "meta.pb")), RuntimeConfigPath: option.Some(filepath.Join(p.TempDir, "runtime_config.pb")), } procConf, err := configGen.AllInOneProc(bld.UseNewRuntimeConfig()) if err != nil { return nil, err } procEnv, err := configGen.ProcEnvs(procConf, bld.UseNewRuntimeConfig()) if err != nil { return nil, errors.Wrap(err, "compute proc envs") } defaultEnv := []string{"ENCORE_RUNTIME_LOG=error"} env := append(defaultEnv, p.Environ...) env = append(env, procConf.ExtraEnv...) env = append(env, procEnv...) tracker.AllDone() return &ExecSpecResponse{ Command: p.Command, Args: p.ScriptArgs, Environ: env, }, nil } ================================================ FILE: cli/daemon/run/exec_script.go ================================================ package run import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "slices" "time" "github.com/cockroachdb/errors" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run/infra" encoreEnv "encr.dev/internal/env" "encr.dev/internal/lookpath" "encr.dev/internal/optracker" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/paths" "encr.dev/pkg/promise" "encr.dev/pkg/vcs" ) // ExecScriptParams groups the parameters for the ExecScript method. type ExecScriptParams struct { // App is the app to execute the script for. App *apps.Instance // NS is the namespace to use. NS *namespace.Namespace // MainPkg is the package path to the command to execute. MainPkg paths.Pkg // ScriptArgs are the arguments to pass to the script binary. ScriptArgs []string // WorkingDir is the working dir to execute the script from. // It's relative to the app root. WorkingDir string // Environ are the environment variables to set when running the tests, // in the same format as os.Environ(). Environ []string // Stdout and Stderr are where "go test" output should be written. Stdout, Stderr io.Writer OpTracker *optracker.OpTracker } // ExecScript executes the script. func (mgr *Manager) ExecScript(ctx context.Context, p ExecScriptParams) (err error) { expSet, err := p.App.Experiments(p.Environ) if err != nil { return err } rm := infra.NewResourceManager(p.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, p.NS, p.Environ, mgr.DBProxyPort, false) defer rm.StopAll() tracker := p.OpTracker jobs := optracker.NewAsyncBuildJobs(ctx, p.App.PlatformOrLocalID(), tracker) // Parse the app to figure out what infrastructure is needed. start := time.Now() parseOp := tracker.Add("Building Encore application graph", start) topoOp := tracker.Add("Analyzing service topology", start) bld := builderimpl.Resolve(p.App.Lang(), expSet) defer fns.CloseIgnore(bld) vcsRevision := vcs.GetRevision(p.App.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: p.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, MainPkg: option.Some(p.MainPkg), // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: p.App, WorkingDir: p.WorkingDir, }) if err != nil { tracker.Fail(parseOp, errors.New("prepare error")) return err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: p.App, Experiments: expSet, WorkingDir: p.WorkingDir, ParseTests: false, Prepare: prepareResult, }) if err != nil { // Don't use the error itself in tracker.Fail, as it will lead to duplicate error output. tracker.Fail(parseOp, errors.New("parse error")) return err } if err := p.App.CacheMetadata(parse.Meta); err != nil { return errors.Wrap(err, "cache metadata") } tracker.Done(parseOp, 500*time.Millisecond) tracker.Done(topoOp, 300*time.Millisecond) rm.StartRequiredServices(jobs, parse.Meta) var secrets map[string]string if usesSecrets(parse.Meta) { jobs.Go("Fetching application secrets", true, 150*time.Millisecond, func(ctx context.Context) error { data, err := mgr.Secret.Load(p.App).Get(ctx, expSet) if err != nil { return err } secrets = data.Values return nil }) } apiBaseURL := fmt.Sprintf("http://localhost:%d", mgr.RuntimePort) configProm := promise.New(func() (*builder.ServiceConfigsResult, error) { return bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: apiBaseURL, EnvName: "local", EnvType: cueutil.EnvType_Development, CloudType: cueutil.CloudType_Local, }, }) }) var build *builder.CompileResult jobs.Go("Compiling application source code", false, 0, func(ctx context.Context) (err error) { build, err = bld.Compile(ctx, builder.CompileParams{ Build: buildInfo, App: p.App, Parse: parse, OpTracker: tracker, Experiments: expSet, WorkingDir: p.WorkingDir, }) if err != nil { return errors.Wrap(err, "compile error on exec") } return nil }) if err := jobs.Wait(); err != nil { return err } gateways := make(map[string]GatewayConfig) for _, gw := range parse.Meta.Gateways { gateways[gw.EncoreName] = GatewayConfig{ BaseURL: apiBaseURL, Hostnames: []string{"localhost"}, } } outputs := build.Outputs if len(outputs) != 1 { return errors.New("ExecScript currently only supports a single build output") } entrypoints := outputs[0].GetEntrypoints() if len(entrypoints) != 1 { return errors.New("ExecScript currently only supports a single entrypoint") } proc := entrypoints[0].Cmd.Expand(outputs[0].GetArtifactDir()) cfg, err := configProm.Get(ctx) if err != nil { return err } tempDir, err := os.MkdirTemp("", "encore-exec") if err != nil { return errors.Wrap(err, "couldn't create temp dir") } defer func() { _ = os.RemoveAll(tempDir) }() authKey := genAuthKey() configGen := &RuntimeConfigGenerator{ app: p.App, infraManager: rm, md: parse.Meta, AppID: option.Some(GenID()), EnvID: option.Some(GenID()), TraceEndpoint: option.Some(fmt.Sprintf("http://localhost:%d/trace", mgr.RuntimePort)), AuthKey: authKey, Gateways: gateways, DefinedSecrets: secrets, SvcConfigs: cfg.Configs, IncludeMeta: bld.NeedsMeta(), MetaPath: option.Some(filepath.Join(tempDir, "meta.pb")), RuntimeConfigPath: option.Some(filepath.Join(tempDir, "runtime_config.json")), } procConf, err := configGen.AllInOneProc(bld.UseNewRuntimeConfig()) if err != nil { return err } procEnv, err := configGen.ProcEnvs(procConf, bld.UseNewRuntimeConfig()) if err != nil { return errors.Wrap(err, "compute proc envs") } env := append(os.Environ(), proc.Env...) env = append(env, p.Environ...) env = append(env, procConf.ExtraEnv...) env = append(env, procEnv...) env = append(env, encodeServiceConfigs(cfg.Configs)...) if runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != "" { env = append(env, "ENCORE_RUNTIME_LIB="+runtimeLibPath) } tracker.AllDone() cwd := filepath.Join(p.App.Root(), p.WorkingDir) binary, err := lookpath.InDir(cwd, env, proc.Command[0]) if err != nil { return err } args := append(slices.Clone(proc.Command[1:]), p.ScriptArgs...) // nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.CommandContext(ctx, binary, args...) cmd.Dir = filepath.Join(p.App.Root(), p.WorkingDir) cmd.Stdout = p.Stdout cmd.Stderr = p.Stderr cmd.Env = env return cmd.Run() } ================================================ FILE: cli/daemon/run/http.go ================================================ package run import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/binary" "fmt" "net/http" "time" "encore.dev/appruntime/exported/config" ) // ServeHTTP implements http.Handler by forwarding the request to the currently running process. func (r *Run) ServeHTTP(w http.ResponseWriter, req *http.Request) { proc := r.proc.Load().(*ProcGroup) proc.ProxyReq(w, req) } func addAuthKeyToRequest(req *http.Request, authKey config.EncoreAuthKey) { if req.Header == nil { req.Header = make(http.Header) } date := time.Now().UTC().Format(http.TimeFormat) req.Header.Set("Date", date) mac := hmac.New(sha256.New, authKey.Data) _, _ = fmt.Fprintf(mac, "%s\x00%s", date, req.URL.Path) bytes := make([]byte, 4, 4+sha256.Size) binary.BigEndian.PutUint32(bytes[0:4], authKey.KeyID) bytes = mac.Sum(bytes) auth := base64.RawStdEncoding.EncodeToString(bytes) req.Header.Set("X-Encore-Auth", auth) } const TestHeaderDisablePlatformAuth = "X-Encore-Test-Disable-Platform-Auth" ================================================ FILE: cli/daemon/run/infra/encorecloudtesting.go ================================================ package infra import ( "encoding/base64" "strconv" "github.com/cockroachdb/errors" "go.encore.dev/platform-sdk/pkg/auth" "encore.dev/appruntime/exported/config" ) // setTestEncoreCloud sets the Encore Cloud API configuration to use a local // Encore Cloud API server. // // It returns true if one has been configured, or false if not. // // To use it the `encore run` command must be started with the following environment variables: // - ENCORECLOUD_LOCAL_SERVER: the URL of the local Encore Cloud API server // - ENCORECLOUD_LOCAL_KEY_ID: the ID of the key to use for authentication // - ENCORECLOUD_LOCAL_KEY_DATA: the base64-encoded data of the key to use for authentication func (rm *ResourceManager) setTestEncoreCloud(cfg *config.Runtime) (useLocalCloudServer bool, err error) { localServer := rm.environ.Get("ENCORECLOUD_LOCAL_SERVER") if localServer == "" { return false, nil } // Get the key and secret keyIDStr := rm.environ.Get("ENCORECLOUD_LOCAL_KEY_ID") keyData64 := rm.environ.Get("ENCORECLOUD_LOCAL_KEY_DATA") if keyIDStr == "" || keyData64 == "" { return false, errors.New("ENCORECLOUD_LOCAL_KEY_ID and ENCORECLOUD_LOCAL_KEY_DATA must be set if using ENCORECLOUD_LOCAL_SERVER") } keyID, err := strconv.Atoi(keyIDStr) if err != nil || keyID <= 0 { return false, errors.New("ENCORECLOUD_LOCAL_KEY_ID must be a positive integer") } keyData, err := base64.StdEncoding.DecodeString(keyData64) if err != nil { return false, errors.New("ENCORECLOUD_LOCAL_KEY_DATA must be a valid base64 string") } cfg.EncoreCloudAPI = &config.EncoreCloudAPI{ Server: localServer, AuthKeys: []auth.Key{ { KeyID: uint32(keyID), Data: keyData, }, }, } return true, nil } ================================================ FILE: cli/daemon/run/infra/infra.go ================================================ package infra import ( "context" "fmt" "strconv" "sync" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encore.dev/appruntime/exported/config" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/objects" "encr.dev/cli/daemon/pubsub" "encr.dev/cli/daemon/redis" "encr.dev/cli/daemon/sqldb" "encr.dev/internal/optracker" "encr.dev/pkg/environ" meta "encr.dev/proto/encore/parser/meta/v1" ) type Type string const ( PubSub Type = "pubsub" Cache Type = "cache" SQLDB Type = "sqldb" Objects Type = "objects" ) const ( // this ID is used in the Encore Cloud README file as an example // on how to create a topic resource encoreCloudExampleTopicID = "res_0o9ioqnrirflhhm3t720" // this ID is used in the Encore Cloud README file as a example // on how to create a subscription on the above topic encoreCloudExampleSubscriptionID = "res_0o9ioqnrirflhhm3t730" ) // ResourceManager manages a set of infrastructure resources // to support the running Encore application. type ResourceManager struct { app *apps.Instance dbProxyPort int sqlMgr *sqldb.ClusterManager objectsMgr *objects.ClusterManager publicBuckets *objects.PublicBucketServer ns *namespace.Namespace environ environ.Environ log zerolog.Logger forTests bool mutex sync.Mutex servers map[Type]Resource } func NewResourceManager(app *apps.Instance, sqlMgr *sqldb.ClusterManager, objectsMgr *objects.ClusterManager, publicBuckets *objects.PublicBucketServer, ns *namespace.Namespace, environ environ.Environ, dbProxyPort int, forTests bool) *ResourceManager { return &ResourceManager{ app: app, dbProxyPort: dbProxyPort, sqlMgr: sqlMgr, objectsMgr: objectsMgr, publicBuckets: publicBuckets, ns: ns, environ: environ, forTests: forTests, servers: make(map[Type]Resource), log: log.With().Str("app_id", app.PlatformOrLocalID()).Logger(), } } func (rm *ResourceManager) StopAll() { rm.mutex.Lock() defer rm.mutex.Unlock() rm.log.Info().Int("num", len(rm.servers)).Msg("Stopping all resource services") for _, daemon := range rm.servers { daemon.Stop() } } type Resource interface { // Stop shuts down the resource. Stop() } // StartRequiredServices will start the required services for the current application // if they are not already running based on the given parse result func (rm *ResourceManager) StartRequiredServices(a *optracker.AsyncBuildJobs, md *meta.Data) { if sqldb.IsUsed(md) && rm.GetSQLCluster() == nil { a.Go("Creating PostgreSQL database cluster", true, 300*time.Millisecond, rm.StartSQLCluster(a, md)) } if pubsub.IsUsed(md) && rm.GetPubSub() == nil { a.Go("Starting PubSub daemon", true, 250*time.Millisecond, rm.StartPubSub) } if redis.IsUsed(md) && rm.GetRedis() == nil { a.Go("Starting Redis server", true, 250*time.Millisecond, rm.StartRedis) } if objects.IsUsed(md) && rm.GetObjects() == nil { a.Go("Starting Object Storage server", true, 250*time.Millisecond, rm.StartObjects(md)) } } // StartPubSub starts a PubSub daemon. func (rm *ResourceManager) StartPubSub(ctx context.Context) error { nsqd := &pubsub.NSQDaemon{} err := nsqd.Start() if err != nil { return err } rm.mutex.Lock() rm.servers[PubSub] = nsqd rm.mutex.Unlock() return nil } // GetPubSub returns the PubSub daemon if it is running otherwise it returns nil func (rm *ResourceManager) GetPubSub() *pubsub.NSQDaemon { rm.mutex.Lock() defer rm.mutex.Unlock() if daemon, found := rm.servers[PubSub]; found { return daemon.(*pubsub.NSQDaemon) } return nil } // StartRedis starts a Redis server. func (rm *ResourceManager) StartRedis(ctx context.Context) error { srv := redis.New() err := srv.Start() if err != nil { return err } rm.mutex.Lock() rm.servers[Cache] = srv rm.mutex.Unlock() return nil } // GetRedis returns the Redis server if it is running otherwise it returns nil func (rm *ResourceManager) GetRedis() *redis.Server { rm.mutex.Lock() defer rm.mutex.Unlock() if srv, found := rm.servers[Cache]; found { return srv.(*redis.Server) } return nil } // StartObjects starts an Object Storage server. func (rm *ResourceManager) StartObjects(md *meta.Data) func(context.Context) error { return func(ctx context.Context) error { var srv *objects.Server if rm.forTests { srv = objects.NewInMemoryServer(rm.publicBuckets) } else { if rm.objectsMgr == nil { return fmt.Errorf("StartObjects: no Object Storage cluster manager provided") } else if rm.publicBuckets == nil { return fmt.Errorf("StartObjects: no Object Storage public bucket server provided") } baseDir, err := rm.objectsMgr.BaseDir(rm.ns.ID) if err != nil { return err } srv = objects.NewDirServer(rm.publicBuckets, rm.ns.ID, baseDir) } if err := srv.Initialize(md); err != nil { return err } else if err := srv.Start(); err != nil { return err } rm.mutex.Lock() rm.servers[Objects] = srv rm.mutex.Unlock() return nil } } // GetObjects returns the Object Storage server if it is running otherwise it returns nil func (rm *ResourceManager) GetObjects() *objects.Server { rm.mutex.Lock() defer rm.mutex.Unlock() if srv, found := rm.servers[Objects]; found { return srv.(*objects.Server) } return nil } func (rm *ResourceManager) StartSQLCluster(a *optracker.AsyncBuildJobs, md *meta.Data) func(ctx context.Context) error { return func(ctx context.Context) error { // This can be the case in tests. if rm.sqlMgr == nil { return fmt.Errorf("StartSQLCluster: no SQL Cluster manager provided") } typ := sqldb.Run if rm.forTests { typ = sqldb.Test } if err := rm.sqlMgr.Ready(); err != nil { return err } cluster := rm.sqlMgr.Create(ctx, &sqldb.CreateParams{ ClusterID: sqldb.GetClusterID(rm.app, typ, rm.ns), Memfs: typ.Memfs(), }) if _, err := cluster.Start(ctx, a.Tracker()); err != nil { return errors.Wrap(err, "failed to start cluster") } rm.mutex.Lock() rm.servers[SQLDB] = cluster rm.mutex.Unlock() // Set up the database asynchronously since it can take a while. if rm.forTests { a.Go("Recreating databases", true, 250*time.Millisecond, func(ctx context.Context) error { err := cluster.Recreate(ctx, rm.app.Root(), nil, md) if err != nil { rm.log.Error().Err(err).Msg("failed to recreate db") return err } return nil }) } else { a.Go("Running database migrations", true, 250*time.Millisecond, func(ctx context.Context) error { err := cluster.SetupAndMigrate(ctx, rm.app.Root(), md.SqlDatabases) if err != nil { rm.log.Error().Err(err).Msg("failed to setup db") return err } return nil }) } return nil } } // GetSQLCluster returns the SQL cluster func (rm *ResourceManager) GetSQLCluster() *sqldb.Cluster { rm.mutex.Lock() defer rm.mutex.Unlock() if cluster, found := rm.servers[SQLDB]; found { return cluster.(*sqldb.Cluster) } return nil } // UpdateConfig updates the given config with infrastructure information. // Note that all the requisite services must have started up already, // which in practice means that (*optracker.AsyncBuildJobs).Wait must have returned first. func (rm *ResourceManager) UpdateConfig(cfg *config.Runtime, md *meta.Data, dbProxyPort int) error { useLocalEncoreCloudAPIForTesting, err := rm.setTestEncoreCloud(cfg) if err != nil { return err } if cluster := rm.GetSQLCluster(); cluster != nil { srv := &config.SQLServer{ Host: "localhost:" + strconv.Itoa(dbProxyPort), } serverID := len(cfg.SQLServers) cfg.SQLServers = append(cfg.SQLServers, srv) for _, db := range md.SqlDatabases { cfg.SQLDatabases = append(cfg.SQLDatabases, &config.SQLDatabase{ ServerID: serverID, EncoreName: db.Name, DatabaseName: db.Name, User: "encore", Password: cluster.Password, }) } // Configure max connections based on 96 connections // divided evenly among the databases maxConns := 96 / len(cfg.SQLDatabases) for _, db := range cfg.SQLDatabases { db.MaxConnections = maxConns } } if nsq := rm.GetPubSub(); nsq != nil { provider := &config.PubsubProvider{ NSQ: &config.NSQProvider{ Host: nsq.Addr(), }, } providerID := len(cfg.PubsubProviders) cfg.PubsubProviders = append(cfg.PubsubProviders, provider) // If we're testing the Encore Cloud API locally, override from NSQ if useLocalEncoreCloudAPIForTesting { providerID = len(cfg.PubsubProviders) cfg.PubsubProviders = append(cfg.PubsubProviders, &config.PubsubProvider{ EncoreCloud: &config.EncoreCloudPubsubProvider{}, }) } cfg.PubsubTopics = make(map[string]*config.PubsubTopic) for _, t := range md.PubsubTopics { providerName := t.Name if useLocalEncoreCloudAPIForTesting { providerName = encoreCloudExampleTopicID } topicCfg := &config.PubsubTopic{ ProviderID: providerID, EncoreName: t.Name, ProviderName: providerName, Subscriptions: make(map[string]*config.PubsubSubscription), } for _, s := range t.Subscriptions { subscriptionID := t.Name if useLocalEncoreCloudAPIForTesting { subscriptionID = encoreCloudExampleSubscriptionID } topicCfg.Subscriptions[s.Name] = &config.PubsubSubscription{ ID: subscriptionID, EncoreName: s.Name, ProviderName: s.Name, } } cfg.PubsubTopics[t.Name] = topicCfg } } if redis := rm.GetRedis(); redis != nil { srv := &config.RedisServer{ Host: redis.Addr(), } serverID := len(cfg.RedisServers) cfg.RedisServers = append(cfg.RedisServers, srv) for _, cluster := range md.CacheClusters { cfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{ ServerID: serverID, Database: 0, EncoreName: cluster.Name, KeyPrefix: cluster.Name + "/", }) } } return nil } // SQLServerConfig returns the SQL server configuration. func (rm *ResourceManager) SQLServerConfig() (config.SQLServer, error) { cluster := rm.GetSQLCluster() if cluster == nil { return config.SQLServer{}, errors.New("no SQL cluster found") } srvCfg := config.SQLServer{ Host: "localhost:" + strconv.Itoa(rm.dbProxyPort), } return srvCfg, nil } // SQLDatabaseConfig returns the SQL server and database configuration for the given database. func (rm *ResourceManager) SQLDatabaseConfig(db *meta.SQLDatabase) (config.SQLDatabase, error) { cluster := rm.GetSQLCluster() if cluster == nil { return config.SQLDatabase{}, errors.New("no SQL cluster found") } dbCfg := config.SQLDatabase{ EncoreName: db.Name, DatabaseName: db.Name, User: "encore", Password: cluster.Password, } return dbCfg, nil } // PubSubProviderConfig returns the PubSub provider configuration. func (rm *ResourceManager) PubSubProviderConfig() (config.PubsubProvider, error) { nsq := rm.GetPubSub() if nsq == nil { return config.PubsubProvider{}, errors.New("no PubSub server found") } return config.PubsubProvider{ NSQ: &config.NSQProvider{ Host: nsq.Addr(), }, }, nil } // PubSubTopicConfig returns the PubSub provider and topic configuration for the given topic. func (rm *ResourceManager) PubSubTopicConfig(topic *meta.PubSubTopic) (config.PubsubProvider, config.PubsubTopic, error) { providerCfg, err := rm.PubSubProviderConfig() if err != nil { return config.PubsubProvider{}, config.PubsubTopic{}, err } topicCfg := config.PubsubTopic{ EncoreName: topic.Name, ProviderName: topic.Name, Subscriptions: make(map[string]*config.PubsubSubscription), } return providerCfg, topicCfg, nil } // PubSubSubscriptionConfig returns the PubSub subscription configuration for the given subscription. func (rm *ResourceManager) PubSubSubscriptionConfig(_ *meta.PubSubTopic, sub *meta.PubSubTopic_Subscription) (config.PubsubSubscription, error) { subCfg := config.PubsubSubscription{ ID: sub.Name, EncoreName: sub.Name, ProviderName: sub.Name, } return subCfg, nil } // RedisConfig returns the Redis server and database configuration for the given database. func (rm *ResourceManager) RedisConfig(redis *meta.CacheCluster) (config.RedisServer, config.RedisDatabase, error) { server := rm.GetRedis() if server == nil { return config.RedisServer{}, config.RedisDatabase{}, errors.New("no Redis server found") } srvCfg := config.RedisServer{ Host: server.Addr(), } dbCfg := config.RedisDatabase{ EncoreName: redis.Name, KeyPrefix: redis.Name + "/", } return srvCfg, dbCfg, nil } // BucketProviderConfig returns the bucket provider configuration. func (rm *ResourceManager) BucketProviderConfig() (config.BucketProvider, string, error) { obj := rm.GetObjects() if obj == nil { return config.BucketProvider{}, "", errors.New("no object storage found") } return config.BucketProvider{ GCS: &config.GCSBucketProvider{ Endpoint: obj.Endpoint(), }, }, obj.PublicBaseURL(), nil } ================================================ FILE: cli/daemon/run/manager.go ================================================ package run import ( "fmt" "sort" "sync" "time" "github.com/cockroachdb/errors" "github.com/rs/xid" encore "encore.dev" "encore.dev/appruntime/exported/config" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/objects" "encr.dev/cli/daemon/run/infra" "encr.dev/cli/daemon/secret" "encr.dev/cli/daemon/sqldb" "encr.dev/pkg/errlist" meta "encr.dev/proto/encore/parser/meta/v1" ) // Manager manages the set of running applications. type Manager struct { RuntimePort int // port for Encore runtime DBProxyPort int // port for sqldb proxy DashBaseURL string // base url for the dev dashboard Secret *secret.Manager ClusterMgr *sqldb.ClusterManager ObjectsMgr *objects.ClusterManager PublicBuckets *objects.PublicBucketServer listeners []EventListener mu sync.Mutex runs map[string]*Run // id -> run } // EventListener is the interface for listening to events // about running apps. type EventListener interface { // OnStart is called when a run starts. OnStart(r *Run) // OnCompileStart is called when a run starts compiling. OnCompileStart(r *Run) // OnReload is called when a run reloads. OnReload(r *Run) // OnStop is called when a run stops. OnStop(r *Run) // OnStdout is called when a run outputs something on stdout. OnStdout(r *Run, out []byte) // OnStderr is called when a run outputs something on stderr. OnStderr(r *Run, out []byte) // OnError is called when a run encounters an error. OnError(r *Run, err *errlist.List) } // FindProc finds the proc with the given id. // It reports nil if no such proc was found. func (mgr *Manager) FindProc(procID string) *ProcGroup { mgr.mu.Lock() defer mgr.mu.Unlock() for _, run := range mgr.runs { if p := run.ProcGroup(); p != nil && p.ID == procID { return p } } return nil } // FindRunByAppID finds the run with the given app id. // It reports nil if no such run was found. func (mgr *Manager) FindRunByAppID(appID string) *Run { mgr.mu.Lock() defer mgr.mu.Unlock() for _, run := range mgr.runs { if appID == run.App.PlatformID() || appID == run.App.LocalID() { select { case <-run.Done(): // exited default: return run } } } return nil } // ListRuns provides a snapshot of all runs. func (mgr *Manager) ListRuns() []*Run { mgr.mu.Lock() runs := make([]*Run, 0, len(mgr.runs)) for _, r := range mgr.runs { runs = append(runs, r) } mgr.mu.Unlock() sort.Slice(runs, func(i, j int) bool { return runs[i].App.PlatformOrLocalID() < runs[j].App.PlatformOrLocalID() }) return runs } // AddListener adds an event listener to mgr. // It must be called before starting the first run. func (mgr *Manager) AddListener(ln EventListener) { mgr.listeners = append(mgr.listeners, ln) } func (mgr *Manager) RunStdout(r *Run, out []byte) { // Make sure the run has started before we start outputting <-r.started for _, ln := range mgr.listeners { ln.OnStdout(r, out) } } func (mgr *Manager) RunStderr(r *Run, out []byte) { // Make sure the run has started before we start outputting <-r.started for _, ln := range mgr.listeners { ln.OnStderr(r, out) } } func (mgr *Manager) RunError(r *Run, err *errlist.List) { for _, ln := range mgr.listeners { ln.OnError(r, err) } } type parseAppParams struct { App *apps.Instance Environ []string WorkingDir string ParseTests bool ScriptMainPkg string } type generateConfigParams struct { App *apps.Instance RM *infra.ResourceManager Meta *meta.Data ForTests bool AuthKey config.EncoreAuthKey APIBaseURL string ConfigAppID string ConfigEnvID string ExternalCalls bool } // generateServiceDiscoveryMap generates a map of service names to // where the Encore daemon is listening to forward to that service binary. func (mgr *Manager) generateServiceDiscoveryMap(p generateConfigParams) (map[string]config.Service, error) { services := make(map[string]config.Service) // Add all the services from the app for _, svc := range p.Meta.Svcs { services[svc.Name] = config.Service{ Name: svc.Name, // For now all services are hosted by the same running instance URL: p.APIBaseURL, Protocol: config.Http, ServiceAuth: mgr.getInternalServiceToServiceAuthMethod(), } } return services, nil } // getInternalServiceToServiceAuthMethod returns the auth method to use // when making service to service calls locally. // // This currently just returns the noop auth method, but in the future // this function will allow us to use environmental variables to configure // the auth method and test different auth methods locally. func (mgr *Manager) getInternalServiceToServiceAuthMethod() config.ServiceAuth { return config.ServiceAuth{Method: "encore-auth"} } func (mgr *Manager) generateConfig(p generateConfigParams) (*config.Runtime, error) { envType := encore.EnvDevelopment if p.ForTests { envType = encore.EnvTest } globalCORS, err := p.App.GlobalCORS() if err != nil { return nil, errors.Wrap(err, "failed to get global CORS") } deployID := xid.New().String() if p.ForTests { deployID = "clitest_" + deployID } else { deployID = "run_" + deployID } serviceDiscovery, err := mgr.generateServiceDiscoveryMap(p) if err != nil { return nil, errors.Wrap(err, "failed to generate service discovery map") } cfg := &config.Runtime{ AppID: p.ConfigAppID, AppSlug: p.App.PlatformID(), APIBaseURL: p.APIBaseURL, DeployID: deployID, DeployedAt: time.Now().UTC(), // Force UTC to not cause confusion EnvID: p.ConfigEnvID, EnvName: "local", EnvCloud: string(encore.CloudLocal), EnvType: string(envType), TraceEndpoint: fmt.Sprintf("http://localhost:%d/trace", mgr.RuntimePort), AuthKeys: []config.EncoreAuthKey{p.AuthKey}, CORS: &config.CORS{ Debug: globalCORS.Debug, AllowOriginsWithCredentials: []string{ // Allow all origins with credentials for local development; // since it's only running on localhost for development this is safe. config.UnsafeAllOriginWithCredentials, }, AllowOriginsWithoutCredentials: []string{"*"}, ExtraAllowedHeaders: globalCORS.AllowHeaders, ExtraExposedHeaders: globalCORS.ExposeHeaders, AllowPrivateNetworkAccess: true, }, ServiceDiscovery: serviceDiscovery, ServiceAuth: []config.ServiceAuth{ mgr.getInternalServiceToServiceAuthMethod(), }, DynamicExperiments: nil, // All experiments would be included in the static config here } if err := p.RM.UpdateConfig(cfg, p.Meta, mgr.DBProxyPort); err != nil { return nil, err } return cfg, nil } ================================================ FILE: cli/daemon/run/nsq_names.go ================================================ package run import ( "encoding/hex" "regexp" "golang.org/x/crypto/sha3" ) var nsqNameRegex = regexp.MustCompile(`^[\.a-zA-Z0-9_-]+(#ephemeral)?$`) // isValidNSQName checks if a name is valid according to NSQ requirements: // - Must match pattern: ^[\.a-zA-Z0-9_-]+(#ephemeral)?$ // - Must be between 1 and 64 characters func isValidNSQName(name string) bool { return len(name) >= 1 && len(name) <= 64 && nsqNameRegex.MatchString(name) } // hashNSQName creates a valid NSQ name by hashing the input. // The hash is a SHA3-256 hash encoded as hex (64 characters). func hashNSQName(name string) string { hash := sha3.Sum256([]byte(name)) return hex.EncodeToString(hash[:]) } // ensureValidNSQName returns the name if it's valid, otherwise returns a hashed version. func ensureValidNSQName(name string) string { if isValidNSQName(name) { return name } return hashNSQName(name) } ================================================ FILE: cli/daemon/run/proc_groups.go ================================================ package run import ( "context" "io" "net" "net/http" "net/http/httputil" "net/netip" "net/url" "os" "os/exec" "path/filepath" "strings" "sync" "sync/atomic" "time" "github.com/cenkalti/backoff/v4" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "encore.dev/appruntime/apisdk/api/transport" "encore.dev/appruntime/exported/config" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/internal/sym" "encr.dev/internal/lookpath" "encr.dev/pkg/builder" "encr.dev/pkg/fns" "encr.dev/pkg/noopgateway" "encr.dev/pkg/noopgwdesc" meta "encr.dev/proto/encore/parser/meta/v1" ) type procGroupOptions struct { Ctx context.Context ProcID string // unique process id Run *Run // the run the process belongs to Meta *meta.Data // app metadata snapshot Experiments *experiments.Set // enabled experiments AuthKey config.EncoreAuthKey Logger RunLogger WorkingDir string ConfigGen *RuntimeConfigGenerator } func newProcGroup(opts procGroupOptions) *ProcGroup { p := &ProcGroup{ ID: opts.ProcID, Run: opts.Run, Meta: opts.Meta, Experiments: opts.Experiments, workingDir: opts.WorkingDir, ctx: opts.Ctx, logger: opts.Logger, log: opts.Run.log.With().Str("proc_id", opts.ProcID).Logger(), ConfigGen: opts.ConfigGen, symParsed: make(chan struct{}), Services: make(map[string]*Proc), Gateways: make(map[string]*Proc), authKey: opts.AuthKey, } p.procCond.L = &p.procMu return p } // ProcGroup represents a running Encore application // // It is a collection of [Proc]'s that are all part of the same application, // where each [Proc] represents a one or more services or an API gateway. type ProcGroup struct { ID string // unique process id Run *Run // the run the process belongs to Meta *meta.Data // app metadata snapshot Experiments *experiments.Set // enabled experiments Gateways map[string]*Proc // the gateway processes, by name (if any) Services map[string]*Proc // all the service processes by name ConfigGen *RuntimeConfigGenerator // generates runtime configuration procMu sync.Mutex // protects both allProcesses and runningProcs procCond sync.Cond // used to signal a change in runningProcs allProcesses []*Proc // all processes in the group runningProcs uint32 // number of running processes ctx context.Context logger RunLogger log zerolog.Logger workingDir string // Used for proxying requests when there is no gateway. noopGW *noopgateway.Gateway authKey config.EncoreAuthKey sym *sym.Table symErr error symParsed chan struct{} // closed when sym and symErr are set } func (pg *ProcGroup) ProxyReq(w http.ResponseWriter, req *http.Request) { // Currently we only support proxying to the default gateway. // Need to rethink how this should work when we support multiple gateways. if gw, ok := pg.Gateways["api-gateway"]; ok { gw.ProxyReq(w, req) } else { pg.noopGW.ServeHTTP(w, req) } } // Done returns a channel that is closed when all processes in the group have exited. func (pg *ProcGroup) Done() <-chan struct{} { c := make(chan struct{}) go func() { pg.procMu.Lock() defer pg.procMu.Unlock() for pg.runningProcs > 0 { // If we have more than one process, wait for one to exit pg.procCond.Wait() } close(c) }() return c } // Start starts all the processes in the group. func (pg *ProcGroup) Start() (err error) { pg.procMu.Lock() defer pg.procMu.Unlock() for _, p := range pg.allProcesses { if err = p.start(); err != nil { p.Kill() return err } } pg.noopGW = newNoopGateway(pg) return nil } // Close closes the process and waits for it to shutdown. // It can safely be called multiple times. func (pg *ProcGroup) Close() { var wg sync.WaitGroup pg.procMu.Lock() wg.Add(len(pg.allProcesses)) for _, p := range pg.allProcesses { go func(p *Proc) { p.Close() wg.Done() }(p) } pg.procMu.Unlock() wg.Wait() } // Kill kills all the processes in the group. // It does not wait for them to exit. func (pg *ProcGroup) Kill() { pg.procMu.Lock() defer pg.procMu.Unlock() for _, p := range pg.allProcesses { p.Kill() } } // parseSymTable parses the symbol table of the binary at binPath // and stores the result in p.sym and p.symErr. func (pg *ProcGroup) parseSymTable(binPath string) { parse := func() (*sym.Table, error) { f, err := os.Open(binPath) if err != nil { return nil, err } defer fns.CloseIgnore(f) return sym.Load(f) } defer close(pg.symParsed) pg.sym, pg.symErr = parse() } // SymTable waits for the proc's symbol table to be parsed and then returns it. // ctx is used to cancel the wait. func (pg *ProcGroup) SymTable(ctx context.Context) (*sym.Table, error) { select { case <-ctx.Done(): return nil, ctx.Err() case <-pg.symParsed: return pg.sym, pg.symErr } } // newProc creates a new process in the group and sets up the required stuff in the struct func (pg *ProcGroup) newProc(processName string, listenAddr netip.AddrPort) (*Proc, error) { dst := &url.URL{ Scheme: "http", Host: listenAddr.String(), } proxy := &httputil.ReverseProxy{ // Enable h2c for the proxy. Transport: transport.NewH2CTransport(http.DefaultTransport), Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(dst) r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] r.SetXForwarded() // Copy the host head over. r.Out.Host = r.In.Host // Add the auth key unless the test header is set. if r.Out.Header.Get(TestHeaderDisablePlatformAuth) == "" { addAuthKeyToRequest(r.Out, pg.authKey) } }, } p := &Proc{ group: pg, log: pg.log.With().Str("proc", processName).Logger(), listenAddr: listenAddr, httpProxy: proxy, exit: make(chan struct{}), } pg.procMu.Lock() pg.allProcesses = append(pg.allProcesses, p) pg.procMu.Unlock() return p, nil } func (pg *ProcGroup) NewAllInOneProc(spec builder.Cmd, listenAddr netip.AddrPort, env []string) error { p, err := pg.newProc("all-in-one", listenAddr) if err != nil { return err } // Append both the command-specific env and the base environment. env = append(env, spec.Env...) cwd := filepath.Join(pg.Run.App.Root(), pg.workingDir) binary, err := lookpath.InDir(cwd, env, spec.Command[0]) if err != nil { return err } // This is safe since the command comes from our build. // nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...) cmd.Env = env cmd.Dir = cwd // Proxy stdout and stderr to the given app logger, if any. if l := pg.logger; l != nil { cmd.Stdout = newLogWriter(pg.Run, l.RunStdout) cmd.Stderr = newLogWriter(pg.Run, l.RunStderr) } p.cmd = cmd // Assign all the gateways to this process. for _, gw := range pg.Meta.Gateways { pg.Gateways[gw.EncoreName] = p } return nil } func (pg *ProcGroup) NewProcForService(serviceName string, listenAddr netip.AddrPort, spec builder.Cmd, env []string) error { if !listenAddr.IsValid() { return errors.New("invalid listen address") } p, err := pg.newProc(serviceName, listenAddr) if err != nil { return err } pg.Services[serviceName] = p // Append both the command-specific env and the base environment. env = append(env, spec.Env...) cwd := filepath.Join(pg.Run.App.Root(), pg.workingDir) binary, err := lookpath.InDir(cwd, env, spec.Command[0]) if err != nil { return err } // This is safe since the command comes from our build. // nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...) cmd.Env = env cmd.Dir = cwd // Proxy stdout and stderr to the given app logger, if any. if l := pg.logger; l != nil { cmd.Stdout = newLogWriter(pg.Run, l.RunStdout) cmd.Stderr = newLogWriter(pg.Run, l.RunStderr) } p.cmd = cmd return nil } func (pg *ProcGroup) NewProcForGateway(gatewayName string, listenAddr netip.AddrPort, spec builder.Cmd, env []string) error { if !listenAddr.IsValid() { return errors.New("invalid listen address") } p, err := pg.newProc("gateway-"+gatewayName, listenAddr) if err != nil { return err } pg.Gateways[gatewayName] = p // Append both the command-specific env and the base environment. env = append(env, spec.Env...) cwd := filepath.Join(pg.Run.App.Root(), pg.workingDir) binary, err := lookpath.InDir(cwd, env, spec.Command[0]) if err != nil { return err } // This is safe since the command comes from our build. // nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...) cmd.Env = env cmd.Dir = cwd // Bound the wait time to esure prompt live reload if something goes wrong // with IO copying. cmd.WaitDelay = 500 * time.Millisecond // Proxy stdout and stderr to the given app logger, if any. if l := pg.logger; l != nil { cmd.Stdout = newLogWriter(pg.Run, l.RunStdout) cmd.Stderr = newLogWriter(pg.Run, l.RunStderr) } p.cmd = cmd return nil } type warning struct { Title string Help string } func (pg *ProcGroup) Warnings() (rtn []warning) { if missing := pg.ConfigGen.MissingSecrets(); len(missing) > 0 { rtn = append(rtn, warning{ Title: "secrets not defined: " + strings.Join(missing, ", "), Help: "undefined secrets are left empty for local development only.\nsee https://encore.dev/docs/primitives/secrets for more information", }) } return rtn } // Proc represents a single Encore process running within a [ProcGroup]. type Proc struct { group *ProcGroup // The group this process belongs to log zerolog.Logger // The logger for this process exit chan struct{} // closed when the process has exited cmd *exec.Cmd // The command for this specific process listenAddr netip.AddrPort // The port the HTTP server of the process should listen on httpProxy *httputil.ReverseProxy // The reverse proxy for the HTTP server of the process // The following fields are only valid after Start() has been called. Started atomic.Bool // whether the process has started StartedAt time.Time // when the process started Pid int // the OS process id } // Start starts the process and returns immediately. // // If the process has already been started, this is a no-op. func (p *Proc) Start() error { p.group.procMu.Lock() defer p.group.procMu.Unlock() return p.start() } // start starts the process and returns immediately // // It must be called while locked under the p.group.procMu lock. func (p *Proc) start() error { if !p.Started.CompareAndSwap(false, true) { return nil } if err := p.cmd.Start(); err != nil { return errors.Wrap(err, "could not start process") } p.log.Info().Str("addr", p.listenAddr.String()).Msg("process started") p.group.runningProcs++ p.Pid = p.cmd.Process.Pid p.StartedAt = time.Now() // Start watching the process for when it quits. go func() { defer close(p.exit) // Wait for the process to exit. err := p.cmd.Wait() if err != nil && p.group.ctx.Err() == nil { p.log.Error().Err(err).Msg("process exited with error") } else { p.log.Info().Msg("process exited successfully") } // Flush the logs in case the output did not end in a newline. for _, w := range [...]io.Writer{p.cmd.Stdout, p.cmd.Stderr} { if w != nil { w.(*logWriter).Flush() } } }() // When the process exits, decrement the running count for the group // and wake up any goroutines waiting for on the running count to shrink go func() { <-p.exit p.group.procMu.Lock() defer p.group.procMu.Unlock() p.group.runningProcs-- p.group.procCond.Broadcast() }() return nil } // Close closes the process and waits for it to exit. // It is safe to call Close multiple times. func (p *Proc) Close() { if err := p.cmd.Process.Signal(os.Interrupt); err != nil { // If there's an error sending the signal, just kill the process. // This might happen because Interrupt is not supported on Windows. p.Kill() } timer := time.NewTimer(gracefulShutdownTime + (500 * time.Millisecond)) defer timer.Stop() select { case <-p.exit: // already exited case <-timer.C: p.group.log.Error().Msg("timed out waiting for process to exit; killing") p.Kill() <-p.exit } } // ProxyReq proxies the request to the Encore app. func (p *Proc) ProxyReq(w http.ResponseWriter, req *http.Request) { p.httpProxy.ServeHTTP(w, req) } // Kill causes the Process to exit immediately. Kill does not wait until // the Process has actually exited. This only kills the Process itself, // not any other processes it may have started. func (p *Proc) Kill() { if p.cmd != nil && p.cmd.Process != nil { _ = p.cmd.Process.Kill() } } // pollUntilProcessIsListening polls the listen address until // the process is actively listening, five seconds have passed, // or the context is canceled, whichever happens first. // // It reports true if the process is listening on return, false otherwise. func (p *Proc) pollUntilProcessIsListening(ctx context.Context) (ok bool) { b := backoff.NewExponentialBackOff() b.InitialInterval = 50 * time.Millisecond b.MaxInterval = 250 * time.Millisecond b.MaxElapsedTime = 5 * time.Second err := backoff.Retry(func() error { if err := ctx.Err(); err != nil { return backoff.Permanent(err) } conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", p.listenAddr.String()) if err == nil { _ = conn.Close() } return err }, b) return err == nil } func newNoopGateway(pg *ProcGroup) *noopgateway.Gateway { svcDiscovery := make(map[noopgateway.ServiceName]string) for _, svc := range pg.Meta.Svcs { if proc, ok := pg.Services[svc.Name]; ok { svcDiscovery[noopgateway.ServiceName(svc.Name)] = proc.listenAddr.String() } } desc := noopgwdesc.Describe(pg.Meta, svcDiscovery) gw := noopgateway.New(desc) gw.Rewrite = func(rp *httputil.ProxyRequest) { // Copy the host head over. rp.Out.Host = rp.In.Host // Add the auth key unless the test header is set. if rp.Out.Header.Get(TestHeaderDisablePlatformAuth) == "" { addAuthKeyToRequest(rp.Out, pg.authKey) } } return gw } ================================================ FILE: cli/daemon/run/run.go ================================================ // Package run starts and tracks running Encore applications. package run import ( "bytes" "context" "crypto/rand" "encoding/base64" "encoding/binary" "fmt" "net" "net/http" "net/netip" "os" "path/filepath" "runtime" "slices" "sort" "strings" "sync/atomic" "time" "github.com/cockroachdb/errors" "github.com/logrusorgru/aurora/v3" "github.com/rs/xid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "golang.org/x/sync/errgroup" "encore.dev/appruntime/exported/config" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run/infra" "encr.dev/cli/daemon/secret" "encr.dev/internal/optracker" "encr.dev/internal/userconfig" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/option" "encr.dev/pkg/promise" "encr.dev/pkg/svcproxy" "encr.dev/pkg/vcs" daemonpb "encr.dev/proto/encore/daemon" meta "encr.dev/proto/encore/parser/meta/v1" ) // Run represents a running Encore application. type Run struct { ID string // unique ID for this instance of the running app App *apps.Instance ListenAddr string // the address the app is listening on SvcProxy *svcproxy.SvcProxy ResourceManager *infra.ResourceManager NS *namespace.Namespace TempDir string Builder builder.Impl log zerolog.Logger Mgr *Manager Params *StartParams secrets *secret.LoadResult ctx context.Context // ctx is closed when the run is to exit proc atomic.Value // current process exited chan struct{} // exit is closed when the run has fully exited started chan struct{} // started is closed once the run has fully started } // StartParams groups the parameters for the Run method. type StartParams struct { // App is the app to start. App *apps.Instance // NS is the namespace to use. NS *namespace.Namespace // WorkingDir is the working dir, for formatting // error messages with relative paths. WorkingDir string // Watch enables watching for code changes for live reloading. Watch bool Listener net.Listener // listener to use ListenAddr string // address we're listening on // Environ are the environment variables to set for the running app, // in the same format as os.Environ(). Environ []string // The Ops tracker being used for this run OpsTracker *optracker.OpTracker // Browser specifies the browser mode to use. Browser BrowserMode // Debug specifies to compile the application for debugging. Debug builder.DebugMode // LogLevel overrides the default log level for the run. LogLevel option.Option[string] // ScrubSensitiveData enables scrubbing of sensitive data in local traces. ScrubSensitiveData bool } // BrowserMode specifies how to open the browser when starting 'encore run'. type BrowserMode int const ( BrowserModeAuto BrowserMode = iota // open if not already open BrowserModeNever // never open BrowserModeAlways // always open ) func BrowserModeFromConfig(cfg *userconfig.Config) BrowserMode { switch cfg.RunBrowser { case "never": return BrowserModeNever case "always": return BrowserModeAlways default: return BrowserModeAuto } } func BrowserModeFromProto(b daemonpb.RunRequest_BrowserMode) BrowserMode { switch b { case daemonpb.RunRequest_BROWSER_AUTO: return BrowserModeAuto case daemonpb.RunRequest_BROWSER_NEVER: return BrowserModeNever case daemonpb.RunRequest_BROWSER_ALWAYS: return BrowserModeAlways default: return BrowserModeAuto } } func DebugModeFromProto(d daemonpb.RunRequest_DebugMode) builder.DebugMode { switch d { case daemonpb.RunRequest_DEBUG_DISABLED: return builder.DebugModeDisabled case daemonpb.RunRequest_DEBUG_ENABLED: return builder.DebugModeEnabled case daemonpb.RunRequest_DEBUG_BREAK: return builder.DebugModeBreak default: return builder.DebugModeDisabled } } // Start starts the application. // Its lifetime is bounded by ctx. func (mgr *Manager) Start(ctx context.Context, params StartParams) (run *Run, err error) { logger := log.With().Str("app_id", params.App.PlatformOrLocalID()).Logger() svcProxy, err := svcproxy.New(ctx, logger) if err != nil { return nil, errors.Wrap(err, "failed to create service proxy") } tempDir, err := os.MkdirTemp("", "encore-run") if err != nil { return nil, errors.Wrap(err, "couldn't create temp dir") } run = &Run{ ID: GenID(), App: params.App, NS: params.NS, ResourceManager: infra.NewResourceManager(params.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, params.NS, params.Environ, mgr.DBProxyPort, false), ListenAddr: params.ListenAddr, SvcProxy: svcProxy, log: logger, Mgr: mgr, Params: ¶ms, TempDir: tempDir, secrets: mgr.Secret.Load(params.App), ctx: ctx, exited: make(chan struct{}), started: make(chan struct{}), } defer func(r *Run) { // Stop all the resource servers if we exit due to an error if err != nil { r.Close() } }(run) // Add the run to our map before starting to avoid // racing with initialization (though it's unlikely to ever matter). mgr.mu.Lock() if mgr.runs == nil { mgr.runs = make(map[string]*Run) } mgr.runs[run.ID] = run mgr.mu.Unlock() if err := run.start(params.Listener, params.OpsTracker); err != nil { if errList := AsErrorList(err); errList != nil { return nil, errList } return nil, err } if params.Watch { if err := mgr.watch(run); err != nil { return nil, err } } return run, nil } func (r *Run) Close() { if r.Builder != nil { _ = r.Builder.Close() } if r.TempDir != "" { _ = os.RemoveAll(r.TempDir) } r.SvcProxy.Close() r.ResourceManager.StopAll() } // RunLogger is the interface for listening to run logs. // The log methods are called for each logline on stdout and stderr respectively. type RunLogger interface { RunStdout(r *Run, line []byte) RunStderr(r *Run, line []byte) } // ProcGroup returns the current running process. // It may have already exited. // If the proc has not yet started it may return nil. // // If run is nil then nil will be returned func (r *Run) ProcGroup() *ProcGroup { if r == nil { return nil } p, _ := r.proc.Load().(*ProcGroup) return p } func (r *Run) StoreProc(p *ProcGroup) { r.proc.Store(p) } // Done returns a channel that is closed when the run is closed. func (r *Run) Done() <-chan struct{} { return r.exited } // Reload rebuilds the app and, if successful, // starts a new proc and switches over. func (r *Run) Reload() error { err := r.buildAndStart(r.ctx, nil, true) if err != nil { return err } for _, ln := range r.Mgr.listeners { ln.OnReload(r) } return nil } // start starts the application and serves requests over HTTP using ln. func (r *Run) start(ln net.Listener, tracker *optracker.OpTracker) (err error) { defer func() { if err != nil { // This is closed below when err == nil, // so handle the other cases. close(r.started) close(r.exited) } }() err = r.buildAndStart(r.ctx, tracker, false) if err != nil { return err } // Below this line the function must never return an error // in order to only ensure we Close r.exited exactly once. go func() { for _, ln := range r.Mgr.listeners { ln.OnStart(r) } close(r.started) }() // Wrap the handler with h2c support to enable HTTP/2 in cleartext // (the std http library only accepts HTTP/2 over TLS). // We need this to be able to forward e.g. gRPC requests to the app. handler := h2c.NewHandler(r, &http2.Server{}) // Run the http server until the app exits. srv := &http.Server{Addr: ln.Addr().String(), Handler: handler} go func() { if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) { r.log.Error().Err(err).Msg("could not serve") } }() go func() { <-r.ctx.Done() _ = srv.Close() }() // Monitor the running proc and Close the app when it exits. go func() { for { p := r.proc.Load().(*ProcGroup) <-p.Done() // p exited, but it could have been a reload. // Check to make sure p is still the active proc. p2 := r.proc.Load().(*ProcGroup) if p2 == p { // We're done. for _, ln := range r.Mgr.listeners { ln.OnStop(r) } close(r.exited) return } } }() return nil } // buildAndStart builds the app, starts the proc, and cleans up // the build dir when it exits. // The proc exits when ctx is canceled. func (r *Run) buildAndStart(ctx context.Context, tracker *optracker.OpTracker, isReload bool) error { // Return early if the ctx is already canceled. if err := ctx.Err(); err != nil { return err } for _, ln := range r.Mgr.listeners { ln.OnCompileStart(r) } jobs := optracker.NewAsyncBuildJobs(ctx, r.App.PlatformOrLocalID(), tracker) // Parse the app source code // Parse the app to figure out what infrastructure is needed. start := time.Now() parseOp := tracker.Add("Building Encore application graph", start) topoOp := tracker.Add("Analyzing service topology", start) expSet, err := r.App.Experiments(r.Params.Environ) if err != nil { return err } if r.Builder == nil { r.Builder = builderimpl.Resolve(r.App.Lang(), expSet) } vcsRevision := vcs.GetRevision(r.App.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: r.Params.Debug, Environ: r.Params.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, DisableSensitiveScrubbing: !r.Params.ScrubSensitiveData, } // A context that is canceled when the proc exits. procCtx, cancelProcCtx := context.WithCancel(ctx) // Cancel the proc context if we exit with a non-nil error. defer func() { if err != nil { cancelProcCtx() } }() prepareResult, err := r.Builder.Prepare(procCtx, builder.PrepareParams{ Build: buildInfo, App: r.App, WorkingDir: r.Params.WorkingDir, }) if err != nil { return err } parse, err := r.Builder.Parse(procCtx, builder.ParseParams{ Build: buildInfo, App: r.App, Experiments: expSet, WorkingDir: r.Params.WorkingDir, ParseTests: false, Prepare: prepareResult, }) if err != nil { // Don't use the error itself in tracker.Fail, as it will lead to duplicate error output. tracker.Fail(parseOp, errors.New("parse error")) return err } if err := r.App.CacheMetadata(parse.Meta); err != nil { return errors.Wrap(err, "cache metadata") } tracker.Done(parseOp, 500*time.Millisecond) tracker.Done(topoOp, 300*time.Millisecond) r.ResourceManager.StartRequiredServices(jobs, parse.Meta) configProm := promise.New(func() (*builder.ServiceConfigsResult, error) { return r.Builder.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: fmt.Sprintf("http://%s", r.ListenAddr), EnvName: "local", EnvType: cueutil.EnvType_Development, CloudType: cueutil.CloudType_Local, }, }) }) var build *builder.CompileResult jobs.Go("Compiling application source code", false, 0, func(ctx context.Context) (err error) { build, err = r.Builder.Compile(ctx, builder.CompileParams{ Build: buildInfo, App: r.App, Parse: parse, OpTracker: tracker, Experiments: expSet, WorkingDir: r.Params.WorkingDir, Environ: r.Params.Environ, }) if err != nil { return errors.Wrap(err, "compile error") } return nil }) var secrets map[string]string jobs.Go("Fetching application secrets", true, 150*time.Millisecond, func(ctx context.Context) error { data, err := r.secrets.Get(ctx, expSet) if err != nil { return err } secrets = data.Values return nil }) if err := jobs.Wait(); err != nil { return err } svcCfg, err := configProm.Get(ctx) if err != nil { return err } startOp := tracker.Add("Starting Encore application", start) newProcess, err := r.StartProcGroup(&StartProcGroupParams{ Ctx: ctx, Outputs: build.Outputs, Meta: parse.Meta, Logger: r.Mgr, Secrets: secrets, ServiceConfigs: svcCfg.Configs, Environ: r.Params.Environ, WorkingDir: r.Params.WorkingDir, IsReload: isReload, Experiments: expSet, }) if err != nil { tracker.Fail(startOp, err) return err } // Close the proc context when the proc exits. go func() { select { case <-procCtx.Done(): // Already done case <-newProcess.Done(): cancelProcCtx() } }() previousProcess := r.proc.Swap(newProcess) if previousProcess != nil { previousProcess.(*ProcGroup).Close() } tracker.Done(startOp, 50*time.Millisecond) go func() { // Wait one second before logging all the missing secrets. time.Sleep(1 * time.Second) // Log any warnings. for _, warning := range newProcess.Warnings() { line := "\n" + aurora.Red(fmt.Sprintf("warning: %s", warning.Title)).String() + "\n" + aurora.Gray(16, fmt.Sprintf("note: %s", warning.Help)).String() + "\n\n" r.Mgr.RunStderr(r, []byte(line)) } }() return nil } type StartProcGroupParams struct { Ctx context.Context Outputs []builder.BuildOutput Meta *meta.Data Secrets map[string]string ServiceConfigs map[string]string Logger RunLogger Environ []string WorkingDir string IsReload bool Experiments *experiments.Set } const gracefulShutdownTime = 10 * time.Second // StartProcGroup starts a single actual OS process for app. func (r *Run) StartProcGroup(params *StartProcGroupParams) (p *ProcGroup, err error) { pid := GenID() userEnv := append([]string{ "ENCORE_RUNTIME_LOG=error", // Always include internal messages when developing locally. "ENCORE_API_INCLUDE_INTERNAL_MESSAGE=1", }, params.Environ...) daemonProxyAddr, err := netip.ParseAddrPort(strings.ReplaceAll(r.ListenAddr, "localhost", "127.0.0.1")) if err != nil { return nil, errors.Wrapf(err, "failed to parse listen address: %s", r.ListenAddr) } gatewayBaseURL := fmt.Sprintf("http://%s", daemonProxyAddr) gateways := make(map[string]GatewayConfig) for _, gw := range params.Meta.Gateways { gateways[gw.EncoreName] = GatewayConfig{ BaseURL: gatewayBaseURL, Hostnames: []string{"localhost"}, } } var runtimeConfigPath option.Option[string] var metaPath option.Option[string] if r.TempDir != "" { if r.Builder.UseNewRuntimeConfig() { runtimeConfigPath = option.Some(filepath.Join(r.TempDir, "runtime_config.pb")) } else { runtimeConfigPath = option.Some(filepath.Join(r.TempDir, "runtime_config.json")) } if r.Builder.NeedsMeta() { metaPath = option.Some(filepath.Join(r.TempDir, "meta.pb")) } } authKey := genAuthKey() p = newProcGroup(procGroupOptions{ ProcID: pid, Run: r, AuthKey: authKey, ConfigGen: &RuntimeConfigGenerator{ app: r.App, infraManager: r.ResourceManager, md: params.Meta, AppID: option.Some(r.ID), EnvID: option.Some(pid), TraceEndpoint: option.Some(fmt.Sprintf("http://localhost:%d/trace", r.Mgr.RuntimePort)), AuthKey: authKey, Gateways: gateways, DefinedSecrets: params.Secrets, SvcConfigs: params.ServiceConfigs, DeployID: option.Some(fmt.Sprintf("run_%s", xid.New().String())), IncludeMeta: r.Builder.NeedsMeta(), MetaPath: metaPath, RuntimeConfigPath: runtimeConfigPath, LogLevel: r.Params.LogLevel, }, Experiments: params.Experiments, Meta: params.Meta, Ctx: params.Ctx, WorkingDir: params.WorkingDir, Logger: params.Logger, }) if isSingleProc(params.Outputs) { entrypoint := params.Outputs[0].GetEntrypoints()[0] conf, err := p.ConfigGen.AllInOneProc(entrypoint.UseRuntimeConfigV2) if err != nil { return nil, err } // Generate the environmental variables for the process procEnv, err := p.ConfigGen.ProcEnvs(conf, entrypoint.UseRuntimeConfigV2) if err != nil { return nil, errors.Wrap(err, "failed to generate environment variables") } env := slices.Clone(userEnv) env = append(env, procEnv...) // Otherwise we're running everything inside a single process cmd := entrypoint.Cmd.Expand(params.Outputs[0].GetArtifactDir()) if err := p.NewAllInOneProc(cmd, conf.ListenAddr, env); err != nil { return nil, err } } else { var ( svcConfs map[string]*ProcConfig gwConfs map[string]*ProcConfig ) if r.Builder.UseNewRuntimeConfig() { _, svcConfs, gwConfs, err = p.ConfigGen.ProcPerServiceWithNewRuntimeConfig(r.SvcProxy) if err != nil { return nil, err } } else { svcConfs, gwConfs, err = p.ConfigGen.ProcPerService(r.SvcProxy) if err != nil { return nil, err } } for _, o := range params.Outputs { for _, ep := range o.GetEntrypoints() { cmd := ep.Cmd.Expand(o.GetArtifactDir()) // create a process for each service for _, svcName := range ep.Services { // Generate the environmental variables for the process procConf, ok := svcConfs[svcName] if !ok { return nil, errors.Newf("unknown service %q", svcName) } procEnv, err := p.ConfigGen.ProcEnvs(procConf, ep.UseRuntimeConfigV2) if err != nil { return nil, errors.Wrap(err, "failed to generate environment variables") } env := slices.Clone(userEnv) env = append(env, procEnv...) if err := p.NewProcForService(svcName, procConf.ListenAddr, cmd, env); err != nil { return nil, err } } for _, gwName := range ep.Gateways { procConf, ok := gwConfs[gwName] if !ok { return nil, errors.Newf("unknown gateway %q", gwName) } procEnv, err := p.ConfigGen.ProcEnvs(procConf, ep.UseRuntimeConfigV2) if err != nil { return nil, errors.Wrap(err, "failed to generate environment variables") } env := slices.Clone(userEnv) env = append(env, procEnv...) if err := p.NewProcForGateway(gwName, procConf.ListenAddr, cmd, env); err != nil { return nil, err } } } } } // Start the processes of the application if err := p.Start(); err != nil { return nil, err } defer func() { if err != nil { p.Kill() } }() // Monitor the context and Close the process when it is done. go func() { select { case <-params.Ctx.Done(): p.Close() case <-p.Done(): } }() // If this is a live reload, wait for the process to be ready. // This way we ensure requests are always hitting a running server, // in case a batch job or something is running. if params.IsReload { g, ctx := errgroup.WithContext(params.Ctx) for _, gw := range p.Gateways { gw := gw g.Go(func() error { gw.pollUntilProcessIsListening(ctx) return nil }) } _ = g.Wait() } return p, nil } // logWriter is an io.Writer that buffers incoming logs // and forwards whole log lines to fn. type logWriter struct { run *Run fn func(r *Run, line []byte) // matches AppLogger.Log* signature maxLine int // max line length, including '\n' buf *bytes.Buffer } func newLogWriter(run *Run, fn func(*Run, []byte)) *logWriter { const maxLine = 100 * 1024 return &logWriter{ run: run, fn: fn, maxLine: maxLine, buf: bytes.NewBuffer(make([]byte, 0, maxLine)), } } func (w *logWriter) Write(b []byte) (int, error) { n := len(b) for { idx := bytes.IndexByte(b, '\n') if idx < 0 { break } // We have a line break; write the data to w.fn if it's not too long if (w.buf.Len() + idx + 1) <= w.maxLine { w.buf.Write(b[:idx+1]) w.fn(w.run, w.buf.Bytes()) w.buf.Reset() } b = b[idx+1:] } // Postcondition: we have some data remaining that doesn't contain a newline. // Write it to buf if it's not too long. if w.buf.Len()+len(b) <= w.maxLine { w.buf.Write(b) } return n, nil } // Flush flushes remaining data to w.fn along with a trailing newline. // It must not be called concurrently with any writes to w. func (w *logWriter) Flush() { if w.buf.Len() > 0 { w.buf.WriteByte('\n') w.fn(w.run, w.buf.Bytes()) w.buf.Reset() } } // GenID generates a random run/process id. // It panics if it cannot get random bytes. func GenID() string { var b [8]byte if _, err := rand.Read(b[:]); err != nil { panic("cannot generate random data: " + err.Error()) } return base64.RawURLEncoding.EncodeToString(b[:]) } // encodeSecretsEnv encodes secrets to a value that can be passed in an env variable. func encodeSecretsEnv(secrets map[string]string) string { if len(secrets) == 0 { return "" } // Sort the keys keys := make([]string, 0, len(secrets)) for k := range secrets { keys = append(keys, k) } sort.Strings(keys) var buf bytes.Buffer first := true for _, k := range keys { if !first { buf.WriteByte(',') } first = false buf.WriteString(k) buf.WriteByte('=') buf.WriteString(base64.RawURLEncoding.EncodeToString([]byte(secrets[k]))) } gzipped := gzipBytes(buf.Bytes()) str := "gzip:" + base64.StdEncoding.EncodeToString(gzipped) return str } func usesSecrets(md *meta.Data) bool { for _, pkg := range md.Pkgs { if len(pkg.Secrets) > 0 { return true } } return false } func genAuthKey() config.EncoreAuthKey { // read a uint32 from crypto/rand to use as the key ID var kidBytes [4]byte if _, err := rand.Read(kidBytes[:]); err != nil { panic("cannot generate random data: " + err.Error()) } kid := binary.BigEndian.Uint32(kidBytes[:]) // kid := mathrand.Uint32() var b [16]byte if _, err := rand.Read(b[:]); err != nil { panic("cannot generate random data: " + err.Error()) } return config.EncoreAuthKey{KeyID: kid, Data: b[:]} } // CanDeleteNamespace implements namespace.DeletionHandler. func (m *Manager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { // Check if any of the active runs are using this namespace. m.mu.Lock() defer m.mu.Unlock() for _, r := range m.runs { if r.NS.ID == ns.ID && r.ctx.Err() == nil { return errors.New("namespace is in use by 'encore run'") } } return nil } // DeleteNamespace implements namespace.DeletionHandler. func (m *Manager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { // We don't need to do anything here; we only implement DeletionHandler for // the CanDeleteNamespace check. return nil } func isSingleProc(outputs []builder.BuildOutput) bool { if len(outputs) != 1 { return false } return len(outputs[0].GetEntrypoints()) == 1 } ================================================ FILE: cli/daemon/run/runtime_config2.go ================================================ package run import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "net" "net/netip" "os" "slices" "sort" "strconv" "strings" "time" "github.com/cockroachdb/errors" "github.com/jackc/pgx/v5" "github.com/rs/xid" "go4.org/syncutil" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "encore.dev/appruntime/exported/config" encoreEnv "encr.dev/internal/env" "encr.dev/pkg/appfile" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/rtconfgen" "encr.dev/pkg/svcproxy" meta "encr.dev/proto/encore/parser/meta/v1" runtimev1 "encr.dev/proto/encore/runtime/v1" ) const ( runtimeCfgEnvVar = "ENCORE_RUNTIME_CONFIG" runtimeCfgPathEnvVar = "ENCORE_RUNTIME_CONFIG_PATH" appSecretsEnvVar = "ENCORE_APP_SECRETS" serviceCfgEnvPrefix = "ENCORE_CFG_" listenEnvVar = "ENCORE_LISTEN_ADDR" metaEnvVar = "ENCORE_APP_META" metaPathEnvVar = "ENCORE_APP_META_PATH" ) type RuntimeConfigGenerator struct { initOnce syncutil.Once md *meta.Data // The application to generate the config for app interface { PlatformID() string PlatformOrLocalID() string GlobalCORS() (appfile.CORS, error) AppFile() (*appfile.File, error) BuildSettings() (appfile.Build, error) } // The infra manager to use infraManager interface { SQLServerConfig() (config.SQLServer, error) PubSubProviderConfig() (config.PubsubProvider, error) SQLDatabaseConfig(db *meta.SQLDatabase) (config.SQLDatabase, error) PubSubTopicConfig(topic *meta.PubSubTopic) (config.PubsubProvider, config.PubsubTopic, error) PubSubSubscriptionConfig(topic *meta.PubSubTopic, sub *meta.PubSubTopic_Subscription) (config.PubsubSubscription, error) RedisConfig(redis *meta.CacheCluster) (config.RedisServer, config.RedisDatabase, error) BucketProviderConfig() (config.BucketProvider, string, error) } AppID option.Option[string] EnvID option.Option[string] EnvName option.Option[string] EnvType option.Option[runtimev1.Environment_Type] EnvCloud option.Option[runtimev1.Environment_Cloud] TraceEndpoint option.Option[string] DeployID option.Option[string] Gateways map[string]GatewayConfig AuthKey config.EncoreAuthKey // Whether to include the metadata. IncludeMeta bool // If set, write the metadata to the given path // instead of including it as an environment variable. MetaPath option.Option[string] // If set, write the runtime config to the given path // instead of including it as an environment variable. RuntimeConfigPath option.Option[string] // Minimum log level, if any. LogLevel option.Option[string] // The values of defined secrets. DefinedSecrets map[string]string // The configs, per service. SvcConfigs map[string]string conf *rtconfgen.Builder authKeys []*runtimev1.EncoreAuthKey } type GatewayConfig struct { BaseURL string Hostnames []string } func (g *RuntimeConfigGenerator) initialize() error { return g.initOnce.Do(func() error { g.conf = rtconfgen.NewBuilder() newRid := func() string { return "res_" + xid.New().String() } if deployID, ok := g.DeployID.Get(); ok { g.conf.DeployID(deployID) } g.conf.DeployedAt(time.Now()) g.conf.Env(&runtimev1.Environment{ AppId: g.AppID.GetOrElseF(g.app.PlatformOrLocalID), AppSlug: g.app.PlatformID(), EnvId: g.EnvID.GetOrElse("local"), EnvName: g.EnvName.GetOrElse("local"), EnvType: g.EnvType.GetOrElse(runtimev1.Environment_TYPE_DEVELOPMENT), Cloud: g.EnvCloud.GetOrElse(runtimev1.Environment_CLOUD_LOCAL), }) toSecret := func(b []byte) *runtimev1.SecretData { return &runtimev1.SecretData{ Source: &runtimev1.SecretData_Embedded{Embedded: b}, } } ak := g.AuthKey g.authKeys = []*runtimev1.EncoreAuthKey{{Id: ak.KeyID, Data: toSecret(ak.Data)}} g.conf.EncorePlatform(&runtimev1.EncorePlatform{ PlatformSigningKeys: g.authKeys, EncoreCloud: nil, }) if traceEndpoint, ok := g.TraceEndpoint.Get(); ok { sampleRate := 1.0 if val, err := strconv.ParseFloat(os.Getenv("ENCORE_TRACE_SAMPLING_RATE"), 64); err == nil { sampleRate = min(max(val, 0), 1) } g.conf.TracingProvider(&runtimev1.TracingProvider{ Rid: newRid(), Provider: &runtimev1.TracingProvider_Encore{ Encore: &runtimev1.TracingProvider_EncoreTracingProvider{ TraceEndpoint: traceEndpoint, SamplingConfig: []*runtimev1.TracingProvider_SamplingConfig{ { Rate: sampleRate, Scope: &runtimev1.TracingProvider_SamplingConfig_Default{Default: &emptypb.Empty{}}, }, }, }, }, }) } appFile, err := g.app.AppFile() if err != nil { return errors.Wrap(err, "failed to get app's build settings") } logLevel := appFile.LogLevel if level, ok := g.LogLevel.Get(); ok { logLevel = level } for _, svc := range g.md.Svcs { cfg := &runtimev1.HostedService{ Name: svc.Name, LogConfig: ptrOrNil(logLevel), } if appFile.Build.WorkerPooling { n := int32(0) cfg.WorkerThreads = &n } g.conf.ServiceConfig(cfg) } g.conf.AuthMethods([]*runtimev1.ServiceAuth{ { AuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{ EncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{ AuthKeys: g.authKeys, }, }, }, }) g.conf.DefaultGracefulShutdown(&runtimev1.GracefulShutdown{ Total: durationpb.New(10 * time.Second), ShutdownHooks: durationpb.New(4 * time.Second), Handlers: durationpb.New(2 * time.Second), }) for _, gw := range g.md.Gateways { cors, err := g.app.GlobalCORS() if err != nil { return errors.Wrap(err, "failed to generate global CORS config") } g.conf.Infra.Gateway(&runtimev1.Gateway{ Rid: newRid(), EncoreName: gw.EncoreName, BaseUrl: g.Gateways[gw.EncoreName].BaseURL, Hostnames: g.Gateways[gw.EncoreName].Hostnames, Cors: &runtimev1.Gateway_CORS{ Debug: cors.Debug, DisableCredentials: false, ExtraAllowedHeaders: cors.AllowHeaders, ExtraExposedHeaders: cors.ExposeHeaders, AllowedOriginsWithCredentials: &runtimev1.Gateway_CORS_UnsafeAllowAllOriginsWithCredentials{ UnsafeAllowAllOriginsWithCredentials: true, }, AllowedOriginsWithoutCredentials: &runtimev1.Gateway_CORSAllowedOrigins{ AllowedOrigins: []string{"*"}, }, AllowPrivateNetworkAccess: true, }, }) } if len(g.md.PubsubTopics) > 0 { pubsubConfig, err := g.infraManager.PubSubProviderConfig() if err != nil { return errors.Wrap(err, "failed to generate pubsub provider config") } cluster := g.conf.Infra.PubSubCluster(&runtimev1.PubSubCluster{ Rid: newRid(), Provider: &runtimev1.PubSubCluster_Nsq{ Nsq: &runtimev1.PubSubCluster_NSQ{Hosts: []string{pubsubConfig.NSQ.Host}}, }, }) for _, topic := range g.md.PubsubTopics { topicRid := newRid() var deliveryGuarantee runtimev1.PubSubTopic_DeliveryGuarantee switch topic.DeliveryGuarantee { case meta.PubSubTopic_AT_LEAST_ONCE: deliveryGuarantee = runtimev1.PubSubTopic_DELIVERY_GUARANTEE_AT_LEAST_ONCE case meta.PubSubTopic_EXACTLY_ONCE: deliveryGuarantee = runtimev1.PubSubTopic_DELIVERY_GUARANTEE_EXACTLY_ONCE default: return errors.Newf("unknown delivery guarantee %q", topic.DeliveryGuarantee) } // Ensure topic name is valid for NSQ topicCloudName := ensureValidNSQName(topic.Name) cluster.PubSubTopic(&runtimev1.PubSubTopic{ Rid: topicRid, EncoreName: topic.Name, CloudName: topicCloudName, DeliveryGuarantee: deliveryGuarantee, OrderingAttr: ptrOrNil(topic.OrderingKey), ProviderConfig: nil, }) for _, sub := range topic.Subscriptions { // Ensure subscription name is valid for NSQ subCloudName := ensureValidNSQName(sub.Name) cluster.PubSubSubscription(&runtimev1.PubSubSubscription{ Rid: newRid(), TopicEncoreName: topic.Name, SubscriptionEncoreName: sub.Name, TopicCloudName: topicCloudName, SubscriptionCloudName: subCloudName, PushOnly: false, ProviderConfig: nil, }) } } } if len(g.md.SqlDatabases) > 0 { srvConfig, err := g.infraManager.SQLServerConfig() if err != nil { return errors.Wrap(err, "failed to generate SQL server config") } cluster := g.conf.Infra.SQLCluster(&runtimev1.SQLCluster{ Rid: newRid(), }) var tlsConfig *runtimev1.TLSConfig if srvConfig.ServerCACert != "" { tlsConfig = &runtimev1.TLSConfig{ ServerCaCert: &srvConfig.ServerCACert, } } cluster.SQLServer(&runtimev1.SQLServer{ Rid: newRid(), Kind: runtimev1.ServerKind_SERVER_KIND_PRIMARY, Host: srvConfig.Host, TlsConfig: tlsConfig, }) for _, db := range g.md.SqlDatabases { if externalDB, ok := g.DefinedSecrets["sqldb::"+db.Name]; ok { var extCfg struct { ConnectionString string `json:"connection_string"` } if err := json.Unmarshal([]byte(externalDB), &extCfg); err != nil { return errors.Wrapf(err, "failed to unmarshal external DB config for %q", db.Name) } pCfg, err := pgx.ParseConfig(extCfg.ConnectionString) if err != nil { return errors.Wrapf(err, "failed to parse external DB connection string for %q", db.Name) } cluster := g.conf.Infra.SQLCluster(&runtimev1.SQLCluster{ Rid: newRid(), }) cluster.SQLServer(&runtimev1.SQLServer{ Rid: newRid(), Kind: runtimev1.ServerKind_SERVER_KIND_PRIMARY, Host: net.JoinHostPort(pCfg.Host, strconv.Itoa(int(pCfg.Port))), TlsConfig: &runtimev1.TLSConfig{ DisableCaValidation: true, }, }) // Generate a role rid based on the cluster+username combination. roleRid := fmt.Sprintf("role:%s:%s", cluster.Val.Rid, pCfg.User) g.conf.Infra.SQLRole(&runtimev1.SQLRole{ Rid: roleRid, Username: pCfg.User, Password: toSecret([]byte(pCfg.Password)), ClientCertRid: nil, }) cluster.SQLDatabase(&runtimev1.SQLDatabase{ Rid: newRid(), EncoreName: db.Name, CloudName: pCfg.Database, ConnPools: nil, }).AddConnectionPool(&runtimev1.SQLConnectionPool{ IsReadonly: false, RoleRid: roleRid, MinConnections: int32(0), MaxConnections: int32(0), }) } else { dbConfig, err := g.infraManager.SQLDatabaseConfig(db) if err != nil { return errors.Wrap(err, "failed to generate SQL database config") } // Generate a role rid based on the cluster+username combination. roleRid := fmt.Sprintf("role:%s:%s", cluster.Val.Rid, dbConfig.User) g.conf.Infra.SQLRole(&runtimev1.SQLRole{ Rid: roleRid, Username: dbConfig.User, Password: toSecret([]byte(dbConfig.Password)), ClientCertRid: nil, }) cluster.SQLDatabase(&runtimev1.SQLDatabase{ Rid: newRid(), EncoreName: dbConfig.EncoreName, CloudName: dbConfig.DatabaseName, ConnPools: nil, }).AddConnectionPool(&runtimev1.SQLConnectionPool{ IsReadonly: false, RoleRid: roleRid, MinConnections: int32(dbConfig.MinConnections), MaxConnections: int32(dbConfig.MaxConnections), }) } } } if len(g.md.CacheClusters) > 0 { for _, cl := range g.md.CacheClusters { srvConfig, dbConfig, err := g.infraManager.RedisConfig(cl) if err != nil { return errors.Wrap(err, "failed to generate Redis cluster config") } cluster := g.conf.Infra.RedisCluster(&runtimev1.RedisCluster{ Rid: newRid(), Servers: nil, }) // Generate a role rid based on the cluster+username combination. roleRid := fmt.Sprintf("role:%s:%s", cluster.Val.Rid, srvConfig.User) g.conf.Infra.RedisRoleFn(roleRid, func() *runtimev1.RedisRole { r := &runtimev1.RedisRole{ Rid: roleRid, ClientCertRid: nil, } switch { case srvConfig.User != "" && srvConfig.Password != "": r.Auth = &runtimev1.RedisRole_Acl{Acl: &runtimev1.RedisRole_AuthACL{ Username: srvConfig.User, Password: toSecret([]byte(srvConfig.Password)), }} case srvConfig.Password != "": r.Auth = &runtimev1.RedisRole_AuthString{AuthString: toSecret([]byte(srvConfig.Password))} default: r.Auth = nil } return r }) var tlsConfig *runtimev1.TLSConfig if srvConfig.EnableTLS || srvConfig.ServerCACert != "" { tlsConfig = &runtimev1.TLSConfig{ ServerCaCert: ptrOrNil(srvConfig.ServerCACert), } } cluster.RedisServer(&runtimev1.RedisServer{ Rid: newRid(), Host: srvConfig.Host, Kind: runtimev1.ServerKind_SERVER_KIND_PRIMARY, TlsConfig: tlsConfig, }) cluster.RedisDatabase(&runtimev1.RedisDatabase{ Rid: newRid(), EncoreName: dbConfig.EncoreName, DatabaseIdx: int32(dbConfig.Database), KeyPrefix: ptrOrNil(dbConfig.KeyPrefix), ConnPools: nil, }).AddConnectionPool(&runtimev1.RedisConnectionPool{ IsReadonly: false, RoleRid: roleRid, MinConnections: int32(dbConfig.MinConnections), MaxConnections: int32(dbConfig.MaxConnections), }) } } if len(g.md.Buckets) > 0 { bktProviderConfig, publicBaseURL, err := g.infraManager.BucketProviderConfig() if err != nil { return errors.Wrap(err, "failed to generate bucket provider config") } cluster := g.conf.Infra.BucketCluster(&runtimev1.BucketCluster{ Rid: newRid(), Provider: &runtimev1.BucketCluster_Gcs{ Gcs: &runtimev1.BucketCluster_GCS{ Endpoint: &bktProviderConfig.GCS.Endpoint, Anonymous: true, LocalSign: &runtimev1.BucketCluster_GCS_LocalSignOptions{ BaseUrl: publicBaseURL, AccessId: "dummy-sa@encore.local", PrivateKey: reverseString(dummyPrivateKeyReversed), }, }, }, }) for _, bkt := range g.md.Buckets { bktRid := newRid() var publicURL *string if bkt.Public { u := publicBaseURL + "/" + bkt.Name publicURL = &u } cluster.Bucket(&runtimev1.Bucket{ Rid: bktRid, EncoreName: bkt.Name, CloudName: bkt.Name, PublicBaseUrl: publicURL, }) } } for secretName, secretVal := range g.DefinedSecrets { g.conf.Infra.AppSecret(&runtimev1.AppSecret{ Rid: newRid(), EncoreName: secretName, Data: toSecret([]byte(secretVal)), }) } return nil }) } type ProcConfig struct { // The runtime config to add to the process, if any. Runtime option.Option[*runtimev1.RuntimeConfig] ListenAddr netip.AddrPort ExtraEnv []string } func (g *RuntimeConfigGenerator) ProcPerService(proxy *svcproxy.SvcProxy) (services, gateways map[string]*ProcConfig, err error) { if err := g.initialize(); err != nil { return nil, nil, err } services = make(map[string]*ProcConfig) gateways = make(map[string]*ProcConfig) newRid := func() string { return "res_" + xid.New().String() } sd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)} svcListenAddr := make(map[string]netip.AddrPort) for _, svc := range g.md.Svcs { listenAddr, err := freeLocalhostAddress() if err != nil { return nil, nil, errors.Wrap(err, "failed to find free localhost address") } svcListenAddr[svc.Name] = listenAddr sd.Services[svc.Name] = &runtimev1.ServiceDiscovery_Location{ BaseUrl: proxy.RegisterService(svc.Name, listenAddr), AuthMethods: []*runtimev1.ServiceAuth{ { AuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{ EncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{ AuthKeys: g.authKeys, }, }, }, }, } } // Set up the service processes. for _, svc := range g.md.Svcs { conf, err := g.conf.Deployment(newRid()). ServiceDiscovery(sd). HostsServices(svc.Name). ReduceWithMeta(g.md). BuildRuntimeConfig() if err != nil { return nil, nil, errors.Wrap(err, "failed to generate runtime config") } usedSecrets := secretsUsedByServices(g.md, svc.Name) listenAddr := svcListenAddr[svc.Name] configEnvs := g.encodeConfigs(svc.Name) services[svc.Name] = &ProcConfig{ Runtime: option.Some(conf), ListenAddr: listenAddr, ExtraEnv: append([]string{ fmt.Sprintf("%s=%s", appSecretsEnvVar, g.encodeSecrets(usedSecrets)), }, configEnvs...), } } // Set up the gateways. for _, gw := range g.md.Gateways { conf, err := g.conf.Deployment(newRid()).ServiceDiscovery(sd).HostsGateways(gw.EncoreName).ReduceWithMeta(g.md).BuildRuntimeConfig() if err != nil { return nil, nil, errors.Wrap(err, "failed to generate runtime config") } listenAddr, err := freeLocalhostAddress() if err != nil { return nil, nil, errors.Wrap(err, "failed to find free localhost address") } gateways[gw.EncoreName] = &ProcConfig{ Runtime: option.Some(conf), ListenAddr: listenAddr, ExtraEnv: []string{}, } } return } func (g *RuntimeConfigGenerator) AllInOneProc(useRuntimeConfigV2 bool) (*ProcConfig, error) { if err := g.initialize(); err != nil { return nil, err } newRid := func() string { return "res_" + xid.New().String() } sd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)} d := g.conf.Deployment(newRid()).ServiceDiscovery(sd) for _, gw := range g.md.Gateways { d.HostsGateways(gw.EncoreName) } for _, svc := range g.md.Svcs { d.HostsServices(svc.Name) } conf, err := d.ReduceWithMeta(g.md).BuildRuntimeConfig() if err != nil { return nil, errors.Wrap(err, "failed to generate runtime config") } listenAddr, err := freeLocalhostAddress() if err != nil { return nil, errors.Wrap(err, "failed to find free localhost address") } configEnvs := g.encodeConfigs(fns.Map(g.md.Svcs, func(svc *meta.Service) string { return svc.Name })...) extraEnv := configEnvs if !useRuntimeConfigV2 { secretsEnv := fmt.Sprintf("%s=%s", appSecretsEnvVar, encodeSecretsEnv(g.DefinedSecrets)) extraEnv = append([]string{secretsEnv}, configEnvs...) } return &ProcConfig{ Runtime: option.Some(conf), ListenAddr: listenAddr, ExtraEnv: extraEnv, }, nil } func (g *RuntimeConfigGenerator) ProcPerServiceWithNewRuntimeConfig(proxy *svcproxy.SvcProxy) (conf *runtimev1.RuntimeConfig, services, gateways map[string]*ProcConfig, err error) { if err := g.initialize(); err != nil { return nil, nil, nil, err } if len(g.SvcConfigs) > 0 { return nil, nil, nil, errors.New("service configs not yet supported") } services = make(map[string]*ProcConfig) gateways = make(map[string]*ProcConfig) newRid := func() string { return "res_" + xid.New().String() } sd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)} svcListenAddr := make(map[string]netip.AddrPort) var svcNames []string for _, svc := range g.md.Svcs { svcNames = append(svcNames, svc.Name) listenAddr, err := freeLocalhostAddress() if err != nil { return nil, nil, nil, errors.Wrap(err, "failed to find free localhost address") } svcListenAddr[svc.Name] = listenAddr sd.Services[svc.Name] = &runtimev1.ServiceDiscovery_Location{ BaseUrl: proxy.RegisterService(svc.Name, listenAddr), AuthMethods: []*runtimev1.ServiceAuth{ { AuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{ EncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{ AuthKeys: g.authKeys, }, }, }, }, } } for _, svc := range g.md.Svcs { conf, err = g.conf.Deployment(newRid()). ServiceDiscovery(sd). HostsServices(svc.Name). ReduceWithMeta(g.md). BuildRuntimeConfig() if err != nil { return nil, nil, nil, errors.Wrap(err, "failed to generate runtime config") } listenAddr := svcListenAddr[svc.Name] services[svc.Name] = &ProcConfig{ Runtime: option.Some(conf), ListenAddr: listenAddr, } } // Set up the gateways. for _, gw := range g.md.Gateways { listenAddr, err := freeLocalhostAddress() if err != nil { return nil, nil, nil, errors.Wrap(err, "failed to find free localhost address") } conf, err = g.conf.Deployment(newRid()). ServiceDiscovery(sd). HostsGateways(gw.EncoreName). //ReduceWithMeta(g.md). BuildRuntimeConfig() if err != nil { return nil, nil, nil, errors.Wrap(err, "failed to generate runtime config") } gateways[gw.EncoreName] = &ProcConfig{ Runtime: option.Some(conf), ListenAddr: listenAddr, } } return } func (g *RuntimeConfigGenerator) ForTests(newRuntimeConf bool) (envs []string, err error) { if err := g.initialize(); err != nil { return nil, err } newRid := func() string { return "res_" + xid.New().String() } sd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)} d := g.conf.Deployment(newRid()).ServiceDiscovery(sd) for _, gw := range g.md.Gateways { d.HostsGateways(gw.EncoreName) } for _, svc := range g.md.Svcs { d.HostsServices(svc.Name) } conf, err := d.ReduceWithMeta(g.md).BuildRuntimeConfig() if err != nil { return nil, errors.Wrap(err, "failed to generate runtime config") } // Write runtime config to file or env var rtEnvs, err := g.writeRuntimeConfig(conf, newRuntimeConf) if err != nil { return nil, err } envs = append(envs, rtEnvs...) // For legacy runtime, also include secrets if !newRuntimeConf { envs = append(envs, fmt.Sprintf("%s=%s", appSecretsEnvVar, encodeSecretsEnv(g.DefinedSecrets)), ) } svcNames := fns.Map(g.md.Svcs, func(svc *meta.Service) string { return svc.Name }) envs = append(envs, g.encodeConfigs(svcNames...)...) // Write metadata to file or env var if g.IncludeMeta { metaEnvs, err := g.writeMetadata() if err != nil { return nil, err } envs = append(envs, metaEnvs...) } if runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != "" { envs = append(envs, "ENCORE_RUNTIME_LIB="+runtimeLibPath) } return envs, nil } func ptrOrNil[T comparable](val T) *T { var zero T if val == zero { return nil } return &val } func (g *RuntimeConfigGenerator) ProcEnvs(proc *ProcConfig, useRuntimeConfigV2 bool) ([]string, error) { env := append([]string{ fmt.Sprintf("%s=%s", listenEnvVar, proc.ListenAddr.String()), }, proc.ExtraEnv...) if rt, ok := proc.Runtime.Get(); ok { rtEnvs, err := g.writeRuntimeConfig(rt, useRuntimeConfigV2) if err != nil { return nil, err } env = append(env, rtEnvs...) } if g.IncludeMeta { metaEnvs, err := g.writeMetadata() if err != nil { return nil, err } env = append(env, metaEnvs...) } if runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != "" { env = append(env, "ENCORE_RUNTIME_LIB="+runtimeLibPath) } return env, nil } // writeRuntimeConfig writes the runtime config to either a file (if RuntimeConfigPath is set) // or returns it as an environment variable string. func (g *RuntimeConfigGenerator) writeRuntimeConfig(rt *runtimev1.RuntimeConfig, useRuntimeConfigV2 bool) ([]string, error) { if runtimeCfgPath, ok := g.RuntimeConfigPath.Get(); ok { // Write to file: marshal the appropriate format directly var data []byte var err error if useRuntimeConfigV2 { data, err = proto.Marshal(rt) if err != nil { return nil, errors.Wrap(err, "failed to marshal runtime config") } } else { // We don't use secretEnvs because for local development we use // plaintext secrets across the board. var secretEnvs map[string][]byte = nil runtimeCfg, err := rtconfgen.ToLegacy(rt, secretEnvs) if err != nil { return nil, errors.Wrap(err, "failed to generate runtime config") } data, err = json.Marshal(runtimeCfg) if err != nil { return nil, errors.Wrap(err, "failed to marshal runtime config") } } if err := os.WriteFile(runtimeCfgPath, data, 0644); err != nil { return nil, errors.Wrap(err, "failed to write runtime config") } return []string{fmt.Sprintf("%s=%s", runtimeCfgPathEnvVar, runtimeCfgPath)}, nil } // Write to environment variable: marshal, optionally gzip, and encode var runtimeCfgStr string if useRuntimeConfigV2 { runtimeCfgBytes, err := proto.Marshal(rt) if err != nil { return nil, errors.Wrap(err, "failed to marshal runtime config") } gzipped := gzipBytes(runtimeCfgBytes) runtimeCfgStr = "gzip:" + base64.StdEncoding.EncodeToString(gzipped) } else { // We don't use secretEnvs because for local development we use // plaintext secrets across the board. var secretEnvs map[string][]byte = nil runtimeCfg, err := rtconfgen.ToLegacy(rt, secretEnvs) if err != nil { return nil, errors.Wrap(err, "failed to generate runtime config") } runtimeCfgBytes, err := json.Marshal(runtimeCfg) if err != nil { return nil, errors.Wrap(err, "failed to marshal runtime config") } runtimeCfgStr = base64.RawURLEncoding.EncodeToString(runtimeCfgBytes) } return []string{fmt.Sprintf("%s=%s", runtimeCfgEnvVar, runtimeCfgStr)}, nil } // writeMetadata writes the metadata to either a file (if MetaPath is set) // or returns it as an environment variable string. func (g *RuntimeConfigGenerator) writeMetadata() ([]string, error) { metaBytes, err := proto.Marshal(g.md) if err != nil { return nil, errors.Wrap(err, "failed to marshal metadata") } if metaPath, ok := g.MetaPath.Get(); ok { if err := os.WriteFile(metaPath, metaBytes, 0644); err != nil { return nil, errors.Wrap(err, "failed to write metadata") } return []string{fmt.Sprintf("%s=%s", metaPathEnvVar, metaPath)}, nil } gzipped := gzipBytes(metaBytes) metaEnvStr := "gzip:" + base64.StdEncoding.EncodeToString(gzipped) return []string{fmt.Sprintf("%s=%s", metaEnvVar, metaEnvStr)}, nil } func (g *RuntimeConfigGenerator) MissingSecrets() []string { var missing []string for _, pkg := range g.md.Pkgs { for _, name := range pkg.Secrets { if _, ok := g.DefinedSecrets[name]; !ok { missing = append(missing, name) } } } sort.Strings(missing) missing = slices.Compact(missing) return missing } func (g *RuntimeConfigGenerator) encodeSecrets(secretNames map[string]bool) string { vals := make(map[string]string) for name := range secretNames { vals[name] = g.DefinedSecrets[name] } return encodeSecretsEnv(vals) } func (g *RuntimeConfigGenerator) encodeConfigs(svcNames ...string) []string { envs := make([]string, 0, len(svcNames)) for _, svcName := range svcNames { cfgStr, ok := g.SvcConfigs[svcName] if !ok { continue } envs = append(envs, fmt.Sprintf( "%s%s=%s", serviceCfgEnvPrefix, strings.ToUpper(svcName), base64.RawURLEncoding.EncodeToString([]byte(cfgStr)), ), ) } return envs } // secretsUsedByServices returns the set of secrets that are accessible by the given services, using the metadata for access control. func secretsUsedByServices(md *meta.Data, svcNames ...string) (secretNames map[string]bool) { svcNameSet := make(map[string]bool) for _, name := range svcNames { svcNameSet[name] = true } secretNames = make(map[string]bool) for _, pkg := range md.Pkgs { if len(pkg.Secrets) > 0 && (pkg.ServiceName == "" || svcNameSet[pkg.ServiceName]) { for _, secret := range pkg.Secrets { secretNames[secret] = true } } } return secretNames } // freeLocalhostAddress returns the first free port number on the system. func freeLocalhostAddress() (netip.AddrPort, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return netip.AddrPort{}, err } defer func() { _ = l.Close() }() return l.Addr().(*net.TCPAddr).AddrPort(), nil } func encodeServiceConfigs(svcCfgs map[string]string) []string { envs := make([]string, 0, len(svcCfgs)) for serviceName, cfgString := range svcCfgs { envs = append(envs, "ENCORE_CFG_"+strings.ToUpper(serviceName)+"="+base64.RawURLEncoding.EncodeToString([]byte(cfgString))) } slices.Sort(envs) return envs } func gzipBytes(data []byte) []byte { var buf bytes.Buffer w := gzip.NewWriter(&buf) _, _ = w.Write(data) _ = w.Close() return buf.Bytes() } func reverseString(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } // We lightly obfuscate the PK to trigger fewer of the tools that warn about // keys in source code. // // $ tail -r pk.pem | rev const dummyPrivateKeyReversed = `-----YEK ETAVIRP DNE----- =AOz3eEM5xAe/71Tfx3sQNkW 4FXBCChkppSrCoQnR6pBeP31wu0S0UTTNDhNmSYcerdSFbRhyZOzNRnhF9o1h5D5 +gKkhRZkC33z5+0p8aWwOVWJY8MDycHwvEYvtwcXLNZBHI8L8++mhp0uFz5c5sNM pPRyurcUY36iDzx7hAJcAGoAvXJwVzTmzXBZtvFPs6Alc5gHti2W1l2bz2mwOV77 BA9xAW4R6EHVTnqaoXvxvocW5Z9I0ecJzx0NPfkXBriW1lNclAnkoRAYqziasa6C WIxePQ2VRFbnLu7XR1M/xqg00GHFV0fTlNPo95lC6tl0PAdoupOX1lwjH3rQnTkB Y4BgBKQQJ8F0PPTSMAvyK1bcHP2Iob8UFxyHuPOm11aHYwM4VZvmHm8jX/8vz4eb 6kbNbEkWzfJbbEen/EJLR1XtzvTdjs9bQnJvhQMZmPGzQalqHcVuilQX+PFV4ezM A23w1HCIq6vZqXLO8rXhe8S5hImwVSAKq6TK5dlYPOTIBp66lCQgBKwjkcQcX7tq mr44FuVB7hqBMfnCB0kKcs1SuYgmfUQE41JGInsqjdpaFOwzQi4Jcx7TK44p9vn2 ik6i/hN7JSVA8kMImWIxtL18uVC/Rg0RpM2vcjd+pfgUDifZ1FVYCiL3WlEzDBlZ bSmYdd57T70mEEiuV8QmGiIRrk6kZAMP4CQgBKQ4mIYJX2RJQ1j0V+iXwY/bg+N5 DPEWLB0w6ReZapNy4DSEMD1zm6IWUuo3rGfCsSKUD0xFR/YkauO5Q+GI2gKvmj5V MRiysBL/8PCBwKiFKo1MFjCUfbV/ks49/OJYSOi9WIJiXEg5Tm56BDTH6I8rNdU1 lGIimbKIuzEBWUHsyDQgBKQQ8O/PDCI/SJSPYjkxw1fpX022hUvVW9pvtmd6v0vX M5kMBkT60IwTWhF0DoAx4Uyn4rlPiJy5TUwjC0po/aCRV+ug5C+wIRTCtVCpqRyz GeB4U/3WXHmSulzK5Dw4ADfbWSP0dAbNNOaFI4y6u+acEl5MFt3GN/jieITLsZNK X18B7zHj7LR2f5k3xiJJ/7uNFl8SCcnVquvEI1qslUSTLEPCNoiy5iX/VVTmVNwv dUi92s5oFMyJOFW5joggeeQ55BN6EsjQTnj/XetnpPe5wf5vvptHg5HOcUjJPmIJ vsGpMXoyCh3mzdQPMUJM9Ha8DKlACadqTjdid9ZsAAYLAEggCEAABMgAvulUiO2B FkdtezbN/f5vpPbr4knO22xylfkUp5Uw0W/HxtntXXobF42guEEiie49zki5fPHK vAMC7bOERRLV4v35Dd9QV/KFe0FxqEfm8bFDM6FoA4c0qnkDaKbMhdvxxs0wVFRm BukfBCLOt+W/XyFhZvUKkxgbcOjXV7HRFQGI+GZnrf00qbCRNOCdlYLoYX1kf3pQ eNY6o9ZCJxIDO+dUATCoP3tmP4hvonrjGfpek99D4Ye3+iDwg0AxDW+bt9qoRFew VdOuGmooPaDDxn95q5IghRhrvrEaHpkN/EZiNEAJWQkZa9wkxGye5T9hMZRBjUkt wGPTyf02fuGquCQABIoAAEgAjSggwcKBCSAAFEQAB0w9GikhqkgBNADABIQvEIIM -----YEK ETAVIRP NIGEB-----` ================================================ FILE: cli/daemon/run/tests.go ================================================ package run import ( "context" "fmt" "io" "path/filepath" "runtime" "strings" "github.com/cockroachdb/errors" "github.com/rs/xid" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/run/infra" "encr.dev/cli/daemon/secret" "encr.dev/internal/optracker" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/paths" "encr.dev/pkg/vcs" runtimev1 "encr.dev/proto/encore/runtime/v1" ) // TestParams groups the parameters for the Test method. type TestParams struct { *TestSpecParams // Stdout and Stderr are where "go test" output should be written. Stdout, Stderr io.Writer } // Test runs the tests. func (mgr *Manager) Test(ctx context.Context, params TestParams) (err error) { expSet, err := params.App.Experiments(params.Environ) if err != nil { return err } bld := builderimpl.Resolve(params.App.Lang(), expSet) defer fns.CloseIgnore(bld) spec, err := mgr.testSpec(ctx, bld, expSet, params.TestSpecParams) if err != nil { return err } workingDir := paths.RootedFSPath(params.App.Root(), params.WorkingDir) return bld.RunTests(ctx, builder.RunTestsParams{ Spec: spec, WorkingDir: workingDir, Stdout: params.Stdout, Stderr: params.Stderr, }) } // TestSpecParams are the parameters for computing a test spec. type TestSpecParams struct { // App is the app to test. App *apps.Instance // NS is the namespace to use. NS *namespace.Namespace // Secrets are the secrets to use. Secrets *secret.LoadResult // Args are the arguments to pass to the test command. Args []string // WorkingDir is the working dir, for formatting // error messages with relative paths. WorkingDir string // Environ are the environment variables to set when running the tests, // in the same format as os.Environ(). Environ []string // CodegenDebug, if true, specifies to keep the output // around for codegen debugging purposes. CodegenDebug bool // TempDir is a path to a temp dir that will be clean up by the test runner. TempDir string } type TestSpecResponse struct { Command string Args []string Environ []string } // TestSpec returns how to run the tests. func (mgr *Manager) TestSpec(ctx context.Context, params TestSpecParams) (*TestSpecResponse, error) { expSet, err := params.App.Experiments(params.Environ) if err != nil { return nil, err } bld := builderimpl.Resolve(params.App.Lang(), expSet) defer fns.CloseIgnore(bld) spec, err := mgr.testSpec(ctx, bld, expSet, ¶ms) if err != nil { return nil, err } return &TestSpecResponse{ Command: spec.Command, Args: spec.Args, Environ: spec.Environ, }, nil } // testSpec returns how to run the tests. func (mgr *Manager) testSpec(ctx context.Context, bld builder.Impl, expSet *experiments.Set, params *TestSpecParams) (*builder.TestSpecResult, error) { var secrets map[string]string if params.Secrets != nil { secretData, err := params.Secrets.Get(ctx, expSet) if err != nil { return nil, err } secrets = secretData.Values // remove db override secrets for tests for k, _ := range secrets { if strings.HasPrefix(k, "sqldb::") { delete(secrets, k) } } } vcsRevision := vcs.GetRevision(params.App.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: params.Environ, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: params.CodegenDebug, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: params.App, WorkingDir: params.WorkingDir, }) if err != nil { return nil, err } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: params.App, Experiments: expSet, WorkingDir: params.WorkingDir, ParseTests: true, Prepare: prepareResult, }) if err != nil { return nil, err } if err := params.App.CacheMetadata(parse.Meta); err != nil { return nil, errors.Wrap(err, "cache metadata") } rm := infra.NewResourceManager(params.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, params.NS, nil, mgr.DBProxyPort, true) jobs := optracker.NewAsyncBuildJobs(ctx, params.App.PlatformOrLocalID(), nil) rm.StartRequiredServices(jobs, parse.Meta) // Note: jobs.Wait must be called before generateConfig. if err := jobs.Wait(); err != nil { return nil, err } gateways := make(map[string]GatewayConfig) gatewayBaseURL := fmt.Sprintf("http://localhost:%d", mgr.RuntimePort) for _, gw := range parse.Meta.Gateways { gateways[gw.EncoreName] = GatewayConfig{ BaseURL: gatewayBaseURL, Hostnames: []string{"localhost"}, } } cfg, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: gatewayBaseURL, EnvName: "local", EnvType: cueutil.EnvType_Test, CloudType: cueutil.CloudType_Local, }, }) if err != nil { return nil, err } var runtimeConfigPath option.Option[string] var metaPath option.Option[string] if params.TempDir != "" { if bld.UseNewRuntimeConfig() { runtimeConfigPath = option.Some(filepath.Join(params.TempDir, "runtime_config.pb")) } else { runtimeConfigPath = option.Some(filepath.Join(params.TempDir, "runtime_config.json")) } if bld.NeedsMeta() { metaPath = option.Some(filepath.Join(params.TempDir, "meta.pb")) } } authKey := genAuthKey() configGen := &RuntimeConfigGenerator{ app: params.App, infraManager: rm, md: parse.Meta, AppID: option.Some(params.App.PlatformOrLocalID()), EnvID: option.Some("test"), TraceEndpoint: option.Some(fmt.Sprintf("http://localhost:%d/trace", mgr.RuntimePort)), AuthKey: authKey, Gateways: gateways, DefinedSecrets: secrets, SvcConfigs: cfg.Configs, EnvName: option.Some("test"), EnvType: option.Some(runtimev1.Environment_TYPE_TEST), DeployID: option.Some(fmt.Sprintf("clitest_%s", xid.New().String())), IncludeMeta: bld.NeedsMeta(), MetaPath: metaPath, RuntimeConfigPath: runtimeConfigPath, } env, err := configGen.ForTests(bld.UseNewRuntimeConfig()) if err != nil { return nil, err } env = append(env, encodeServiceConfigs(cfg.Configs)...) return bld.TestSpec(ctx, builder.TestSpecParams{ Compile: builder.CompileParams{ Build: buildInfo, App: params.App, Parse: parse, OpTracker: nil, Experiments: expSet, WorkingDir: params.WorkingDir, }, Env: append(params.Environ, env...), Args: params.Args, }) } ================================================ FILE: cli/daemon/run/watch.go ================================================ package run import ( "path/filepath" "strings" "encr.dev/cli/daemon/apps" "encr.dev/pkg/watcher" ) // watch watches the given app for changes, and reports // them on c. func (mgr *Manager) watch(run *Run) error { sub, err := run.App.Watch(func(i *apps.Instance, event []watcher.Event) { if IgnoreEvents(event) { return } mgr.RunStdout(run, []byte("Changes detected, recompiling...\n")) if err := run.Reload(); err != nil { if errList := AsErrorList(err); errList != nil { mgr.RunError(run, errList) } else { errStr := err.Error() if !strings.HasSuffix(errStr, "\n") { errStr += "\n" } mgr.RunStderr(run, []byte(errStr)) } } else { mgr.RunStdout(run, []byte("Reloaded successfully.\n")) } }) if err != nil { return err } go func() { <-run.Done() run.App.Unwatch(sub) }() return nil } // IgnoreEvents will return true if _all_ events are on files that should be ignored // as the do not impact the running app, or are the result of Encore itself generating code. func IgnoreEvents(events []watcher.Event) bool { for _, event := range events { if !ignoreEvent(event) { return false } } return true } func ignoreEvent(ev watcher.Event) bool { filename := filepath.Base(ev.Path) if strings.HasPrefix(strings.ToLower(filename), "encore.gen.") { // Ignore generated code return true } // Ignore files which wouldn't impact the running app ext := filepath.Ext(ev.Path) switch ext { case ".go", ".sql", ".mod", ".sum", ".work", ".app", ".cue", ".ts", ".js", ".tsx", ".jsx", ".mts", ".mjs", ".cjs", ".cts": return false default: return true } } ================================================ FILE: cli/daemon/run.go ================================================ package daemon import ( "encoding/json" "fmt" "net" "net/url" "os" "strings" "time" "github.com/logrusorgru/aurora/v3" "github.com/rs/zerolog/log" "encr.dev/cli/daemon/run" "encr.dev/internal/optracker" "encr.dev/internal/userconfig" "encr.dev/internal/version" "encr.dev/pkg/fns" "encr.dev/pkg/option" daemonpb "encr.dev/proto/encore/daemon" ) // Run runs the application. func (s *Server) Run(req *daemonpb.RunRequest, stream daemonpb.Daemon_RunServer) error { ctx := stream.Context() slog := &streamLog{stream: stream, buffered: true} stderr := slog.Stderr(false) sendExit := func(code int32) { _ = stream.Send(&daemonpb.CommandMessage{ Msg: &daemonpb.CommandMessage_Exit{Exit: &daemonpb.CommandExit{ Code: code, }}, }) } userConfig, err := userconfig.ForApp(req.AppRoot).Get() if err != nil { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("failed to load config: %v"), err)) sendExit(1) return nil } ctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile) if err != nil { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("failed to begin tracing: %v"), err)) sendExit(1) return nil } defer fns.CloseIgnore(tracer) // ListenAddr should always be passed but guard against old clients. listenAddr := req.ListenAddr if listenAddr == "" { listenAddr = ":4000" } ln, err := net.Listen("tcp", listenAddr) if err != nil { if errIsAddrInUse(err) { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("Failed to run on %s - port is already in use"), listenAddr)) } else { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("Failed to run on %s - %v"), listenAddr, err)) } if host, port, ok := findAvailableAddr(listenAddr); ok { if host == "localhost" || host == "127.0.0.1" { _, _ = fmt.Fprintf(stderr, "Note: port %d is available; specify %s to use it\n", port, aurora.Sprintf(aurora.Cyan("--port=%d"), port)) } else { _, _ = fmt.Fprintf(stderr, "Note: address %s:%d is available; specify %s to use it\n", host, port, aurora.Sprintf(aurora.Cyan("--listen=%s:%d"), host, port)) } } else { _, _ = fmt.Fprintf(stderr, "Note: specify %s to run on another port\n", aurora.Cyan("--port=NUMBER")) } sendExit(1) return nil } defer fns.CloseIgnore(ln) app, err := s.apps.Track(req.AppRoot) if err != nil { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("failed to resolve app: %v"), err)) sendExit(1) return nil } ns, err := s.namespaceOrActive(ctx, app, req.Namespace) if err != nil { _, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red("failed to resolve namespace: %v"), err)) sendExit(1) return nil } ops := optracker.New(stderr, stream) defer ops.AllDone() // Kill the tracker when we exit this function // Check for available update before we start the proc // so the output from the proc doesn't race with our // prints below. newVer := s.availableUpdate() // If force upgrade has been enabled, we force the upgrade now before we try and run the app if newVer != nil && newVer.ForceUpgrade { _, _ = fmt.Fprint(stderr, aurora.Red("An urgent security update for Encore is available.").String()+"\n") if newVer.SecurityNotes != "" { _, _ = fmt.Fprint(stderr, aurora.Sprintf(aurora.Yellow("%s"), newVer.SecurityNotes)+"\n") } _, _ = fmt.Fprintf(stderr, "Upgrading Encore to %v...\n", newVer.Version()) if err := newVer.DoUpgrade(stderr, stderr); err != nil { _, _ = fmt.Fprint(stderr, aurora.Sprintf(aurora.Red("Upgrade failed: %v"), err)+"\n") } slog.FlushBuffers() sendExit(1) // Kill the client os.Exit(1) // Kill the daemon too return nil } // Hold the stream mutex so we can set up the stream map // before output starts. s.mu.Lock() // If the listen addr contains no interface, render it as "localhost:port" // instead of just ":port". displayListenAddr := req.ListenAddr if strings.HasPrefix(listenAddr, ":") { displayListenAddr = "localhost" + req.ListenAddr } browser := run.BrowserModeFromProto(req.Browser) if browser == run.BrowserModeAuto { browser = run.BrowserModeFromConfig(userConfig) } runInstance, err := s.mgr.Start(ctx, run.StartParams{ App: app, NS: ns, WorkingDir: req.WorkingDir, Listener: ln, ListenAddr: displayListenAddr, Watch: req.Watch, Environ: req.Environ, OpsTracker: ops, Browser: browser, Debug: run.DebugModeFromProto(req.DebugMode), LogLevel: option.FromPointer(req.LogLevel), ScrubSensitiveData: req.ScrubSensitiveData, }) if err != nil { s.mu.Unlock() if errList := run.AsErrorList(err); errList != nil { _ = errList.SendToStream(stream) } else { errStr := err.Error() if !strings.HasSuffix(errStr, "\n") { errStr += "\n" } _, _ = stderr.Write([]byte(errStr)) } sendExit(1) return nil } defer runInstance.Close() s.streams[runInstance.ID] = slog s.mu.Unlock() ops.AllDone() secrets, _ := s.sm.Load(app).Get(ctx, nil) externalDBs := map[string]string{} for key, val := range secrets.Values { if db, ok := strings.CutPrefix(key, "sqldb::"); ok { var connCfg struct { ConnString string `json:"connection_string"` } err := json.Unmarshal([]byte(val), &connCfg) if err != nil { log.Warn().Err(err).Str("key", key).Msg("failed to unmarshal connection string") continue } connURL, err := url.Parse(connCfg.ConnString) if err != nil { log.Warn().Err(err).Str("key", key).Msg("failed to parse connection string") continue } connURL.User = url.User(connURL.User.Username()) externalDBs[db] = connURL.String() } } _, _ = stderr.Write([]byte("\n")) _, _ = fmt.Fprintf(stderr, " Encore development server running!\n\n") _, _ = fmt.Fprintf(stderr, " Your API is running at: %s\n", aurora.Cyan("http://"+runInstance.ListenAddr)) _, _ = fmt.Fprintf(stderr, " Development Dashboard URL: %s\n", aurora.Cyan(fmt.Sprintf( "%s/%s", s.mgr.DashBaseURL, app.PlatformOrLocalID()))) _, _ = fmt.Fprintf(stderr, " MCP SSE URL: %s\n", aurora.Cyan(fmt.Sprintf( "%s/sse?appID=%s", s.mcp.BaseURL, app.PlatformOrLocalID()))) if ns := runInstance.NS; !ns.Active || ns.Name != "default" { _, _ = fmt.Fprintf(stderr, " Namespace: %s\n", aurora.Cyan(ns.Name)) if len(externalDBs) > 0 { _, _ = fmt.Fprintln(stderr, " External databases:") } } for db, connStr := range externalDBs { _, _ = fmt.Fprintf(stderr, " %s: %s\n", db, aurora.Cyan(connStr)) } if req.DebugMode == daemonpb.RunRequest_DEBUG_ENABLED { // Print the pid for debugging. Currently we only support this if we have a default gateway. if gw, ok := runInstance.ProcGroup().Gateways["api-gateway"]; ok { _, _ = fmt.Fprintf(stderr, " Process ID: %d\n", aurora.Cyan(gw.Pid)) } } // Log which experiments are enabled, if any if exp := runInstance.ProcGroup().Experiments.List(); len(exp) > 0 { strs := make([]string, len(exp)) for i, e := range exp { strs[i] = string(e) } _, _ = fmt.Fprintf(stderr, " Enabled experiment(s): %s\n", aurora.Yellow(strings.Join(strs, ", "))) } // If there's a newer version available, print a message. if newVer != nil { if newVer.SecurityUpdate { _, _ = stderr.Write([]byte(aurora.Sprintf( aurora.Yellow("\n New Encore release available with security updates: %s (you have %s)\n Update with: encore version update\n"), newVer.Version(), version.Version))) if newVer.SecurityNotes != "" { _, _ = stderr.Write([]byte(aurora.Sprintf( aurora.Faint("\n %s\n"), newVer.SecurityNotes))) } } else { _, _ = stderr.Write([]byte(aurora.Sprintf( aurora.Faint("\n New Encore release available: %s (you have %s)\n Update with: encore version update\n"), newVer.Version(), version.Version))) } } _, _ = stderr.Write([]byte("\n")) slog.FlushBuffers() go func() { // Wait a little bit for the app to start select { case <-runInstance.Done(): return case <-time.After(5 * time.Second): if proc := runInstance.ProcGroup(); proc != nil { showFirstRunExperience(runInstance, proc.Meta, stderr) } } }() <-runInstance.Done() // wait for run to complete s.mu.Lock() delete(s.streams, runInstance.ID) s.mu.Unlock() return nil } ================================================ FILE: cli/daemon/schema.go ================================================ package daemon import ( "fmt" "time" jsoniter "github.com/json-iterator/go" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) // genSchema generates a JSON payload to match the schema. func genSchema(meta *meta.Data, decl *schema.Type) []byte { if decl == nil { return nil } r := &schemaRenderer{ Stream: jsoniter.NewStream(jsoniter.ConfigDefault, nil, 256), meta: meta, seenDecls: make(map[uint32]*schema.Decl), } return r.Render(decl) } type schemaRenderer struct { *jsoniter.Stream meta *meta.Data seenDecls map[uint32]*schema.Decl typeArgs []*schema.Type } func (r *schemaRenderer) Render(d *schema.Type) []byte { r.renderType(d) return r.Buffer() } func (r *schemaRenderer) renderType(typ *schema.Type) { switch typ := typ.Typ.(type) { case *schema.Type_Struct: r.renderStruct(typ.Struct) case *schema.Type_Map: r.renderMap(typ.Map) case *schema.Type_List: r.renderList(typ.List) case *schema.Type_Builtin: r.renderBuiltin(typ.Builtin) case *schema.Type_Named: r.renderNamed(typ.Named) case *schema.Type_Pointer: r.renderType(typ.Pointer.Base) case *schema.Type_Option: r.WriteNil() case *schema.Type_Union: r.renderType(typ.Union.Types[0]) case *schema.Type_Literal: switch v := typ.Literal.Value.(type) { case *schema.Literal_Str: r.WriteString(v.Str) case *schema.Literal_Int: r.WriteInt(int(v.Int)) case *schema.Literal_Float: r.WriteFloat64(v.Float) case *schema.Literal_Boolean: r.WriteBool(v.Boolean) case *schema.Literal_Null: r.WriteNil() default: panic(fmt.Sprintf("unknown literal type %T", v)) } case *schema.Type_TypeParameter: if idx := typ.TypeParameter.ParamIdx; len(r.typeArgs) > int(idx) { r.renderType(r.typeArgs[idx]) } else { r.WriteNil() } case *schema.Type_Config: // Config is invisible here r.renderType(typ.Config.Elem) default: panic(fmt.Sprintf("unknown schema type %T", typ)) } } func (r *schemaRenderer) renderStruct(s *schema.Struct) { r.WriteObjectStart() written := false for _, f := range s.Fields { n := f.JsonName if n == "-" { continue } else if n == "" { n = f.Name } if written { r.WriteMore() } r.WriteObjectField(n) r.renderType(f.Typ) written = true } r.WriteObjectEnd() } func (r *schemaRenderer) renderMap(m *schema.Map) { r.WriteObjectStart() r.renderType(m.Key) r.WriteRaw(": ") r.renderType(m.Value) r.WriteObjectEnd() } func (r *schemaRenderer) renderList(l *schema.List) { r.WriteArrayStart() r.renderType(l.Elem) r.WriteArrayEnd() } func (r *schemaRenderer) renderBuiltin(b schema.Builtin) { switch b { case schema.Builtin_ANY: r.WriteString("") case schema.Builtin_BOOL: r.WriteBool(true) case schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64, schema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64: r.WriteInt(1) case schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: r.WriteRaw("2.3") case schema.Builtin_STRING: r.WriteString("hello") case schema.Builtin_BYTES: r.WriteString("YmFzZTY0Cg==") // "base64" case schema.Builtin_TIME: s, _ := time.Now().MarshalText() r.WriteString(string(s)) case schema.Builtin_UUID: r.WriteString("7d42f515-3517-4e76-be13-30880443546f") case schema.Builtin_JSON: r.WriteObjectStart() r.WriteObjectField("some json data") r.WriteBool(true) r.WriteObjectEnd() case schema.Builtin_USER_ID: r.WriteString("userID") default: r.WriteString("") } } func (r *schemaRenderer) renderNamed(n *schema.Named) { if _, ok := r.seenDecls[n.Id]; ok { // Already seen this name before r.WriteNil() return } // Store type arguments in scope. Restore the previous // type arguments when we're done. prevTypeArgs := r.typeArgs defer func() { r.typeArgs = prevTypeArgs }() r.typeArgs = n.TypeArguments // Avoid infinite recursion decl := r.meta.Decls[n.Id] r.seenDecls[n.Id] = decl r.renderType(decl.Type) delete(r.seenDecls, n.Id) } ================================================ FILE: cli/daemon/secret/secret.go ================================================ // Package secret fetches and caches development secrets for Encore apps. package secret import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "time" "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/load" "github.com/rs/zerolog/log" "go4.org/syncutil" "golang.org/x/sync/singleflight" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/apps" "encr.dev/cli/internal/platform" "encr.dev/pkg/xos" ) // New returns a new manager. func New() *Manager { return &Manager{cache: make(map[string]*Data)} } // Manager manages the secrets cache for running Encore apps. type Manager struct { group singleflight.Group pollOnce sync.Once mu sync.Mutex cache map[string]*Data } // Data is a snapshot of an Encore app's development secret values. type Data struct { // Synced is when the values were last synced, // or the zero value if no sync has taken place. Synced time.Time // Values is a key-value map of defined secrets. Values map[string]string } type LoadResult struct { mgr *Manager app *apps.Instance once syncutil.Once ch <-chan singleflight.Result initial singleflight.Result localSecretMu sync.Mutex } // Load loads the secrets for appSlug. // If appSlug is empty, (*LoadResult).Get resolves to empty secret data. func (mgr *Manager) Load(app *apps.Instance) *LoadResult { mgr.pollOnce.Do(mgr.startPolling) // Ignore cases when the app isn't linked. if app.PlatformID() == "" { return &LoadResult{mgr: mgr, app: app} } ch := mgr.fetch(app.PlatformID(), false) return &LoadResult{mgr: mgr, app: app, ch: ch} } // Get returns the result of the prefetch. // It blocks until the initial fetch is ready or until ctx is cancelled. // For subsequent calls to Get (such as during live reload), it returns any // more recent data that has been subsequently cached. func (lr *LoadResult) Get(ctx context.Context, expSet *experiments.Set) (data *Data, err error) { defer func() { if err == nil { // load.Instances in cue is not safe for concurrent access. // https://github.com/cue-lang/cue/issues/1746 lr.localSecretMu.Lock() defer lr.localSecretMu.Unlock() // Return a new data object so we don't write the overrides to the cache. data, err = applyLocalOverrides(lr.app, data) } }() if lr == nil || lr.app.PlatformID() == "" { return &Data{}, nil } // Fetch the initial result the first time. err = lr.once.Do(func() error { select { case lr.initial = <-lr.ch: // The fetch was successful so mark the Once as completed. return nil case <-ctx.Done(): // We timed out before the fetch completed. return ctx.Err() } }) if err != nil { return nil, err } initial, _ := lr.initial.Val.(*Data) haveInitial := lr.initial.Err == nil cached, haveCache := lr.mgr.loadFromCache(lr.app.PlatformID()) switch { case haveCache && haveInitial: // Which is most recent? if initial.Synced.After(cached.Synced) { return initial, nil } else { return cached, nil } case haveCache: return cached, nil case haveInitial: return initial, nil default: // We have a prefetch error; return it. return nil, lr.initial.Err } } // UpdateKey updates the cached secret key to the given value. func (mgr *Manager) UpdateKey(appSlug, key, value string) { mgr.mu.Lock() defer mgr.mu.Unlock() if data, ok := mgr.cache[appSlug]; ok { vals := make(map[string]string) for k, v := range data.Values { vals[k] = v } vals[key] = value mgr.cache[appSlug] = &Data{ Synced: time.Now(), Values: vals, } if err := mgr.writeToDisk(appSlug, data); err != nil { log.Error().Err(err).Msg("failed to write secrets to disk cache") } } } // fetch fetches secrets from the server. // mu must not be held when running. func (mgr *Manager) fetch(appSlug string, poll bool) <-chan singleflight.Result { return mgr.group.DoChan(appSlug, func() (any, error) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() secrets, err := platform.GetLocalSecretValues(ctx, appSlug, poll) if err != nil { // check for access to the app before stating that we failed to fetch secrets var pErr platform.Error _, appErr := platform.GetApp(ctx, appSlug) if errors.As(appErr, &pErr) && (pErr.HTTPCode == 404 || pErr.HTTPCode == 403) { return nil, fmt.Errorf("access denied: you do not have access to the app %q", appSlug) } return nil, fmt.Errorf("fetch secrets for %s: %v", appSlug, err) } data := &Data{ Synced: time.Now(), Values: secrets, } // Update our caches mgr.mu.Lock() mgr.cache[appSlug] = data mgr.mu.Unlock() if err := mgr.writeToDisk(appSlug, data); err != nil { log.Error().Err(err).Msg("failed to write secrets to disk cache") } return data, nil }) } func (mgr *Manager) loadFromCache(appSlug string) (*Data, bool) { // Do we have the secrets in our cache? mgr.mu.Lock() data, ok := mgr.cache[appSlug] mgr.mu.Unlock() if ok { return data, true } // Do we have them on disk? if data, err := mgr.readFromDisk(appSlug); err == nil { mgr.mu.Lock() mgr.cache[appSlug] = data mgr.mu.Unlock() return data, true } return nil, false } // startPolling begins polling for secret updates every 5 minutes for the apps // that have been run. func (mgr *Manager) startPolling() { go func() { for range time.Tick(5 * time.Minute) { var slugs []string mgr.mu.Lock() for s := range mgr.cache { slugs = append(slugs, s) } mgr.mu.Unlock() for _, s := range slugs { res := <-mgr.fetch(s, true) if res.Err != nil { log.Error().Err(res.Err).Str("app_id", s).Msg("failed to sync secrets") } else { log.Info().Str("app_id", s).Msg("successfully synced app secrets") } } } }() } // writeToDisk serializes the secret data and writes it to disk // readable only for the current user. func (mgr *Manager) writeToDisk(appSlug string, data *Data) (err error) { defer func() { if err != nil { err = fmt.Errorf("write secrets %s: %v", appSlug, err) } }() path, err := mgr.secretsPath(appSlug) if err != nil { return err } // Create all parent dirs and then chmod the secrets dir to be only user-readable secretsDir := filepath.Dir(path) if err := os.MkdirAll(secretsDir, 0755); err != nil { return err } else if err := os.Chmod(secretsDir, 0700); err != nil { return err } out, err := json.Marshal(data) if err != nil { return err } return xos.WriteFile(path, out, 0600) } // readFromDisk reads the cached secrets from disk. func (mgr *Manager) readFromDisk(appSlug string) (data *Data, err error) { defer func() { if err != nil { err = fmt.Errorf("read secrets %s: %v", appSlug, err) } }() path, err := mgr.secretsPath(appSlug) if err != nil { return nil, err } fdata, err := os.ReadFile(path) if err != nil { return nil, err } data = new(Data) err = json.Unmarshal(fdata, data) return data, err } // secretsPath returns the file path to where the given app's secrets are stored on disk. func (mgr *Manager) secretsPath(appSlug string) (string, error) { dir, err := os.UserCacheDir() if err != nil { return "", err } return filepath.Join(dir, "encore", "secrets", appSlug+".json"), nil } // applyLocalOverrides parses the local secrets override file, if any, // and returns a new Data object with the overrides applied. // // If there are no overrides src is returned directly. // The original src data object is never modified. func applyLocalOverrides(app *apps.Instance, src *Data) (*Data, error) { const name = ".secrets.local.cue" data, err := os.ReadFile(filepath.Join(app.Root(), name)) if err != nil { if errors.Is(err, fs.ErrNotExist) { return src, nil } return nil, err } updated := &Data{ Synced: src.Synced, Values: make(map[string]string, len(src.Values)), } for k, v := range src.Values { updated.Values[k] = v } ctx := cuecontext.New() loadCfg := &load.Config{ Stdin: bytes.NewReader(data), } inst := load.Instances([]string{"-"}, loadCfg)[0] if inst.Err != nil { return nil, fmt.Errorf("parse local secrets: %v", inst.Err) } secrets := ctx.BuildInstance(inst) if err := secrets.Err(); err != nil { return nil, fmt.Errorf("parse local secrets: %v", err) } it, err := secrets.Fields(cue.Hidden(false), cue.Concrete(true)) if err != nil { return nil, fmt.Errorf("parse local secrets: %v", err) } for it.Next() { key := it.Selector().String() val, err := it.Value().String() if err != nil { return nil, fmt.Errorf("parse local secrets: secret key %s is not a string", key) } updated.Values[key] = val } return updated, nil } ================================================ FILE: cli/daemon/sqldb/cluster.go ================================================ package sqldb import ( "context" "fmt" "net" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/cockroachdb/errors" "github.com/jackc/pgx/v5" "github.com/rs/zerolog" "go4.org/syncutil" "golang.org/x/sync/errgroup" "encr.dev/internal/optracker" meta "encr.dev/proto/encore/parser/meta/v1" // stdlib registers the "pgx" driver to database/sql. _ "github.com/jackc/pgx/v5/stdlib" ) // Cluster represents a running database Cluster. type Cluster struct { ID ClusterID // cluster ID Memfs bool // use an in-memory filesystem? Password string // randomly generated password for this cluster driver Driver log zerolog.Logger startOnce syncutil.Once // started is closed when the cluster has been successfully started. started chan struct{} // cachedStatus is the cached cluster status; it should be accessed // via status(). cachedStatus atomic.Pointer[ClusterStatus] Roles EncoreRoles // set by Start // Ctx is canceled when the cluster is being torn down. Ctx context.Context cancel func() // for canceling Ctx mu sync.Mutex dbs map[string]*DB // name -> db isExternal func(name string) bool } func (c *Cluster) Stop() { // no-op } // Ready returns a channel that is closed when the cluster is up and running. func (c *Cluster) Ready() <-chan struct{} { return c.started } // Start creates the cluster if necessary and starts it. // If the cluster is already running it does nothing. func (c *Cluster) Start(ctx context.Context, tracker *optracker.OpTracker) (*ClusterStatus, error) { var status *ClusterStatus err := c.startOnce.Do(func() (err error) { c.log.Debug().Msg("starting cluster") defer func() { if err == nil { close(c.started) c.log.Debug().Msg("successfully started cluster") } else { c.log.Error().Err(err).Msg("failed to start cluster") } }() st, err := c.driver.CreateCluster(ctx, &CreateParams{ ClusterID: c.ID, Memfs: c.Memfs, Tracker: tracker, }, c.log) if err != nil { return errors.WithStack(err) } status = st c.cachedStatus.Store(st) go c.pollStatus() // Setup the roles c.Roles, err = c.setupRoles(ctx, st) return err }) if err != nil { return nil, errors.WithStack(err) } else if status == nil { // We've already set it up; query the current status return c.Status(ctx) } return status, nil } // setupRoles ensures the necessary database roles exist // for admin/write/read access. func (c *Cluster) setupRoles(ctx context.Context, st *ClusterStatus) (EncoreRoles, error) { uri := st.ConnURI(st.Config.RootDatabase, st.Config.Superuser) conn, err := pgx.Connect(ctx, uri) if err != nil { return nil, fmt.Errorf("connect: %v", err) } defer conn.Close(context.Background()) roles, err := c.determineRoles(ctx, st, conn) if err != nil { return nil, fmt.Errorf("determine roles: %v", err) } for _, role := range roles { sanitizedUsername := (pgx.Identifier{role.Username}).Sanitize() c.log.Debug().Str("role", role.Username).Msg("creating role") _, err := conn.Exec(ctx, ` CREATE USER `+sanitizedUsername+` WITH LOGIN ENCRYPTED PASSWORD `+quoteString(role.Password)+` `) if err != nil { var exists bool err2 := conn.QueryRow(context.Background(), ` SELECT COALESCE(MAX(oid), 0) > 0 AS exists FROM pg_roles WHERE rolname = $1 `, role.Username).Scan(&exists) if err2 != nil { c.log.Error().Err(err2).Str("role", role.Username).Msg("unable to lookup role") return nil, fmt.Errorf("get role %q: %v", role.Username, err2) } else if !exists { c.log.Error().Err(err).Str("role", role.Username).Msg("unable to create role") return nil, fmt.Errorf("create role %q: %v", role.Username, err) } c.log.Debug().Str("role", role.Username).Msg("role already exists") } // Add cluster-level permissions. switch role.Type { case RoleAdmin: // Grant admins the ability to create databases. _, err := conn.Exec(ctx, ` ALTER USER `+sanitizedUsername+` CREATEDB CREATEROLE `) if err != nil { c.log.Error().Err(err).Str("role", role.Username).Msg("unable to grant CREATEDB") return nil, fmt.Errorf("grant CREATEDB to %q: %v", role.Username, err) } } } return roles, nil } // determineRoles determines the roles to create based on the server version. func (c *Cluster) determineRoles(ctx context.Context, st *ClusterStatus, conn *pgx.Conn) (EncoreRoles, error) { // We always support an admin role (PostgreSQL 11+) // We support read/write roles on PostgreSQL 14+ only, // as support for predefined roles was added then. var supportsPredefinedRoles bool { var version string if err := conn.QueryRow(ctx, "SHOW server_version").Scan(&version); err != nil { return nil, fmt.Errorf("determine server version: %v", err) } c.log.Debug().Str("version", version).Msg("got postgres server version") major, _, _ := strings.Cut(version, ".") if n, err := strconv.Atoi(major); err != nil { return nil, fmt.Errorf("determine server version: %v", err) } else if n >= 14 { supportsPredefinedRoles = true } } // For legacy databases, just use the predefined admin role that we set up before. roles := EncoreRoles{st.Config.Superuser} if supportsPredefinedRoles { // Otherwise if we support predefined roles, add more roles to use. roles = append(roles, Role{RoleAdmin, "encore-admin", "admin"}, Role{RoleWrite, "encore-write", "write"}, Role{RoleRead, "encore-read", "read"}, ) } return roles, nil } // initDB initializes the database for svc and adds it to c.dbs. // The cluster mutex must be held. func (c *Cluster) initDB(encoreName string) *DB { driverName := encoreName if !c.driver.Meta().ClusterIsolation { driverName += fmt.Sprintf("-%s-%s", c.ID.NS.App.PlatformOrLocalID(), c.ID.Type) // Add the namespace id, as long as it's not the default namespace // (for backwards compatibility). if c.ID.NS.Name != "default" { driverName += "-" + string(c.ID.NS.ID) } } dbCtx, cancel := context.WithCancel(c.Ctx) db := &DB{ EncoreName: encoreName, Cluster: c, driverName: driverName, // Use a template database when running tests. template: c.ID.Type == Test, Ctx: dbCtx, cancel: cancel, ready: make(chan struct{}), log: c.log.With().Str("db", encoreName).Logger(), } c.dbs[encoreName] = db return db } // Setup sets up the given databases. func (c *Cluster) Setup(ctx context.Context, appRoot string, md *meta.Data) error { c.log.Debug().Msg("creating cluster") g, ctx := errgroup.WithContext(ctx) g.SetLimit(50) c.mu.Lock() for _, dbMeta := range md.SqlDatabases { dbMeta := dbMeta db, ok := c.dbs[dbMeta.Name] if c.isExternal(dbMeta.Name) { continue } if !ok { db = c.initDB(dbMeta.Name) } g.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, false, false) }) } c.mu.Unlock() return g.Wait() } // SetupAndMigrate creates and migrates the given databases. func (c *Cluster) SetupAndMigrate(ctx context.Context, appRoot string, dbs []*meta.SQLDatabase) error { c.log.Debug().Msg("creating and migrating cluster") g, ctx := errgroup.WithContext(ctx) g.SetLimit(50) c.mu.Lock() for _, dbMeta := range dbs { if c.IsExternalDB(dbMeta.Name) { continue } dbMeta := dbMeta db, ok := c.dbs[dbMeta.Name] if !ok { db = c.initDB(dbMeta.Name) } g.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, true, false) }) } c.mu.Unlock() return g.Wait() } // GetDB gets the database with the given name. func (c *Cluster) GetDB(name string) (*DB, bool) { c.mu.Lock() db, ok := c.dbs[name] c.mu.Unlock() return db, ok } func (c *Cluster) IsExternalDB(name string) bool { if c.isExternal == nil { return false } return c.isExternal(name) } // Recreate recreates the databases for the given database names. // If databaseNames is the nil slice it recreates all databases. func (c *Cluster) Recreate(ctx context.Context, appRoot string, databaseNames []string, md *meta.Data) error { c.log.Debug().Msg("recreating cluster") var filter map[string]bool if databaseNames != nil { filter = make(map[string]bool) for _, name := range databaseNames { filter[name] = true } } g, ctx := errgroup.WithContext(ctx) g.SetLimit(50) c.mu.Lock() for _, dbMeta := range md.SqlDatabases { dbMeta := dbMeta if filter == nil || filter[dbMeta.Name] { db, ok := c.dbs[dbMeta.Name] if c.isExternal(dbMeta.Name) { if filter[dbMeta.Name] { c.mu.Unlock() return fmt.Errorf("cannot reset %q: resetting external databases is disabled", dbMeta.Name) } continue } if !ok { db = c.initDB(dbMeta.Name) } g.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, true, true) }) } } c.mu.Unlock() err := g.Wait() c.log.Debug().Err(err).Msg("recreated cluster") return err } // Status reports the cluster's status. func (c *Cluster) Status(ctx context.Context) (*ClusterStatus, error) { if st := c.cachedStatus.Load(); st != nil { return st, nil } return c.updateStatusFromDriver(ctx) } func (c *Cluster) updateStatusFromDriver(ctx context.Context) (*ClusterStatus, error) { st, err := c.driver.ClusterStatus(ctx, c.ID) if err == nil { c.cachedStatus.Store(st) } return st, err } // pollStatus polls the driver for status changes. func (c *Cluster) pollStatus() { ch := time.NewTicker(10 * time.Second) defer ch.Stop() for { select { case <-ch.C: ctx, cancel := context.WithTimeout(c.Ctx, 5*time.Second) _, _ = c.updateStatusFromDriver(ctx) cancel() case <-c.Ctx.Done(): return } } } // Info reports information about a cluster. func (c *Cluster) Info(ctx context.Context) (*ClusterInfo, error) { st, err := c.Start(ctx, nil) if err != nil { return nil, err } info := &ClusterInfo{ClusterStatus: st} info.Encore = c.Roles return info, nil } // ClusterInfo returns information about a cluster. type ClusterInfo struct { *ClusterStatus // Encore contains the roles to use to connect for an Encore app. // It is set if and only if the cluster is running. Encore EncoreRoles } // ConnURI reports the connection URI to connect to the given database // in the cluster, authenticating with the given role. func (s *ClusterStatus) ConnURI(database string, r Role) string { uri := fmt.Sprintf("user=%s password=%s dbname=%s", r.Username, r.Password, database) // Handle different ways of expressing the host cfg := s.Config if strings.HasPrefix(cfg.Host, "/") { uri += " host=" + cfg.Host // unix socket } else if host, port, err := net.SplitHostPort(cfg.Host); err == nil { uri += fmt.Sprintf(" host=%s port=%s", host, port) // host:port } else { uri += " host=" + cfg.Host // hostname } return uri } // EncoreRoles describes the credentials to use when connecting // to the cluster as an Encore user. type EncoreRoles []Role func (roles EncoreRoles) Superuser() (Role, bool) { return roles.find(RoleSuperuser) } func (roles EncoreRoles) Admin() (Role, bool) { return roles.find(RoleAdmin) } func (roles EncoreRoles) Write() (Role, bool) { return roles.find(RoleWrite) } func (roles EncoreRoles) Read() (Role, bool) { return roles.find(RoleRead) } func (roles EncoreRoles) First(typs ...RoleType) (Role, bool) { for _, typ := range typs { if r, ok := roles.find(typ); ok { return r, true } } return Role{}, false } func (roles EncoreRoles) find(typ RoleType) (Role, bool) { for _, r := range roles { if r.Type == typ { return r, true } } return Role{}, false } type RoleType string func (r RoleType) String() string { return string(r) } const ( RoleSuperuser RoleType = "superuser" RoleAdmin RoleType = "admin" RoleWrite RoleType = "write" RoleRead RoleType = "read" ) type Role struct { Type RoleType Username string Password string } // quoteString quotes a string for use in SQL. func quoteString(str string) string { return "'" + strings.ReplaceAll(str, "'", "''") + "'" } ================================================ FILE: cli/daemon/sqldb/db.go ================================================ package sqldb import ( "context" "database/sql" "fmt" "io/fs" "path/filepath" "sync" "time" "github.com/cockroachdb/errors" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database" "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source" "github.com/jackc/pgx/v5" "github.com/rs/zerolog" "encr.dev/pkg/fns" "encr.dev/pkg/option" meta "encr.dev/proto/encore/parser/meta/v1" ) // DB represents a single database instance within a cluster. type DB struct { EncoreName string Cluster *Cluster driverName string // Ctx is canceled when the database is being torn down. Ctx context.Context cancel func() // to cancel ctx setupMu sync.Mutex // ready is closed when the database is migrated and ready. ready chan struct{} readied bool migrated bool // template indicates the database is backed by a template database. template bool log zerolog.Logger } // ApplicationCloudName reports the "cloud name" of the application-facing database. func (db *DB) ApplicationCloudName() string { return db.driverName } // TemplateCloudName reports the "cloud name" of the template database, if any. func (db *DB) TemplateCloudName() option.Option[string] { if db.template { return option.Some(db.driverName + "_template") } return option.None[string]() } // Ready returns a channel that is closed when the database is up and running. func (db *DB) Ready() <-chan struct{} { return db.ready } // Setup sets up the database, (re)creating it if necessary and running schema migrations. func (db *DB) Setup(ctx context.Context, appRoot string, dbMeta *meta.SQLDatabase, migrate, recreate bool) (err error) { db.log.Debug().Msg("setting up database") db.setupMu.Lock() defer db.setupMu.Unlock() defer func() { if err == nil { if !db.readied { db.readied = true close(db.ready) } db.log.Debug().Msg("successfully set up database") } else { db.log.Error().Err(err).Msg("failed to set up database") } }() if recreate { if err := db.drop(ctx); err != nil { return err } } setupDB := func(cloudName string) error { if err := db.doCreate(ctx, cloudName, option.None[string]()); err != nil { return errors.Wrapf(err, "create db %s: %v", cloudName, err) } if err := db.ensureRoles(ctx, cloudName, db.Cluster.Roles...); err != nil { return fmt.Errorf("ensure db roles %s: %v", cloudName, err) } if migrate || recreate || !db.migrated { if err := db.doMigrate(ctx, cloudName, appRoot, dbMeta); err != nil { // Only report an error if we asked to migrate or recreate. // Otherwise we might fail to open a database shell when there // is a migration issue. if migrate || recreate { return fmt.Errorf("migrate db %s: %v", cloudName, err) } } } return nil } // First set up the database with the application name. if err := setupDB(db.ApplicationCloudName()); err != nil { return err } if tmplName, ok := db.TemplateCloudName().Get(); ok { // If we want a template database, rename the application database to the template name. // We do it this way in case the migrations assume the database is named according to the application name. // Terminate the connections to the template database to prevent "database is being accessed by other users" errors. _ = db.terminateConnectionsToDB(ctx, db.ApplicationCloudName()) if err := db.doDrop(ctx, tmplName); err != nil { return fmt.Errorf("drop db %s: %v", tmplName, err) } if err := db.renameDB(ctx, db.ApplicationCloudName(), tmplName); err != nil { return fmt.Errorf("rename db %s to %s: %v", db.ApplicationCloudName(), tmplName, err) } // Then create the application database based on the template if err := db.doCreate(ctx, db.ApplicationCloudName(), option.Some(tmplName)); err != nil { return errors.Wrapf(err, "create db %s: %v", db.ApplicationCloudName(), err) } // Ensure the application database has the right roles, too. if err := db.ensureRoles(ctx, db.ApplicationCloudName(), db.Cluster.Roles...); err != nil { return fmt.Errorf("ensure db roles %s: %v", db.ApplicationCloudName(), err) } } return nil } func (db *DB) doCreate(ctx context.Context, cloudName string, template option.Option[string]) error { adm, err := db.connectSuperuser(ctx) if err != nil { return err } defer func() { _ = adm.Close(context.Background()) }() // Does it already exist? var dummy int err = adm.QueryRow(ctx, "SELECT 1 FROM pg_database WHERE datname = $1", cloudName).Scan(&dummy) owner, ok := db.Cluster.Roles.First(RoleAdmin, RoleSuperuser) if !ok { return errors.New("unable to find admin or superuser roles") } if errors.Is(err, pgx.ErrNoRows) { db.log.Debug().Msg("creating database") // Sanitize names since this query does not support query params dbName := (pgx.Identifier{cloudName}).Sanitize() ownerName := (pgx.Identifier{owner.Username}).Sanitize() // Use the template if one is provided. var tmplSnippet string if tmplName, ok := template.Get(); ok { tmplSnippet = fmt.Sprintf("WITH TEMPLATE %s", (pgx.Identifier{tmplName}).Sanitize()) } _, err = adm.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s %s OWNER %s;", dbName, tmplSnippet, ownerName)) } if err != nil { db.log.Error().Err(err).Msg("failed to create database") } return err } func (db *DB) renameDB(ctx context.Context, from, to string) error { adm, err := db.connectSuperuser(ctx) if err != nil { return err } defer func() { _ = adm.Close(context.Background()) }() _, err = adm.Exec(ctx, fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", (pgx.Identifier{from}).Sanitize(), (pgx.Identifier{to}).Sanitize(), )) return err } // ensureRoles ensures the roles have been granted access to this database. func (db *DB) ensureRoles(ctx context.Context, cloudName string, roles ...Role) error { adm, err := db.connectSuperuser(ctx) if err != nil { return err } defer func() { _ = adm.Close(context.Background()) }() db.log.Debug().Msg("revoking public access") safeDBName := (pgx.Identifier{cloudName}).Sanitize() _, err = adm.Exec(ctx, "REVOKE ALL ON DATABASE "+safeDBName+" FROM public") if err != nil { return fmt.Errorf("revoke public: %v", err) } for _, role := range roles { var stmt string safeRoleName := (pgx.Identifier{role.Username}).Sanitize() switch role.Type { case RoleSuperuser: // Already granted; nothing to do continue case RoleAdmin: stmt = fmt.Sprintf("GRANT ALL ON DATABASE %s TO %s;", safeDBName, safeRoleName) case RoleWrite: stmt = fmt.Sprintf(` GRANT TEMP, CONNECT ON DATABASE %s TO %s; GRANT pg_read_all_data TO %s; GRANT pg_write_all_data TO %s; `, safeDBName, safeRoleName, safeRoleName, safeRoleName) case RoleRead: stmt = fmt.Sprintf(` GRANT TEMP, CONNECT ON DATABASE %s TO %s; GRANT pg_read_all_data TO %s; `, safeDBName, safeRoleName, safeRoleName) default: return fmt.Errorf("unknown role type %q", role.Type) } db.log.Debug().Str("role", role.Username).Str("db", cloudName).Msg("granting access to role") // We've observed race conditions in Postgres to grant access. Retry a few times. { var err error for i := 0; i < 5; i++ { _, err = adm.Exec(ctx, stmt) if err == nil { break } db.log.Debug().Str("role", role.Username).Str("db", cloudName).Err(err).Msg("error granting role, retrying") time.Sleep(250 * time.Millisecond) } if err != nil { return fmt.Errorf("grant %s role %s: %v", role.Type, role.Username, err) } } db.log.Debug().Str("role", role.Username).Str("db", cloudName).Msg("successfully granted access") } return nil } // Migrate migrates the database. func (db *DB) doMigrate(ctx context.Context, cloudName, appRoot string, dbMeta *meta.SQLDatabase) (err error) { if db.Cluster.ID.Type == Shadow { db.log.Debug().Msg("not applying migrations to shadow cluster") return nil } if len(dbMeta.Migrations) == 0 || dbMeta.MigrationRelPath == nil { db.log.Debug().Msg("no database migrations to run, skipping") return nil } db.log.Debug().Msg("running database migrations") defer func() { if err != nil { db.log.Error().Err(err).Msg("migrations failed") } else { db.migrated = true db.log.Debug().Msg("migrations completed successfully") } }() info, err := db.Cluster.Info(ctx) if err != nil { return err } else if info.Status != Running { return errors.New("cluster not running") } admin, ok := info.Encore.First(RoleAdmin, RoleSuperuser) if !ok { return errors.New("unable to find superuser or admin roles") } uri := info.ConnURI(cloudName, admin) db.log.Debug().Str("uri", uri).Msg("running migrations") pool, err := sql.Open("pgx", uri) if err != nil { return err } defer fns.CloseIgnore(pool) path := filepath.Join(appRoot, *dbMeta.MigrationRelPath) mdSrc := NewMetadataSource(NewOsMigrationReader(path), dbMeta.Migrations) conn, err := pool.Conn(ctx) if err != nil { return errors.Wrap(err, "failed to connect to postgres") } err = RunMigration(ctx, cloudName, dbMeta.AllowNonSequentialMigrations, conn, mdSrc) // If we have removed a migration that failed to apply we can get an ErrNoChange error // after forcing the migration down to the previous version. if errors.Is(err, migrate.ErrNoChange) { db.log.Info().Msg("database already up to date") return nil } else if err != nil { return fmt.Errorf("could not migrate database %s: %v", cloudName, err) } db.log.Info().Msg("migration completed") return nil } func (db *DB) ListAppliedMigrations(ctx context.Context) (map[uint64]bool, error) { conn, err := db.connectToDB(ctx) if err != nil { return nil, err } defer fns.CloseIgnore(conn) return LoadAppliedVersions(ctx, conn, "public", "schema_migrations") } func RunMigration(ctx context.Context, dbName string, allowNonSeq bool, conn *sql.Conn, mdSrc *MetadataSource) (err error) { var ( dbDriver database.Driver srcDriver source.Driver ) if allowNonSeq { dbDriver, srcDriver, err = NonSequentialMigrator(ctx, conn, mdSrc) if err != nil { return errors.Wrap(err, "failed to connect to postgres") } } else { dbDriver, err = postgres.WithConnection(ctx, conn, &postgres.Config{}) if err != nil { return errors.Wrap(err, "failed to connect to postgres") } srcDriver = mdSrc } curVersion, _, err := dbDriver.Version() if err != nil { return errors.Wrap(err, "failed to get current version") } else if curVersion < -1 { return errors.Newf("invalid current version (%d) for db %s", curVersion, dbName) } m, err := migrate.NewWithInstance("src", srcDriver, "postgres", dbDriver) if err != nil { return errors.Wrap(err, "failed to create migration instance") } err = m.Up() if errors.Is(err, migrate.ErrNoChange) { return err } // If we have a dirty migration, reset the dirty flag and try again. // This is safe since all migrations run inside transactions. var dirty migrate.ErrDirty if errors.As(err, &dirty) { // Find the version that preceded the dirty version so // we can force the migration to that version and then // re-apply the migration. var prevVer uint prevVer, err = srcDriver.Prev(uint(dirty.Version)) targetVer := int(prevVer) if errors.Is(err, fs.ErrNotExist) { // If Prev returns ErrNotExist, the original migration might // have been deleted. In this case, we'll need to search for // the version that is the closest lower version starting at the // first version. targetVer, err = findClosestLowerVersion(srcDriver.First, dirty.Version, srcDriver.Next) if err != nil { return errors.Wrapf(err, "could not automatically reset the schema_migrations "+ "dirty flag for database %s. Please reset it manually by connecting "+ "to the database modify the schema_migrations table", dbName) } } else if err != nil { return errors.Wrap(err, "failed to find previous version") } if err = m.Force(targetVer); err == nil { err = m.Up() } } return errors.Wrap(err, "failed to migrate database") } func findClosestLowerVersion(first func() (uint, error), dirtyVer int, next func(i uint) (uint, error)) (int, error) { firstVer, err := first() // If the first version doesn't exist, we can't reset the dirty flag // and we'll need to return an error. if err != nil { return 0, errors.Wrapf(err, "failed to find first version") } // otherwise we'll need to find the version that is the closest lower version rtn := database.NilVersion for nextVer := firstVer; err == nil && nextVer < uint(dirtyVer); nextVer, err = next(nextVer) { rtn = int(nextVer) } return rtn, nil } func (db *DB) drop(ctx context.Context) error { if err := db.doDrop(ctx, db.ApplicationCloudName()); err != nil { return errors.Wrapf(err, "drop database %s", db.ApplicationCloudName()) } if name, ok := db.TemplateCloudName().Get(); ok { if err := db.doDrop(ctx, name); err != nil { return errors.Wrapf(err, "drop database %s", name) } } return nil } func (db *DB) terminateConnectionsToDB(ctx context.Context, cloudName string) error { adm, err := db.connectSuperuser(ctx) if err != nil { return err } defer func() { _ = adm.Close(context.Background()) }() // Drop all connections to prevent "database is being accessed by other users" errors. _, _ = adm.Exec(ctx, "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1", cloudName) return nil } func (db *DB) doDrop(ctx context.Context, cloudName string) error { adm, err := db.connectSuperuser(ctx) if err != nil { return err } defer func() { _ = adm.Close(context.Background()) }() var dummy int err = adm.QueryRow(ctx, "SELECT 1 FROM pg_database WHERE datname = $1", cloudName).Scan(&dummy) if err == nil { // Drop all connections to prevent "database is being accessed by other users" errors. _, _ = adm.Exec(ctx, "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1", cloudName) name := (pgx.Identifier{cloudName}).Sanitize() // sanitize database name, to be safe _, err = adm.Exec(ctx, fmt.Sprintf("DROP DATABASE %s;", name)) db.log.Debug().Err(err).Msgf("dropped database") } else if errors.Is(err, pgx.ErrNoRows) { return nil } if err != nil { db.log.Debug().Err(err).Msgf("failed to drop database") } return err } // CloseConns closes all connections to this database through the dbproxy, // and prevents future ones from being established. func (db *DB) CloseConns() { db.cancel() } // connectSuperuser creates a superuser connection to the root database for the cluster. // On success the returned conn must be closed by the caller. func (db *DB) connectSuperuser(ctx context.Context) (*pgx.Conn, error) { // Wait for the cluster to be setup select { case <-ctx.Done(): return nil, ctx.Err() case <-db.Cluster.started: } info, err := db.Cluster.Info(ctx) if err != nil { return nil, err } else if info.Status != Running { return nil, fmt.Errorf("cluster not running") } uri := info.ConnURI(info.Config.RootDatabase, info.Config.Superuser) // Wait for the connection to be established; this might take a little bit // when we're racing with spinning up a Docker container. for i := 0; i < 40; i++ { var conn *pgx.Conn conn, err = pgx.Connect(ctx, uri) if err == nil { return conn, nil } else if ctx.Err() != nil { // We'll never succeed once the context has been canceled. // Give up straight away. db.log.Debug().Err(err).Msgf("failed to connect to superuser db") return nil, err } time.Sleep(250 * time.Millisecond) } db.log.Debug().Err(err).Msgf("failed to connect to admin db") return nil, fmt.Errorf("failed to connect to superuser database: %v", err) } // Connects as a superuser or admin to the database. Fails fast if the cluster // is not running yet. // On success the returned conn must be closed by the caller. func (db *DB) connectToDB(ctx context.Context) (*sql.Conn, error) { info, err := db.Cluster.Info(ctx) if err != nil { return nil, err } uri := info.ConnURI(db.EncoreName, info.Config.Superuser) pool, err := sql.Open("pgx", uri) if err != nil { return nil, err } defer fns.CloseIgnore(pool) conn, err := pool.Conn(ctx) if err != nil { return nil, err } return conn, nil } ================================================ FILE: cli/daemon/sqldb/db_test.go ================================================ package sqldb import ( "io/fs" "testing" qt "github.com/frankban/quicktest" _ "github.com/golang-migrate/migrate/v4/source/file" // for running migrations from the filesystem ) func TestFindClosestVersion(t *testing.T) { c := qt.New(t) testCases := map[string]struct { versions []uint dirty int expected int expectedErr bool }{ "first": { versions: []uint{1, 2, 3}, dirty: 1, expected: -1, }, "middle": { versions: []uint{1, 2, 3}, dirty: 2, expected: 1, }, "last": { versions: []uint{1, 2, 3}, dirty: 3, expected: 2, }, "deleted": { versions: []uint{1, 2, 4}, dirty: 3, expected: 2, }, "deleted_first": { versions: []uint{2, 3, 4}, dirty: 1, expected: -1, }, "empty": { dirty: 5, expectedErr: true, }, } for name, tc := range testCases { c.Run(name, func(c *qt.C) { result, err := findClosestLowerVersion(func() (uint, error) { if len(tc.versions) == 0 { return 0, fs.ErrNotExist } return tc.versions[0], nil }, tc.dirty, func(version uint) (uint, error) { for _, v := range tc.versions { if v > version { return v, nil } } return 0, fs.ErrNotExist }) if tc.expectedErr { c.Assert(err, qt.IsNotNil) } else { c.Assert(err, qt.IsNil) c.Assert(result, qt.Equals, tc.expected) } }) } } ================================================ FILE: cli/daemon/sqldb/docker/docker.go ================================================ package docker import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "os/exec" "strings" "time" "github.com/cockroachdb/errors" "github.com/jackc/pgx/v5" "github.com/rs/zerolog" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/sqldb" "encr.dev/pkg/idents" ) type Driver struct{} var _ sqldb.Driver = (*Driver)(nil) const ( DefaultSuperuserUsername = "postgres" DefaultSuperuserPassword = "postgres" DefaultRootDatabase = "postgres" defaultDataDir = "/var/lib/postgresql/data" ) func (d *Driver) CreateCluster(ctx context.Context, p *sqldb.CreateParams, log zerolog.Logger) (status *sqldb.ClusterStatus, err error) { // Ensure the docker image exists first. { checkExistsCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if ok, err := ImageExists(checkExistsCtx); err != nil { return nil, errors.Wrap(err, "check docker image") } else if !ok { log.Debug().Msg("PostgreSQL image does not exist, pulling") pullOp := p.Tracker.Add("Pulling PostgreSQL docker image", time.Now()) if err := PullImage(context.Background()); err != nil { log.Error().Err(err).Msg("failed to pull PostgreSQL image") p.Tracker.Fail(pullOp, err) return nil, errors.Wrap(err, "pull docker image") } else { p.Tracker.Done(pullOp, 0) log.Info().Msg("successfully pulled sqldb image") } } } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() // If we return with a connection, wait until we can connect. defer func() { if err != nil { return } // Wait for the database to come up; this might take a little bit // when we're racing with spinning up a Docker container. uri := status.ConnURI(status.Config.RootDatabase, status.Config.Superuser) const sleepTime = 250 * time.Millisecond const maxLoops = (30 * time.Second) / sleepTime for i := 0; i < int(maxLoops); i++ { var conn *pgx.Conn connCtx, cancel := context.WithTimeout(ctx, 5*time.Second) conn, err = pgx.Connect(connCtx, uri) cancel() if err == nil { _ = conn.Close(ctx) return } else if ctx.Err() != nil { // We'll never succeed once the context has been canceled. // Give up straight away. log.Debug().Err(err).Msgf("failed to connect to db") err = errors.Wrap(err, "database did not come up") } else if errors.Is(err, io.ErrUnexpectedEOF) { // This is a transient error that can happen when the database first initialises err = errors.Wrap(err, "database is not ready yet") } else { err = errors.WithStack(err) } time.Sleep(250 * time.Millisecond) } }() cid := p.ClusterID cnames := containerNames(cid) status, existingContainerName, err := d.clusterStatus(ctx, cid) if err != nil { log.Error().Err(err).Msg("failed to get container status") return nil, errors.WithStack(err) } // waitForPort waits for the port to become available before returning. waitForPort := func() (*sqldb.ClusterStatus, error) { for i := 0; i < 20; i++ { status, err = d.ClusterStatus(ctx, cid) if err != nil { return nil, errors.Wrap(err, "unable to wait for port") } if status.Config.Host != "" { log.Debug().Str("hostport", status.Config.Host).Msg("cluster started") return status, nil } time.Sleep(500 * time.Millisecond) } return nil, errors.New("timed out waiting for cluster to start") } switch status.Status { case sqldb.Running: log.Debug().Str("hostport", status.Config.Host).Msg("cluster already running") return status, nil case sqldb.Stopped: log.Debug().Msg("cluster stopped, restarting") if out, err := exec.CommandContext(ctx, "docker", "start", existingContainerName).CombinedOutput(); err != nil { return nil, errors.Wrapf(err, "could not start sqldb container: %s", string(out)) } return waitForPort() case sqldb.NotFound: log.Debug().Msg("cluster not found, creating") args := []string{ "run", "-d", "-p", "5432", "--shm-size=1gb", "-e", "POSTGRES_USER=" + DefaultSuperuserUsername, "-e", "POSTGRES_PASSWORD=" + DefaultSuperuserPassword, "-e", "POSTGRES_DB=" + DefaultRootDatabase, "-e", "PGDATA=" + defaultDataDir, "--name", cnames[0], } if p.Memfs { args = append(args, "--mount", "type=tmpfs,destination="+defaultDataDir, Image, "-c", "fsync=off", ) } else { volumeName := clusterVolumeNames(p.ClusterID.NS)[0] // guaranteed to be non-empty if err := d.createVolumeIfNeeded(ctx, volumeName); err != nil { return nil, errors.Wrap(err, "create data volume") } args = append(args, "-v", fmt.Sprintf("%s:%s", volumeName, defaultDataDir), Image) } cmd := exec.CommandContext(ctx, "docker", args...) if out, err := cmd.CombinedOutput(); err != nil { return nil, errors.Wrapf(err, "could not start sql database as docker container: %s", out) } log.Debug().Msg("cluster created") return waitForPort() default: return nil, errors.Newf("unknown cluster status %q", status.Status) } } func (d *Driver) ClusterStatus(ctx context.Context, id sqldb.ClusterID) (*sqldb.ClusterStatus, error) { status, _, err := d.clusterStatus(ctx, id) return status, errors.WithStack(err) } func (d *Driver) CheckRequirements(ctx context.Context) error { if _, err := exec.LookPath("docker"); err != nil { return errors.New("This application requires docker to run since it uses an SQL database. Install docker first.") } else if !isDockerRunning(ctx) { return errors.New("The docker daemon is not running. Start it first.") } return nil } // clusterStatus reports both the standard ClusterStatus but also the container name we actually resolved to. func (d *Driver) clusterStatus(ctx context.Context, id sqldb.ClusterID) (status *sqldb.ClusterStatus, containerName string, err error) { var output []byte // Try the candidate container names in order. cnames := containerNames(id) for _, cname := range cnames { var err error out, err := exec.CommandContext(ctx, "docker", "container", "inspect", cname).CombinedOutput() if errors.Is(err, exec.ErrNotFound) { return nil, "", errors.New("docker not found: is it installed and in your PATH?") } else if err != nil { // Docker returns a non-zero exit code if the container does not exist. // Try to tell this apart from an error by parsing the output. if bytes.Contains(out, []byte("No such container")) { continue } // Podman has slightly different output when a container is not found. if bytes.Contains(out, []byte("no such container")) { continue } return nil, "", errors.Wrapf(err, "docker container inspect failed: %s", out) } else { // Found our container; use it. output, containerName = out, cname break } } if output == nil { return &sqldb.ClusterStatus{Status: sqldb.NotFound}, containerName, nil } var resp []struct { Name string State struct { Running bool } Config struct { Env []string } NetworkSettings struct { Ports map[string][]struct { HostIP string HostPort string } } } if err := json.Unmarshal(output, &resp); err != nil { return nil, "", errors.Wrap(err, "parse `docker container inspect` response") } for _, c := range resp { // Docker prefixes `/` to the container name, Podman doesn't. if c.Name == "/"+containerName || c.Name == containerName { status := &sqldb.ClusterStatus{Status: sqldb.Stopped, Config: &sqldb.ConnConfig{ // Defaults if we don't find anything else configured. Superuser: sqldb.Role{ Type: sqldb.RoleSuperuser, Username: DefaultSuperuserUsername, Password: DefaultSuperuserPassword, }, RootDatabase: DefaultRootDatabase, }} if c.State.Running { status.Status = sqldb.Running } ports := c.NetworkSettings.Ports["5432/tcp"] if len(ports) > 0 { hostIP := ports[0].HostIP // Podman can keep HostIP empty or 0.0.0.0. // https://github.com/containers/podman/issues/17780 if hostIP == "" || hostIP == "0.0.0.0" { hostIP = "127.0.0.1" } status.Config.Host = hostIP + ":" + ports[0].HostPort } // Read the Postgres config from the docker container's environment. for _, env := range c.Config.Env { if name, value, ok := strings.Cut(env, "="); ok { switch name { case "POSTGRES_USER": status.Config.Superuser.Username = value case "POSTGRES_PASSWORD": status.Config.Superuser.Password = value case "POSTGRES_DB": status.Config.RootDatabase = value } } } return status, containerName, nil } } return &sqldb.ClusterStatus{Status: sqldb.NotFound}, containerName, nil } func (d *Driver) CanDestroyCluster(ctx context.Context, id sqldb.ClusterID) error { // Check that we can communicate with Docker. if !isDockerRunning(ctx) { return errors.New("cannot delete sql database: docker is not running") } return nil } func (d *Driver) DestroyCluster(ctx context.Context, id sqldb.ClusterID) error { cnames := containerNames(id) for _, cname := range cnames { out, err := exec.CommandContext(ctx, "docker", "rm", "-f", cname).CombinedOutput() if err != nil { if bytes.Contains(out, []byte("No such container")) { continue } return errors.Wrapf(err, "could not delete cluster: %s", out) } } return nil } func (d *Driver) DestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error { candidates := clusterVolumeNames(ns) for _, c := range candidates { if out, err := exec.CommandContext(ctx, "docker", "volume", "rm", "-f", c).CombinedOutput(); err != nil { if strings.Contains(strings.ToLower(err.Error()), "no such volume") { continue } return errors.Wrapf(err, "could not delete volume %s: %s", c, string(out)) } } return nil } func (d *Driver) createVolumeIfNeeded(ctx context.Context, name string) error { if err := exec.CommandContext(ctx, "docker", "volume", "inspect", name).Run(); err == nil { return nil } out, err := exec.CommandContext(ctx, "docker", "volume", "create", name).CombinedOutput() return errors.Wrapf(err, "create volume %s: %s", name, out) } func (d *Driver) Meta() sqldb.DriverMeta { return sqldb.DriverMeta{ClusterIsolation: true} } // containerNames computes the container name candidates for a given clusterID. func containerNames(id sqldb.ClusterID) []string { // candidates returns possible candidate names for a given app id. candidates := func(appID string) (names []string) { base := "sqldb-" + appID if id.Type != sqldb.Run { base += "-" + string(id.Type) } // Convert the namespace to kebab case to remove invalid characters like ':'. nsName := idents.Convert(string(id.NS.Name), idents.KebabCase) names = []string{base + "-" + nsName + "-" + string(id.NS.ID)} // If this is the default namespace look up the container without // the namespace suffix as well, for backwards compatibility. if id.NS.Name == "default" { names = append(names, base) } return names } var names []string if pid := id.NS.App.PlatformID(); pid != "" { names = append(names, candidates(pid)...) } names = append(names, candidates(id.NS.App.LocalID())...) return names } // ImageExists reports whether the docker image exists. func ImageExists(ctx context.Context) (ok bool, err error) { out, err := exec.CommandContext(ctx, "docker", "image", "inspect", Image).CombinedOutput() switch { case err == nil: return true, nil case bytes.Contains(out, []byte("No such image")): return false, nil // Podman has a different error message. case bytes.Contains(out, []byte("failed to find image")): return false, nil default: return false, errors.WithStack(errors.Wrapf(err, "docker image inspect failed: %s", Image)) } } // PullImage pulls the image. func PullImage(ctx context.Context) error { cmd := exec.CommandContext(ctx, "docker", "pull", Image) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } const Image = "encoredotdev/postgres:15" func isDockerRunning(ctx context.Context) bool { err := exec.CommandContext(ctx, "docker", "info").Run() return err == nil } // clusterVolumeNames reports the candidate names for the docker volume. func clusterVolumeNames(ns *namespace.Namespace) (candidates []string) { nsName := idents.Convert(string(ns.Name), idents.KebabCase) suffix := fmt.Sprintf("%s-%s", ns.ID, nsName) for _, id := range [...]string{ns.App.PlatformID(), ns.App.LocalID()} { if id != "" { candidates = append(candidates, fmt.Sprintf("sqldb-%s-%s", id, suffix)) } } return candidates } ================================================ FILE: cli/daemon/sqldb/driver.go ================================================ package sqldb import ( "context" "errors" "github.com/rs/zerolog" "encr.dev/cli/daemon/namespace" "encr.dev/internal/optracker" ) var ErrUnsupported = errors.New("unsupported operation") // A Driver abstracts away how a cluster is actually operated. type Driver interface { // CreateCluster creates (if necessary) and starts (if necessary) a new cluster using the driver, // and returns its status. // err is nil if and only if the cluster could not be started. CreateCluster(ctx context.Context, p *CreateParams, log zerolog.Logger) (*ClusterStatus, error) // CanDestroyCluster reports whether the cluster could be destroyed, if desired. // If a Driver doesn't support destroying the cluster it reports ErrUnsupported. CanDestroyCluster(ctx context.Context, id ClusterID) error // DestroyCluster destroys a cluster with the given id. // If a Driver doesn't support destroying the cluster it reports ErrUnsupported. DestroyCluster(ctx context.Context, id ClusterID) error // DestroyNamespaceData destroys the data associated with a namespace. // If a Driver doesn't support destroying data it reports ErrUnsupported. DestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error // ClusterStatus reports the current status of a cluster. ClusterStatus(ctx context.Context, id ClusterID) (*ClusterStatus, error) // CheckRequirements checks whether all the requirements are met // to use the driver. CheckRequirements(ctx context.Context) error // Meta reports driver metadata. Meta() DriverMeta } type DriverMeta struct { // ClusterIsolation reports whether clusters are isolated by the driver. // If false, database names will be prefixed with the cluster id. ClusterIsolation bool } type ConnConfig struct { // Host is the host address to connect to the database. // It is only set when Status == Running. Host string // Superuser is the role to use to connect as the superuser, // for creating and managing Encore databases. Superuser Role RootDatabase string // root database to connect to } type ClusterType string const ( Run ClusterType = "run" Shadow ClusterType = "shadow" Test ClusterType = "test" ) func (ct ClusterType) Memfs() bool { switch ct { case Run: return false case Shadow, Test: return true default: return false } } // CreateParams are the params to (*ClusterManager).Create. type CreateParams struct { ClusterID ClusterID // Memfs, if true, configures the database container to use an // in-memory filesystem as opposed to persisting the database to disk. Memfs bool // Tracker allows tracking the progress of the operation. Tracker *optracker.OpTracker } // Status represents the status of a container. type Status string const ( // Running indicates the cluster is running. Running Status = "running" // Stopped indicates the container exists but is not running. Stopped Status = "stopped" // NotFound indicates the container does not exist. NotFound Status = "notfound" ) // ClusterStatus represents the status of a database cluster. type ClusterStatus struct { // Status is the status of the underlying container. Status Status // Config is how to connect to the cluster. // It is non-nil if Status == Running. Config *ConnConfig } ================================================ FILE: cli/daemon/sqldb/external/external.go ================================================ // Package external implements a cluster driver for an external cluster. package external import ( "context" "github.com/rs/zerolog" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/sqldb" ) type Driver struct { Host string // "host", "host:port", "/path/to/unix.socket", Database string // database name SuperuserUsername string SuperuserPassword string } var _ sqldb.Driver = (*Driver)(nil) func (d *Driver) CreateCluster(ctx context.Context, p *sqldb.CreateParams, log zerolog.Logger) (*sqldb.ClusterStatus, error) { // The external driver does not actually create the cluster; just return the status. return d.ClusterStatus(ctx, p.ClusterID) } func (d *Driver) ClusterStatus(ctx context.Context, id sqldb.ClusterID) (*sqldb.ClusterStatus, error) { st := &sqldb.ClusterStatus{ Status: sqldb.Running, Config: &sqldb.ConnConfig{ Host: d.Host, Superuser: sqldb.Role{ Type: sqldb.RoleSuperuser, Username: def(d.SuperuserUsername, "postgres"), Password: def(d.SuperuserPassword, "postgres"), }, RootDatabase: def(d.Database, "postgres"), }, } return st, nil } func (d *Driver) CanDestroyCluster(ctx context.Context, id sqldb.ClusterID) error { return sqldb.ErrUnsupported } func (d *Driver) DestroyCluster(ctx context.Context, id sqldb.ClusterID) error { return sqldb.ErrUnsupported } func (d *Driver) DestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error { return sqldb.ErrUnsupported } func (d *Driver) CheckRequirements(ctx context.Context) error { return nil } func (d *Driver) Meta() sqldb.DriverMeta { return sqldb.DriverMeta{ClusterIsolation: false} } func def(val, orDefault string) string { if val == "" { val = orDefault } return val } ================================================ FILE: cli/daemon/sqldb/manager.go ================================================ package sqldb import ( "context" "crypto/rand" "encoding/base64" "fmt" "sync" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/sync/singleflight" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/secret" ) // NewClusterManager creates a new ClusterManager. func NewClusterManager(driver Driver, apps *apps.Manager, ns *namespace.Manager, secretMgr *secret.Manager) *ClusterManager { log := log.Logger return &ClusterManager{ log: log, driver: driver, apps: apps, ns: ns, clusters: make(map[clusterKey]*Cluster), backendKeyData: make(map[uint32]*Cluster), secretMgr: secretMgr, } } // A ClusterManager manages running local sqldb clusters. type ClusterManager struct { log zerolog.Logger driver Driver apps *apps.Manager ns *namespace.Manager startGroup singleflight.Group secretMgr *secret.Manager mu sync.Mutex clusters map[clusterKey]*Cluster // backendKeyData maps the secret data to a cluster, // for forwarding cancel requests to the right cluster. // Access is guarded by mu. backendKeyData map[uint32]*Cluster } // ClusterID uniquely identifies a cluster. type ClusterID struct { NS *namespace.Namespace Type ClusterType } // clusterKey is the key to use to store a cluster in the cluster map. type clusterKey string func (id ClusterID) clusterKey() clusterKey { return clusterKey(fmt.Sprintf("%s-%s", id.NS.ID, id.Type)) } func GetClusterID(app *apps.Instance, typ ClusterType, ns *namespace.Namespace) ClusterID { return ClusterID{ns, typ} } // Ready reports whether the cluster manager is ready and all requirements are met. func (cm *ClusterManager) Ready() error { return cm.driver.CheckRequirements(context.Background()) } // Create creates a database cluster but does not start it. // If the cluster already exists it is returned. // It does not perform any database migrations. func (cm *ClusterManager) Create(ctx context.Context, params *CreateParams) *Cluster { cm.mu.Lock() defer cm.mu.Unlock() c, ok := cm.get(params.ClusterID) if ok { if status, err := c.Status(ctx); err != nil || status.Status != Running { // The cluster is no longer running; recreate it to clear our cached state. c.cancel() ok = false } } if !ok { ctx, cancel := context.WithCancel(context.Background()) key := params.ClusterID.clusterKey() passwd := genPassword() secretLoader := cm.secretMgr.Load(params.ClusterID.NS.App) c = &Cluster{ ID: params.ClusterID, Memfs: params.Memfs, Password: passwd, Ctx: ctx, driver: cm.driver, cancel: cancel, started: make(chan struct{}), log: cm.log.With().Interface("cluster", params.ClusterID).Logger(), dbs: make(map[string]*DB), isExternal: func(name string) bool { // Don't use external databases for Memfs clusters (tests/shadows). if params.Memfs { return false } secrets, err := secretLoader.Get(ctx, nil) if err != nil { c.log.Error().Err(err).Msg("failed to load secrets for external database check") return false } _, ok := secrets.Values["sqldb::"+name] return ok }, } cm.clusters[key] = c } return c } // LookupPassword looks up a cluster based on its password. func (cm *ClusterManager) LookupPassword(password string) (*Cluster, bool) { cm.mu.Lock() defer cm.mu.Unlock() for _, c := range cm.clusters { if c.Password == password { return c, true } } return nil, false } // Get retrieves the cluster keyed by id. func (cm *ClusterManager) Get(id ClusterID) (*Cluster, bool) { cm.mu.Lock() defer cm.mu.Unlock() return cm.get(id) } // get retrieves the cluster keyed by id. // cm.mu must be held. func (cm *ClusterManager) get(id ClusterID) (*Cluster, bool) { c, ok := cm.clusters[id.clusterKey()] return c, ok } // CanDeleteNamespace implements namespace.DeletionHandler. func (cm *ClusterManager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { c, ok := cm.Get(GetClusterID(app, Run, ns)) if !ok { return nil } err := c.driver.CanDestroyCluster(ctx, c.ID) if errors.Is(err, ErrUnsupported) { err = nil } return nil } // DeleteNamespace implements namespace.DeletionHandler. func (cm *ClusterManager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error { // Find all clusters matching this namespace. // Use a closure for the lock to avoid holding it while we destroy the clusters. var clusters []*Cluster (func() { cm.mu.Lock() defer cm.mu.Unlock() for _, c := range cm.clusters { if c.ID.NS.ID == ns.ID { clusters = append(clusters, c) } } })() // Destroy the clusters. for _, c := range clusters { if err := c.driver.DestroyCluster(ctx, c.ID); err != nil && !errors.Is(err, ErrUnsupported) { return errors.Wrapf(err, "destroy cluster %s", c.ID) } c.cancel() } // If that succeeded, destroy the namespace data. err := cm.driver.DestroyNamespaceData(ctx, ns) if errors.Is(err, ErrUnsupported) { err = nil } return err } func genPassword() string { var data [8]byte if _, err := rand.Read(data[:]); err != nil { log.Fatal().Err(err).Msg("unable to generate random data") } return base64.RawURLEncoding.EncodeToString(data[:]) } ================================================ FILE: cli/daemon/sqldb/migrate.go ================================================ package sqldb import ( "bytes" "context" "database/sql" "fmt" "io" "os" "path/filepath" "slices" "strings" "github.com/cockroachdb/errors" "github.com/golang-migrate/migrate/v4/database" "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source" "github.com/hashicorp/go-multierror" "github.com/lib/pq" meta "encr.dev/proto/encore/parser/meta/v1" ) // MigrationReader is an interface for reading migration files. It has two main // implementations: OsMigrationReader and ZipFSMigrationReader. type MigrationReader interface { Read(*meta.DBMigration) (r io.ReadCloser, err error) } // The OsMigrationReader reads migrations from the local filesystem. func NewOsMigrationReader(path string) *OsMigrationReader { return &OsMigrationReader{path: path} } type OsMigrationReader struct { path string } func (src *OsMigrationReader) Read(m *meta.DBMigration) (r io.ReadCloser, err error) { fpath := filepath.Join(src.path, m.Filename) data, err := os.ReadFile(fpath) if err != nil { return nil, err } return io.NopCloser(bytes.NewReader(data)), nil } // MultiReadCloser is a helper wrapper which extends the io.MultiReader to also // close the underlying closeable readers. It's used by the MetadataSource to // append a statement to mark a migration as successful. func MultiReadCloser(r ...io.Reader) io.ReadCloser { return &multiReadCloser{ readers: r, multiReader: io.MultiReader(r...), } } type multiReadCloser struct { readers []io.Reader multiReader io.Reader } func (m multiReadCloser) Read(p []byte) (n int, err error) { return m.multiReader.Read(p) } func (m multiReadCloser) Close() error { var errs []error for _, r := range m.readers { if c, ok := r.(io.Closer); !ok { continue } else if err := c.Close(); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } var _ io.ReadCloser = (*multiReadCloser)(nil) // NewMetadataSource creates a new MetadataSource instance. func NewMetadataSource(reader MigrationReader, migrations []*meta.DBMigration) *MetadataSource { src := &MetadataSource{ MigrationReader: reader, migrations: migrations, } src.validate() return src } func (src *MetadataSource) validate() { if src.err != nil { return } seen := make(map[uint64]struct{}) for _, m := range src.migrations { if _, ok := seen[m.Number]; ok { src.err = fmt.Errorf("duplicate migration identifier %q", m.Filename) return } seen[m.Number] = struct{}{} } } // MetadataSource is a source.Driver implementation that keeps a list of migrations retrieved from // the Encore metadata. It relies on a MigrationReader to read the migration files. type MetadataSource struct { MigrationReader migrations []*meta.DBMigration err error } func (src *MetadataSource) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) { m, err := src.migration(version, 0) if err != nil { return nil, "", err } r, err = src.Read(m) if err != nil { return nil, "", err } // This is used to make sure that a migration is marked successful in the // same statement as it's run. Otherwise we may end up with a finished migration // which is marked dirty because the SetVersion is run as a separate statement. statement := fmt.Sprintf( ";\ninsert into schema_migrations (version, dirty) values (%d, false) ON CONFLICT (version) DO UPDATE SET dirty = false;", version) return MultiReadCloser( r, strings.NewReader(statement), ), m.Description, nil } func (src *MetadataSource) Open(url string) (source.Driver, error) { return nil, fmt.Errorf("driver.Open is not implemented") } func (src *MetadataSource) Close() error { return nil } func (src *MetadataSource) First() (version uint, err error) { if len(src.migrations) == 0 { return 0, os.ErrNotExist } return uint(src.migrations[0].Number), nil } func (src *MetadataSource) Prev(version uint) (prevVersion uint, err error) { m, err := src.migration(version, -1) if err != nil { return 0, err } return uint(m.Number), nil } func (src *MetadataSource) Next(version uint) (nextVersion uint, err error) { m, err := src.migration(version, +1) if err != nil { return 0, err } return uint(m.Number), nil } func (src *MetadataSource) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) { return nil, "", os.ErrNotExist } func (src *MetadataSource) migration(version uint, offset int) (*meta.DBMigration, error) { if src.err != nil { return nil, src.err } idx := slices.IndexFunc(src.migrations, func(m *meta.DBMigration) bool { return m.Number == uint64(version) }) if idx < 0 { return nil, os.ErrNotExist } idx += offset if idx < 0 || idx >= len(src.migrations) { return nil, os.ErrNotExist } return src.migrations[idx], nil } type nonSequentialDbDriver struct { *postgres.Postgres source *nonSequentialSource schemaName string migrationsTable string conn *sql.Conn appliedVersions map[uint64]bool } type nonSequentialSource struct { *MetadataSource dbDriver *nonSequentialDbDriver } // NonSequentialMigrator creates a new migrator that doesn't require migrations to be sequential. // It does this by keeping track of applied migrations in a table and using that to determine the // current version and which migrations need to be applied. It's effectively extending the logic of // the go-migrate library to support non-sequential migrations and is semi-compatible since it's using the // same underlying table. func NonSequentialMigrator(ctx context.Context, conn *sql.Conn, mdSource *MetadataSource) (database.Driver, source.Driver, error) { src := &nonSequentialSource{ MetadataSource: mdSource, } db := &nonSequentialDbDriver{ conn: conn, migrationsTable: "schema_migrations", source: src, } src.dbDriver = db query := `SELECT CURRENT_SCHEMA()` if err := conn.QueryRowContext(ctx, query).Scan(&db.schemaName); err != nil { return nil, nil, &database.Error{OrigErr: err, Query: []byte(query)} } if len(db.schemaName) == 0 { return nil, nil, postgres.ErrNoSchema } p, err := postgres.WithConnection(ctx, conn, &postgres.Config{ MigrationsTable: db.migrationsTable, SchemaName: db.schemaName, }) if err != nil { return nil, nil, errors.Wrap(err, "failed to create migration instance") } db.Postgres = p if err := db.loadAppliedVersions(); err != nil { return nil, nil, errors.Wrap(err, "failed to load applied versions") } return db, src, nil } func (p *nonSequentialDbDriver) Version() (version int, dirty bool, err error) { if len(p.appliedVersions) == 0 { return database.NilVersion, false, nil } var ok bool prevVersion := database.NilVersion for _, mg := range p.source.migrations { dirty, ok = p.appliedVersions[mg.Number] if !ok { return prevVersion, false, nil } else if dirty { return int(mg.Number), true, nil } prevVersion = int(mg.Number) } return prevVersion, false, nil } func (p *nonSequentialDbDriver) SetVersion(version int, dirty bool) error { // In PSQL, all migrations are applied within the same statement/transaction. // If the migration fails to apply, it is automatically rolled back. // Therefore, we don't need to worry about marking a migration as dirty. if dirty { return nil } tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{}) if err != nil { return &database.Error{OrigErr: err, Err: "transaction start failed"} } if version >= 0 { query := `INSERT INTO ` + pq.QuoteIdentifier(p.schemaName) + `.` + pq.QuoteIdentifier(p.migrationsTable) + ` (version, dirty) VALUES ($1, $2) ON CONFLICT (version) DO UPDATE SET dirty = $2` if _, err := tx.Exec(query, version, dirty); err != nil { if errRollback := tx.Rollback(); errRollback != nil { err = multierror.Append(err, errRollback) } return &database.Error{OrigErr: err, Query: []byte(query)} } } if err := tx.Commit(); err != nil { return &database.Error{OrigErr: err, Err: "transaction commit failed"} } return nil } func LoadAppliedVersions(ctx context.Context, conn *sql.Conn, schemaName, migrationsTable string) (map[uint64]bool, error) { appliedVersions := map[uint64]bool{} query := `SELECT version, dirty FROM ` + pq.QuoteIdentifier(schemaName) + `.` + pq.QuoteIdentifier(migrationsTable) + ` ORDER BY version` rows, err := conn.QueryContext(context.Background(), query) if err != nil { if e, ok := err.(*pq.Error); ok { if e.Code.Name() == "undefined_table" { return appliedVersions, nil } } return nil, &database.Error{OrigErr: err, Query: []byte(query)} } defer rows.Close() var version uint64 var dirty bool for rows.Next() { err := rows.Scan(&version, &dirty) if err != nil { return nil, &database.Error{OrigErr: err, Query: []byte(query)} } appliedVersions[version] = dirty } return appliedVersions, nil } func (p *nonSequentialDbDriver) loadAppliedVersions() error { if p.appliedVersions != nil { return nil } applied, err := LoadAppliedVersions(context.Background(), p.conn, p.schemaName, p.migrationsTable) if err != nil { return err } p.appliedVersions = applied return nil } func (src *nonSequentialSource) Prev(version uint) (prevVersion uint, err error) { m, err := src.migration(version, -1) if err != nil { return 0, err } // If the migration is applied, return this version if _, ok := src.dbDriver.appliedVersions[m.Number]; ok { return uint(m.Number), nil } // Otherwise skip to the previous version return src.Prev(uint(m.Number)) } func (src *nonSequentialSource) Next(version uint) (nextVersion uint, err error) { m, err := src.migration(version, +1) if err != nil { return 0, err } // If the migration is applied, return the next version if _, ok := src.dbDriver.appliedVersions[m.Number]; ok { return src.Next(uint(m.Number)) } // Otherwise, return this version return uint(m.Number), nil } ================================================ FILE: cli/daemon/sqldb/proxy.go ================================================ package sqldb import ( "context" "crypto/tls" "fmt" "io" "net" "strings" "time" "github.com/jackc/pgproto3/v2" "github.com/rs/zerolog/log" "encr.dev/cli/daemon/namespace" "encr.dev/pkg/fns" "encr.dev/pkg/pgproxy" ) // ServeProxy serves the database proxy using the given listener. func (cm *ClusterManager) ServeProxy(ln net.Listener) error { var tempDelay time.Duration // how long to sleep on accept failure for { conn, e := ln.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Error().Err(e).Msgf("dbproxy: accept error, retrying in %v", tempDelay) time.Sleep(tempDelay) continue } return fmt.Errorf("dbproxy: could not accept: %v", e) } tempDelay = 0 go func() { if err := cm.ProxyConn(conn, true); err != nil && err != context.Canceled { log.Error().Err(err).Msg("dbproxy: proxy error") } }() } } // ProxyConn authenticates and proxies a conn to the appropriate // database cluster and database. // If waitForSetup is true, it will wait for initial setup to complete // before proxying the connection. func (cm *ClusterManager) ProxyConn(client net.Conn, waitForSetup bool) error { defer fns.CloseIgnore(client) cl, err := pgproxy.SetupClient(client, &pgproxy.ClientConfig{ TLS: nil, WantPassword: true, }) if err != nil { return err } if cancel, ok := cl.Hello.(*pgproxy.CancelData); ok { cm.cancelRequest(client, cancel) return nil } startup := cl.Hello.(*pgproxy.StartupData) // If the username is "encore" we're connecting to a database cluster // which may not be local var cluster *Cluster if startup.Username == "encore" { password := startup.Password found, ok := cm.LookupPassword(password) if !ok { cm.log.Error().Msg("dbproxy: could not find cluster") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database cluster not found or invalid connection string", }) return nil } cluster = found } else { // The username is the app slug we want to connect to app, err := cm.apps.FindLatestByPlatformOrLocalID(startup.Username) if err != nil { cm.log.Error().Err(err).Msg("dbproxy: could not find app") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "unknown app ID", }) return nil } ctx := context.Background() clusterType, nsID, ok := strings.Cut(startup.Password, "-") // Look up the namespace to use. var ns *namespace.Namespace if !ok { ns, err = cm.ns.GetActive(ctx, app) } else { ns, err = cm.ns.GetByID(ctx, app, namespace.ID(nsID)) } if err != nil { cm.log.Error().Err(err).Msg("dbproxy: could not find infra namespace") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "unknown active infra namespace", }) return nil } // Resolve the cluster type. var ct ClusterType switch clusterType { case "local": ct = Run case "test": ct = Test case "shadow": ct = Shadow default: cm.log.Error().Str("password", startup.Password).Msg("dbproxy: invalid password for connection URI") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "28P01", // 28P01 = invalid password Message: "if connecting with an app slug as the username, the only accepted passwords are 'local' or 'test' to route to those instances on your local system", }) return nil } // Create the cluster if it doesn't exist in memory yet // This might be because the daemon is running, but the hasn't done anything // with the app in question yet on this run cluster = cm.Create(context.Background(), &CreateParams{ ClusterID: GetClusterID(app, ct, ns), Memfs: ct.Memfs(), }) // Ensure the cluster is started _, err = cluster.Start(context.Background(), nil) if err != nil { cm.log.Error().Err(err).Msg("dbproxy: could not start cluster") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "could not start database cluster", }) return nil } } // If Encore knows about the database, check if it's ready // however if the cluster doesn't know about the database, skip this part. // // This is because either: // 1. The database exists and is connected to // 2. The database does not exist, and the remote server will return a "database doesn't exist" error. dbname := startup.Database db, ok := cluster.GetDB(dbname) if ok { var ready <-chan struct{} if waitForSetup { ready = db.Ready() } else { s := make(chan struct{}) close(s) ready = s } // Wait for up to 60s for the cluster and database to come online. select { case <-db.Ctx.Done(): _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "db is shutting down", }) return nil case <-time.After(60 * time.Second): cm.log.Error().Str("db", db.ApplicationCloudName()).Msg("dbproxy: timed out waiting for database to come online") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "timed out waiting for db to complete setup", }) return nil case <-ready: // Continue connecting to backend, below } } info, err := cluster.Info(context.Background()) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "cluster not running: " + err.Error(), }) return nil } server, err := net.Dial("tcp", info.Config.Host) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database not running: " + err.Error(), }) return nil } defer fns.CloseIgnore(server) // Send a modified startup message to the backend admin, _ := info.Encore.First(RoleAdmin, RoleSuperuser) startup.Username = admin.Username startup.Password = admin.Password if db == nil { // We don't know about this database, we'll use the requested name // in case it does actually exist within the cluster. // // If it doesn't the cluster will return an SQL error to the client. startup.Database = dbname } else { startup.Database = db.ApplicationCloudName() } fe, err := pgproxy.SetupServer(server, &pgproxy.ServerConfig{ TLS: nil, Startup: startup, }) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "could not connect: " + err.Error(), }) return nil } log.Trace().Msg("backend connection established, notifying client") if err := pgproxy.AuthenticateClient(cl.Backend); err != nil { return err } keyData, err := pgproxy.FinalizeInitialHandshake(cl.Backend, fe) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "could not establish connection: " + err.Error(), }) return nil } log.Trace().Msg("connection handshake completed, proxying steady-state data") // Store the key data so we know where to route cancellation requests. if keyData != nil { cm.mu.Lock() cm.backendKeyData[keyData.SecretKey] = cluster cm.mu.Unlock() defer func() { cm.mu.Lock() delete(cm.backendKeyData, keyData.SecretKey) cm.mu.Unlock() }() } return pgproxy.CopySteadyState(cl.Backend, fe) } // PreauthProxyConn is a pre-authenticated proxy conn directly specifically to the given cluster. func (cm *ClusterManager) PreauthProxyConn(client net.Conn, id ClusterID) error { defer fns.CloseIgnore(client) cl, err := pgproxy.SetupClient(client, &pgproxy.ClientConfig{ TLS: &tls.Config{MinVersion: tls.VersionTLS12}, }) if err != nil { log.Error().Err(err).Msg("failed to setup client") return err } if cancel, ok := cl.Hello.(*pgproxy.CancelData); ok { cm.cancelRequest(client, cancel) return nil } startup := cl.Hello.(*pgproxy.StartupData) cluster, ok := cm.Get(id) if !ok { cm.log.Error().Interface("cluster", id).Msg("dbproxy: could not find cluster") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database cluster not running", }) return nil } if cluster.IsExternalDB(startup.Database) { cm.log.Error().Str("db", startup.Database).Msg("dbproxy: cannot proxy external database") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "proxy to external databases is disabled", }) return nil } db, ok := cluster.GetDB(startup.Database) if !ok { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database not found", }) return nil } // Wait for up to 60s for the cluster to come online. select { case <-db.Ctx.Done(): _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "db is shutting down", }) return nil case <-time.After(60 * time.Second): cm.log.Error().Str("db", startup.Database).Msg("dbproxy: timed out waiting for database to come online") _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "timed out waiting for db to complete setup", }) return nil case <-cluster.Ready(): // Continue connecting to backend, below } info, err := cluster.Info(context.Background()) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "cluster not running: " + err.Error(), }) return nil } server, err := net.Dial("tcp", info.Config.Host) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database not running: " + err.Error(), }) return nil } defer fns.CloseIgnore(server) admin, _ := info.Encore.First(RoleAdmin, RoleSuperuser) startup.Username = admin.Username startup.Password = admin.Password startup.Database = db.ApplicationCloudName() fe, err := pgproxy.SetupServer(server, &pgproxy.ServerConfig{ TLS: nil, Startup: startup, }) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "could not connect: " + err.Error(), }) return nil } if err := pgproxy.AuthenticateClient(cl.Backend); err != nil { return err } keyData, err := pgproxy.FinalizeInitialHandshake(cl.Backend, fe) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "could not establish connection: " + err.Error(), }) return nil } // Store the key data so we know where to route cancellation requests. if keyData != nil { cm.mu.Lock() cm.backendKeyData[keyData.SecretKey] = cluster cm.mu.Unlock() defer func() { cm.mu.Lock() delete(cm.backendKeyData, keyData.SecretKey) cm.mu.Unlock() }() } log.Trace().Msg("successfully completed handshake, copying data back and forth") return pgproxy.CopySteadyState(cl.Backend, fe) } // cancelRequest handles a cancel request. func (cm *ClusterManager) cancelRequest(client io.Writer, req *pgproxy.CancelData) { cm.mu.Lock() cluster, ok := cm.backendKeyData[req.Raw.SecretKey] cm.mu.Unlock() if !ok { return } info, err := cluster.Info(context.Background()) if err != nil { msg := &pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database cluster not running", } encode, _ := msg.Encode(nil) _, _ = client.Write(encode) return } backend, err := net.Dial("tcp", info.Config.Host) if err != nil { msg := &pgproto3.ErrorResponse{ Severity: "FATAL", Code: "08006", Message: "database cluster not running", } encode, _ := msg.Encode(nil) _, _ = client.Write(encode) return } defer fns.CloseIgnore(backend) _ = pgproxy.SendCancelRequest(backend, req.Raw) } func writeMsg(w io.Writer, msg pgproto3.Message) error { encode, err := msg.Encode(nil) if err != nil { return err } _, err = w.Write(encode) return err } ================================================ FILE: cli/daemon/sqldb/remote.go ================================================ package sqldb import ( "context" "crypto/rand" "encoding/base64" "errors" "fmt" "net" "time" "github.com/gorilla/websocket" "github.com/rs/zerolog/log" "encr.dev/cli/internal/platform" "encr.dev/pkg/pgproxy" ) // OneshotProxy listens on a random port for a single connection, and proxies that connection to a remote db. // It reports the one-time password and port to use. // Once a connection has been established, it stops listening. func OneshotProxy(appSlug, envSlug string, role RoleType) (port int, passwd string, err error) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { return 0, "", err } var passwdBytes [8]byte if _, err := rand.Read(passwdBytes[:]); err != nil { return 0, "", err } passwd = base64.RawURLEncoding.EncodeToString(passwdBytes[:]) go oneshotServer(context.Background(), ln, passwd, appSlug, envSlug, role) return ln.Addr().(*net.TCPAddr).Port, passwd, nil } func oneshotServer(ctx context.Context, ln net.Listener, passwd, appSlug, envSlug string, role RoleType) error { proxy := &pgproxy.SingleBackendProxy{ RequirePassword: passwd != "", FrontendTLS: nil, DialBackend: func(ctx context.Context, startup *pgproxy.StartupData) (pgproxy.LogicalConn, error) { if startup.Password != passwd { return nil, fmt.Errorf("bad password") } startupData, err := startup.Raw.Encode(nil) if err != nil { return nil, err } ws, err := platform.DBConnect(ctx, appSlug, envSlug, startup.Database, role.String(), startupData) if err != nil { var e platform.Error if errors.As(err, &e) && e.HTTPCode == 404 { return nil, pgproxy.DatabaseNotFoundError{Database: startup.Database} } return nil, err } conn := &WebsocketLogicalConn{Conn: ws} return conn, nil }, } return proxy.Serve(ctx, ln) } type WebsocketLogicalConn struct { *websocket.Conn buf []byte } var _ pgproxy.LogicalConn = (*WebsocketLogicalConn)(nil) func (c *WebsocketLogicalConn) Write(p []byte) (int, error) { err := c.Conn.WriteMessage(websocket.BinaryMessage, p) if err != nil { return 0, err } return len(p), nil } func (c *WebsocketLogicalConn) Read(p []byte) (int, error) { // If we have remaining data from the previous message we received // from the stream, simply return that. if len(c.buf) > 0 { n := copy(p, c.buf) c.buf = c.buf[n:] return n, nil } // No more buffered data, wait for a new message from the stream. for { typ, data, err := c.Conn.ReadMessage() if err != nil { return 0, err } else if typ != websocket.BinaryMessage { continue } // Read as much data as possible directly to the waiting caller. // Anything remaining beyond that gets buffered until the next Read call. n := copy(p, data) c.buf = data[n:] return n, nil } } func (c *WebsocketLogicalConn) Cancel(req *pgproxy.CancelData) error { enc := base64.StdEncoding data, err := req.Raw.Encode(nil) if err != nil { return err } encoded := make([]byte, enc.EncodedLen(len(data))) enc.Encode(encoded, data) log.Info().Msgf("sending cancel request %x", data) return c.Conn.WriteMessage(websocket.TextMessage, encoded) } func (c *WebsocketLogicalConn) SetDeadline(t time.Time) error { _ = c.Conn.SetReadDeadline(t) err := c.Conn.SetWriteDeadline(t) return err } ================================================ FILE: cli/daemon/sqldb/utils.go ================================================ package sqldb import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" meta "encr.dev/proto/encore/parser/meta/v1" ) // WaitForConn waits for a successful connection to uri to be established. func WaitForConn(ctx context.Context, uri string) error { var err error for i := 0; i < 40; i++ { var conn *pgx.Conn conn, err = pgx.Connect(ctx, uri) if err == nil { err = conn.Ping(ctx) _ = conn.Close(ctx) if err == nil { return nil } } else if ctx.Err() != nil { // We'll never succeed once the context has been canceled. // Give up straight away. break } time.Sleep(250 * time.Millisecond) } return fmt.Errorf("database did not come up: %v", err) } // IsUsed reports whether the application uses SQL databases at all. func IsUsed(md *meta.Data) bool { return len(md.SqlDatabases) > 0 } ================================================ FILE: cli/daemon/telemetry.go ================================================ package daemon import ( "context" "google.golang.org/protobuf/types/known/emptypb" "encr.dev/cli/internal/telemetry" daemonpb "encr.dev/proto/encore/daemon" ) func (s *Server) Telemetry(ctx context.Context, req *daemonpb.TelemetryConfig) (*emptypb.Empty, error) { telemetry.UpdateConfig(req.AnonId, req.Enabled, req.Debug) return new(emptypb.Empty), nil } ================================================ FILE: cli/daemon/test.go ================================================ package daemon import ( "context" "fmt" "runtime/debug" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "encr.dev/cli/daemon/run" "encr.dev/pkg/builder" "encr.dev/pkg/fns" daemonpb "encr.dev/proto/encore/daemon" ) // Test runs tests. func (s *Server) Test(req *daemonpb.TestRequest, stream daemonpb.Daemon_TestServer) error { ctx := stream.Context() slog := &streamLog{stream: stream, buffered: false} stderr := slog.Stderr(false) sendErr := func(err error) { stderr.Write([]byte(err.Error() + "\n")) streamExit(stream, 1) } ctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile) if err != nil { sendErr(err) return nil } defer tracer.Close() app, err := s.apps.Track(req.AppRoot) if err != nil { sendErr(err) return nil } ns, err := s.namespaceOrActive(ctx, app, nil /* tests don't support different namespaces */) if err != nil { sendErr(err) return nil } secrets := s.sm.Load(app) testCtx, cancel := context.WithCancel(ctx) defer cancel() testResults := make(chan error, 1) go func() { defer func() { if recovered := recover(); recovered != nil { var err error switch recovered := recovered.(type) { case error: err = recovered default: err = fmt.Errorf("%+v", recovered) } stack := debug.Stack() log.Err(err).Msgf("panic during test run:\n%s", stack) testResults <- fmt.Errorf("panic occured within Encore during test run: %v\n%s\n", recovered, stack) } }() testEnv := append([]string{"ENCORE_RUNTIME_LOG=error"}, req.Environ...) tp := run.TestParams{ TestSpecParams: &run.TestSpecParams{ App: app, NS: ns, WorkingDir: req.WorkingDir, Environ: testEnv, Args: req.Args, Secrets: secrets, CodegenDebug: req.CodegenDebug, TempDir: req.TempDir, }, Stdout: slog.Stdout(false), Stderr: slog.Stderr(false), } testResults <- s.mgr.Test(testCtx, tp) }() if err := <-testResults; err != nil { sendErr(err) } else { streamExit(stream, 0) } return nil } // TestSpec runs tests. func (s *Server) TestSpec(ctx context.Context, req *daemonpb.TestSpecRequest) (resp *daemonpb.TestSpecResponse, err error) { ctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, nil) if err != nil { return nil, errors.Wrap(err, "unable to begin tracing") } defer fns.CloseIgnore(tracer) app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, errors.Wrap(err, "unable to track app") } ns, err := s.namespaceOrActive(ctx, app, nil /* tests don't support different namespaces */) if err != nil { return nil, errors.Wrap(err, "unable to get namespace") } secrets := s.sm.Load(app) defer func() { if recovered := recover(); recovered != nil { var panicErr error switch recovered := recovered.(type) { case error: panicErr = recovered default: panicErr = fmt.Errorf("%+v", recovered) } stack := debug.Stack() log.Err(panicErr).Msgf("panic during test run:\n%s", stack) err = fmt.Errorf("panic during test run: %v", panicErr) } }() testEnv := append([]string{"ENCORE_RUNTIME_LOG=error"}, req.Environ...) spec, err := s.mgr.TestSpec(ctx, run.TestSpecParams{ App: app, NS: ns, WorkingDir: req.WorkingDir, Environ: testEnv, Args: req.Args, Secrets: secrets, TempDir: req.TempDir, }) if errors.Is(err, builder.ErrNoTests) { return nil, status.Error(codes.NotFound, "no tests defined") } else if err != nil { return nil, err } return &daemonpb.TestSpecResponse{ Command: spec.Command, Args: spec.Args, Environ: spec.Environ, }, nil } ================================================ FILE: cli/daemon/tracing.go ================================================ package daemon import ( "context" "path/filepath" "encr.dev/internal/etrace" ) func (s *Server) beginTracing(ctx context.Context, appRoot, workingDir string, traceFile *string) (context.Context, *etrace.Tracer, error) { if traceFile == nil { return ctx, nil, nil } var dst string if filepath.IsAbs(*traceFile) { dst = *traceFile } else { dst = filepath.Join(appRoot, workingDir, *traceFile) } return etrace.WithFileTracer(ctx, dst) } ================================================ FILE: cli/daemon/userfacing.go ================================================ package daemon import ( "context" "runtime" "github.com/cockroachdb/errors" "encr.dev/cli/daemon/apps" "encr.dev/internal/version" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/fns" "encr.dev/pkg/vcs" daemonpb "encr.dev/proto/encore/daemon" ) // GenWrappers generates Encore wrappers. func (s *Server) GenWrappers(ctx context.Context, req *daemonpb.GenWrappersRequest) (*daemonpb.GenWrappersResponse, error) { app, err := s.apps.Track(req.AppRoot) if err != nil { return nil, errors.Wrap(err, "resolve app") } if err := s.genUserFacing(ctx, app); err != nil { return nil, err } return &daemonpb.GenWrappersResponse{}, nil } // genUserFacing generates user-facing wrappers. func (s *Server) genUserFacing(ctx context.Context, app *apps.Instance) error { expSet, err := app.Experiments(nil) if err != nil { return errors.Wrap(err, "resolve experiments") } vcsRevision := vcs.GetRevision(app.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, Environ: nil, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: app, WorkingDir: ".", }) if err != nil { return errors.Wrap(err, "prepare app") } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { return errors.Wrap(err, "parse app") } if err := app.CacheMetadata(parse.Meta); err != nil { return errors.Wrap(err, "cache metadata") } err = bld.GenUserFacing(ctx, builder.GenUserFacingParams{ Build: buildInfo, App: app, Parse: parse, }) return errors.Wrap(err, "generate wrappers") } ================================================ FILE: cli/daemon/watch.go ================================================ package daemon import ( "bufio" "bytes" "context" "io/fs" "os" "path/filepath" "sync" "time" "github.com/bep/debounce" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/run" "encr.dev/pkg/watcher" "encr.dev/pkg/xos" ) func (s *Server) watchApps() { if os.Getenv("ENCORE_DAEMON_WATCH") == "0" { return } s.apps.RegisterAppListener(func(i *apps.Instance) { s.regenerateUserCode(context.Background(), i) if err := s.updateGitIgnore(i); err != nil { log.Error().Err(err).Msg("unable to update app gitignore") } }) if err := s.apps.WatchAll(s.onWatchEvent); err != nil { log.Error().Err(err).Msg("unable to set up app watchers") } else { log.Info().Msg("successfully set up file watchers") } } func (s *Server) onWatchEvent(i *apps.Instance, events []watcher.Event) { if run.IgnoreEvents(events) { return } // Use debounce to avoid calling this on every single change. s.appDebounceMu.Lock() deb := s.appDebouncers[i] if deb == nil { deb = ®enerateCodeDebouncer{ debounce: debounce.New(100 * time.Millisecond), doRun: func() { s.regenerateUserCode(context.Background(), i) }, } s.appDebouncers[i] = deb } s.appDebounceMu.Unlock() deb.ChangeEvent() } type regenerateCodeDebouncer struct { debounce func(func()) mu sync.Mutex running bool runAfter bool doRun func() } func (g *regenerateCodeDebouncer) ChangeEvent() { g.debounce(func() { g.mu.Lock() // If we're already running, mark to run again when complete. if g.running { g.runAfter = true g.mu.Unlock() return } // Otherwise, keep re-running for as long as change events come in. g.running = true g.runAfter = true // to start us off, at least once. for g.runAfter { g.runAfter = false // reset for next time g.mu.Unlock() g.doRun() // actually run g.mu.Lock() } // If we get here g.runAfter nobody requested another run, so we can stop. g.running = false g.mu.Unlock() }) } func (s *Server) regenerateUserCode(ctx context.Context, app *apps.Instance) { if err := s.genUserFacing(ctx, app); err != nil { log.Error().Err(err).Str("app", app.PlatformOrLocalID()).Msg("failed to regenerate app") } else { log.Info().Str("app", app.PlatformOrLocalID()).Msg("successfully generated user code") } } // updateGitIgnore updates the gitignore file to include Encore directives, if needed. func (s *Server) updateGitIgnore(i *apps.Instance) error { dst := filepath.Join(i.Root(), ".gitignore") data, err := os.ReadFile(dst) if err != nil && !errors.Is(err, fs.ErrNotExist) { return errors.Wrap(err, "read .gitignore") } // Find which directives are already present directives := []string{"encore.gen.go", "encore.gen.cue", "/.encore", "/encore.gen"} found := make([]bool, len(directives)) scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { ln := scanner.Text() for i, directive := range directives { if ln == directive { found[i] = true } } } // Add the ones that are missing updated := false for i, directive := range directives { if !found[i] { if len(data) > 0 && !bytes.HasSuffix(data, []byte("\n")) { data = append(data, '\n') } data = append(data, directive+"\n"...) updated = true } } // Write the file back if there were any changes if updated { return xos.WriteFile(dst, data, 0644) } return nil } ================================================ FILE: cli/internal/browser/browser.go ================================================ // This package is vendored from an internal package in the // Go standard library at cmd/internal/browser. // Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package browser provides utilities for interacting with users' browsers. package browser import ( "os" "runtime" "time" exec "golang.org/x/sys/execabs" ) // Commands returns a list of possible commands to use to open a url. func Commands() [][]string { var cmds [][]string if exe := os.Getenv("BROWSER"); exe != "" { cmds = append(cmds, []string{exe}) } switch runtime.GOOS { case "darwin": cmds = append(cmds, []string{"/usr/bin/open"}) case "windows": cmds = append(cmds, []string{"cmd", "/c", "start"}) default: if os.Getenv("DISPLAY") != "" { // xdg-open is only for use in a desktop environment. cmds = append(cmds, []string{"xdg-open"}) } } cmds = append(cmds, []string{"chrome"}, []string{"google-chrome"}, []string{"chromium"}, []string{"firefox"}, ) return cmds } // CanOpen reports whether it's likely that Open will succeed. func CanOpen() bool { cmds := Commands() for _, cmd := range cmds { if _, err := exec.LookPath(cmd[0]); err == nil { return true } } return false } // Open tries to open url in a browser and reports whether it succeeded. func Open(url string) bool { for _, args := range Commands() { cmd := exec.Command(args[0], append(args[1:], url)...) if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) { return true } } return false } // appearsSuccessful reports whether the command appears to have run successfully. // If the command runs longer than the timeout, it's deemed successful. // If the command runs within the timeout, it's deemed successful if it exited cleanly. func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool { errc := make(chan error, 1) go func() { errc <- cmd.Wait() }() select { case <-time.After(timeout): return true case err := <-errc: return err == nil } } ================================================ FILE: cli/internal/bubbles/checklist/checklist.go ================================================ package checklist import ( "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/mattn/go-runewidth" ) type Item interface { Render(selected, checked bool) string } type Model[I Item] struct { Data []I PerPage int Initial []bool // init indicates whether the data model has completed initialization init bool checked []bool // len(checked) == len(pageData) // index global real time index index int // maxIndex global max index maxIndex int // pageIndex real time index of current page pageIndex int // pageMaxIndex current page max index pageMaxIndex int // pageData data set rendered in real time on the current page pageData []I } func (m Model[I]) Selected() (I, bool) { idx := m.pageIndex if idx >= 0 && idx < len(m.pageData) { return m.pageData[idx], true } var zero I return zero, false } func (m Model[I]) Checked() []I { indices := m.CheckedIndices() items := make([]I, len(indices)) for i, idx := range indices { items[i] = m.pageData[idx] } return items } func (m Model[I]) CheckedIndices() []int { var indices []int for i, checked := range m.checked { if checked { indices = append(indices, i) } } return indices } func (m Model[I]) View() string { var out strings.Builder cursor := "»" // TODO color etc for i, obj := range m.pageData { selected := i == m.pageIndex checked := m.checked[i] if selected { out.WriteString(cursor) out.WriteString(" ") } else { out.WriteString(strings.Repeat(" ", runewidth.StringWidth(cursor)+1)) } if checked { out.WriteString("[x] ") } else { out.WriteString("[ ] ") } out.WriteString(obj.Render(selected, checked)) out.WriteString("\n") } return out.String() } // Update method responds to various events and modifies the data model // according to the corresponding events func (m Model[I]) Update(msg tea.Msg) (Model[I], tea.Cmd) { if !m.init { m.initData() return m, nil } switch msg := msg.(type) { case tea.KeyMsg: switch strings.ToLower(msg.String()) { case "down": m.moveDown() case "up": m.moveUp() case "right", "pgdown", "l", "k": m.nextPage() case "left", "pgup", "h", "j": m.prePage() case "1", "2", "3", "4", "5", "6", "7", "8", "9": num, _ := strconv.Atoi(msg.String()) idx := num - 1 m.forward(idx) case "x", " ": m.toggle() } } return m, nil } func (m *Model[I]) toggle() { idx := m.pageIndex if idx >= 0 && idx < len(m.pageData) { m.checked[idx] = !m.checked[idx] } } // moveDown executes the downward movement of the cursor, // while adjusting the internal index and refreshing the data area func (m *Model[I]) moveDown() { // the page index has not reached the maximum value, and the page // data area does not need to be updated if m.pageIndex < m.pageMaxIndex { m.pageIndex++ // check whether the global index reaches the maximum value before sliding if m.index < m.maxIndex { m.index++ } return } // the page index reaches the maximum value, slide the page data area window, // the page index maintains the maximum value if m.pageIndex == m.pageMaxIndex { // check whether the global index reaches the maximum value before sliding if m.index < m.maxIndex { // global index increment m.index++ // window slide down one data m.pageData = m.Data[m.index+1-m.PerPage : m.index+1] return } } } // moveUp performs an upward movement of the cursor, // while adjusting the internal index and refreshing the data area func (m *Model[I]) moveUp() { // the page index has not reached the minimum value, and the page // data area does not need to be updated if m.pageIndex > 0 { m.pageIndex-- // check whether the global index reaches the minimum before sliding if m.index > 0 { m.index-- } return } // the page index reaches the minimum value, slide the page data window, // and the page index maintains the minimum value if m.pageIndex == 0 { // check whether the global index reaches the minimum before sliding if m.index > 0 { // window slide up one data m.pageData = m.Data[m.index-1 : m.index-1+m.PerPage] // global index decrement m.index-- return } } } // nextPage triggers the page-down action, and does not change // the real-time page index(pageIndex) func (m *Model[I]) nextPage() { // Get the start and end position of the page data area slice: m.Data[start:end] // // note: the slice is closed left and opened right: `[start,end)` // assuming that the global data area has unlimited length, // end should always be the actual page `length+1`, // the maximum value of end should be equal to `len(m.Data)` // under limited length pageStart, pageEnd := m.pageIndexInfo() // there are two cases when `end` does not reach the maximum value if pageEnd < len(m.Data) { // the `end` value is at least one page length away from the global maximum index if len(m.Data)-pageEnd >= m.PerPage { // slide back one page in the page data area m.pageData = m.Data[pageStart+m.PerPage : pageEnd+m.PerPage] // Global real-time index increases by one page length m.index += m.PerPage } else { // `end` is less than a page length from the global maximum index // slide the page data area directly to the end m.pageData = m.Data[len(m.Data)-m.PerPage : len(m.Data)] // `sliding distance` = `position after sliding` - `position before sliding` // the global real-time index should also synchronize the same sliding distance m.index += len(m.Data) - pageEnd } } } // prePage triggers the page-up action, and does not change // the real-time page index(pageIndex) func (m *Model[I]) prePage() { // Get the start and end position of the page data area slice: m.Data[start:end] // // note: the slice is closed left and opened right: `[start,end)` // assuming that the global data area has unlimited length, // end should always be the actual page `length+1`, // the maximum value of end should be equal to `len(m.Data)` // under limited length pageStart, pageEnd := m.pageIndexInfo() // there are two cases when `start` does not reach the minimum value if pageStart > 0 { // `start` is at least one page length from the minimum if pageStart >= m.PerPage { // slide the page data area forward one page m.pageData = m.Data[pageStart-m.PerPage : pageEnd-m.PerPage] // Global real-time index reduces the length of one page m.index -= m.PerPage } else { // `start` to the minimum value less than one page length // slide the page data area directly to the start m.pageData = m.Data[:m.PerPage] // `sliding distance` = `position before sliding` - `minimum value(0)` // the global real-time index should also synchronize the same sliding distance m.index -= pageStart - 0 } } } // forward triggers a fast jump action, if the pageIndex // is invalid, keep it as it is func (m *Model[I]) forward(pageIndex int) { // pageIndex has exceeded the maximum index of the page, ignore if pageIndex > m.pageMaxIndex { return } // calculate the distance moved to pageIndex l := pageIndex - m.pageIndex // update the global real time index m.index += l // update the page real time index m.pageIndex = pageIndex } // initData initialize the data model, set the default value and // fix the wrong parameter settings during initialization func (m *Model[I]) initData() { if m.PerPage > len(m.Data) || m.PerPage < 1 { m.PerPage = len(m.Data) m.pageData = m.Data } else { m.pageData = m.Data[:m.PerPage] } m.pageIndex = 0 m.pageMaxIndex = m.PerPage - 1 m.index = 0 m.maxIndex = len(m.Data) - 1 m.checked = make([]bool, len(m.Data)) copy(m.checked, m.Initial) m.init = true } // pageIndexInfo return the start and end positions of the slice of the // page data area corresponding to the global data area func (m *Model[I]) pageIndexInfo() (start, end int) { // `Global real-time index` - `page real-time index` = `start index of page data area` start = m.index - m.pageIndex // `Page data area start index` + `single page size` = `page data area end index` end = start + m.PerPage return } ================================================ FILE: cli/internal/bubbles/selector/selector.go ================================================ package selector import ( "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/mattn/go-runewidth" ) type Item interface { Render(selected bool) string } func New[I Item](data []I, perPage int) Model[I] { m := Model[I]{data: data, perPage: perPage} m.initData() return m } type Model[I Item] struct { data []I pageData []I perPage int focused bool // init indicates whether the data model has completed initialization init bool // index global real time index index int // maxIndex global max index maxIndex int // pageIndex real time index of current page pageIndex int // pageMaxIndex current page max index pageMaxIndex int } func (m Model[I]) Selected() (I, bool) { idx := m.index if idx >= 0 && idx < len(m.data) { return m.data[idx], true } var zero I return zero, false } func (m Model[I]) View() string { var out strings.Builder cursor := "»" // TODO color etc for i, obj := range m.pageData { selected := i == m.pageIndex if selected { out.WriteString(cursor) out.WriteString(" ") } else { out.WriteString(strings.Repeat(" ", runewidth.StringWidth(cursor)+1)) } out.WriteString(obj.Render(selected)) out.WriteString("\n") } return out.String() } func (m *Model[I]) Focus() tea.Cmd { m.focused = true return nil } func (m *Model[I]) Blur() tea.Cmd { m.focused = false return nil } // Update method responds to various events and modifies the data model // according to the corresponding events func (m Model[I]) Update(msg tea.Msg) (Model[I], tea.Cmd) { if !m.init { m.initData() return m, nil } else if !m.focused { return m, nil } switch msg := msg.(type) { case tea.KeyMsg: switch strings.ToLower(msg.String()) { case "down": m.moveDown() case "up": m.moveUp() case "right", "pgdown", "l", "k": m.nextPage() case "left", "pgup", "h", "j": m.prePage() case "1", "2", "3", "4", "5", "6", "7", "8", "9": num, _ := strconv.Atoi(msg.String()) idx := num - 1 m.forward(idx) } } return m, nil } // moveDown executes the downward movement of the cursor, // while adjusting the internal index and refreshing the data area func (m *Model[I]) moveDown() { // the page index has not reached the maximum value, and the page // data area does not need to be updated if m.pageIndex < m.pageMaxIndex { m.pageIndex++ // check whether the global index reaches the maximum value before sliding if m.index < m.maxIndex { m.index++ } return } // the page index reaches the maximum value, slide the page data area window, // the page index maintains the maximum value if m.pageIndex == m.pageMaxIndex { // check whether the global index reaches the maximum value before sliding if m.index < m.maxIndex { // global index increment m.index++ // window slide down one data m.pageData = m.data[m.index+1-m.perPage : m.index+1] return } } } // moveUp performs an upward movement of the cursor, // while adjusting the internal index and refreshing the data area func (m *Model[I]) moveUp() { // the page index has not reached the minimum value, and the page // data area does not need to be updated if m.pageIndex > 0 { m.pageIndex-- // check whether the global index reaches the minimum before sliding if m.index > 0 { m.index-- } return } // the page index reaches the minimum value, slide the page data window, // and the page index maintains the minimum value if m.pageIndex == 0 { // check whether the global index reaches the minimum before sliding if m.index > 0 { // window slide up one data m.pageData = m.data[m.index-1 : m.index-1+m.perPage] // global index decrement m.index-- return } } } // nextPage triggers the page-down action, and does not change // the real-time page index(pageIndex) func (m *Model[I]) nextPage() { // Get the start and end position of the page data area slice: m.Data[start:end] // // note: the slice is closed left and opened right: `[start,end)` // assuming that the global data area has unlimited length, // end should always be the actual page `length+1`, // the maximum value of end should be equal to `len(m.Data)` // under limited length pageStart, pageEnd := m.pageIndexInfo() // there are two cases when `end` does not reach the maximum value if pageEnd < len(m.data) { // the `end` value is at least one page length away from the global maximum index if len(m.data)-pageEnd >= m.perPage { // slide back one page in the page data area m.pageData = m.data[pageStart+m.perPage : pageEnd+m.perPage] // Global real-time index increases by one page length m.index += m.perPage } else { // `end` is less than a page length from the global maximum index // slide the page data area directly to the end m.pageData = m.data[len(m.data)-m.perPage : len(m.data)] // `sliding distance` = `position after sliding` - `position before sliding` // the global real-time index should also synchronize the same sliding distance m.index += len(m.data) - pageEnd } } } // prePage triggers the page-up action, and does not change // the real-time page index(pageIndex) func (m *Model[I]) prePage() { // Get the start and end position of the page data area slice: m.Data[start:end] // // note: the slice is closed left and opened right: `[start,end)` // assuming that the global data area has unlimited length, // end should always be the actual page `length+1`, // the maximum value of end should be equal to `len(m.Data)` // under limited length pageStart, pageEnd := m.pageIndexInfo() // there are two cases when `start` does not reach the minimum value if pageStart > 0 { // `start` is at least one page length from the minimum if pageStart >= m.perPage { // slide the page data area forward one page m.pageData = m.data[pageStart-m.perPage : pageEnd-m.perPage] // Global real-time index reduces the length of one page m.index -= m.perPage } else { // `start` to the minimum value less than one page length // slide the page data area directly to the start m.pageData = m.data[:m.perPage] // `sliding distance` = `position before sliding` - `minimum value(0)` // the global real-time index should also synchronize the same sliding distance m.index -= pageStart - 0 } } } // forward triggers a fast jump action, if the pageIndex // is invalid, keep it as it is func (m *Model[I]) forward(pageIndex int) { // pageIndex has exceeded the maximum index of the page, ignore if pageIndex > m.pageMaxIndex { return } // calculate the distance moved to pageIndex l := pageIndex - m.pageIndex // update the global real time index m.index += l // update the page real time index m.pageIndex = pageIndex } // initData initialize the data model, set the default value and // fix the wrong parameter settings during initialization func (m *Model[I]) initData() { if m.perPage > len(m.data) || m.perPage < 1 { m.perPage = len(m.data) m.pageData = m.data } else { m.pageData = m.data[:m.perPage] } m.pageIndex = 0 m.pageMaxIndex = m.perPage - 1 m.index = 0 m.maxIndex = len(m.data) - 1 m.init = true } // pageIndexInfo return the start and end positions of the slice of the // page data area corresponding to the global data area func (m *Model[I]) pageIndexInfo() (start, end int) { // `Global real-time index` - `page real-time index` = `start index of page data area` start = m.index - m.pageIndex // `Page data area start index` + `single page size` = `page data area end index` end = start + m.perPage return } ================================================ FILE: cli/internal/dedent/dedent.go ================================================ /* The MIT License (MIT) Copyright (c) 2018 Peter Lithammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package dedent import ( "bytes" "regexp" "strings" ) var ( whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") ) // Dedent removes any common leading whitespace from every line in text. // // This can be used to make multiline strings to line up with the left edge of // the display, while still presenting them in the source code in indented // form. func Dedent(text string) string { if strings.HasPrefix(text, "\n") { text = strings.TrimPrefix(text, "\n") } var margin string text = whitespaceOnly.ReplaceAllString(text, "") indents := leadingWhitespace.FindAllStringSubmatch(text, -1) // Look for the longest leading string of spaces and tabs common to all // lines. for i, indent := range indents { if i == 0 { margin = indent[1] } else if strings.HasPrefix(indent[1], margin) { // Current line more deeply indented than previous winner: // no change (previous winner is still on top). continue } else if strings.HasPrefix(margin, indent[1]) { // Current line consistent with and no deeper than previous winner: // it's the new winner. margin = indent[1] } else { // Current line and previous winner have no common whitespace: // there is no margin. margin = "" break } } if margin != "" { text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "") } return text } // Bytes is like Dedent but for []bytes. func Bytes(text []byte) []byte { if bytes.HasPrefix(text, []byte("\n")) { text = bytes.TrimPrefix(text, []byte("\n")) } var margin []byte text = whitespaceOnly.ReplaceAll(text, []byte{}) indents := leadingWhitespace.FindAllSubmatch(text, -1) // Look for the longest leading string of spaces and tabs common to all // lines. for i, indent := range indents { if i == 0 { margin = indent[1] } else if bytes.HasPrefix(indent[1], margin) { // Current line more deeply indented than previous winner: // no change (previous winner is still on top). continue } else if bytes.HasPrefix(margin, indent[1]) { // Current line consistent with and no deeper than previous winner: // it's the new winner. margin = indent[1] } else { // Current line and previous winner have no common whitespace: // there is no margin. margin = []byte{} break } } if len(margin) > 0 { text = regexp.MustCompile("(?m)^"+string(margin)).ReplaceAll(text, []byte{}) } return text } ================================================ FILE: cli/internal/dedent/dedent_test.go ================================================ package dedent import ( "fmt" "testing" ) const errorMsg = "\nexpected %q\ngot %q" type dedentTest struct { text, expect string } func TestDedentNoMargin(t *testing.T) { texts := []string{ // No lines indented "Hello there.\nHow are you?\nOh good, I'm glad.", // Similar with a blank line "Hello there.\n\nBoo!", // Some lines indented, but overall margin is still zero "Hello there.\n This is indented.", // Again, add a blank line. "Hello there.\n\n Boo!\n", } for _, text := range texts { if text != Dedent(text) { t.Errorf(errorMsg, text, Dedent(text)) } } } func TestDedentEven(t *testing.T) { texts := []dedentTest{ { // All lines indented by two spaces text: " Hello there.\n How are ya?\n Oh good.", expect: "Hello there.\nHow are ya?\nOh good.", }, { // Same, with blank lines text: " Hello there.\n\n How are ya?\n Oh good.\n", expect: "Hello there.\n\nHow are ya?\nOh good.\n", }, { // Now indent one of the blank lines text: " Hello there.\n \n How are ya?\n Oh good.\n", expect: "Hello there.\n\nHow are ya?\nOh good.\n", }, } for _, text := range texts { if text.expect != Dedent(text.text) { t.Errorf(errorMsg, text.expect, Dedent(text.text)) } } } func TestDedentUneven(t *testing.T) { texts := []dedentTest{ { // Lines indented unevenly text: ` def foo(): while 1: return foo `, expect: `def foo(): while 1: return foo `, }, { // Uneven indentation with a blank line text: " Foo\n Bar\n\n Baz\n", expect: "Foo\n Bar\n\n Baz\n", }, { // Uneven indentation with a whitespace-only line text: " Foo\n Bar\n \n Baz\n", expect: "Foo\n Bar\n\n Baz\n", }, } for _, text := range texts { if text.expect != Dedent(text.text) { t.Errorf(errorMsg, text.expect, Dedent(text.text)) } } } // Dedent() should not mangle internal tabs. func TestDedentPreserveInternalTabs(t *testing.T) { text := " hello\tthere\n how are\tyou?" expect := "hello\tthere\nhow are\tyou?" if expect != Dedent(text) { t.Errorf(errorMsg, expect, Dedent(text)) } // Make sure that it preserves tabs when it's not making any changes at all if expect != Dedent(expect) { t.Errorf(errorMsg, expect, Dedent(expect)) } } // Dedent() should not mangle tabs in the margin (i.e. tabs and spaces both // count as margin, but are *not* considered equivalent). func TestDedentPreserveMarginTabs(t *testing.T) { texts := []string{ " hello there\n\thow are you?", // Same effect even if we have 8 spaces " hello there\n\thow are you?", } for _, text := range texts { d := Dedent(text) if text != d { t.Errorf(errorMsg, text, d) } } texts2 := []dedentTest{ { // Dedent() only removes whitespace that can be uniformly removed! text: "\thello there\n\thow are you?", expect: "hello there\nhow are you?", }, { text: " \thello there\n \thow are you?", expect: "hello there\nhow are you?", }, { text: " \t hello there\n \t how are you?", expect: "hello there\nhow are you?", }, { text: " \thello there\n \t how are you?", expect: "hello there\n how are you?", }, } for _, text := range texts2 { if text.expect != Dedent(text.text) { t.Errorf(errorMsg, text.expect, Dedent(text.text)) } } } func ExampleDedent() { s := ` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo tellus, facilisis nec efficitur dictum, fermentum vitae ligula. Sed eu convallis sapien.` fmt.Println(Dedent(s)) fmt.Println("-------------") fmt.Println(s) // Output: // Lorem ipsum dolor sit amet, // consectetur adipiscing elit. // Curabitur justo tellus, facilisis nec efficitur dictum, // fermentum vitae ligula. Sed eu convallis sapien. // ------------- // // Lorem ipsum dolor sit amet, // consectetur adipiscing elit. // Curabitur justo tellus, facilisis nec efficitur dictum, // fermentum vitae ligula. Sed eu convallis sapien. } func BenchmarkDedent(b *testing.B) { for i := 0; i < b.N; i++ { Dedent(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo tellus, facilisis nec efficitur dictum, fermentum vitae ligula. Sed eu convallis sapien.`) } } ================================================ FILE: cli/internal/gosym/pclntab.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* * Line tables */ package gosym import ( "bytes" "encoding/binary" "sync" ) // version of the pclntab type version int const ( verUnknown version = iota ver11 ver12 ver116 ) // A LineTable is a data structure mapping program counters to line numbers. // // In Go 1.1 and earlier, each function (represented by a Func) had its own LineTable, // and the line number corresponded to a numbering of all source lines in the // program, across all files. That absolute line number would then have to be // converted separately to a file name and line number within the file. // // In Go 1.2, the format of the data changed so that there is a single LineTable // for the entire program, shared by all Funcs, and there are no absolute line // numbers, just line numbers within specific files. // // For the most part, LineTable's methods should be treated as an internal // detail of the package; callers should use the methods on Table instead. type LineTable struct { Data []byte PC uint64 Line int // This mutex is used to keep parsing of pclntab synchronous. mu sync.Mutex // Contains the version of the pclntab section. version version // Go 1.2/1.16 state binary binary.ByteOrder quantum uint32 ptrsize uint32 funcnametab []byte cutab []byte funcdata []byte functab []byte nfunctab uint32 filetab []byte pctab []byte // points to the pctables. nfiletab uint32 funcNames map[uint32]string // cache the function names strings map[uint32]string // interned substrings of Data, keyed by offset // fileMap varies depending on the version of the object file. // For ver12, it maps the name to the index in the file table. // For ver116, it maps the name to the offset in filetab. fileMap map[string]uint32 } // NOTE(rsc): This is wrong for GOARCH=arm, which uses a quantum of 4, // but we have no idea whether we're using arm or not. This only // matters in the old (pre-Go 1.2) symbol table format, so it's not worth // fixing. const oldQuantum = 1 func (t *LineTable) parse(targetPC uint64, targetLine int) (b []byte, pc uint64, line int) { // The PC/line table can be thought of as a sequence of // * // batches. Each update batch results in a (pc, line) pair, // where line applies to every PC from pc up to but not // including the pc of the next pair. // // Here we process each update individually, which simplifies // the code, but makes the corner cases more confusing. b, pc, line = t.Data, t.PC, t.Line for pc <= targetPC && line != targetLine && len(b) > 0 { code := b[0] b = b[1:] switch { case code == 0: if len(b) < 4 { b = b[0:0] break } val := binary.BigEndian.Uint32(b) b = b[4:] line += int(val) case code <= 64: line += int(code) case code <= 128: line -= int(code - 64) default: pc += oldQuantum * uint64(code-128) continue } pc += oldQuantum } return b, pc, line } func (t *LineTable) slice(pc uint64) *LineTable { data, pc, line := t.parse(pc, -1) return &LineTable{Data: data, PC: pc, Line: line} } // PCToLine returns the line number for the given program counter. // // Deprecated: Use Table's PCToLine method instead. func (t *LineTable) PCToLine(pc uint64) int { if t.isGo12() { return t.go12PCToLine(pc, nil) } _, _, line := t.parse(pc, -1) return line } // LineToPC returns the program counter for the given line number, // considering only program counters before maxpc. // // Deprecated: Use Table's LineToPC method instead. func (t *LineTable) LineToPC(line int, maxpc uint64) uint64 { if t.isGo12() { return 0 } _, pc, line1 := t.parse(maxpc, line) if line1 != line { return 0 } // Subtract quantum from PC to account for post-line increment return pc - oldQuantum } // NewLineTable returns a new PC/line table // corresponding to the encoded data. // Text must be the start address of the // corresponding text segment. func NewLineTable(data []byte, text uint64) *LineTable { return &LineTable{Data: data, PC: text, Line: 0, funcNames: make(map[uint32]string), strings: make(map[uint32]string)} } // Go 1.2 symbol table format. // See golang.org/s/go12symtab. // // A general note about the methods here: rather than try to avoid // index out of bounds errors, we trust Go to detect them, and then // we recover from the panics and treat them as indicative of a malformed // or incomplete table. // // The methods called by symtab.go, which begin with "go12" prefixes, // are expected to have that recovery logic. // isGo12 reports whether this is a Go 1.2 (or later) symbol table. func (t *LineTable) isGo12() bool { t.parsePclnTab() return t.version >= ver12 } const go12magic = 0xfffffffb const go116magic = 0xfffffffa // uintptr returns the pointer-sized value encoded at b. // The pointer size is dictated by the table being read. func (t *LineTable) uintptr(b []byte) uint64 { if t.ptrsize == 4 { return uint64(t.binary.Uint32(b)) } return t.binary.Uint64(b) } // parsePclnTab parses the pclntab, setting the version. func (t *LineTable) parsePclnTab() { t.mu.Lock() defer t.mu.Unlock() if t.version != verUnknown { return } // Note that during this function, setting the version is the last thing we do. // If we set the version too early, and parsing failed (likely as a panic on // slice lookups), we'd have a mistaken version. // // Error paths through this code will default the version to 1.1. t.version = ver11 defer func() { // If we panic parsing, assume it's a Go 1.1 pclntab. recover() }() // Check header: 4-byte magic, two zeros, pc quantum, pointer size. if len(t.Data) < 16 || t.Data[4] != 0 || t.Data[5] != 0 || (t.Data[6] != 1 && t.Data[6] != 2 && t.Data[6] != 4) || // pc quantum (t.Data[7] != 4 && t.Data[7] != 8) { // pointer size return } var possibleVersion version leMagic := binary.LittleEndian.Uint32(t.Data) beMagic := binary.BigEndian.Uint32(t.Data) switch { case leMagic == go12magic: t.binary, possibleVersion = binary.LittleEndian, ver12 case beMagic == go12magic: t.binary, possibleVersion = binary.BigEndian, ver12 case leMagic == go116magic: t.binary, possibleVersion = binary.LittleEndian, ver116 case beMagic == go116magic: t.binary, possibleVersion = binary.BigEndian, ver116 default: return } // quantum and ptrSize are the same between 1.2 and 1.16 t.quantum = uint32(t.Data[6]) t.ptrsize = uint32(t.Data[7]) switch possibleVersion { case ver116: t.nfunctab = uint32(t.uintptr(t.Data[8:])) t.nfiletab = uint32(t.uintptr(t.Data[8+t.ptrsize:])) offset := t.uintptr(t.Data[8+2*t.ptrsize:]) t.funcnametab = t.Data[offset:] offset = t.uintptr(t.Data[8+3*t.ptrsize:]) t.cutab = t.Data[offset:] offset = t.uintptr(t.Data[8+4*t.ptrsize:]) t.filetab = t.Data[offset:] offset = t.uintptr(t.Data[8+5*t.ptrsize:]) t.pctab = t.Data[offset:] offset = t.uintptr(t.Data[8+6*t.ptrsize:]) t.funcdata = t.Data[offset:] t.functab = t.Data[offset:] functabsize := t.nfunctab*2*t.ptrsize + t.ptrsize t.functab = t.functab[:functabsize] case ver12: t.nfunctab = uint32(t.uintptr(t.Data[8:])) t.funcdata = t.Data t.funcnametab = t.Data t.functab = t.Data[8+t.ptrsize:] t.pctab = t.Data functabsize := t.nfunctab*2*t.ptrsize + t.ptrsize fileoff := t.binary.Uint32(t.functab[functabsize:]) t.functab = t.functab[:functabsize] t.filetab = t.Data[fileoff:] t.nfiletab = t.binary.Uint32(t.filetab) t.filetab = t.filetab[:t.nfiletab*4] default: panic("unreachable") } t.version = possibleVersion } // go12Funcs returns a slice of Funcs derived from the Go 1.2 pcln table. func (t *LineTable) go12Funcs() []Func { // Assume it is malformed and return nil on error. defer func() { recover() }() n := len(t.functab) / int(t.ptrsize) / 2 funcs := make([]Func, n) for i := range funcs { f := &funcs[i] f.Entry = t.uintptr(t.functab[2*i*int(t.ptrsize):]) f.End = t.uintptr(t.functab[(2*i+2)*int(t.ptrsize):]) info := t.funcdata[t.uintptr(t.functab[(2*i+1)*int(t.ptrsize):]):] f.LineTable = t f.FrameSize = int(t.binary.Uint32(info[t.ptrsize+2*4:])) f.Sym = &Sym{ Value: f.Entry, Type: 'T', Name: t.funcName(t.binary.Uint32(info[t.ptrsize:])), GoType: 0, Func: f, } } return funcs } // findFunc returns the func corresponding to the given program counter. func (t *LineTable) findFunc(pc uint64) []byte { if pc < t.uintptr(t.functab) || pc >= t.uintptr(t.functab[len(t.functab)-int(t.ptrsize):]) { return nil } // The function table is a list of 2*nfunctab+1 uintptrs, // alternating program counters and offsets to func structures. f := t.functab nf := t.nfunctab for nf > 0 { m := nf / 2 fm := f[2*t.ptrsize*m:] if t.uintptr(fm) <= pc && pc < t.uintptr(fm[2*t.ptrsize:]) { return t.funcdata[t.uintptr(fm[t.ptrsize:]):] } else if pc < t.uintptr(fm) { nf = m } else { f = f[(m+1)*2*t.ptrsize:] nf -= m + 1 } } return nil } // readvarint reads, removes, and returns a varint from *pp. func (t *LineTable) readvarint(pp *[]byte) uint32 { var v, shift uint32 p := *pp for shift = 0; ; shift += 7 { b := p[0] p = p[1:] v |= (uint32(b) & 0x7F) << shift if b&0x80 == 0 { break } } *pp = p return v } // funcName returns the name of the function found at off. func (t *LineTable) funcName(off uint32) string { if s, ok := t.funcNames[off]; ok { return s } i := bytes.IndexByte(t.funcnametab[off:], 0) s := string(t.funcnametab[off : off+uint32(i)]) t.funcNames[off] = s return s } // stringFrom returns a Go string found at off from a position. func (t *LineTable) stringFrom(arr []byte, off uint32) string { if s, ok := t.strings[off]; ok { return s } i := bytes.IndexByte(arr[off:], 0) s := string(arr[off : off+uint32(i)]) t.strings[off] = s return s } // string returns a Go string found at off. func (t *LineTable) string(off uint32) string { return t.stringFrom(t.funcdata, off) } // step advances to the next pc, value pair in the encoded table. func (t *LineTable) step(p *[]byte, pc *uint64, val *int32, first bool) bool { uvdelta := t.readvarint(p) if uvdelta == 0 && !first { return false } pcdelta := t.readvarint(p) * t.quantum *pc += uint64(pcdelta) *val += int32(-(uvdelta & 1) ^ (uvdelta >> 1)) return true } // pcvalue reports the value associated with the target pc. // off is the offset to the beginning of the pc-value table, // and entry is the start PC for the corresponding function. func (t *LineTable) pcvalue(off uint32, entry, targetpc uint64, fn *Func) int32 { p := t.pctab[off:] val := int32(-1) pc := entry for t.step(&p, &pc, &val, pc == entry) { if targetpc < pc { return val } } return -1 } // findFileLine scans one function in the binary looking for a // program counter in the given file on the given line. // It does so by running the pc-value tables mapping program counter // to file number. Since most functions come from a single file, these // are usually short and quick to scan. If a file match is found, then the // code goes to the expense of looking for a simultaneous line number match. func (t *LineTable) findFileLine(entry uint64, filetab, linetab uint32, filenum, line int32, cutab []byte) uint64 { if filetab == 0 || linetab == 0 { return 0 } fp := t.pctab[filetab:] fl := t.pctab[linetab:] fileVal := int32(-1) filePC := entry lineVal := int32(-1) linePC := entry fileStartPC := filePC for t.step(&fp, &filePC, &fileVal, filePC == entry) { fileIndex := fileVal if t.version == ver116 { fileIndex = int32(t.binary.Uint32(cutab[fileVal*4:])) } if fileIndex == filenum && fileStartPC < filePC { // fileIndex is in effect starting at fileStartPC up to // but not including filePC, and it's the file we want. // Run the PC table looking for a matching line number // or until we reach filePC. lineStartPC := linePC for linePC < filePC && t.step(&fl, &linePC, &lineVal, linePC == entry) { // lineVal is in effect until linePC, and lineStartPC < filePC. if lineVal == line { if fileStartPC <= lineStartPC { return lineStartPC } if fileStartPC < linePC { return fileStartPC } } lineStartPC = linePC } } fileStartPC = filePC } return 0 } // go12PCToLine maps program counter to line number for the Go 1.2 pcln table. func (t *LineTable) go12PCToLine(pc uint64, fn *Func) (line int) { defer func() { if recover() != nil { line = -1 } }() f := t.findFunc(pc) if f == nil { return -1 } entry := t.uintptr(f) if pc > entry { pc-- } linetab := t.binary.Uint32(f[t.ptrsize+5*4:]) return int(t.pcvalue(linetab, entry, pc, fn)) } // go12PCToFile maps program counter to file name for the Go 1.2 pcln table. func (t *LineTable) go12PCToFile(pc uint64, fn *Func) (file string) { defer func() { if recover() != nil { file = "" } }() f := t.findFunc(pc) if f == nil { return "" } entry := t.uintptr(f) if pc > entry { pc-- } filetab := t.binary.Uint32(f[t.ptrsize+4*4:]) fno := t.pcvalue(filetab, entry, pc, fn) if t.version == ver12 { if fno <= 0 { return "" } return t.string(t.binary.Uint32(t.filetab[4*fno:])) } // Go ≥ 1.16 if fno < 0 { // 0 is valid for ≥ 1.16 return "" } cuoff := t.binary.Uint32(f[t.ptrsize+7*4:]) if fnoff := t.binary.Uint32(t.cutab[(cuoff+uint32(fno))*4:]); fnoff != ^uint32(0) { return t.stringFrom(t.filetab, fnoff) } return "" } // go12LineToPC maps a (file, line) pair to a program counter for the Go 1.2/1.16 pcln table. func (t *LineTable) go12LineToPC(file string, line int) (pc uint64) { defer func() { if recover() != nil { pc = 0 } }() t.initFileMap() filenum, ok := t.fileMap[file] if !ok { return 0 } // Scan all functions. // If this turns out to be a bottleneck, we could build a map[int32][]int32 // mapping file number to a list of functions with code from that file. var cutab []byte for i := uint32(0); i < t.nfunctab; i++ { f := t.funcdata[t.uintptr(t.functab[2*t.ptrsize*i+t.ptrsize:]):] entry := t.uintptr(f) filetab := t.binary.Uint32(f[t.ptrsize+4*4:]) linetab := t.binary.Uint32(f[t.ptrsize+5*4:]) if t.version == ver116 { cuoff := t.binary.Uint32(f[t.ptrsize+7*4:]) * 4 cutab = t.cutab[cuoff:] } pc := t.findFileLine(entry, filetab, linetab, int32(filenum), int32(line), cutab) if pc != 0 { return pc } } return 0 } // initFileMap initializes the map from file name to file number. func (t *LineTable) initFileMap() { t.mu.Lock() defer t.mu.Unlock() if t.fileMap != nil { return } m := make(map[string]uint32) if t.version == ver12 { for i := uint32(1); i < t.nfiletab; i++ { s := t.string(t.binary.Uint32(t.filetab[4*i:])) m[s] = i } } else { var pos uint32 for i := uint32(0); i < t.nfiletab; i++ { s := t.stringFrom(t.filetab, pos) m[s] = pos pos += uint32(len(s) + 1) } } t.fileMap = m } // go12MapFiles adds to m a key for every file in the Go 1.2 LineTable. // Every key maps to obj. That's not a very interesting map, but it provides // a way for callers to obtain the list of files in the program. func (t *LineTable) go12MapFiles(m map[string]*Obj, obj *Obj) { defer func() { recover() }() t.initFileMap() for file := range t.fileMap { m[file] = obj } } ================================================ FILE: cli/internal/gosym/symtab.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package gosym implements access to the Go symbol // and line number tables embedded in Go binaries generated // by the gc compilers. package gosym import ( "bytes" "encoding/binary" "fmt" "strconv" "strings" ) /* * Symbols */ // A Sym represents a single symbol table entry. type Sym struct { Value uint64 Type byte Name string GoType uint64 // If this symbol is a function symbol, the corresponding Func Func *Func } // Static reports whether this symbol is static (not visible outside its file). func (s *Sym) Static() bool { return s.Type >= 'a' } // PackageName returns the package part of the symbol name, // or the empty string if there is none. func (s *Sym) PackageName() string { name := s.Name // A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package. // See variable reservedimports in cmd/compile/internal/gc/subr.go if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") { return "" } pathend := strings.LastIndex(name, "/") if pathend < 0 { pathend = 0 } if i := strings.Index(name[pathend:], "."); i != -1 { return name[:pathend+i] } return "" } // ReceiverName returns the receiver type name of this symbol, // or the empty string if there is none. func (s *Sym) ReceiverName() string { pathend := strings.LastIndex(s.Name, "/") if pathend < 0 { pathend = 0 } l := strings.Index(s.Name[pathend:], ".") r := strings.LastIndex(s.Name[pathend:], ".") if l == -1 || r == -1 || l == r { return "" } return s.Name[pathend+l+1 : pathend+r] } // BaseName returns the symbol name without the package or receiver name. func (s *Sym) BaseName() string { if i := strings.LastIndex(s.Name, "."); i != -1 { return s.Name[i+1:] } return s.Name } // A Func collects information about a single function. type Func struct { Entry uint64 *Sym End uint64 Params []*Sym // nil for Go 1.3 and later binaries Locals []*Sym // nil for Go 1.3 and later binaries FrameSize int LineTable *LineTable Obj *Obj } // An Obj represents a collection of functions in a symbol table. // // The exact method of division of a binary into separate Objs is an internal detail // of the symbol table format. // // In early versions of Go each source file became a different Obj. // // In Go 1 and Go 1.1, each package produced one Obj for all Go sources // and one Obj per C source file. // // In Go 1.2, there is a single Obj for the entire program. type Obj struct { // Funcs is a list of functions in the Obj. Funcs []Func // In Go 1.1 and earlier, Paths is a list of symbols corresponding // to the source file names that produced the Obj. // In Go 1.2, Paths is nil. // Use the keys of Table.Files to obtain a list of source files. Paths []Sym // meta } /* * Symbol tables */ // Table represents a Go symbol table. It stores all of the // symbols decoded from the program and provides methods to translate // between symbols, names, and addresses. type Table struct { Syms []Sym // nil for Go 1.3 and later binaries Funcs []Func Files map[string]*Obj // for Go 1.2 and later all files map to one Obj Objs []Obj // for Go 1.2 and later only one Obj in slice go12line *LineTable // Go 1.2 line number table } type sym struct { value uint64 gotype uint64 typ byte name []byte } var ( littleEndianSymtab = []byte{0xFD, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00} bigEndianSymtab = []byte{0xFF, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00} oldLittleEndianSymtab = []byte{0xFE, 0xFF, 0xFF, 0xFF, 0x00, 0x00} ) func walksymtab(data []byte, fn func(sym) error) error { if len(data) == 0 { // missing symtab is okay return nil } var order binary.ByteOrder = binary.BigEndian newTable := false switch { case bytes.HasPrefix(data, oldLittleEndianSymtab): // Same as Go 1.0, but little endian. // Format was used during interim development between Go 1.0 and Go 1.1. // Should not be widespread, but easy to support. data = data[6:] order = binary.LittleEndian case bytes.HasPrefix(data, bigEndianSymtab): newTable = true case bytes.HasPrefix(data, littleEndianSymtab): newTable = true order = binary.LittleEndian } var ptrsz int if newTable { if len(data) < 8 { return &DecodingError{len(data), "unexpected EOF", nil} } ptrsz = int(data[7]) if ptrsz != 4 && ptrsz != 8 { return &DecodingError{7, "invalid pointer size", ptrsz} } data = data[8:] } var s sym p := data for len(p) >= 4 { var typ byte if newTable { // Symbol type, value, Go type. typ = p[0] & 0x3F wideValue := p[0]&0x40 != 0 goType := p[0]&0x80 != 0 if typ < 26 { typ += 'A' } else { typ += 'a' - 26 } s.typ = typ p = p[1:] if wideValue { if len(p) < ptrsz { return &DecodingError{len(data), "unexpected EOF", nil} } // fixed-width value if ptrsz == 8 { s.value = order.Uint64(p[0:8]) p = p[8:] } else { s.value = uint64(order.Uint32(p[0:4])) p = p[4:] } } else { // varint value s.value = 0 shift := uint(0) for len(p) > 0 && p[0]&0x80 != 0 { s.value |= uint64(p[0]&0x7F) << shift shift += 7 p = p[1:] } if len(p) == 0 { return &DecodingError{len(data), "unexpected EOF", nil} } s.value |= uint64(p[0]) << shift p = p[1:] } if goType { if len(p) < ptrsz { return &DecodingError{len(data), "unexpected EOF", nil} } // fixed-width go type if ptrsz == 8 { s.gotype = order.Uint64(p[0:8]) p = p[8:] } else { s.gotype = uint64(order.Uint32(p[0:4])) p = p[4:] } } } else { // Value, symbol type. s.value = uint64(order.Uint32(p[0:4])) if len(p) < 5 { return &DecodingError{len(data), "unexpected EOF", nil} } typ = p[4] if typ&0x80 == 0 { return &DecodingError{len(data) - len(p) + 4, "bad symbol type", typ} } typ &^= 0x80 s.typ = typ p = p[5:] } // Name. var i int var nnul int for i = 0; i < len(p); i++ { if p[i] == 0 { nnul = 1 break } } switch typ { case 'z', 'Z': p = p[i+nnul:] for i = 0; i+2 <= len(p); i += 2 { if p[i] == 0 && p[i+1] == 0 { nnul = 2 break } } } if len(p) < i+nnul { return &DecodingError{len(data), "unexpected EOF", nil} } s.name = p[0:i] i += nnul p = p[i:] if !newTable { if len(p) < 4 { return &DecodingError{len(data), "unexpected EOF", nil} } // Go type. s.gotype = uint64(order.Uint32(p[:4])) p = p[4:] } fn(s) } return nil } // NewTable decodes the Go symbol table (the ".gosymtab" section in ELF), // returning an in-memory representation. // Starting with Go 1.3, the Go symbol table no longer includes symbol data. func NewTable(symtab []byte, pcln *LineTable) (*Table, error) { var n int err := walksymtab(symtab, func(s sym) error { n++ return nil }) if err != nil { return nil, err } var t Table if pcln.isGo12() { t.go12line = pcln } fname := make(map[uint16]string) t.Syms = make([]Sym, 0, n) nf := 0 nz := 0 lasttyp := uint8(0) err = walksymtab(symtab, func(s sym) error { n := len(t.Syms) t.Syms = t.Syms[0 : n+1] ts := &t.Syms[n] ts.Type = s.typ ts.Value = s.value ts.GoType = s.gotype switch s.typ { default: // rewrite name to use . instead of · (c2 b7) w := 0 b := s.name for i := 0; i < len(b); i++ { if b[i] == 0xc2 && i+1 < len(b) && b[i+1] == 0xb7 { i++ b[i] = '.' } b[w] = b[i] w++ } ts.Name = string(s.name[0:w]) case 'z', 'Z': if lasttyp != 'z' && lasttyp != 'Z' { nz++ } for i := 0; i < len(s.name); i += 2 { eltIdx := binary.BigEndian.Uint16(s.name[i : i+2]) elt, ok := fname[eltIdx] if !ok { return &DecodingError{-1, "bad filename code", eltIdx} } if n := len(ts.Name); n > 0 && ts.Name[n-1] != '/' { ts.Name += "/" } ts.Name += elt } } switch s.typ { case 'T', 't', 'L', 'l': nf++ case 'f': fname[uint16(s.value)] = ts.Name } lasttyp = s.typ return nil }) if err != nil { return nil, err } t.Funcs = make([]Func, 0, nf) t.Files = make(map[string]*Obj) var obj *Obj if t.go12line != nil { // Put all functions into one Obj. t.Objs = make([]Obj, 1) obj = &t.Objs[0] t.go12line.go12MapFiles(t.Files, obj) } else { t.Objs = make([]Obj, 0, nz) } // Count text symbols and attach frame sizes, parameters, and // locals to them. Also, find object file boundaries. lastf := 0 for i := 0; i < len(t.Syms); i++ { sym := &t.Syms[i] switch sym.Type { case 'Z', 'z': // path symbol if t.go12line != nil { // Go 1.2 binaries have the file information elsewhere. Ignore. break } // Finish the current object if obj != nil { obj.Funcs = t.Funcs[lastf:] } lastf = len(t.Funcs) // Start new object n := len(t.Objs) t.Objs = t.Objs[0 : n+1] obj = &t.Objs[n] // Count & copy path symbols var end int for end = i + 1; end < len(t.Syms); end++ { if c := t.Syms[end].Type; c != 'Z' && c != 'z' { break } } obj.Paths = t.Syms[i:end] i = end - 1 // loop will i++ // Record file names depth := 0 for j := range obj.Paths { s := &obj.Paths[j] if s.Name == "" { depth-- } else { if depth == 0 { t.Files[s.Name] = obj } depth++ } } case 'T', 't', 'L', 'l': // text symbol if n := len(t.Funcs); n > 0 { t.Funcs[n-1].End = sym.Value } if sym.Name == "runtime.etext" || sym.Name == "etext" { continue } // Count parameter and local (auto) syms var np, na int var end int countloop: for end = i + 1; end < len(t.Syms); end++ { switch t.Syms[end].Type { case 'T', 't', 'L', 'l', 'Z', 'z': break countloop case 'p': np++ case 'a': na++ } } // Fill in the function symbol n := len(t.Funcs) t.Funcs = t.Funcs[0 : n+1] fn := &t.Funcs[n] sym.Func = fn fn.Params = make([]*Sym, 0, np) fn.Locals = make([]*Sym, 0, na) fn.Sym = sym fn.Entry = sym.Value fn.Obj = obj if t.go12line != nil { // All functions share the same line table. // It knows how to narrow down to a specific // function quickly. fn.LineTable = t.go12line } else if pcln != nil { fn.LineTable = pcln.slice(fn.Entry) pcln = fn.LineTable } for j := i; j < end; j++ { s := &t.Syms[j] switch s.Type { case 'm': fn.FrameSize = int(s.Value) case 'p': n := len(fn.Params) fn.Params = fn.Params[0 : n+1] fn.Params[n] = s case 'a': n := len(fn.Locals) fn.Locals = fn.Locals[0 : n+1] fn.Locals[n] = s } } i = end - 1 // loop will i++ } } if t.go12line != nil && nf == 0 { t.Funcs = t.go12line.go12Funcs() } if obj != nil { obj.Funcs = t.Funcs[lastf:] } return &t, nil } // PCToFunc returns the function containing the program counter pc, // or nil if there is no such function. func (t *Table) PCToFunc(pc uint64) *Func { funcs := t.Funcs for len(funcs) > 0 { m := len(funcs) / 2 fn := &funcs[m] switch { case pc < fn.Entry: funcs = funcs[0:m] case fn.Entry <= pc && pc < fn.End: return fn default: funcs = funcs[m+1:] } } return nil } // PCToLine looks up line number information for a program counter. // If there is no information, it returns fn == nil. func (t *Table) PCToLine(pc uint64) (file string, line int, fn *Func) { if fn = t.PCToFunc(pc); fn == nil { return } if t.go12line != nil { file = t.go12line.go12PCToFile(pc, fn) line = t.go12line.go12PCToLine(pc, fn) } else { file, line = fn.Obj.lineFromAline(fn.LineTable.PCToLine(pc)) } return } // LineToPC looks up the first program counter on the given line in // the named file. It returns UnknownPathError or UnknownLineError if // there is an error looking up this line. func (t *Table) LineToPC(file string, line int) (pc uint64, fn *Func, err error) { obj, ok := t.Files[file] if !ok { return 0, nil, UnknownFileError(file) } if t.go12line != nil { pc := t.go12line.go12LineToPC(file, line) if pc == 0 { return 0, nil, &UnknownLineError{file, line} } return pc, t.PCToFunc(pc), nil } abs, err := obj.alineFromLine(file, line) if err != nil { return } for i := range obj.Funcs { f := &obj.Funcs[i] pc := f.LineTable.LineToPC(abs, f.End) if pc != 0 { return pc, f, nil } } return 0, nil, &UnknownLineError{file, line} } // LookupSym returns the text, data, or bss symbol with the given name, // or nil if no such symbol is found. func (t *Table) LookupSym(name string) *Sym { // TODO(austin) Maybe make a map for i := range t.Syms { s := &t.Syms[i] switch s.Type { case 'T', 't', 'L', 'l', 'D', 'd', 'B', 'b': if s.Name == name { return s } } } return nil } // LookupFunc returns the text, data, or bss symbol with the given name, // or nil if no such symbol is found. func (t *Table) LookupFunc(name string) *Func { for i := range t.Funcs { f := &t.Funcs[i] if f.Sym.Name == name { return f } } return nil } // SymByAddr returns the text, data, or bss symbol starting at the given address. func (t *Table) SymByAddr(addr uint64) *Sym { for i := range t.Syms { s := &t.Syms[i] switch s.Type { case 'T', 't', 'L', 'l', 'D', 'd', 'B', 'b': if s.Value == addr { return s } } } return nil } /* * Object files */ // This is legacy code for Go 1.1 and earlier, which used the // Plan 9 format for pc-line tables. This code was never quite // correct. It's probably very close, and it's usually correct, but // we never quite found all the corner cases. // // Go 1.2 and later use a simpler format, documented at golang.org/s/go12symtab. func (o *Obj) lineFromAline(aline int) (string, int) { type stackEnt struct { path string start int offset int prev *stackEnt } noPath := &stackEnt{"", 0, 0, nil} tos := noPath pathloop: for _, s := range o.Paths { val := int(s.Value) switch { case val > aline: break pathloop case val == 1: // Start a new stack tos = &stackEnt{s.Name, val, 0, noPath} case s.Name == "": // Pop if tos == noPath { return "", 0 } tos.prev.offset += val - tos.start tos = tos.prev default: // Push tos = &stackEnt{s.Name, val, 0, tos} } } if tos == noPath { return "", 0 } return tos.path, aline - tos.start - tos.offset + 1 } func (o *Obj) alineFromLine(path string, line int) (int, error) { if line < 1 { return 0, &UnknownLineError{path, line} } for i, s := range o.Paths { // Find this path if s.Name != path { continue } // Find this line at this stack level depth := 0 var incstart int line += int(s.Value) pathloop: for _, s := range o.Paths[i:] { val := int(s.Value) switch { case depth == 1 && val >= line: return line - 1, nil case s.Name == "": depth-- if depth == 0 { break pathloop } else if depth == 1 { line += val - incstart } default: if depth == 1 { incstart = val } depth++ } } return 0, &UnknownLineError{path, line} } return 0, UnknownFileError(path) } /* * Errors */ // UnknownFileError represents a failure to find the specific file in // the symbol table. type UnknownFileError string func (e UnknownFileError) Error() string { return "unknown file: " + string(e) } // UnknownLineError represents a failure to map a line to a program // counter, either because the line is beyond the bounds of the file // or because there is no code on the given line. type UnknownLineError struct { File string Line int } func (e *UnknownLineError) Error() string { return "no code at " + e.File + ":" + strconv.Itoa(e.Line) } // DecodingError represents an error during the decoding of // the symbol table. type DecodingError struct { off int msg string val interface{} } func (e *DecodingError) Error() string { msg := e.msg if e.val != nil { msg += fmt.Sprintf(" '%v'", e.val) } msg += fmt.Sprintf(" at byte %#x", e.off) return msg } ================================================ FILE: cli/internal/gosym/symtab_test.go ================================================ // Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gosym import ( "fmt" "testing" ) func assertString(t *testing.T, dsc, out, tgt string) { if out != tgt { t.Fatalf("Expected: %q Actual: %q for %s", tgt, out, dsc) } } func TestStandardLibPackage(t *testing.T) { s1 := Sym{Name: "io.(*LimitedReader).Read"} s2 := Sym{Name: "io.NewSectionReader"} assertString(t, fmt.Sprintf("package of %q", s1.Name), s1.PackageName(), "io") assertString(t, fmt.Sprintf("package of %q", s2.Name), s2.PackageName(), "io") assertString(t, fmt.Sprintf("receiver of %q", s1.Name), s1.ReceiverName(), "(*LimitedReader)") assertString(t, fmt.Sprintf("receiver of %q", s2.Name), s2.ReceiverName(), "") } func TestStandardLibPathPackage(t *testing.T) { s1 := Sym{Name: "debug/gosym.(*LineTable).PCToLine"} s2 := Sym{Name: "debug/gosym.NewTable"} assertString(t, fmt.Sprintf("package of %q", s1.Name), s1.PackageName(), "debug/gosym") assertString(t, fmt.Sprintf("package of %q", s2.Name), s2.PackageName(), "debug/gosym") assertString(t, fmt.Sprintf("receiver of %q", s1.Name), s1.ReceiverName(), "(*LineTable)") assertString(t, fmt.Sprintf("receiver of %q", s2.Name), s2.ReceiverName(), "") } func TestRemotePackage(t *testing.T) { s1 := Sym{Name: "github.com/docker/doc.ker/pkg/mflag.(*FlagSet).PrintDefaults"} s2 := Sym{Name: "github.com/docker/doc.ker/pkg/mflag.PrintDefaults"} assertString(t, fmt.Sprintf("package of %q", s1.Name), s1.PackageName(), "github.com/docker/doc.ker/pkg/mflag") assertString(t, fmt.Sprintf("package of %q", s2.Name), s2.PackageName(), "github.com/docker/doc.ker/pkg/mflag") assertString(t, fmt.Sprintf("receiver of %q", s1.Name), s1.ReceiverName(), "(*FlagSet)") assertString(t, fmt.Sprintf("receiver of %q", s2.Name), s2.ReceiverName(), "") } func TestIssue29551(t *testing.T) { symNames := []string{ "type..eq.[9]debug/elf.intName", "type..hash.debug/elf.ProgHeader", "type..eq.runtime._panic", "type..hash.struct { runtime.gList; runtime.n int32 }", "go.(*struct { sync.Mutex; math/big.table [64]math/big", } for _, symName := range symNames { s := Sym{Name: symName} assertString(t, fmt.Sprintf("package of %q", s.Name), s.PackageName(), "") } } ================================================ FILE: cli/internal/gosym/testdata/main.go ================================================ package main func linefrompc() func pcfromline() func main() { // Prevent GC of our test symbols linefrompc() pcfromline() } ================================================ FILE: cli/internal/gosym/testdata/pclinetest.h ================================================ // +build ignore // Empty include file to generate z symbols // EOF ================================================ FILE: cli/internal/gosym/testdata/pclinetest.s ================================================ TEXT ·linefrompc(SB),4,$0 // Each byte stores its line delta BYTE $2; BYTE $1; BYTE $1; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $1; BYTE $1; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; #include "pclinetest.h" BYTE $2; #include "pclinetest.h" BYTE $2; BYTE $255; TEXT ·pcfromline(SB),4,$0 // Each record stores its line delta, then n, then n more bytes BYTE $32; BYTE $0; BYTE $1; BYTE $1; BYTE $0; BYTE $1; BYTE $0; BYTE $2; BYTE $4; BYTE $0; BYTE $0; BYTE $0; BYTE $0; #include "pclinetest.h" BYTE $4; BYTE $0; BYTE $3; BYTE $3; BYTE $0; BYTE $0; BYTE $0; #include "pclinetest.h" BYTE $4; BYTE $3; BYTE $0; BYTE $0; BYTE $0; BYTE $255; ================================================ FILE: cli/internal/jsonrpc2/conn.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "context" "encoding/json" "fmt" "sync" "sync/atomic" "github.com/rs/zerolog/log" ) // Conn is the common interface to jsonrpc clients and servers. // Conn is bidirectional; it does not have a designated server or client end. // It manages the jsonrpc2 protocol, connecting responses back to their calls. type Conn interface { // Call invokes the target method and waits for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. // The response will be unmarshaled from JSON into the result. // The id returned will be unique from this connection, and can be used for // logging or tracking. Call(ctx context.Context, method string, params, result interface{}) (ID, error) // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. Notify(ctx context.Context, method string, params interface{}) error // Go starts a goroutine to handle the connection. // It must be called exactly once for each Conn. // It returns immediately. // You must block on Done() to wait for the connection to shut down. // This is a temporary measure, this should be started automatically in the // future. Go(ctx context.Context, handler Handler) // Close closes the connection and it's underlying stream. // It does not wait for the close to complete, use the Done() channel for // that. Close() error // Done returns a channel that will be closed when the processing goroutine // has terminated, which will happen if Close() is called or an underlying // stream is closed. Done() <-chan struct{} // Err returns an error if there was one from within the processing goroutine. // If err returns non nil, the connection will be already closed or closing. Err() error } type conn struct { seq int64 // must only be accessed using atomic operations writeMu sync.Mutex // protects writes to the stream stream Stream pendingMu sync.Mutex // protects the pending map pending map[ID]chan *Response done chan struct{} err atomic.Value } // NewConn creates a new connection object around the supplied stream. func NewConn(s Stream) Conn { conn := &conn{ stream: s, pending: make(map[ID]chan *Response), done: make(chan struct{}), } return conn } func (c *conn) Notify(ctx context.Context, method string, params interface{}) (err error) { notify, err := NewNotification(method, params) if err != nil { return fmt.Errorf("marshaling notify parameters: %v", err) } _, err = c.write(ctx, notify) return err } func (c *conn) Call(ctx context.Context, method string, params, result interface{}) (_ ID, err error) { // generate a new request identifier id := ID{number: atomic.AddInt64(&c.seq, 1)} call, err := NewCall(id, method, params) if err != nil { return id, fmt.Errorf("marshaling call parameters: %v", err) } // We have to add ourselves to the pending map before we send, otherwise we // are racing the response. Also add a buffer to rchan, so that if we get a // wire response between the time this call is cancelled and id is deleted // from c.pending, the send to rchan will not block. rchan := make(chan *Response, 1) c.pendingMu.Lock() c.pending[id] = rchan c.pendingMu.Unlock() defer func() { c.pendingMu.Lock() delete(c.pending, id) c.pendingMu.Unlock() }() // now we are ready to send _, err = c.write(ctx, call) if err != nil { // sending failed, we will never get a response, so don't leave it pending return id, err } // now wait for the response select { case response := <-rchan: // is it an error response? if response.err != nil { return id, response.err } if result == nil || len(response.result) == 0 { return id, nil } if err := json.Unmarshal(response.result, result); err != nil { return id, fmt.Errorf("unmarshaling result: %v", err) } return id, nil case <-ctx.Done(): return id, ctx.Err() } } func (c *conn) replier(req Request) Replier { return func(ctx context.Context, result interface{}, err error) error { call, ok := req.(*Call) if !ok { // request was a notify, no need to respond return nil } response, err := NewResponse(call.id, result, err) if err != nil { return err } _, err = c.write(ctx, response) if err != nil { // TODO(iancottrell): if a stream write fails, we really need to shut down // the whole stream return err } return nil } } func (c *conn) write(ctx context.Context, msg Message) (int64, error) { c.writeMu.Lock() defer c.writeMu.Unlock() return c.stream.Write(ctx, msg) } func (c *conn) Go(ctx context.Context, handler Handler) { go c.run(ctx, handler) } func (c *conn) run(ctx context.Context, handler Handler) { defer close(c.done) for { // get the next message msg, _, err := c.stream.Read(ctx) if err != nil { // The stream failed, we cannot continue. c.fail(err) return } switch msg := msg.(type) { case Request: if err := handler(ctx, c.replier(msg), msg); err != nil { // delivery failed, not much we can do log.Error().Err(err).Msg("jsonrpc2: message delivery failed") } case *Response: // If method is not set, this should be a response, in which case we must // have an id to send the response back to the caller. c.pendingMu.Lock() rchan, ok := c.pending[msg.id] c.pendingMu.Unlock() if ok { rchan <- msg } } } } func (c *conn) Close() error { return c.stream.Close() } func (c *conn) Done() <-chan struct{} { return c.done } func (c *conn) Err() error { if err := c.err.Load(); err != nil { return err.(error) } return nil } // fail sets a failure condition on the stream and closes it. func (c *conn) fail(err error) { c.err.Store(err) c.stream.Close() } ================================================ FILE: cli/internal/jsonrpc2/handler.go ================================================ // Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "context" "fmt" "sync" "github.com/rs/zerolog/log" ) // Handler is invoked to handle incoming requests. // The Replier sends a reply to the request and must be called exactly once. type Handler func(ctx context.Context, reply Replier, req Request) error // Replier is passed to handlers to allow them to reply to the request. // If err is set then result will be ignored. type Replier func(ctx context.Context, result interface{}, err error) error // MethodNotFound is a Handler that replies to all call requests with the // standard method not found response. // This should normally be the final handler in a chain. func MethodNotFound(ctx context.Context, reply Replier, req Request) error { return reply(ctx, nil, fmt.Errorf("%w: %q", ErrMethodNotFound, req.Method())) } // MustReplyHandler creates a Handler that panics if the wrapped handler does // not call Reply for every request that it is passed. func MustReplyHandler(handler Handler) Handler { return func(ctx context.Context, reply Replier, req Request) error { called := false err := handler(ctx, func(ctx context.Context, result interface{}, err error) error { if called { panic(fmt.Errorf("request %q replied to more than once", req.Method())) } called = true return reply(ctx, result, err) }, req) if !called { panic(fmt.Errorf("request %q was never replied to", req.Method())) } return err } } // CancelHandler returns a handler that supports cancellation, and a function // that can be used to trigger canceling in progress requests. func CancelHandler(handler Handler) (Handler, func(id ID)) { var mu sync.Mutex handling := make(map[ID]context.CancelFunc) wrapped := func(ctx context.Context, reply Replier, req Request) error { if call, ok := req.(*Call); ok { cancelCtx, cancel := context.WithCancel(ctx) ctx = cancelCtx mu.Lock() handling[call.ID()] = cancel mu.Unlock() innerReply := reply reply = func(ctx context.Context, result interface{}, err error) error { mu.Lock() delete(handling, call.ID()) mu.Unlock() return innerReply(ctx, result, err) } } return handler(ctx, reply, req) } return wrapped, func(id ID) { mu.Lock() cancel, found := handling[id] mu.Unlock() if found { cancel() } } } // AsyncHandler returns a handler that processes each request goes in its own // goroutine. // The handler returns immediately, without the request being processed. // Each request then waits for the previous request to finish before it starts. // This allows the stream to unblock at the cost of unbounded goroutines // all stalled on the previous one. func AsyncHandler(handler Handler) Handler { nextRequest := make(chan struct{}) close(nextRequest) return func(ctx context.Context, reply Replier, req Request) error { waitForPrevious := nextRequest nextRequest = make(chan struct{}) unlockNext := nextRequest innerReply := reply reply = func(ctx context.Context, result interface{}, err error) error { close(unlockNext) return innerReply(ctx, result, err) } go func() { <-waitForPrevious if err := handler(ctx, reply, req); err != nil { log.Error().Err(err).Msg("jsonrpc2: async message delivery failed") } }() return nil } } ================================================ FILE: cli/internal/jsonrpc2/jsonrpc2.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package jsonrpc2 is a minimal implementation of the JSON RPC 2 spec. // https://www.jsonrpc.org/specification // It is intended to be compatible with other implementations at the wire level. package jsonrpc2 const ( // ErrIdleTimeout is returned when serving timed out waiting for new connections. ErrIdleTimeout = constError("timed out waiting for new connections") ) type constError string func (e constError) Error() string { return string(e) } ================================================ FILE: cli/internal/jsonrpc2/jsonrpc2_test.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2_test import ( "context" "encoding/json" "flag" "fmt" "net" "path" "reflect" "testing" "encr.dev/cli/internal/jsonrpc2" ) var logRPC = flag.Bool("logrpc", false, "Enable jsonrpc2 communication logging") type callTest struct { method string params interface{} expect interface{} } var callTests = []callTest{ {"no_args", nil, true}, {"one_string", "fish", "got:fish"}, {"one_number", 10, "got:10"}, {"join", []string{"a", "b", "c"}, "a/b/c"}, // TODO: expand the test cases } func (test *callTest) newResults() interface{} { switch e := test.expect.(type) { case []interface{}: var r []interface{} for _, v := range e { r = append(r, reflect.New(reflect.TypeOf(v)).Interface()) } return r case nil: return nil default: return reflect.New(reflect.TypeOf(test.expect)).Interface() } } func (test *callTest) verifyResults(t *testing.T, results interface{}) { if results == nil { return } val := reflect.Indirect(reflect.ValueOf(results)).Interface() if !reflect.DeepEqual(val, test.expect) { t.Errorf("%v:Results are incorrect, got %+v expect %+v", test.method, val, test.expect) } } func TestCall(t *testing.T) { ctx := context.Background() for _, headers := range []bool{false, true} { name := "Plain" if headers { name = "Headers" } t.Run(name, func(t *testing.T) { a, b, done := prepare(ctx, t, headers) defer done() for _, test := range callTests { t.Run(test.method, func(t *testing.T) { results := test.newResults() if _, err := a.Call(ctx, test.method, test.params, results); err != nil { t.Fatalf("%v:Call failed: %v", test.method, err) } test.verifyResults(t, results) if _, err := b.Call(ctx, test.method, test.params, results); err != nil { t.Fatalf("%v:Call failed: %v", test.method, err) } test.verifyResults(t, results) }) } }) } } func prepare(ctx context.Context, t *testing.T, withHeaders bool) (jsonrpc2.Conn, jsonrpc2.Conn, func()) { // make a wait group that can be used to wait for the system to shut down aPipe, bPipe := net.Pipe() a := run(ctx, withHeaders, aPipe) b := run(ctx, withHeaders, bPipe) return a, b, func() { a.Close() b.Close() <-a.Done() <-b.Done() } } func run(ctx context.Context, withHeaders bool, nc net.Conn) jsonrpc2.Conn { var stream jsonrpc2.Stream if withHeaders { stream = jsonrpc2.NewHeaderStream(nc) } else { stream = jsonrpc2.NewRawStream(nc) } conn := jsonrpc2.NewConn(stream) conn.Go(ctx, testHandler(*logRPC)) return conn } func testHandler(log bool) jsonrpc2.Handler { return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { switch req.Method() { case "no_args": if len(req.Params()) > 0 { return reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) } return reply(ctx, true, nil) case "one_string": var v string if err := json.Unmarshal(req.Params(), &v); err != nil { return reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return reply(ctx, "got:"+v, nil) case "one_number": var v int if err := json.Unmarshal(req.Params(), &v); err != nil { return reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return reply(ctx, fmt.Sprintf("got:%d", v), nil) case "join": var v []string if err := json.Unmarshal(req.Params(), &v); err != nil { return reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return reply(ctx, path.Join(v...), nil) default: return jsonrpc2.MethodNotFound(ctx, reply, req) } } } ================================================ FILE: cli/internal/jsonrpc2/messages.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "encoding/json" "errors" "fmt" ) // Message is the interface to all jsonrpc2 message types. // They share no common functionality, but are a closed set of concrete types // that are allowed to implement this interface. The message types are *Call, // *Notification and *Response. type Message interface { // isJSONRPC2Message is used to make the set of message implementations a // closed set. isJSONRPC2Message() } // Request is the shared interface to jsonrpc2 messages that request // a method be invoked. // The request types are a closed set of *Call and *Notification. type Request interface { Message // Method is a string containing the method name to invoke. Method() string // Params is either a struct or an array with the parameters of the method. Params() json.RawMessage // isJSONRPC2Request is used to make the set of request implementations closed. isJSONRPC2Request() } // Notification is a request for which a response cannot occur, and as such // it has not ID. type Notification struct { // Method is a string containing the method name to invoke. method string params json.RawMessage } // Call is a request that expects a response. // The response will have a matching ID. type Call struct { // Method is a string containing the method name to invoke. method string // Params is either a struct or an array with the parameters of the method. params json.RawMessage // id of this request, used to tie the Response back to the request. id ID } // Response is a reply to a Call. // It will have the same ID as the call it is a response to. type Response struct { // result is the content of the response. result json.RawMessage // err is set only if the call failed. err error // ID of the request this is a response to. id ID } // NewNotification constructs a new Notification message for the supplied // method and parameters. func NewNotification(method string, params interface{}) (*Notification, error) { p, merr := marshalToRaw(params) return &Notification{method: method, params: p}, merr } func (msg *Notification) Method() string { return msg.method } func (msg *Notification) Params() json.RawMessage { return msg.params } func (msg *Notification) isJSONRPC2Message() {} func (msg *Notification) isJSONRPC2Request() {} func (n *Notification) MarshalJSON() ([]byte, error) { msg := wireRequest{Method: n.method, Params: &n.params} data, err := json.Marshal(msg) if err != nil { return data, fmt.Errorf("marshaling notification: %w", err) } return data, nil } func (n *Notification) UnmarshalJSON(data []byte) error { msg := wireRequest{} if err := json.Unmarshal(data, &msg); err != nil { return fmt.Errorf("unmarshaling notification: %w", err) } n.method = msg.Method if msg.Params != nil { n.params = *msg.Params } return nil } // NewCall constructs a new Call message for the supplied ID, method and // parameters. func NewCall(id ID, method string, params interface{}) (*Call, error) { p, merr := marshalToRaw(params) return &Call{id: id, method: method, params: p}, merr } func (msg *Call) Method() string { return msg.method } func (msg *Call) Params() json.RawMessage { return msg.params } func (msg *Call) ID() ID { return msg.id } func (msg *Call) isJSONRPC2Message() {} func (msg *Call) isJSONRPC2Request() {} func (c *Call) MarshalJSON() ([]byte, error) { msg := wireRequest{Method: c.method, Params: &c.params, ID: &c.id} data, err := json.Marshal(msg) if err != nil { return data, fmt.Errorf("marshaling call: %w", err) } return data, nil } func (c *Call) UnmarshalJSON(data []byte) error { msg := wireRequest{} if err := json.Unmarshal(data, &msg); err != nil { return fmt.Errorf("unmarshaling call: %w", err) } c.method = msg.Method if msg.Params != nil { c.params = *msg.Params } if msg.ID != nil { c.id = *msg.ID } return nil } // NewResponse constructs a new Response message that is a reply to the // supplied. If err is set result may be ignored. func NewResponse(id ID, result interface{}, err error) (*Response, error) { r, merr := marshalToRaw(result) return &Response{id: id, result: r, err: err}, merr } func (msg *Response) ID() ID { return msg.id } func (msg *Response) Result() json.RawMessage { return msg.result } func (msg *Response) Err() error { return msg.err } func (msg *Response) isJSONRPC2Message() {} func (r *Response) MarshalJSON() ([]byte, error) { msg := &wireResponse{Error: toWireError(r.err), ID: &r.id} if msg.Error == nil { msg.Result = &r.result } data, err := json.Marshal(msg) if err != nil { return data, fmt.Errorf("marshaling notification: %w", err) } return data, nil } func toWireError(err error) *wireError { if err == nil { // no error, the response is complete return nil } if err, ok := err.(*wireError); ok { // already a wire error, just use it return err } result := &wireError{Message: err.Error()} var wrapped *wireError if errors.As(err, &wrapped) { // if we wrapped a wire error, keep the code from the wrapped error // but the message from the outer error result.Code = wrapped.Code } return result } func (r *Response) UnmarshalJSON(data []byte) error { msg := wireResponse{} if err := json.Unmarshal(data, &msg); err != nil { return fmt.Errorf("unmarshaling jsonrpc response: %w", err) } if msg.Result != nil { r.result = *msg.Result } if msg.Error != nil { r.err = msg.Error } if msg.ID != nil { r.id = *msg.ID } return nil } func DecodeMessage(data []byte) (Message, error) { msg := wireCombined{} if err := json.Unmarshal(data, &msg); err != nil { return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err) } if msg.Method == "" { // no method, should be a response if msg.ID == nil { return nil, ErrInvalidRequest } response := &Response{id: *msg.ID} if msg.Error != nil { response.err = msg.Error } if msg.Result != nil { response.result = *msg.Result } return response, nil } // has a method, must be a request if msg.ID == nil { // request with no ID is a notify notify := &Notification{method: msg.Method} if msg.Params != nil { notify.params = *msg.Params } return notify, nil } // request with an ID, must be a call call := &Call{method: msg.Method, id: *msg.ID} if msg.Params != nil { call.params = *msg.Params } return call, nil } func marshalToRaw(obj interface{}) (json.RawMessage, error) { data, err := json.Marshal(obj) if err != nil { return json.RawMessage{}, err } return json.RawMessage(data), nil } ================================================ FILE: cli/internal/jsonrpc2/serve.go ================================================ // Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "context" "errors" "fmt" "io" "net" "os" "time" "github.com/rs/zerolog/log" ) // NOTE: This file provides an experimental API for serving multiple remote // jsonrpc2 clients over the network. For now, it is intentionally similar to // net/http, but that may change in the future as we figure out the correct // semantics. // A StreamServer is used to serve incoming jsonrpc2 clients communicating over // a newly created connection. type StreamServer interface { ServeStream(context.Context, Conn) error } // The ServerFunc type is an adapter that implements the StreamServer interface // using an ordinary function. type ServerFunc func(context.Context, Conn) error // ServeStream calls f(ctx, s). func (f ServerFunc) ServeStream(ctx context.Context, c Conn) error { return f(ctx, c) } // HandlerServer returns a StreamServer that handles incoming streams using the // provided handler. func HandlerServer(h Handler) StreamServer { return ServerFunc(func(ctx context.Context, conn Conn) error { conn.Go(ctx, h) <-conn.Done() return conn.Err() }) } // ListenAndServe starts an jsonrpc2 server on the given address. If // idleTimeout is non-zero, ListenAndServe exits after there are no clients for // this duration, otherwise it exits only on error. func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error { ln, err := net.Listen(network, addr) if err != nil { return err } defer ln.Close() if network == "unix" { defer os.Remove(addr) } return Serve(ctx, ln, server, idleTimeout) } // Serve accepts incoming connections from the network, and handles them using // the provided server. If idleTimeout is non-zero, ListenAndServe exits after // there are no clients for this duration, otherwise it exits only on error. func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error { ctx, cancel := context.WithCancel(ctx) defer cancel() // Max duration: ~290 years; surely that's long enough. const forever = 1<<63 - 1 if idleTimeout <= 0 { idleTimeout = forever } connTimer := time.NewTimer(idleTimeout) newConns := make(chan net.Conn) doneListening := make(chan error) closedConns := make(chan error) go func() { for { nc, err := ln.Accept() if err != nil { select { case doneListening <- fmt.Errorf("Accept(): %w", err): case <-ctx.Done(): } return } newConns <- nc } }() activeConns := 0 for { select { case netConn := <-newConns: activeConns++ connTimer.Stop() stream := NewHeaderStream(netConn) go func() { conn := NewConn(stream) closedConns <- server.ServeStream(ctx, conn) stream.Close() }() case err := <-doneListening: return err case err := <-closedConns: if !isClosingError(err) { log.Error().Err(err).Msg("jsonrpc2: closed connection due to error") } activeConns-- if activeConns == 0 { connTimer.Reset(idleTimeout) } case <-connTimer.C: return ErrIdleTimeout case <-ctx.Done(): return ctx.Err() } } } // isClosingError reports if the error occurs normally during the process of // closing a network connection. It uses imperfect heuristics that err on the // side of false negatives, and should not be used for anything critical. func isClosingError(err error) bool { if errors.Is(err, io.EOF) { return true } // Per https://github.com/golang/go/issues/4373, this error string should not // change. This is not ideal, but since the worst that could happen here is // some superfluous logging, it is acceptable. if err.Error() == "use of closed network connection" { return true } return false } ================================================ FILE: cli/internal/jsonrpc2/serve_test.go ================================================ // Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "context" "net" "sync" "testing" "time" ) func TestIdleTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal(err) } defer ln.Close() connect := func() net.Conn { conn, err := net.DialTimeout("tcp", ln.Addr().String(), 5*time.Second) if err != nil { panic(err) } return conn } server := HandlerServer(MethodNotFound) // connTimer := &fakeTimer{c: make(chan time.Time, 1)} var ( runErr error wg sync.WaitGroup ) wg.Add(1) go func() { defer wg.Done() runErr = Serve(ctx, ln, server, 100*time.Millisecond) }() // Exercise some connection/disconnection patterns, and then assert that when // our timer fires, the server exits. conn1 := connect() conn2 := connect() conn1.Close() conn2.Close() conn3 := connect() conn3.Close() wg.Wait() if runErr != ErrIdleTimeout { t.Errorf("run() returned error %v, want %v", runErr, ErrIdleTimeout) } } ================================================ FILE: cli/internal/jsonrpc2/servertest/servertest.go ================================================ // Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package servertest provides utilities for running tests against a remote LSP // server. package servertest import ( "context" "fmt" "net" "strings" "sync" "encr.dev/cli/internal/jsonrpc2" ) // Connector is the interface used to connect to a server. type Connector interface { Connect(context.Context) jsonrpc2.Conn } // TCPServer is a helper for executing tests against a remote jsonrpc2 // connection. Once initialized, its Addr field may be used to connect a // jsonrpc2 client. type TCPServer struct { *connList Addr string ln net.Listener framer jsonrpc2.Framer } // NewTCPServer returns a new test server listening on local tcp port and // serving incoming jsonrpc2 streams using the provided stream server. It // panics on any error. func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *TCPServer { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { panic(fmt.Sprintf("servertest: failed to listen: %v", err)) } if framer == nil { framer = jsonrpc2.NewHeaderStream } go jsonrpc2.Serve(ctx, ln, server, 0) return &TCPServer{Addr: ln.Addr().String(), ln: ln, framer: framer, connList: &connList{}} } // Connect dials the test server and returns a jsonrpc2 Connection that is // ready for use. func (s *TCPServer) Connect(ctx context.Context) jsonrpc2.Conn { netConn, err := net.Dial("tcp", s.Addr) if err != nil { panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err)) } conn := jsonrpc2.NewConn(s.framer(netConn)) s.add(conn) return conn } // PipeServer is a test server that handles connections over io.Pipes. type PipeServer struct { *connList server jsonrpc2.StreamServer framer jsonrpc2.Framer } // NewPipeServer returns a test server that can be connected to via io.Pipes. func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer { if framer == nil { framer = jsonrpc2.NewRawStream } return &PipeServer{server: server, framer: framer, connList: &connList{}} } // Connect creates new io.Pipes and binds them to the underlying StreamServer. func (s *PipeServer) Connect(ctx context.Context) jsonrpc2.Conn { sPipe, cPipe := net.Pipe() serverStream := s.framer(sPipe) serverConn := jsonrpc2.NewConn(serverStream) s.add(serverConn) go s.server.ServeStream(ctx, serverConn) clientStream := s.framer(cPipe) clientConn := jsonrpc2.NewConn(clientStream) s.add(clientConn) return clientConn } // connList tracks closers to run when a testserver is closed. This is a // convenience, so that callers don't have to worry about closing each // connection. type connList struct { mu sync.Mutex conns []jsonrpc2.Conn } func (l *connList) add(conn jsonrpc2.Conn) { l.mu.Lock() defer l.mu.Unlock() l.conns = append(l.conns, conn) } func (l *connList) Close() error { l.mu.Lock() defer l.mu.Unlock() var errmsgs []string for _, conn := range l.conns { if err := conn.Close(); err != nil { errmsgs = append(errmsgs, err.Error()) } } if len(errmsgs) > 0 { return fmt.Errorf("closing errors:\n%s", strings.Join(errmsgs, "\n")) } return nil } ================================================ FILE: cli/internal/jsonrpc2/servertest/servertest_test.go ================================================ // Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package servertest import ( "context" "testing" "time" "encr.dev/cli/internal/jsonrpc2" ) type msg struct { Msg string } func fakeHandler(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { return reply(ctx, &msg{"pong"}, nil) } func TestTestServer(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server := jsonrpc2.HandlerServer(fakeHandler) tcpTS := NewTCPServer(ctx, server, nil) defer tcpTS.Close() pipeTS := NewPipeServer(ctx, server, nil) defer pipeTS.Close() tests := []struct { name string connector Connector }{ {"tcp", tcpTS}, {"pipe", pipeTS}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conn := test.connector.Connect(ctx) conn.Go(ctx, jsonrpc2.MethodNotFound) var got msg if _, err := conn.Call(ctx, "ping", &msg{"ping"}, &got); err != nil { t.Fatal(err) } if want := "pong"; got.Msg != want { t.Errorf("conn.Call(...): returned %q, want %q", got, want) } }) } } ================================================ FILE: cli/internal/jsonrpc2/stream.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "bufio" "context" "encoding/json" "fmt" "io" "net" "strconv" "strings" ) // Stream abstracts the transport mechanics from the JSON RPC protocol. // A Conn reads and writes messages using the stream it was provided on // construction, and assumes that each call to Read or Write fully transfers // a single message, or returns an error. // A stream is not safe for concurrent use, it is expected it will be used by // a single Conn in a safe manner. type Stream interface { // Read gets the next message from the stream. Read(context.Context) (Message, int64, error) // Write sends a message to the stream. Write(context.Context, Message) (int64, error) // Close closes the connection. // Any blocked Read or Write operations will be unblocked and return errors. Close() error } // Framer wraps a network connection up into a Stream. // It is responsible for the framing and encoding of messages into wire form. // NewRawStream and NewHeaderStream are implementations of a Framer. type Framer func(conn net.Conn) Stream // NewRawStream returns a Stream built on top of a net.Conn. // The messages are sent with no wrapping, and rely on json decode consistency // to determine message boundaries. func NewRawStream(conn net.Conn) Stream { return &rawStream{ conn: conn, in: json.NewDecoder(conn), } } type rawStream struct { conn net.Conn in *json.Decoder } func (s *rawStream) Read(ctx context.Context) (Message, int64, error) { select { case <-ctx.Done(): return nil, 0, ctx.Err() default: } var raw json.RawMessage if err := s.in.Decode(&raw); err != nil { return nil, 0, err } msg, err := DecodeMessage(raw) return msg, int64(len(raw)), err } func (s *rawStream) Write(ctx context.Context, msg Message) (int64, error) { select { case <-ctx.Done(): return 0, ctx.Err() default: } data, err := json.Marshal(msg) if err != nil { return 0, fmt.Errorf("marshaling message: %v", err) } n, err := s.conn.Write(data) return int64(n), err } func (s *rawStream) Close() error { return s.conn.Close() } // NewHeaderStream returns a Stream built on top of a net.Conn. // The messages are sent with HTTP content length and MIME type headers. // This is the format used by LSP and others. func NewHeaderStream(conn net.Conn) Stream { return &headerStream{ conn: conn, in: bufio.NewReader(conn), } } type headerStream struct { conn net.Conn in *bufio.Reader } func (s *headerStream) Read(ctx context.Context) (Message, int64, error) { select { case <-ctx.Done(): return nil, 0, ctx.Err() default: } var total, length int64 // read the header, stop on the first empty line for { line, err := s.in.ReadString('\n') total += int64(len(line)) if err != nil { return nil, total, fmt.Errorf("failed reading header line: %w", err) } line = strings.TrimSpace(line) // check we have a header line if line == "" { break } colon := strings.IndexRune(line, ':') if colon < 0 { return nil, total, fmt.Errorf("invalid header line %q", line) } name, value := line[:colon], strings.TrimSpace(line[colon+1:]) switch name { case "Content-Length": if length, err = strconv.ParseInt(value, 10, 32); err != nil { return nil, total, fmt.Errorf("failed parsing Content-Length: %v", value) } if length <= 0 { return nil, total, fmt.Errorf("invalid Content-Length: %v", length) } default: // ignoring unknown headers } } if length == 0 { return nil, total, fmt.Errorf("missing Content-Length header") } data := make([]byte, length) if _, err := io.ReadFull(s.in, data); err != nil { return nil, total, err } total += length msg, err := DecodeMessage(data) return msg, total, err } func (s *headerStream) Write(ctx context.Context, msg Message) (int64, error) { select { case <-ctx.Done(): return 0, ctx.Err() default: } data, err := json.Marshal(msg) if err != nil { return 0, fmt.Errorf("marshaling message: %v", err) } n, err := fmt.Fprintf(s.conn, "Content-Length: %v\r\n\r\n", len(data)) total := int64(n) if err == nil { n, err = s.conn.Write(data) total += int64(n) } return total, err } func (s *headerStream) Close() error { return s.conn.Close() } ================================================ FILE: cli/internal/jsonrpc2/wire.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2 import ( "encoding/json" "fmt" ) // this file contains the go forms of the wire specification // see http://www.jsonrpc.org/specification for details var ( // ErrUnknown should be used for all non coded errors. ErrUnknown = NewError(-32001, "JSON RPC unknown error") // ErrParse is used when invalid JSON was received by the server. ErrParse = NewError(-32700, "JSON RPC parse error") //ErrInvalidRequest is used when the JSON sent is not a valid Request object. ErrInvalidRequest = NewError(-32600, "JSON RPC invalid request") // ErrMethodNotFound should be returned by the handler when the method does // not exist / is not available. ErrMethodNotFound = NewError(-32601, "JSON RPC method not found") // ErrInvalidParams should be returned by the handler when method // parameter(s) were invalid. ErrInvalidParams = NewError(-32602, "JSON RPC invalid params") // ErrInternal is not currently returned but defined for completeness. ErrInternal = NewError(-32603, "JSON RPC internal error") //ErrServerOverloaded is returned when a message was refused due to a //server being temporarily unable to accept any new messages. ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded") ) // wireRequest is sent to a server to represent a Call or Notify operation. type wireRequest struct { // VersionTag is always encoded as the string "2.0" VersionTag wireVersionTag `json:"jsonrpc"` // Method is a string containing the method name to invoke. Method string `json:"method"` // Params is either a struct or an array with the parameters of the method. Params *json.RawMessage `json:"params,omitempty"` // The id of this request, used to tie the Response back to the request. // Will be either a string or a number. If not set, the Request is a notify, // and no response is possible. ID *ID `json:"id,omitempty"` } // WireResponse is a reply to a Request. // It will always have the ID field set to tie it back to a request, and will // have either the Result or Error fields set depending on whether it is a // success or failure response. type wireResponse struct { // VersionTag is always encoded as the string "2.0" VersionTag wireVersionTag `json:"jsonrpc"` // Result is the response value, and is required on success. Result *json.RawMessage `json:"result,omitempty"` // Error is a structured error response if the call fails. Error *wireError `json:"error,omitempty"` // ID must be set and is the identifier of the Request this is a response to. ID *ID `json:"id,omitempty"` } // wireCombined has all the fields of both Request and Response. // We can decode this and then work out which it is. type wireCombined struct { VersionTag wireVersionTag `json:"jsonrpc"` ID *ID `json:"id,omitempty"` Method string `json:"method"` Params *json.RawMessage `json:"params,omitempty"` Result *json.RawMessage `json:"result,omitempty"` Error *wireError `json:"error,omitempty"` } // wireError represents a structured error in a Response. type wireError struct { // Code is an error code indicating the type of failure. Code int64 `json:"code"` // Message is a short description of the error. Message string `json:"message"` // Data is optional structured data containing additional information about the error. Data *json.RawMessage `json:"data,omitempty"` } // wireVersionTag is a special 0 sized struct that encodes as the jsonrpc version // tag. // It will fail during decode if it is not the correct version tag in the // stream. type wireVersionTag struct{} // ID is a Request identifier. type ID struct { name string number int64 } func NewError(code int64, message string) error { return &wireError{ Code: code, Message: message, } } func (err *wireError) Error() string { return err.Message } func (wireVersionTag) MarshalJSON() ([]byte, error) { return json.Marshal("2.0") } func (wireVersionTag) UnmarshalJSON(data []byte) error { version := "" if err := json.Unmarshal(data, &version); err != nil { return err } if version != "2.0" { return fmt.Errorf("invalid RPC version %v", version) } return nil } // NewIntID returns a new numerical request ID. func NewIntID(v int64) ID { return ID{number: v} } // NewStringID returns a new string request ID. func NewStringID(v string) ID { return ID{name: v} } // Format writes the ID to the formatter. // If the rune is q the representation is non ambiguous, // string forms are quoted, number forms are preceded by a # func (id ID) Format(f fmt.State, r rune) { numF, strF := `%d`, `%s` if r == 'q' { numF, strF = `#%d`, `%q` } switch { case id.name != "": _, _ = fmt.Fprintf(f, strF, id.name) default: _, _ = fmt.Fprintf(f, numF, id.number) } } func (id *ID) MarshalJSON() ([]byte, error) { if id.name != "" { return json.Marshal(id.name) } return json.Marshal(id.number) } func (id *ID) UnmarshalJSON(data []byte) error { *id = ID{} if err := json.Unmarshal(data, &id.number); err == nil { return nil } return json.Unmarshal(data, &id.name) } ================================================ FILE: cli/internal/jsonrpc2/wire_test.go ================================================ // Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package jsonrpc2_test import ( "bytes" "encoding/json" "fmt" "testing" "encr.dev/cli/internal/jsonrpc2" ) var wireIDTestData = []struct { name string id jsonrpc2.ID encoded []byte plain string quoted string }{ { name: `empty`, encoded: []byte(`0`), plain: `0`, quoted: `#0`, }, { name: `number`, id: jsonrpc2.NewIntID(43), encoded: []byte(`43`), plain: `43`, quoted: `#43`, }, { name: `string`, id: jsonrpc2.NewStringID("life"), encoded: []byte(`"life"`), plain: `life`, quoted: `"life"`, }, } func TestIDFormat(t *testing.T) { for _, test := range wireIDTestData { t.Run(test.name, func(t *testing.T) { if got := fmt.Sprint(test.id); got != test.plain { t.Errorf("got %s expected %s", got, test.plain) } if got := fmt.Sprintf("%q", test.id); got != test.quoted { t.Errorf("got %s want %s", got, test.quoted) } }) } } func TestIDEncode(t *testing.T) { for _, test := range wireIDTestData { t.Run(test.name, func(t *testing.T) { data, err := json.Marshal(&test.id) if err != nil { t.Fatal(err) } checkJSON(t, data, test.encoded) }) } } func TestIDDecode(t *testing.T) { for _, test := range wireIDTestData { t.Run(test.name, func(t *testing.T) { var got *jsonrpc2.ID if err := json.Unmarshal(test.encoded, &got); err != nil { t.Fatal(err) } if got == nil { t.Errorf("got nil want %s", test.id) } else if *got != test.id { t.Errorf("got %s want %s", got, test.id) } }) } } func TestErrorEncode(t *testing.T) { b, err := json.Marshal(jsonrpc2.NewError(0, "")) if err != nil { t.Fatal(err) } checkJSON(t, b, []byte(`{ "code": 0, "message": "" }`)) } func TestErrorResponse(t *testing.T) { // originally reported in #39719, this checks that result is not present if // it is an error response r, _ := jsonrpc2.NewResponse(jsonrpc2.NewIntID(3), nil, fmt.Errorf("computing fix edits")) data, err := json.Marshal(r) if err != nil { t.Fatal(err) } checkJSON(t, data, []byte(`{ "jsonrpc":"2.0", "error":{ "code":0, "message":"computing fix edits" }, "id":3 }`)) } func checkJSON(t *testing.T, got, want []byte) { // compare the compact form, to allow for formatting differences g := &bytes.Buffer{} if err := json.Compact(g, []byte(got)); err != nil { t.Fatal(err) } w := &bytes.Buffer{} if err := json.Compact(w, []byte(want)); err != nil { t.Fatal(err) } if g.String() != w.String() { t.Fatalf("Got:\n%s\nWant:\n%s", g, w) } } ================================================ FILE: cli/internal/login/deviceauth.go ================================================ package login import ( "context" "crypto/sha256" "encoding/base64" "fmt" "os" "time" "github.com/briandowns/spinner" "github.com/fatih/color" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/browser" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" "encr.dev/internal/env" ) // DeviceAuth logs in the suser with the device auth flow. func DeviceAuth() (*conf.Config, error) { // Generate PKCE challenge. randData, err := genRandData() if err != nil { return nil, fmt.Errorf("could not generate random data: %v", err) } codeVerifier := base64.RawURLEncoding.EncodeToString([]byte(randData)) challengeHash := sha256.Sum256([]byte(codeVerifier)) codeChallenge := base64.RawURLEncoding.EncodeToString(challengeHash[:]) resp, err := platform.BeginDeviceAuthFlow(context.Background(), platform.BeginAuthorizationFlowParams{ CodeChallenge: codeChallenge, ClientID: "encore_cli", }) if err != nil { return nil, err } var ( bold = color.New(color.Bold) faint = color.New(color.Faint) ) fmt.Printf("Your pairing code is %s\n", bold.Sprint(resp.UserCode)) faint.Println("This pairing code verifies your authentication with Encore.") inputCh := make(chan struct{}, 1) spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) spin.Prefix = "Waiting for login confirmation..." if !env.IsSSH() && browser.CanOpen() { fmt.Fprintf(os.Stdout, "Press Enter to open the browser or visit %s (^C to quit)\n", resp.VerificationURI) // Asynchronously wait for input. w := waitForEnterPress() defer w.Stop() go func() { select { case <-w.pressed: inputCh <- struct{}{} case <-w.quit: } }() } else { // On Windows we need a proper \r\n newline to ensure the URL detection doesn't extend to the next line. // fmt.Fprintln and family prints just a simple \n, so don't use that. fmt.Fprintf(os.Stdout, "To authenticate with Encore, please go to: %s%s", resp.VerificationURI, cmdutil.Newline) spin.Start() } resultCh := make(chan deviceAuthResult, 1) go pollForDeviceAuthResult(codeVerifier, resp, resultCh) for { select { case <-inputCh: // The user hit Enter; show a spinner and try to open the browser. spin.Start() if !browser.Open(resp.VerificationURI) { spin.FinalMSG = fmt.Sprintf("Failed to open browser, please go to %s manually.", resp.VerificationURI) spin.Stop() // Create a new spinner so the message above stays around. spin = spinner.New(spinner.CharSets[14], 100*time.Millisecond) spin.Prefix = "Waiting for login confirmation..." spin.Start() } case res := <-resultCh: if res.err != nil { spin.FinalMSG = fmt.Sprintf("Failed to log in: %v", res.err) spin.Stop() return nil, res.err } spin.Stop() return res.cfg, nil } } } type deviceAuthResult struct { cfg *conf.Config err error } func pollForDeviceAuthResult(codeVerifier string, data *platform.BeginAuthorizationFlowResponse, resultCh chan<- deviceAuthResult) { PollLoop: for { interval := data.Interval if interval <= 0 { interval = 5 } time.Sleep(time.Duration(interval) * time.Second) resp, err := platform.PollDeviceAuthFlow(context.Background(), platform.PollDeviceAuthFlowParams{ DeviceCode: data.DeviceCode, CodeVerifier: codeVerifier, }) if err != nil { if e, ok := err.(platform.Error); ok { switch e.Code { case "auth_pending": // Not yet authorized, continue polling. continue PollLoop case "rate_limited": // Spurious error; sleep a bit extra before retrying to be safe. time.Sleep(5 * time.Second) continue PollLoop } } resultCh <- deviceAuthResult{err: err} return } cfg := &conf.Config{Token: *resp.Token, Actor: resp.Actor, Email: resp.Email, AppSlug: resp.AppSlug} resultCh <- deviceAuthResult{cfg: cfg} return } } type enterPressWaiter struct { quit chan struct{} // close to abort the waiter pressed chan struct{} // closed when enter has been pressed runDone chan struct{} // closed when the run goroutine has exited } func waitForEnterPress() *enterPressWaiter { w := &enterPressWaiter{ quit: make(chan struct{}), pressed: make(chan struct{}, 1), runDone: make(chan struct{}), } go w.run() return w } func (w *enterPressWaiter) run() { defer close(w.runDone) fmt.Fscanln(os.Stdin) select { case w.pressed <- struct{}{}: case <-w.quit: } } func (w *enterPressWaiter) Stop() { close(w.quit) os.Stdin.SetReadDeadline(time.Now()) // interrupt the pending read // Asynchronously wait for the run goroutine to exit before // we reset the read deadline. go func() { <-w.runDone os.Stdin.SetReadDeadline(time.Time{}) // reset read deadline }() } ================================================ FILE: cli/internal/login/interactive.go ================================================ package login import ( "context" "crypto/sha256" "encoding/base64" "errors" "fmt" "net" "net/http" "os" "time" "github.com/briandowns/spinner" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/internal/browser" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" "encr.dev/internal/env" ) // interactive keeps the state of an ongoing login flow. type interactive struct { result chan *conf.Config // Successful logins are sent on this state string challenge string pubKey, privKey string srv *http.Server ln net.Listener } // Interactive begins an interactive login attempt. func Interactive() (*conf.Config, error) { // Generate initial request state state, err1 := genRandData() challenge, err2 := genRandData() if err1 != nil || err2 != nil { return nil, fmt.Errorf("could not generate random data: %v/%v", err1, err2) } challengeHash := sha256.Sum256([]byte(challenge)) encodedChallenge := base64.RawURLEncoding.EncodeToString(challengeHash[:]) ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } defer ln.Close() addr := ln.Addr().(*net.TCPAddr) url := fmt.Sprintf("http://localhost:%d/oauth", addr.Port) req := &platform.CreateOAuthSessionParams{ Challenge: encodedChallenge, State: state, RedirectURL: url, } authURL, err := platform.CreateOAuthSession(context.Background(), req) if err != nil { return nil, err } flow := &interactive{ result: make(chan *conf.Config), state: state, challenge: challenge, } flow.srv = &http.Server{Handler: http.HandlerFunc(flow.oauthHandler)} go flow.srv.Serve(ln) spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) if env.IsSSH() || !browser.Open(authURL) { // On Windows we need a proper \r\n newline to ensure the URL detection doesn't extend to the next line. // fmt.Fprintln and family prints just a simple \n, so don't use that. fmt.Fprint(os.Stdout, "Log in to Encore using your browser here: ", authURL, cmdutil.Newline) } else { spin.Prefix = "Waiting for login to complete " spin.Start() defer spin.Stop() } select { case res := <-flow.result: return res, nil case <-time.After(10 * time.Minute): return nil, errors.New("Timed out waiting for login confirmation") } } func (f *interactive) oauthHandler(w http.ResponseWriter, req *http.Request) { if req.URL.Path != "/oauth" { http.Error(w, "Not Found", http.StatusNotFound) return } code := req.FormValue("code") reqState := req.FormValue("state") if code == "" || reqState != f.state { http.Error(w, "Bad Request (bad code or state)", http.StatusBadRequest) return } params := &platform.ExchangeOAuthTokenParams{ Challenge: f.challenge, Code: code, } resp, err := platform.ExchangeOAuthToken(req.Context(), params) if err != nil { http.Error(w, "Could not exchange token: "+err.Error(), http.StatusBadGateway) return } else if resp.Token == nil { http.Error(w, "Invalid response: missing token", http.StatusBadGateway) return } conf := &conf.Config{Token: *resp.Token, Actor: resp.Actor, Email: resp.Email, AppSlug: resp.AppSlug} select { case f.result <- conf: http.Redirect(w, req, "https://www.encore.dev/auth/success", http.StatusFound) default: http.Error(w, "Unexpected request", http.StatusBadRequest) } } ================================================ FILE: cli/internal/login/login.go ================================================ // Package login handles login and authentication with Encore's platform. package login import ( "context" "crypto/rand" "encoding/base64" "fmt" "encr.dev/cli/internal/browser" "encr.dev/cli/internal/platform" "encr.dev/internal/conf" "encr.dev/internal/env" ) func DecideFlow() (*conf.Config, error) { if env.IsSSH() || !browser.CanOpen() { return DeviceAuth() } return Interactive() } func WithAuthKey(authKey string) (*conf.Config, error) { params := &platform.ExchangeAuthKeyParams{ AuthKey: authKey, } resp, err := platform.ExchangeAuthKey(context.Background(), params) if err != nil { return nil, err } else if resp.Token == nil { return nil, fmt.Errorf("invalid response: missing token") } tok := resp.Token conf := &conf.Config{Token: *tok, Actor: resp.Actor, AppSlug: resp.AppSlug} return conf, nil } func genRandData() (string, error) { data := make([]byte, 32) _, err := rand.Read(data[:]) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(data), nil } ================================================ FILE: cli/internal/manifest/manifest.go ================================================ // Package manifest reads and writes Encore app manifests. package manifest import ( "crypto/rand" "encoding/base32" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "encr.dev/pkg/xos" ) // Manifest represents the persisted manifest for // an Encore application. It is not intended to be committed to // source control. type Manifest struct { // AppID is a unique identifier for the app. // It uses the encore.dev app slug if the app // is linked, and is otherwise a randomly generated id. AppID string `json:"appID,omitempty"` // LocalID is a unique id for the app that's only used locally. // It is randomly generated on first use. LocalID string `json:"local_id"` // Tutorial is set to the name of the tutorial the user is currently on or empty. Tutorial string `json:"tutorial"` } // SetTutorial sets the tutorial field on the app manifest func SetTutorial(appRoot string, tutorial string) (err error) { defer func() { if err != nil { err = fmt.Errorf("read/create manifest: %v", err) } }() var man Manifest // Use the existing manifest if we have one. cfgPath := filepath.Join(appRoot, ".encore", "manifest.json") if data, err := os.ReadFile(cfgPath); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } else if err == nil { err = json.Unmarshal(data, &man) if err != nil { return err } } man.Tutorial = tutorial // Write it back. out, _ := json.Marshal(&man) if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil { return err } else if err := xos.WriteFile(cfgPath, out, 0644); err != nil { return err } return nil } // ReadOrCreate reads the manifest for the app rooted at appRoot. // If it doesn't exist it creates it first. func ReadOrCreate(appRoot string) (mf *Manifest, err error) { defer func() { if err != nil { err = fmt.Errorf("read/create manifest: %v", err) } }() var man Manifest // Use the existing manifest if we have one. cfgPath := filepath.Join(appRoot, ".encore", "manifest.json") if data, err := os.ReadFile(cfgPath); err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, err } else if err == nil { err = json.Unmarshal(data, &man) if err != nil { return nil, err } } // Generate a local ID if we don't have one. if man.LocalID == "" { // If we have a legacy AppID, migrate that over to the local id. if man.AppID != "" { man.LocalID = man.AppID man.AppID = "" } else { id, err := genID() if err != nil { return nil, err } man.LocalID = id } } // Write it back. out, _ := json.Marshal(&man) if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil { return nil, err } else if err := xos.WriteFile(cfgPath, out, 0644); err != nil { return nil, err } return &man, nil } const encodeStr = "23456789abcdefghikmnopqrstuvwxyz" var encoding = base32.NewEncoding(encodeStr).WithPadding(base32.NoPadding) // genID generates a random id for a local ID // // Note: the fact this generates without a hyphen is expected and used // to identify a local ID vs a platform ID func genID() (string, error) { var data [3]byte if _, err := rand.Read(data[:]); err != nil { return "", err } return encoding.EncodeToString(data[:]), nil } ================================================ FILE: cli/internal/onboarding/onboarding.go ================================================ package onboarding import ( "encoding/json" "errors" "io/fs" "os" "path/filepath" "time" "encr.dev/pkg/xos" ) type Event struct { time.Time } type State struct { FirstRun Event `json:"first_run"` DeployHint Event `json:"deploy_hint"` EventMap map[string]*Event `json:"carousel"` } func (e *State) Property(prop string) *Event { if e.EventMap == nil { e.EventMap = map[string]*Event{} } _, ok := e.EventMap[prop] if !ok { e.EventMap[prop] = &Event{} } return e.EventMap[prop] } func (e *Event) IsSet() bool { return !e.IsZero() } func (e *Event) Set() bool { if !e.IsSet() { e.Time = time.Now() return true } return false } func Load() (*State, error) { cfg := &State{EventMap: map[string]*Event{}} path, err := configPath() if err != nil { return cfg, err } data, err := os.ReadFile(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { err = nil } return cfg, err } err = json.Unmarshal(data, &cfg) if err != nil { return cfg, err } if cfg.FirstRun.IsSet() && time.Since(cfg.FirstRun.Time) > 14*24*time.Hour { cfg.Property("carousel").Set() } return cfg, err } func (cfg *State) Write() error { path, err := configPath() if err != nil { return err } data, err := json.Marshal(cfg) if err != nil { return err } else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return xos.WriteFile(path, data, 0644) } func configPath() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "encore", "onboarding.json"), nil } ================================================ FILE: cli/internal/platform/api.go ================================================ package platform import ( "context" "encoding/base64" "fmt" "io" "net/url" "time" "github.com/golang/protobuf/proto" "github.com/gorilla/websocket" "encr.dev/pkg/fns" metav1 "encr.dev/proto/encore/parser/meta/v1" ) type CreateAppParams struct { Name string InitialSecrets map[string]string AppRootDir string } type App struct { ID string `json:"eid"` LegacyID string `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Description string `json:"description"` // can be blank MainBranch *string `json:"main_branch"` // nil if not set } type Rollout struct { ID string `json:"id"` EnvName string `json:"env_name"` } type Env struct { ID string `json:"id"` Slug string `json:"slug"` Type string `json:"type"` Cloud string `json:"cloud"` } func CreateApp(ctx context.Context, p *CreateAppParams) (*App, error) { var resp App err := call(ctx, "POST", "/apps", p, &resp, true) return &resp, err } func Deploy(ctx context.Context, appSlug, env, sha, branch string) (*Rollout, error) { var resp Rollout err := call( ctx, "POST", fmt.Sprintf( "/apps/%s/envs/%s/rollouts", url.PathEscape(appSlug), url.PathEscape(env), ), map[string]string{ "sha": sha, "branch": branch, }, &resp, true) return &resp, err } func ListApps(ctx context.Context) ([]*App, error) { var resp []*App err := call(ctx, "GET", "/user/apps", nil, &resp, true) return resp, err } func GetApp(ctx context.Context, appSlug string) (*App, error) { var resp App err := call(ctx, "GET", "/apps/"+url.PathEscape(appSlug), nil, &resp, true) return &resp, err } func ListEnvs(ctx context.Context, appSlug string) ([]*Env, error) { var resp []*Env err := call(ctx, "GET", "/apps/"+url.PathEscape(appSlug)+"/envs", nil, &resp, true) return resp, err } type SecretKind string const ( DevelopmentSecrets SecretKind = "development" ProductionSecrets SecretKind = "production" ) func GetLocalSecretValues(ctx context.Context, appSlug string, poll bool) (secrets map[string]string, err error) { url := "/apps/" + url.PathEscape(appSlug) + "/secrets:values?kind=development" if poll { url += "&poll=true" } err = call(ctx, "GET", url, nil, &secrets, true) return secrets, err } type SecretVersion struct { Number int `json:"number"` Created time.Time `json:"created"` } func SetAppSecret(ctx context.Context, appSlug string, kind SecretKind, secretKey, value string) (*SecretVersion, error) { params := struct { Kind SecretKind Value string }{Kind: kind, Value: value} url := fmt.Sprintf("/apps/%s/secrets/%s/versions", url.PathEscape(appSlug), url.PathEscape(secretKey), ) var resp SecretVersion err := call(ctx, "POST", url, ¶ms, &resp, true) return &resp, err } func GetEnvMeta(ctx context.Context, appSlug, envName string) (*metav1.Data, error) { url := "/apps/" + url.PathEscape(appSlug) + "/envs/" + url.PathEscape(envName) + "/meta" body, err := rawCall(ctx, "GET", url, nil, true) if err != nil { return nil, err } defer fns.CloseIgnore(body) data, err := io.ReadAll(body) if err != nil { return nil, fmt.Errorf("platform.GetEnvMeta: %v", err) } var md metav1.Data if err := proto.Unmarshal(data, &md); err != nil { return nil, fmt.Errorf("platform.GetEnvMeta: %v", err) } return &md, nil } func DBConnect(ctx context.Context, appSlug, envSlug, dbName, role string, startupData []byte) (*websocket.Conn, error) { path := escapef("/apps/%s/envs/%s/sqldb-connect/%s", appSlug, envSlug, dbName) if role != "" { path += "?role=" + url.QueryEscape(role) } return wsDial(ctx, path, true, map[string]string{ "X-Startup-Message": base64.StdEncoding.EncodeToString(startupData), }) } func EnvLogs(ctx context.Context, appSlug, envSlug string) (*websocket.Conn, error) { path := escapef("/apps/%s/envs/%s/log", appSlug, envSlug) return wsDial(ctx, path, true, nil) } func KubernetesClusters(ctx context.Context, appSlug string, envName string) (string, string, []KubeCtlConfig, error) { type K8SClusterConfigs struct { AppSlug string `json:"app"` EnvName string `json:"env"` Clusters []KubeCtlConfig `json:"clusters"` } var resp K8SClusterConfigs err := call(ctx, "GET", "/apps/"+url.PathEscape(appSlug)+"/envs/"+url.PathEscape(envName)+"/k8s-clusters", nil, &resp, true) return resp.AppSlug, resp.EnvName, resp.Clusters, err } type KubeCtlConfig struct { EnvID string `json:"env_id"` // The ID of the environment ResID string `json:"res_id"` // The ID of the cluster Name string `json:"name"` // The name of the cluster DefaultNamespace string `json:"namespace,omitempty"` // The default namespace for the cluster (if any) } func escapef(format string, args ...string) string { ifaces := make([]interface{}, len(args)) for i, arg := range args { ifaces[i] = url.PathEscape(arg) } return fmt.Sprintf(format, ifaces...) } ================================================ FILE: cli/internal/platform/client.go ================================================ package platform import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "runtime" "github.com/gorilla/websocket" jsoniter "github.com/json-iterator/go" "github.com/rs/zerolog/log" "encr.dev/cli/internal/platform/gql" "encr.dev/internal/conf" "encr.dev/internal/version" "encr.dev/pkg/fns" ) type Error struct { HTTPStatus string `json:"-"` HTTPCode int `json:"-"` Code string Detail json.RawMessage } type ValidationDetails struct { Field string `json:"field"` Type string `json:"type"` } func (e Error) Error() string { if len(e.Detail) > 0 { return fmt.Sprintf("http %s: code=%s detail=%s", e.HTTPStatus, e.Code, e.Detail) } return fmt.Sprintf("http %s: code=%s", e.HTTPStatus, e.Code) } // call makes a call to the API endpoint given by method and path. // If reqParams and respParams are non-nil they are JSON-marshalled/unmarshalled. func call(ctx context.Context, method, path string, reqParams, respParams interface{}, auth bool) (err error) { log.Trace().Interface("request", reqParams).Msgf("-> %s %s", method, path) defer func() { if err != nil { log.Trace().Err(err).Msgf("<- ERR %s %s", method, path) } else { log.Trace().Interface("response", respParams).Msgf("<- OK %s %s", method, path) } }() resp, err := sendPlatformReq(ctx, method, path, reqParams, auth) if err != nil { return err } defer fns.CloseIgnore(resp.Body) var respStruct struct { OK bool Error Error Data json.RawMessage } if err := json.NewDecoder(resp.Body).Decode(&respStruct); err != nil { return fmt.Errorf("decode response: %v", err) } else if !respStruct.OK { e := respStruct.Error e.HTTPCode = resp.StatusCode e.HTTPStatus = resp.Status return e } if respParams != nil { if err := json.Unmarshal([]byte(respStruct.Data), respParams); err != nil { return fmt.Errorf("decode response data: %v", err) } } return nil } type graphqlRequest struct { Query string `json:"query"` Variables map[string]interface{} `json:"variables,omitempty"` OperationName string `json:"operationName,omitempty"` Extensions map[string]interface{} `json:"extensions,omitempty"` } var graphqlDecoder = (func() jsoniter.API { enc := jsoniter.Config{}.Froze() enc.RegisterExtension(NewInterfaceCodecExtension()) return enc })() // graphqlCall makes a GraphQL request. func graphqlCall(ctx context.Context, req graphqlRequest, respData any, auth bool) (err error) { log.Trace().Msgf("-> graphql %s: %+v", req.OperationName, req.Variables) httpResp, err := sendPlatformReq(ctx, "POST", "/graphql", req, auth) if err != nil { return err } defer fns.CloseIgnore(httpResp.Body) var respStruct struct { Data json.RawMessage Errors gql.ErrorList Extensions map[string]interface{} } defer func() { if err != nil { log.Trace().Msgf("<- ERR graphql %s: %v", req.OperationName, err) } else { log.Trace().Msgf("<- OK graphql %s: %s", req.OperationName, respStruct.Data) } }() if err := json.NewDecoder(httpResp.Body).Decode(&respStruct); err != nil { return fmt.Errorf("decode response: %v", err) } else if len(respStruct.Errors) > 0 { return fmt.Errorf("graphql request failed: %w", respStruct.Errors) } if respData != nil { if err := graphqlDecoder.NewDecoder(bytes.NewReader(respStruct.Data)).Decode(respData); err != nil { return fmt.Errorf("decode graphql data: %v", err) } } return nil } // rawCall makes a call to the API endpoint given by method and path. // It returns the raw HTTP response body on success; it must be closed by the caller. func rawCall(ctx context.Context, method, path string, reqParams interface{}, auth bool) (respBody io.ReadCloser, err error) { log.Trace().Msgf("-> %s %s: %+v", method, path, reqParams) defer func() { if err != nil { log.Trace().Msgf("<- ERR %s %s: %v", method, path, err) } else { log.Trace().Msgf("<- OK %s %s", method, path) } }() resp, err := sendPlatformReq(ctx, method, path, reqParams, auth) if err != nil { return nil, err } defer func() { if err != nil { _ = resp.Body.Close() } }() if resp.StatusCode >= 400 { return nil, decodeErrorResponse(resp) } return resp.Body, nil } func sendPlatformReq(ctx context.Context, method, path string, reqParams any, auth bool) (httpResp *http.Response, err error) { defer func() { if err != nil { err = fmt.Errorf("%s %s: %w", method, path, err) } }() var body io.Reader if reqParams != nil { reqData, err := json.Marshal(reqParams) if err != nil { return nil, fmt.Errorf("marshal request: %v", err) } body = bytes.NewReader(reqData) } req, err := http.NewRequestWithContext(ctx, method, conf.APIBaseURL+path, body) if err != nil { return nil, err } if reqParams != nil { req.Header.Set("Content-Type", "application/json") } return doPlatformReq(req, auth) } func doPlatformReq(req *http.Request, auth bool) (httpResp *http.Response, err error) { // Add a very limited amount of information for diagnostics req.Header.Set("User-Agent", "EncoreCLI/"+version.Version) req.Header.Set("X-Encore-Version", version.Version) req.Header.Set("X-Encore-GOOS", runtime.GOOS) req.Header.Set("X-Encore-GOARCH", runtime.GOARCH) client := http.DefaultClient if auth { client = conf.AuthClient } return client.Do(req) } // wsDial sets up a WebSocket connection to the API endpoint given by method and path. func wsDial(ctx context.Context, path string, auth bool, extraHeaders map[string]string) (ws *websocket.Conn, err error) { defer func() { if err != nil { err = fmt.Errorf("WS %s: %w", path, err) } }() // Add a very limited amount of information for diagnostics header := make(http.Header) header.Set("User-Agent", "EncoreCLI/"+version.Version) header.Set("X-Encore-Version", version.Version) header.Set("X-Encore-GOOS", runtime.GOOS) header.Set("X-Encore-GOARCH", runtime.GOARCH) header.Set("Origin", "http://encore-cli.local") for k, v := range extraHeaders { header.Set(k, v) } log.Trace().Msgf("-> %s %s: %+v", "WS", path, extraHeaders) defer func() { if err != nil { log.Trace().Msgf("<- ERR %s %s: %v", "WS", path, err) } else { log.Trace().Msgf("<- OK %s %s", "WS", path) } }() if auth { tok, err := conf.DefaultTokenSource.Token() if err != nil { return nil, err } header.Set("Authorization", "Bearer "+tok.AccessToken) } url := conf.WSBaseURL + path log.Trace().Msgf("-> %s %s: connecting to %s", "WS", path, url) ws, httpResp, err := websocket.DefaultDialer.DialContext(ctx, url, header) if httpResp != nil && httpResp.StatusCode >= 400 { var respStruct struct { OK bool Error Error Data json.RawMessage } if err := json.NewDecoder(httpResp.Body).Decode(&respStruct); err != nil { return nil, fmt.Errorf("decode response: %v", err) } else if !respStruct.OK { e := respStruct.Error e.HTTPCode = httpResp.StatusCode e.HTTPStatus = httpResp.Status return nil, e } } return ws, err } func decodeErrorResponse(resp *http.Response) error { var respStruct struct { OK bool Error Error Data json.RawMessage } if err := json.NewDecoder(resp.Body).Decode(&respStruct); err != nil { return fmt.Errorf("decode response: %v", err) } e := respStruct.Error e.HTTPCode = resp.StatusCode e.HTTPStatus = resp.Status return e } ================================================ FILE: cli/internal/platform/gql/app.go ================================================ package gql import ( "encoding/json" "fmt" ) type App struct { ID string Slug string } type Error struct { Message string `json:"message"` Path []string `json:"path"` Extensions map[string]json.RawMessage `json:"extensions"` } func (e *Error) Error() string { return e.Message } type ErrorList []*Error func (err ErrorList) Error() string { if len(err) == 0 { return "no errors" } else if len(err) == 1 { return err[0].Error() } return fmt.Sprintf("%s (and %d more errors)", err[0].Error(), len(err)-1) } ================================================ FILE: cli/internal/platform/gql/env.go ================================================ package gql type Env struct { ID string App *App Name string } ================================================ FILE: cli/internal/platform/gql/secrets.go ================================================ package gql import ( "time" "github.com/modern-go/reflect2" ) type Secret struct { Key string Groups []*SecretGroup } type SecretGroup struct { ID string Key string Selector []SecretSelector Description string Etag string ArchivedAt *time.Time DestroyedAt *time.Time } type SecretSelector interface { secretSelector() String() string } type SecretSelectorEnvType struct { Kind string } func (SecretSelectorEnvType) secretSelector() {} func (s *SecretSelectorEnvType) String() string { return "type:" + s.Kind } type SecretSelectorSpecificEnv struct { Env *Env } func (s *SecretSelectorSpecificEnv) String() string { return "id:" + s.Env.ID } func (SecretSelectorSpecificEnv) secretSelector() {} type ConflictError struct { AppID string Key string Conflicts []GroupConflict } type GroupConflict struct { GroupID string Conflicts []string } // TypeRegistry contains all the types that are used in the graphql schema, // in order to ensure they are not dead-code eliminated. var TypeRegistry = []reflect2.Type{ reflect2.TypeOf((*SecretSelectorEnvType)(nil)), reflect2.TypeOf((*SecretSelectorSpecificEnv)(nil)), } ================================================ FILE: cli/internal/platform/jsoniter_ext.go ================================================ package platform import ( "reflect" "unsafe" jsoniter "github.com/json-iterator/go" "github.com/modern-go/reflect2" ) // InterfaceCodecExtension is used to decode interface fields // it'll store the type of the values in a wrapper object type InterfaceCodecExtension struct { jsoniter.DummyExtension } func NewInterfaceCodecExtension() *InterfaceCodecExtension { return &InterfaceCodecExtension{} } func (e *InterfaceCodecExtension) DecorateDecoder(typ reflect2.Type, decoder jsoniter.ValDecoder) jsoniter.ValDecoder { if typ.Kind() == reflect.Interface { return &interfaceCodec{typ: typ, decoder: decoder} } return decoder } const gqlPackage = "encr.dev/cli/internal/platform/gql" type interfaceCodec struct { typ reflect2.Type decoder jsoniter.ValDecoder } // Decode decodes an interface value from a iterator func (codec *interfaceCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { // if it's not an objectvalue, we don't need to bother if iter.WhatIsNext() != jsoniter.ObjectValue { codec.decoder.Decode(ptr, iter) return } // if it is, we try to resolve the pkgPath, type and content val := iter.ReadAny() typeName := val.Get("__typename").ToString() if typeName == "" { iter.ReportError("InterfaceCodecExtension", "missing __typename field") return } // try to instantiate the type t := reflect2.TypeByPackageName(gqlPackage, typeName) if t == nil { iter.ReportError("InterfaceCodecExtension", "cannot find type "+typeName+" in package "+gqlPackage) return } // Need to create a pointer to the pointer of the type to be able to be able // to replace placeholder values with the actual values item := reflect2.PtrTo(reflect2.PtrTo(t)).New() val.ToVal(item) if err := val.LastError(); err != nil { iter.ReportError("decode", err.Error()) return } n := reflect.New(codec.typ.Type1()) n.Elem().Set(reflect.ValueOf(item).Elem().Elem()) codec.typ.UnsafeSet(ptr, n.UnsafePointer()) } // IsEmpty checks if a ptr is empty/nil func (codec *interfaceCodec) IsEmpty(ptr unsafe.Pointer) bool { return codec.typ.UnsafeIsNil(ptr) } ================================================ FILE: cli/internal/platform/jsoniter_ext_test.go ================================================ package platform import ( "testing" qt "github.com/frankban/quicktest" jsoniter "github.com/json-iterator/go" "encr.dev/cli/internal/platform/gql" ) func TestInterfaceDecoder(t *testing.T) { c := qt.New(t) enc := jsoniter.Config{}.Froze() enc.RegisterExtension(NewInterfaceCodecExtension()) data := []byte(`{ "key": "test", "selector": [ {"__typename": "SecretSelectorEnvType", "kind": "type:production"}, {"__typename": "SecretSelectorSpecificEnv", "env": {"name": "test"}} ] }`) var group *gql.SecretGroup err := enc.Unmarshal(data, &group) c.Assert(err, qt.IsNil) c.Assert(group, qt.DeepEquals, &gql.SecretGroup{ Key: "test", Selector: []gql.SecretSelector{ &gql.SecretSelectorEnvType{Kind: "type:production"}, &gql.SecretSelectorSpecificEnv{Env: &gql.Env{Name: "test"}}, }, }) } ================================================ FILE: cli/internal/platform/login.go ================================================ package platform import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "golang.org/x/oauth2" "encr.dev/internal/conf" ) type CreateOAuthSessionParams struct { Challenge string `json:"challenge"` State string `json:"state"` RedirectURL string `json:"redirect_url"` } func CreateOAuthSession(ctx context.Context, p *CreateOAuthSessionParams) (authURL string, err error) { var resp struct { AuthURL string `json:"auth_url"` } err = call(ctx, "POST", "/login/oauth:create-session", p, &resp, false) return resp.AuthURL, err } type BeginAuthorizationFlowParams struct { CodeChallenge string ClientID string } type BeginAuthorizationFlowResponse struct { // DeviceCode is the device verification code. DeviceCode string `json:"device_code"` // UserCode is the end-user verification code. UserCode string `json:"user_code"` // VerificationURI is the end-user URL to use to login. VerificationURI string `json:"verification_uri"` // ExpiresIn is the lifetime in seconds of the device code and user code. ExpiresIn int `json:"expires_in"` // Interval is the number of seconds to wait between polling requests. // If not provided, defaults to 5. Interval int `json:"interval,omitempty"` } func BeginDeviceAuthFlow(ctx context.Context, p BeginAuthorizationFlowParams) (*BeginAuthorizationFlowResponse, error) { vals := url.Values{} vals.Set("code_challenge", p.CodeChallenge) vals.Set("client_id", p.ClientID) body := strings.NewReader(vals.Encode()) req, err := http.NewRequestWithContext(ctx, "POST", conf.APIBaseURL+"/oauth/device-auth", body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := doPlatformReq(req, false) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, decodeErrorResponse(resp) } var respData BeginAuthorizationFlowResponse if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil { return nil, fmt.Errorf("decoding response body: %w", err) } return &respData, nil } type PollDeviceAuthFlowParams struct { DeviceCode string CodeVerifier string } type OAuthToken struct { *oauth2.Token Actor string `json:"actor,omitempty"` // The ID of the user or app that authorized the token. Email string `json:"email"` // empty if logging in as an app AppSlug string `json:"app_slug"` // empty if logging in as a user } func PollDeviceAuthFlow(ctx context.Context, p PollDeviceAuthFlowParams) (*OAuthToken, error) { vals := url.Values{} vals.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") vals.Set("device_code", p.DeviceCode) vals.Set("code_verifier", p.CodeVerifier) body := strings.NewReader(vals.Encode()) req, err := http.NewRequestWithContext(ctx, "POST", conf.APIBaseURL+"/oauth/token", body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := doPlatformReq(req, false) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, decodeErrorResponse(resp) } var tok OAuthToken if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil { return nil, fmt.Errorf("decoding response body: %w", err) } return &tok, nil } type ExchangeOAuthTokenParams struct { Challenge string `json:"challenge"` Code string `json:"code"` } type OAuthData struct { Token *oauth2.Token `json:"token"` Actor string `json:"actor,omitempty"` // The ID of the user or app that authorized the token. Email string `json:"email"` // empty if logging in as an app AppSlug string `json:"app_slug"` // empty if logging in as a user } func ExchangeOAuthToken(ctx context.Context, p *ExchangeOAuthTokenParams) (*OAuthData, error) { var resp OAuthData err := call(ctx, "POST", "/login/oauth:exchange-token", p, &resp, false) return &resp, err } type ExchangeAuthKeyParams struct { AuthKey string `json:"auth_key"` } func ExchangeAuthKey(ctx context.Context, p *ExchangeAuthKeyParams) (*OAuthData, error) { var resp OAuthData err := call(ctx, "POST", "/login/auth-key", p, &resp, false) return &resp, err } ================================================ FILE: cli/internal/platform/secrets.go ================================================ package platform import ( "context" "github.com/cockroachdb/errors" "encr.dev/cli/internal/platform/gql" ) func ListSecretGroups(ctx context.Context, appSlug string, keys []string) ([]*gql.Secret, error) { query := ` query ListSecretGroups($appSlug: String!, $keys: [String!]) { app(slug: $appSlug) { secrets(keys: $keys) { key groups { id, etag, description, archivedAt selector { __typename ...on SecretSelectorEnvType { kind } ...on SecretSelectorSpecificEnv { env { id, name } } } versions { id } } } } }` var out struct { App struct { *gql.App Secrets []*gql.Secret } } in := graphqlRequest{Query: query, Variables: map[string]any{"appSlug": appSlug, "keys": keys}} if err := graphqlCall(ctx, in, &out, true); err != nil { return nil, err } return out.App.Secrets, nil } type CreateSecretGroupParams struct { AppID string Key string PlaintextValue string Description string Selector []gql.SecretSelector } func CreateSecretGroup(ctx context.Context, p CreateSecretGroupParams) error { query := ` mutation CreateSecretGroup($input: CreateSecretGroups!) { createSecretGroups(input: $input) { id } }` envTypes, envIDs, err := mapSecretSelector(p.Selector) if err != nil { return err } in := graphqlRequest{Query: query, Variables: map[string]any{"input": map[string]any{ "appID": p.AppID, "key": p.Key, "entries": []map[string]any{ { "plaintextValue": p.PlaintextValue, "envTypes": envTypes, "envIDs": envIDs, "description": p.Description, }, }, }}} if err := graphqlCall(ctx, in, nil, true); err != nil { return errors.Wrap(err, "create secret group") } return nil } type CreateSecretVersionParams struct { GroupID string PlaintextValue string Etag string } func CreateSecretVersion(ctx context.Context, p CreateSecretVersionParams) error { query := ` mutation CreateSecretVersion($input: CreateSecretVersion!) { createSecretVersion(input: $input) { id } }` in := graphqlRequest{Query: query, Variables: map[string]any{"input": map[string]any{ "groupID": p.GroupID, "plaintextValue": p.PlaintextValue, "etag": p.Etag, }}} if err := graphqlCall(ctx, in, nil, true); err != nil { return errors.Wrap(err, "create secret version") } return nil } type UpdateSecretGroupParams struct { ID string Etag *string // Nil fore ach field here means it's kept unchanged. Selector []gql.SecretSelector // nil means no changes Archived *bool Delete *bool Description *string } func UpdateSecretGroup(ctx context.Context, p UpdateSecretGroupParams) error { query := ` mutation UpdateSecretGroup($input: UpdateSecretGroup!) { updateSecretGroup(input: $input) { id } }` var selector map[string]any if p.Selector != nil { envTypes, envIDs, err := mapSecretSelector(p.Selector) if err != nil { return err } selector = map[string]any{ "envTypes": envTypes, "envIDs": envIDs, } } in := graphqlRequest{Query: query, Variables: map[string]any{"input": map[string]any{ "id": p.ID, "etag": p.Etag, "selector": selector, "archived": p.Archived, "delete": p.Delete, "description": p.Description, }}} if err := graphqlCall(ctx, in, nil, true); err != nil { return errors.Wrap(err, "update secret group") } return nil } func mapSecretSelector(selector []gql.SecretSelector) (envTypes, envIDs []string, err error) { envTypes, envIDs = []string{}, []string{} for _, sel := range selector { switch s := sel.(type) { case *gql.SecretSelectorEnvType: envTypes = append(envTypes, s.Kind) case *gql.SecretSelectorSpecificEnv: envIDs = append(envIDs, s.Env.ID) default: return nil, nil, errors.Newf("unknown secret selector type %T", s) } } return envTypes, envIDs, nil } ================================================ FILE: cli/internal/telemetry/telemetry.go ================================================ package telemetry import ( "context" "encoding/json" "errors" "io/fs" "os" "path/filepath" "sync" "github.com/hasura/go-graphql-client" "github.com/rs/zerolog/log" "encore.dev/types/uuid" "encr.dev/internal/conf" "encr.dev/pkg/fns" "encr.dev/pkg/xos" ) var singleton = func() *telemetry { t := &telemetry{ client: graphql.NewClient(conf.APIBaseURL+"/graphql", conf.DefaultClient), } path, err := configPath() if err != nil { return t } data, err := os.ReadFile(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { // If the file does not exist, telemetry is enabled by default t.cfg.Enabled = true t.cfg.AnonID = uuid.Must(uuid.NewV4()).String() t.cfg.SentEvents = make(map[string]struct{}) _ = t.saveConfig() err = nil } return t } err = json.Unmarshal(data, &t.cfg) if err != nil { log.Debug().Err(err).Msg("failed to unmarshal telemetry config") } return t }() type telemetry struct { mu sync.Mutex cfg telemetryCfg client *graphql.Client } type telemetryCfg struct { Enabled bool `json:"enabled"` AnonID string `json:"anon_id"` SentEvents map[string]struct{} `json:"sent_events"` ShownWarning bool `json:"shown_warning"` Debug bool `json:"debug"` } type TelemetryMessage struct { Event string `json:"event"` AnonymousId string `json:"anonymousId"` Properties map[string]any `json:"properties,omitempty"` } func (t *telemetry) sendOnce(event string, props ...map[string]any) { t.mu.Lock() if _, ok := t.cfg.SentEvents[event]; ok { t.mu.Unlock() return } t.cfg.SentEvents[event] = struct{}{} if err := t.saveConfig(); err != nil { log.Debug().Err(err).Msg("failed to save telemetry config") } t.mu.Unlock() if err := t.send(event, props...); err != nil { log.Debug().Err(err).Msg("failed to send telemetry message") t.mu.Lock() delete(t.cfg.SentEvents, event) t.mu.Unlock() } } func (t *telemetry) send(event string, props ...map[string]any) error { var m struct { Result bool `graphql:"telemetry(msg: $msg)"` } message := TelemetryMessage{ Event: event, AnonymousId: t.cfg.AnonID, Properties: fns.MergeMaps(props...), } if t.cfg.Debug { data, err := json.Marshal(message) if err != nil { log.Info().Msgf("[telemetry] failed to marshal message") } else { log.Info().Msgf("[telemetry] %s", string(data)) } } err := t.client.Mutate(context.Background(), &m, map[string]any{ "msg": message}) if !m.Result { return errors.New("failed to send telemetry message") } return err } func (t *telemetry) trySend(event string, props ...map[string]any) { if err := t.send(event, props...); err != nil { log.Debug().Msg("failed to send telemetry message") } } func (t *telemetry) saveConfig() error { // Write the telemetry configuration to a file path, err := configPath() if err != nil { return err } data, err := json.Marshal(t.cfg) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return xos.WriteFile(path, data, 0644) } func IsEnabled() bool { return singleton.cfg.Enabled } func SetEnabled(enabled bool) bool { return UpdateConfig(singleton.cfg.AnonID, enabled, singleton.cfg.Debug) } func SetDebug(debug bool) bool { return UpdateConfig(singleton.cfg.AnonID, singleton.cfg.Enabled, debug) } func UpdateConfig(anonID string, enabled, debug bool) (changed bool) { changed = singleton.cfg.Enabled != enabled || singleton.cfg.Debug != debug || singleton.cfg.AnonID != anonID singleton.cfg.AnonID = anonID singleton.cfg.Enabled = enabled singleton.cfg.Debug = debug return changed } func ShouldShowWarning() bool { return !singleton.cfg.ShownWarning && IsEnabled() } func SetShownWarning() { singleton.cfg.ShownWarning = true if err := singleton.saveConfig(); err != nil { log.Debug().Err(err).Msg("failed to save telemetry config") } } func SaveConfig() error { return singleton.saveConfig() } func SendOnce(event string, props ...map[string]any) { if !IsEnabled() { return } go singleton.sendOnce(event, props...) } func Send(event string, props ...map[string]any) { if !IsEnabled() { return } go singleton.trySend(event, props...) } func SendSync(event string, props ...map[string]any) { if !IsEnabled() { return } singleton.trySend(event, props...) } func configPath() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "encore", "telemetry.json"), nil } func GetAnonID() string { return singleton.cfg.AnonID } func IsDebug() bool { return singleton.cfg.Debug } ================================================ FILE: cli/internal/update/update.go ================================================ package update import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "golang.org/x/mod/semver" "encr.dev/internal/conf" "encr.dev/internal/version" ) var ErrUnknownVersion = errors.New("unknown version") // Check checks for the latest Encore version. // It reports ErrUnknownVersion if it cannot determine the version. func Check(ctx context.Context) (latestVersion *LatestVersion, err error) { defer func() { if err != nil { err = fmt.Errorf("update.Check: %w", err) } }() releaseAPI, err := url.Parse("https://encore.dev/api/releases") if err != nil { return nil, fmt.Errorf("parse release api url: %w", err) } // Filter the request down to the release for the current version. qry := releaseAPI.Query() // These three are used to determine the latest release for the given channel, os and arch qry.Set("channel", string(version.Channel)) qry.Set("os", runtime.GOOS) qry.Set("arch", runtime.GOARCH) // This is used to determine if the returned release contains security updates not present // in the currently running version of Encore, as well as if we need to force an upgrade // on the user due to a critical security issue. qry.Set("current", version.Version) // For specific app ID's or user ID's we can provide pre-releases to them // Mainly used if they've encountered a bug and we need to get them a fix asap for testing if cfg, err := conf.CurrentUser(); err == nil && cfg != nil { qry.Set("actor", cfg.Actor) } releaseAPI.RawQuery = qry.Encode() // url := "https://encore.dev/api/releases" req, err := http.NewRequestWithContext(ctx, "GET", releaseAPI.String(), nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("GET %s: responded with %s: %s", releaseAPI, resp.Status, body) } latestVersion = &LatestVersion{} if err := json.NewDecoder(resp.Body).Decode(latestVersion); err != nil { return nil, fmt.Errorf("GET %s: invalid json: %v", releaseAPI, err) } if !latestVersion.Supported && latestVersion.Channel != version.DevBuild { return nil, ErrUnknownVersion } return latestVersion, nil } // LatestVersion contains the parsed response from the update server type LatestVersion struct { // The channel the release is from Channel version.ReleaseChannel `json:"channel"` // Whether the requested target is supported or not Supported bool `json:"supported"` // The latest version available // Access via Version() to ensure the version is prefixed with "v" for GA releases RawVersion string `json:"version"` // The URL for that version (if supported) URL string `json:"url,omitempty"` // Whether the version contains a security fix from the current version running SecurityUpdate bool `json:"security_update"` // Optional notes about what the security update fixes and why the user should install it SecurityNotes string `json:"security_notes,omitempty"` // If we need to force an upgrade. This is only used for security updates and only for // the most urgent ones, i.e we should never use it unless the world is on fire. ForceUpgrade bool `json:"force_upgrade,omitempty"` } // Version returns the version string referenced by the LatestVersion. // ensuring that it is prefixed with "v" for GA releases. func (lv *LatestVersion) Version() string { // Server side doesn't include the "v" in nightly versions. if lv.Channel == version.GA { // Note: this trim prefix is future proofing in case we decide to start returning versions // which include the "v" prefix return "v" + strings.TrimPrefix(lv.RawVersion, "v") } return lv.RawVersion } // IsNewer returns true if LatestVersion is newer than current // // This is safe to call on a nil LatestVersion func (lv *LatestVersion) IsNewer(current string) bool { if lv == nil { return false } switch lv.Channel { case version.GA: return semver.Compare(lv.Version(), current) > 0 case version.Nightly: return nightlyToNumber(lv.Version()) > nightlyToNumber(current) } return false } // DoUpgrade upgrades Encore. // // Adapted from flyctl: https://github.com/superfly/flyctl func (lv *LatestVersion) DoUpgrade(stdout, stderr io.Writer) error { // What shell do we need to run? arg := "-c" shell, ok := os.LookupEnv("SHELL") if !ok { //goland:noinspection GoBoolExpressions if runtime.GOOS == "windows" { shell = "powershell.exe" arg = "-Command" } else { shell = "/bin/bash" } } // Base script for *nix systems script := "curl -L \"https://encore.dev/install.sh\" | sh" brewManaged := false // Script overrides for windows and systems with homebrew installed switch runtime.GOOS { case "windows": script = "iwr https://encore.dev/install.ps1 -useb | iex" case "darwin", "linux": // Upgrade via homebrew if we can if wasInstalledViaHomebrew(shell, arg, lv.Channel) { brewManaged = true script = "brew upgrade encore --fetch-head" } } // Sainty check we can perform the update switch lv.Channel { case version.GA: // no-op case version.Nightly: if brewManaged { script = "brew upgrade encore-nightly --fetch-head" } else { return errors.New("nightly can not be automatically updated without homebrew") } case version.Beta: if brewManaged { script = "brew upgrade encore-beta --fetch-head" } else { return errors.New("beta can not be automatically updated without homebrew") } case version.DevBuild: return errors.New("dev builds can not be automatically updated") default: return fmt.Errorf("unknown release channel %s", lv.Channel) } fmt.Println("Running update [" + script + "]") if brewManaged { updateBrewTap(stdout, stderr) } // nosemgrep cmd := exec.Command(shell, arg, script) cmd.Stdout = stdout cmd.Stderr = stderr cmd.Stdin = os.Stdin return cmd.Run() } func nightlyToNumber(version string) int64 { // version looks like: nightly-20221010 if !strings.HasPrefix(version, "nightly-") || len(version) != 16 { return 0 } // slice(8) removes "nightly-" date, err := strconv.ParseInt(version[8:], 10, 64) if err != nil { return 0 } return date } func wasInstalledViaHomebrew(shell string, arg string, channel version.ReleaseChannel) bool { if _, err := exec.LookPath("brew"); err != nil { return false } formulaName := "encore" if channel == version.Nightly { formulaName = "encore-nightly" } else if channel == version.Beta { formulaName = "encore-beta" } buf := new(bytes.Buffer) // nosemgrep cmd := exec.Command(shell, arg, fmt.Sprintf("brew list %s -1", formulaName)) cmd.Stdout = buf cmd.Stderr = buf cmd.Stdin = os.Stdin // No error means it was installed via homebrew, error means homebrew doesn't know about it // or isn't installed return cmd.Run() == nil } func updateBrewTap(stdout, stderr io.Writer) { // Attempt to update the tap if it exists. var outBuf bytes.Buffer cmd := exec.Command("brew", "--prefix") cmd.Stdout = &outBuf if err := cmd.Run(); err == nil { gitDir := filepath.Join(strings.TrimSpace(outBuf.String()), "Library", "Taps", "encoredev", "homebrew-tap") if _, err := os.Stat(gitDir); err == nil { // Get the current branch branchName := "main" { outBuf.Reset() cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") cmd.Stdout = &outBuf cmd.Stderr = stderr cmd.Dir = gitDir if err := cmd.Run(); err == nil { branchName = strings.TrimSpace(outBuf.String()) } } // Only update if we're on the main branch. if branchName == "main" { cmd := exec.Command("git", "pull", "--rebase", "origin", "main") cmd.Stdout = stdout cmd.Stderr = stderr cmd.Dir = gitDir _ = cmd.Run() } } } } ================================================ FILE: clippy.toml ================================================ ignore-interior-mutability = ["bytes::Bytes", "http::header::HeaderName"] ================================================ FILE: context7.json ================================================ { "url": "https://context7.com/encoredev/encore", "public_key": "pk_hiFaGPl6nQmXCFl3gjJvk" } ================================================ FILE: docs/go/ai-integration.md ================================================ --- seotitle: Using Encore with AI Tools seodesc: Learn how to set up Encore with AI-powered development tools like Cursor and Claude Code to supercharge your backend development workflow. title: AI Tools Integration subtitle: Supercharge your development with AI-powered coding assistants lang: go --- Encore is built for AI-assisted development. Encore-specific rules and [MCP](/docs/go/ai-integration#mcp-server) integration let AI understand your architecture and generate type-safe code that follows your patterns. Run `encore run` to start your app; Encore provisions local infrastructure automatically. For production, [self-host](/docs/go/self-host/build) or use [Encore Cloud](https://encore.cloud) to provision infrastructure in your own AWS or GCP account. ## What AI Enables Encore's declarative APIs and infrastructure primitives give AI a clear model to work with. AI can add databases, pub/sub topics, and other resources with built-in guardrails, and use MCP to introspect your app—services, APIs, databases, and traces—so it can suggest accurate, pattern-consistent code. ## Enabling AI for Your Project There are two ways to set up AI support: - [Method 1: Using the CLI](#method-1-using-the-cli) (recommended) - [Method 2: Using Encore Skills](#method-2-using-encore-skills) ### Method 1: Using the CLI **New projects:** When you run `encore app create`, you'll be prompted to select an AI tool. Encore generates the appropriate configuration files for your chosen tool. **Existing projects:** Run `encore llm-rules init` to add AI support: ```bash encore llm-rules init ``` This prompts you to select a tool and generates the appropriate configuration file (`.cursorrules`, `CLAUDE.md`, etc.). Both commands also set up MCP server configuration for tools that support it (Cursor, Claude Code). If you want to set up MCP manually, see [MCP Server](#mcp-server) below. Supported tools: Cursor, Claude Code, VS Code, AGENTS.md, and Zed. ### Method 2: Using Encore Skills Use the [Encore skills package](https://github.com/encoredev/skills) which works with Cursor, Claude Code, GitHub Copilot, and 10+ other AI agents: ```bash npx add-skill encoredev/skills ``` You can also install specific skills or target specific agents: ```bash # List available skills npx add-skill encoredev/skills --list # Install to specific agents npx add-skill encoredev/skills -a cursor -a claude-code ``` The skills package includes a migration skill that can automatically migrate your existing backend to Encore Go. See the [Migrate using AI agent](/docs/go/migration/ai-migration) guide to learn more. ## MCP Server Encore's [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server gives AI agents deep introspection into your application: querying databases, calling APIs, inspecting services, and analyzing traces. ### Start the Server From your Encore app directory: ```bash encore mcp start ``` This displays connection information. Keep it running while using your AI tools. ### Connect Cursor **Quick setup:** Use this button (update `your-app-id` to your actual app ID): Add encore-mcp MCP server to Cursor **Manual setup:** Create `.cursor/mcp.json`: ```json { "mcpServers": { "encore-mcp": { "command": "encore", "args": ["mcp", "run", "--app=your-app-id"] } } } ``` Find your app ID in the `encore.app` file or in the [Encore dashboard](https://app.encore.dev). ### Connect Claude Code From your Encore app directory: ```bash claude mcp add --transport stdio encore-mcp -- encore mcp run --app=your-app-id ``` Verify with `claude mcp list`. You should see `encore-mcp` in the list. ## What AI Can Do With Encore skills and MCP connected, AI can: - **Define infrastructure in code** - AI declares databases, pub/sub, cron jobs, buckets, and other [primitives](/docs/go/primitives) - **Generate type-safe APIs** - code that follows your patterns and passes validation - **Understand architecture** - inspect services and how they connect via MCP - **Query databases** - introspect schema and data to generate accurate queries - **Debug with tracing** - view request traces, timing, and span details to pinpoint issues - **Test instantly** - run `encore run` to test with real infrastructure, not mocks ### In Practice #### Smarter Debugging with Tracing AI can access Encore's distributed tracing via MCP to debug issues intelligently. Instead of guessing, AI can view actual request traces, analyze timing across services, and inspect span details to pinpoint exactly where things went wrong. This creates a powerful feedback loop: generate code, test it, analyze the traces, and iterate. #### Database Introspection AI can query your actual database schema and data via MCP. This means AI understands your real data model and can generate accurate queries, suggest schema changes, and debug data issues by inspecting actual records. #### Instant Validation with Real Infrastructure When you run `encore run`, Encore provisions real local infrastructure (databases, pub/sub, etc.). AI can generate code and immediately test it against real services, catching issues early and ensuring the code works before you deploy. Example prompts: - "Add an endpoint that publishes to a pub/sub topic, call it and verify in traces" - "Query the users database and show accounts created in the last week" - "Create a new service with CRUD endpoints connected to PostgreSQL" ## Learn More - [MCP Server Documentation](/docs/go/cli/mcp) - Complete MCP reference - [Encore Skills Repository](https://github.com/encoredev/skills) - Available skills and installation - [Quick Start Guide](/docs/go/quick-start) - Build your first Encore app ================================================ FILE: docs/go/cli/cli-reference.md ================================================ --- seotitle: Encore CLI Reference seodesc: The Encore CLI lets you run your local development environment, create apps, and much more. See all CLI commands in this reference guide. title: CLI Reference subtitle: The Encore CLI lets you run your local environment and much more. lang: go --- ## Running #### Run Runs your application. ```shell $ encore run [--debug] [--watch=true] [--port=] [--listen=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-w, --watch` | Watch for changes and live-reload | `true` | | `--listen` | Address to listen on (e.g. `0.0.0.0:4000`) | | | `-p, --port` | Port to listen on | `4000` | | `--json` | Display logs in JSON format | `false` | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `--color` | Whether to display colorized output | auto-detected | | `--redact` | Redact sensitive data in traces when running locally | `false` | | `-l, --level` | Minimum log level to display (`trace\|debug\|info\|warn\|error`) | | | `--debug` | Compile for debugging (`enabled\|break`) | | | `--browser` | Open local dev dashboard in browser on startup (`auto\|never\|always`) | `auto` | #### Test Tests your application Takes all the same flags as `go test`. ```shell $ encore test ./... [go test flags] ``` Additional flags recognized by `encore test`: | Flag | Description | | --- | --- | | `--codegen-debug` | Dump generated code (for debugging Encore's code generation) | | `--prepare` | Prepare for running tests without running them | | `--trace` | Write trace information about the parse and compilation process to a file | | `--no-color` | Disable colorized output | #### Check Checks your application for compile-time errors using Encore's compiler. ```shell $ encore check [flags] ``` **Flags** | Flag | Description | | --- | --- | | `--codegen-debug` | Dump generated code (for debugging Encore's code generation) | | `--tests` | Parse tests as well | #### Exec Runs executable scripts against the local Encore app. Compiles and runs a Go script with the local Encore app environment setup. ```shell $ encore exec [...args] ``` The command directory should contain Go files with package main with a main function. The additional arguments are passed directly to the built binary. **Flags** | Flag | Description | | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | ##### Example Run a database seed script ```shell $ encore exec cmd/seed ``` ## App Commands to create and link Encore apps #### Clone Clone an Encore app to your computer ```shell $ encore app clone [app-id] [directory] ``` #### Create Create a new Encore app ```shell $ encore app create [name] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--example` | URL to example code to use | | | `-l, --lang` | Programming language to use for the app | | | `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool | | | `--platform` | Whether to create the app with the Encore Platform | `true` | #### Init Create a new Encore app from an existing repository ```shell $ encore app init [name] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-l, --lang` | Programming language to use for the app | #### Link Link an Encore app with the server ```shell $ encore app link [app-id] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-f, --force` | Force link even if the app is already linked | ## Auth Commands to authenticate with Encore #### Login Log in to Encore ```shell $ encore auth login [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-k, --auth-key` | Auth Key to use for login | #### Logout Logs out the currently logged in user ```shell $ encore auth logout ``` #### Signup Create a new Encore account ```shell $ encore auth signup ``` #### Whoami Show the current logged in user ```shell $ encore auth whoami ``` ## Daemon Encore CLI daemon commands #### Restart If you experience unexpected behavior, try restarting the daemon using: ```shell $ encore daemon ``` #### Env Outputs Encore environment information ```shell $ encore daemon env ``` ## Database Management Database management commands #### Connect to database via shell Connects to the database via psql shell Defaults to connecting to your local environment. Specify --env to connect to another environment. Use `--test` to connect to databases used for integration testing. Use `--shadow` to connect to the shadow database, used for database drift detection when using tools like Prisma. `--test` and `--shadow` imply `--env=local`. ```shell $ encore db shell [DATABASE_NAME] [--env=] [flags] ``` `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with. **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Connection URI Outputs a database connection string. Defaults to connecting to your local environment. Specify --env to connect to another environment. ```shell $ encore db conn-uri [] [--env=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Proxy Sets up local proxy that forwards any incoming connection to the databases in the specified environment. ```shell $ encore db proxy [--env=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-p, --port` | Port to listen on (defaults to a random port) | `0` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Reset Resets the databases for the given services. Use --all to reset all databases. ```shell $ encore db reset [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `--all` | Reset all services in the application | `false` | | `-t, --test` | Reset databases in the test cluster instead | `false` | | `--shadow` | Reset databases in the shadow cluster instead | `false` | ## Code Generation Code generation commands #### Generate client Generates an API client for your app. For more information about the generated clients, see [this page](/docs/go/cli/client-generation). By default, `encore gen client` generates the client based on the version of your application currently running in your local environment. You can change this using the `--env` flag and specifying the environment name. Use `--lang=` to specify the language. Supported language codes are: - `go`: A Go client using the net/http package - `typescript`: A TypeScript client using the in-browser Fetch API - `javascript`: A JavaScript client using the in-browser Fetch API - `openapi`: An OpenAPI spec ```shell $ encore gen client [] [--env=] [--lang=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-l, --lang` | Language to generate code for | | | `-o, --output` | Filename to write the generated client code to | | | `-e, --env` | Environment to fetch the API for | `local` | | `-s, --services` | Names of the services to include in the output | | | `-x, --excluded-services` | Names of the services to exclude in the output | | | `-t, --tags` | Names of endpoint tags to include in the output | | | `--excluded-tags` | Names of endpoint tags to exclude in the output | | | `--openapi-exclude-private-endpoints` | Exclude private endpoints from the OpenAPI spec | `false` | | `--ts:shared-types` | Import types from ~backend instead of re-generating them | `false` | | `--target` | An optional target for the client (`leap`) | | ## Logs Streams logs from your application ```shell $ encore logs [--env=prod] [--json] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-e, --env` | Environment name to stream logs from (defaults to the primary environment) | | `--json` | Whether to print logs in raw JSON format | | `-q, --quiet` | Whether to print initial message when the command is waiting for logs | ## Kubernetes Kubernetes management commands #### Configure Updates your kubectl config to point to the Kubernetes cluster(s) for the specified environment ```shell $ encore k8s configure --env=ENV_NAME ``` ## Secrets Management Secret management commands #### Set Set a secret value for a specific environment: ```shell $ encore secret set --env ``` Set a secret value for an environment type: ```shell $ encore secret set --type ``` Where `` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`. **Examples** Entering a secret directly in terminal: ```shell $ encore secret set --type dev MySecret Enter secret value: ... Successfully created secret value for MySecret. ``` Piping a secret from a file: ```shell $ encore secret set --type dev,local MySecret < my-secret.txt Successfully created secret value for MySecret. ``` Note that this strips trailing newlines from the secret value. #### List Lists secrets, optionally for a specific key ```shell $ encore secret list [keys...] ``` #### Delete Deletes a secret value ```shell $ encore secret delete ``` ## Namespaces Manage infrastructure namespaces for isolating local infrastructure. See [Infrastructure Namespaces](/docs/go/cli/infra-namespaces) for more details. #### List List infrastructure namespaces ```shell $ encore namespace list [--output=columns|json] ``` #### Create Create a new infrastructure namespace ```shell $ encore namespace create NAME ``` #### Delete Delete an infrastructure namespace ```shell $ encore namespace delete NAME ``` #### Switch Switch to a different infrastructure namespace. Subsequent commands will use the given namespace by default. Use `-` as the namespace name to switch back to the previously active namespace. ```shell $ encore namespace switch [--create] NAME ``` **Flags** | Flag | Description | | --- | --- | | `-c, --create` | Create the namespace before switching | ## Config Gets or sets configuration values for customizing the behavior of the Encore CLI. Configuration options can be set both for individual Encore applications, as well as globally for the local user. ```shell $ encore config [] [flags] ``` When running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag. **Flags** | Flag | Description | | --- | --- | | `--all` | View all settings | | `--app` | Set the value for the current app | | `--global` | Set the value at the global level | ## Telemetry Reports the current telemetry status ```shell $ encore telemetry ``` #### Enable Enables telemetry reporting ```shell $ encore telemetry enable ``` #### Disable Disables telemetry reporting ```shell $ encore telemetry disable ``` ## MCP MCP (Model Context Protocol) commands for integrating with AI assistants. See [MCP](/docs/go/cli/mcp) for more details. #### Start Starts an SSE-based MCP session and prints the SSE URL ```shell $ encore mcp start [--app=] ``` #### Run Runs a stdio-based MCP session ```shell $ encore mcp run [--app=] ``` ## Random Utilities for generating cryptographically secure random data. #### UUID Generates a random UUID (defaults to version 4) ```shell $ encore rand uuid [-1|-4|-6|-7] ``` **Flags** | Flag | Description | | --- | --- | | `-1, --v1` | Generate a version 1 UUID | | `-4, --v4` | Generate a version 4 UUID (default) | | `-6, --v6` | Generate a version 6 UUID | | `-7, --v7` | Generate a version 7 UUID | #### Bytes Generates random bytes and outputs them in the specified format ```shell $ encore rand bytes BYTES [-f ] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-f, --format` | Output format (`hex\|base32\|base32hex\|base32crockford\|base64\|base64url\|raw`) | `hex` | | `--no-padding` | Omit padding characters from base32/base64 output | `false` | #### Words Generates random 4-5 letter words for memorable passphrases ```shell $ encore rand words [--sep=SEPARATOR] NUM ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-s, --sep` | Separator between words | ` ` (space) | ## Deploy Deploy an Encore app to a cloud environment. Requires either `--commit` or `--branch` to be specified. ```shell $ encore deploy --env= (--commit= | --branch=) [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--app` | App slug to deploy to (defaults to current app) | | | `-e, --env` | Environment to deploy to (required) | | | `--commit` | Commit SHA to deploy | | | `--branch` | Branch to deploy | | | `-f, --format` | Output format (`text\|json`) | `text` | ## Version Reports the current version of the encore application ```shell $ encore version ``` #### Update Checks for an update of encore and, if one is available, runs the appropriate command to update it. ```shell $ encore version update ``` ## Build Generates an image for your app, which can be used to [self-host](/docs/go/self-host/docker-build) your app. #### Docker Builds a portable Docker image of your Encore application. ```shell $ encore build docker IMAGE_TAG [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--base` | Base image to build from | `scratch` | | `-p, --push` | Push image to remote repository | `false` | | `--cgo` | Enable cgo | `false` | | `--config` | Infra configuration file path | | | `--skip-config` | Do not read or generate an infra configuration file | `false` | | `--services` | Services to include in the image | | | `--gateways` | Gateways to include in the image | | | `--os` | Target operating system | `linux` | | `--arch` | Target architecture (`amd64\|arm64`) | `amd64` | ## LLM Rules Generate LLM rules in an existing app #### Init Initialize the LLM rules files ```shell $ encore llm-rules init [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool (`cursor\|claudecode\|vscode\|agentsmd\|zed`) | ================================================ FILE: docs/go/cli/client-generation.md ================================================ --- seotitle: Automatic API Client Generation seodesc: Learn how you can use automatic API client generation to get clients for your backend. See how to integrate with your frontend using a type-safe generated client. title: Client Library Generation subtitle: Stop writing the same types everywhere lang: go --- Encore makes it simple to write scalable distributed backends by allowing you to make function calls that Encore translates into RPC calls. Encore also generates API clients with interfaces that look like the original Go functions, with the same parameters and response signature as the server. The generated clients are single files that use only the standard functionality of the target language, with full type safety. This allow anyone to look at the generated client and understand exactly how it works. The structure of the generated code varies by language, to ensure it's idiomatic and easy to use, but always includes all publicly accessible endpoints, data structures, and documentation strings. Encore currently supports generating the following clients: - **Go** - Using `net/http` for the underlying HTTP transport. - **TypeScript** - Using the browser `fetch` API for the underlying HTTP client. - **JavaScript** - Using the browser `fetch` API for the underlying HTTP client. - **OpenAPI** - Using the OpenAPI Specification's language-agnostic interface to HTTP APIs. (Experimental) If there's a language you think should be added, please submit a pull request or create a feature request on [GitHub](https://github.com/encoredev/encore/issues/new), or [reach out on Discord](/discord). If you ship the generated client to end customers, keep in mind that old clients will continue to be used after you make changes. To prevent issues with the generated clients, avoid making breaking changes in APIs that your clients access.
## Generating a Client To generate a client, use the `encore gen client` command. It generates a type-safe client using the most recent API metadata running in a particular environment for the given Encore application. For example: ```shell # Generate a TypeScript client for calling the hello-a8bc application based on the primary environment encore gen client hello-a8bc --output=./client.ts # Generate a Go client for the hello-a8bc application based on the locally running code encore gen client hello-a8bc --output=./client.go --env=local # Generate an OpenAPI client for the hello-a8bc application based on the primary environment encore gen client hello-a8bc --lang=openapi --output=./openapi.json ``` ### Environment Selection By default, `encore gen client` generates the client based on the version of your application currently running in your local environment. You can change this using the `--env` flag and specifying the environment name. The generated client can be used with any environment, not just the one it was generated for. However, the APIs, data structures and marshalling logic will be based on whatever is present and running in that environment at the point in time the client is generated. ### Service filtering By default `encore gen client` outputs code for all services with at least one publicly accessible (or authenticated) API. You can narrow down this set of services by specifying the `--services` (or `-s`) flag. It takes a comma-separated list of service names. For example, to generate a typescript client for the `email` and `users` services, run: ```shell encore gen client --services=email,users -o client.ts ``` ### Output Mode By default the client's code will be output to stdout, allowing you to pipe it into your clipboard, or another tool. However, using `--output` you can specify a file location to write the client to. If output is specified, you do not need to specify the language as Encore will detect the language based on the file extension. ### Example Script You could combine this into a `package.json` file for your Typescript frontend, to allow you to run `npm run gen` in that project to update the client to match the code running in your staging environment. ```json { "scripts": { // ... "gen": "encore gen client hello-a8bc --output=./client.ts --env=staging" // ... } } ``` ## Using the Client The generated client has all the data structures required as parameters or returned as response values as needed by any of the public or authenticated API's of your Encore application. Each service is exposed as object on the client, with each public or authenticated API exposed as a function on those objects. For instance, if you had a service called `email` with a function `Send`, on the generated client you would call this using; `client.email.Send(...)`. For more tips and examples of using a generated JavaScript/Typescript client, see the [Integrate with a web frontend](/docs/how-to/integrate-frontend#generating-a-request-client) docs. ### Creating an instance When constructing a client, you need to pass a `BaseURL` as the first parameter; this is the URL at which the API can be accessed. The client provides two helpers: - `Local` - This is a constant provided, which will always point at your locally running instance environment. - `Environment("name")` - This is a function which allows you to specify an environment by name However, BaseURL is a string, so if the two helpers do not provide enough flexibility you can pass any valid URL to be used as the BaseURL. ### Authentication If your application has any API's which require [authentication](/docs/develop/auth), then additional options will generated into the client, which can be used when constructing the client. Just like with API's schemas, the data type required by your application's `auth handler` will be part of the client library, allowing you to set it in two ways: If your credentials won't change during the lifetime of the client, simply passing the authentication data to the client through the `WithAuth` (Go) or `auth` (TypeScript) options. However, if the authentication credentials can change, you can also pass a function which will be called before each request and can return a new instance of the authentication data structure or return the existing instance. ### HTTP Client Override If required, you can override the underlying HTTP implementation with your own implementation. This is useful if you want to perform logging of the requests being made, or route the traffic over a secured tunnel such as a VPN. In Go this can be configured using the `WithHTTPClient` option. You are required to provide an implementation of the `HTTPDoer` interface, which the [http.Client](https://pkg.go.dev/net/http#Client) implements. For TypeScript clients, this can be configured using the `fetcher` option and must conform to the same prototype as the browsers inbuilt [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch). ### Structured Errors Errors created or wrapped using Encore's [`errs package`](/docs/develop/errors) will be returned to the client and deserialized as an `APIError`, allowing the client to perform adaptive error handling based on the type of error returned. You can perform a type check on errors caused by calling an API to see if it is an `APIError`, and once cast as an `APIError` you can access the `Code`, `Message` and `Details` fields. For TypeScript Encore generates a `isAPIError` type guard which can be used. The `Code` field is an enum with all the possible values generated in the library, alone with description of when we would expect them to be returned by your API. See the [errors documentation](/docs/develop/errors#error-codes) for an online reference of this list. ## Example CLI Tool For instance, we could build a simple CLI application to use our [url shortener](/docs/tutorials/rest-api), and handle any structured errors in a way which makes sense for that error code. ```go package main import ( "context" "fmt" "os" "time" "shorten_cli/client" ) func main() { // Create a new client with the default BaseURL client, err := client.New( client.Environment("production"), client.WithAuth(os.Getenv("SHORTEN_API_KEY")), ) if err != nil { panic(err) } // Timeout if the request takes more than 5 seconds ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Call the Shorten function in the URL service resp, err := client.Url.Shorten( ctx, client.UrlShortenParams{ URL: os.Args[1] }, ) if err != nil { // Check the error returned if err, ok := err.(*client.APIError); ok { switch err.Code { case client.ErrUnauthenticated: fmt.Println("SHORTEN_API_KEY was invalid, please check your environment") os.Exit(1) case client.ErrAlreadyExists: fmt.Println("The URL you provided was already shortened") os.Exit(0) } } panic(err) // if here then something has gone wrong in an unexpected way } fmt.Printf("https://short.encr.app/%s", resp.ID) } ``` ================================================ FILE: docs/go/cli/config-reference.md ================================================ --- seotitle: Encore CLI Configuration Options seodesc: Configuration options to customize the behavior of the Encore CLI. title: Configuration Reference subtitle: Configuration options to customize the behavior of the Encore CLI. lang: go --- The Encore CLI has a number of configuration options to customize its behavior. Configuration options can be set both for individual Encore applications, as well as globally for the local user. Configuration options can be set using `encore config `, and options can similarly be read using `encore config `. When running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag. ## Configuration files The configuration is stored in one ore more TOML files on the filesystem. The configuration is read from the following files, in order: ### Global configuration * `$XDG_CONFIG_HOME/encore/config` * `$HOME/.config/encore/config` * `$HOME/.encoreconfig` ### Application-specific configuration * `$APP_ROOT/.encore/config` Where `$APP_ROOT` is the directory containing the `encore.app` file. The files are read and merged, in the order defined above, with latter files taking precedence over earlier files. ## Configuration options #### run.browser Type: string
Default: auto
Must be one of: always, never, or auto Whether to open the Local Development Dashboard in the browser on `encore run`. If set to "auto", the browser will be opened if the dashboard is not already open. ================================================ FILE: docs/go/cli/infra-namespaces.md ================================================ --- seotitle: Infrastructure Namespaces seodesc: Learn how Encore's infrastructure namespaces makes it easy to task switch. Stash your infrastructure state and switch to a different task with a single command. title: Infrastructure Namespaces subtitle: Task switching made easy lang: go --- Encore's CLI allows you to create and switch between multiple, independent *infrastructure namespaces*. Infrastructure namespaces are isolated from each other, and each namespace contains its own independent data. This makes it trivial to switch tasks, confident your old state and data will be waiting for you when you return. If you've ever worked on a new feature that involves making changes to the database schema, only to context switch to reviewing a Pull Request and had to reset your database, you know the feeling. With Encore's infrastructure namespaces, this is a problem of the past. Run `encore namespace switch --create pr:123` (or `encore ns switch -c pr:123` for short) to create and switch to a new namespace. The next `encore run` will run in the new namespace, with a completely fresh database. When you're done, run `encore namespace switch -` to switch back to your previous namespace. ## Usage Below are the commands for working with namespaces. Note that you can use `encore ns` as a short form for `encore namespace`. ```shell # List your namespaces (* indicates the current namespace) $ encore namespace list # Create a new namespace $ encore namespace create my-ns # Switch to a namespace $ encore namespace switch my-ns # Switch to a namespace, creating it if it doesn't exist $ encore namespace switch --create my-ns # Switch to the previous namespace $ encore namespace switch - # Delete a namespace (and all associated data) $ encore namespace delete my-ns ``` Most other Encore commands that interact or use infrastructure take an optional `--namespace` (`-n` for short) that overrides the current namespace. If left unspecified, the current namespace is used. For example: ```shell # Run the app using the "my-ns" namespace $ encore run --namespace my-ns # Open a database shell to the "my-ns" namespace $ encore db shell DATABASE_NAME --namespace my-ns # Reset all databases within the "my-ns" namespace $ encore db reset --all --namespace my-ns ``` ================================================ FILE: docs/go/cli/mcp.md ================================================ --- seotitle: Encore MCP Server seodesc: Encore's Model Context Protocol (MCP) server provides deep introspection of your application to AI development tools. title: MCP Server subtitle: The Model Context Provider (MCP) exposes tools that provide application context to LLMs. lang: go --- Encore provides an MCP server that implements the [Model Context Protocol](https://modelcontextprotocol.io/introduction), an open standard that enables large language models (LLMs) to access contextual information about your application. Think of MCP as a standardized interface—like a "USB-C port for AI applications"—that connects your Encore app's data and functionality to any LLM that supports the protocol. You can connect to Encore's MCP server from any MCP host (such as Claude Desktop, IDEs, or other AI tools) using either Server-Sent Events (SSE) or stdio transport. To set up this connection, simply run: ```bash cd my-encore-app encore mcp start MCP Service is running! MCP SSE URL: http://localhost:9900/sse?app=your-app-id MCP stdio Command: encore mcp run --app=your-app-id ``` Copy the appropriate URL or command to your MCP host's configuration, and you're ready to give your AI assistants rich context about your application. ## Example: Integrating with Cursor [Cursor](https://cursor.com) is one of the most popular AI powered IDE's, and it's simple to use Encore's MCP server together with Cursor. In order to add the Encore MCP server to Cursor, the fastest way is via the button below (make sure to update `your-app-id` in the configuration to your actual Encore app ID). Add encore-mcp MCP server to Cursor If you prefer to configure it manually, create the file `.cursor/mcp.json` with the following settings: ```json { "mcpServers": { "encore-mcp": { "command": "encore", "args": ["mcp", "run", "--app=your-app-id"] } } } ``` Learn more in [Cursor's MCP docs](https://docs.cursor.com/context/model-context-protocol) Now when using Cursor's Agent mode, you can ask it to do advanced actions, such as: "Add an endpoint that publishes to a pub/sub topic, call it and verify that the publish is in the traces" ## Command Reference #### Start Starts an SSE-based MCP server and displays connection information. ```shell $ encore mcp start [--app=] ``` #### Run Establishes an stdio-based MCP session. This command is typically used by MCP hosts to communicate with the server through standard input/output streams. ```shell $ encore mcp run [--app=] ``` ## Exposed Tools Encore's MCP server exposes the following tools that provide AI models with detailed context about your application. These tools enable LLMs to understand your application's structure, retrieve relevant information, and take actions within your system. #### Database Tools - **get_databases**: Retrieve metadata about all SQL databases defined in the application, including their schema, tables, and relationships. - **query_database**: Execute SQL queries against one or more databases in the application. #### API Tools - **call_endpoint**: Make HTTP requests to any API endpoint in the application. - **get_services**: Retrieve comprehensive information about all services and their endpoints in the application. - **get_middleware**: Retrieve detailed information about all middleware components in the application. - **get_auth_handlers**: Retrieve information about all authentication handlers in the application. #### Trace Tools - **get_traces**: Retrieve a list of request traces from the application, including their timing, status, and associated metadata. - **get_trace_spans**: Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata. #### Source Code Tools - **get_metadata**: Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components. - **get_src_files**: Retrieve the contents of one or more source files from the application. #### PubSub Tools - **get_pubsub**: Retrieve detailed information about all PubSub topics and their subscriptions in the application. #### Storage Tools - **get_storage_buckets**: Retrieve comprehensive information about all storage buckets in the application. - **get_objects**: List and retrieve metadata about objects stored in one or more storage buckets. #### Cache Tools - **get_cache_keyspaces**: Retrieve comprehensive information about all cache keyspaces in the application. #### Metrics Tools - **get_metrics**: Retrieve comprehensive information about all metrics defined in the application. #### Cron Tools - **get_cronjobs**: Retrieve detailed information about all scheduled cron jobs in the application. #### Secret Tools - **get_secrets**: Retrieve metadata about all secrets used in the application. #### Documentation Tools - **search_docs**: Search the Encore documentation using Algolia's search engine. - **get_docs**: Retrieve the full content of specific documentation pages. ================================================ FILE: docs/go/cli/telemetry.md ================================================ --- seotitle: Encore Telemetry seodesc: Encore collects telemetry data about app usage title: Telemetry lang: go --- Telemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently. Encore only collects telemetry data in the local development tools and the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations. ## Why We Collect Data We collect telemetry data for several important reasons: 1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development. 2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience. 3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur. 4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly. ## How Data is Collected Encore collects data in a way that prioritizes user privacy and security. Here's how we do it: 1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts. 2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports. 3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols. 4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance. ### Example of Data Being Sent Here is an example of the type of data that is sent: ```json { "event": "app.create", "anonymousId": "a-uuid-unique-for-the-installation", "properties": { "error": false, "lang": "go", "template": "graphql" } } ``` ## Data We Don't Collect At Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect: 1. **Environment Variables**: We do not collect any environment variables set in your development or production environments. 2. **File Paths**: The specific paths of your files and directories are not collected. 3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects. 4. **Logs**: No log files from your application or development environment are collected. 5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information. Our goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure. ## Disabling Telemetry While telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways: 1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal. ```sh encore telemetry disable ``` 2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable. ```sh export DISABLE_ENCORE_TELEMETRY=1 ``` 3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled. 4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command: ```sh encore telemetry enable ``` ## Debugging Telemetry For users who want more visibility into what telemetry data is being sent, you can enable debug mode: 1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable. ```sh export ENCORE_TELEMETRY_DEBUG=1 ``` 2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent. ## Conclusion Telemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team. Thank you for helping us make Encore better! ================================================ FILE: docs/go/community/contribute.md ================================================ --- seotitle: How to contribute to Encore Open Source Project seodesc: Learn how to contribute to the Encore Open Source project by submitting pull requests, reporting bugs, or contributing documentation or example projects. title: Ways to contribute subtitle: Guidelines for contributing to Encore lang: go --- We’re so excited that you are interested in contributing to Encore! All contributions are welcome, and there are several valuable ways to contribute. ### Open Source Project If you want to contribute to the Encore Open Source project, you can submit a pull request on [GitHub](https://github.com/encoredev/encore/pulls). ### Report issues If you have run into an issue or think you’ve found a bug, please report it via the [issue tracker](https://github.com/encoredev/encore/issues). ### Add or update docs If there’s something you think would be helpful to add to the docs or if there’s something that seems out of date, we appreciate your input. You can view the docs and contribute fixes or improvements directly in [GitHub](https://github.com/encoredev/encore/tree/main/docs). You can also email your feedback to us at [hello@encore.dev](mailto:hello@encore.dev). ### Blog posts If you’ve built something cool using Encore, we’d really like you to talk about it! We love it when developers share their projects on blogs and on Twitter. Use the hashtag **#builtwithencore** and we’ll have an easier time finding your work. – We might also showcase it on the [Encore Twitter account](https://twitter.com/encoredotdev)! ### Meetups & Workshops Organizing a meetup or workshop is a great way to connect with other developers using Encore. It can also be a great first step in trying out Encore for development in your company or other professional organization. If you want help with organizing or planning an event, please don’t hesitate to reach out to us via email at [hello@encore.dev](mailto:hello@encore.dev). ================================================ FILE: docs/go/community/get-involved.md ================================================ --- seotitle: Encore's Open Source Developer Community seodesc: Learn how to engage in the Open Source Developer Community supporting Encore. title: Community subtitle: Join the most pioneering developer community! lang: go --- Developers building with Encore are forward-thinkers, who are working on exciting and innovative applications. We rely on this group's feedback, and contributions to the Open Source project, to improve Encore for developers everywhere. Getting involved is a fantastic way of finding support and inspiration among peers. Everyone is welcome in the Encore community, and we hope you to get involved too! ## Get involved There are many ways to get involved. Here's where you can start straight away.

Contribute on GitHub

Use GitHub to report bugs, feedback on proposals, or contribute your ideas.

Join Discord

Connect with fellow Encore developers, ask questions, or just hang out!

Follow on Twitter

Follow Encore on Twitter to keep up with the latest. Share what you've built to help spread the word about the project. ### Contribute to the project Want to make a contribution to Encore? Great, start by reading about the different [ways to contribute](/docs/go/community/contribute). ### Feedback on the Roadmap [The Encore Roadmap](https://encore.dev/roadmap) is public. It's open to your comments, feature requests, and you can vote on existing entries. ## Community Governance We recommend everyone read the [Community Principles](/docs/go/community/principles). If you need assistance, have concerns, or have questions for the Community team, please email us at [support@encore.dev](mailto:support@encore.dev). ================================================ FILE: docs/go/community/open-source.md ================================================ --- seotitle: Encore is Open Source seodesc: We believe Open Source is key to a sustainable and prosperous technology community. Encore builds on Open Source software, and is itself Open Source. title: Open Source subtitle: Encore is Open Source Software lang: go --- We believe Open Source is key to a long-term sustainable and prosperous technology community. Encore builds on Open Source software, and is largely Open Source itself. ## License Encore's Backend Framework, parser, and compiler are Open Source under Mozilla Public License 2.0. > The MPL is a simple copyleft license. The MPL's "file-level" copyleft is designed to encourage contributors to share modifications they make to your code, while still allowing them to combine your code with code under other licenses (open or proprietary) with minimal restrictions. You can learn more about MPL 2.0 on [the official website](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). ## Contribute Contributions to improve Encore are very welcome. Contribute to Encore on [GitHub](https://github.com/encoredev/encore). ================================================ FILE: docs/go/community/principles.md ================================================ --- seotitle: Encore Community Principles seodesc: Everyone is welcome in the Encore community, and we want everyone to feel at home and free to contribute. title: Community principles subtitle: Everyone belongs in the Encore community lang: go --- Everyone is welcome in the Encore community, and it is of utmost importance to us that everyone is able to feel at home and contribute. Therefore we as maintainers, and you as a contributor, must pledge to make participation in our community a harassment-free experience for everyone, regardless of: age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity. ### Code of Conduct To this end, the Encore community is guided by the [Contributor Covenant 2.0 Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/) to ensure everyone is welcome and able to participate. ================================================ FILE: docs/go/community/submit-template.md ================================================ --- seotitle: Submit a Template to Encore's Templates repo seodesc: Learn how to contribute to Encore's Templates repository and get features in the Encore Templates marketplace. title: Submit a Template subtitle: Your contributions help other developers build lang: go --- [Templates](/templates) help and inspire developers to build applications using Encore. You are welcome to contribute your own templates! Two types of templates that are especially useful: - **Starters:** Runnable Encore applications for others to use as is, or take inspiration from. - **Bits:** Re-usable code samples to solve common development patterns or integrate Encore applications with third-party APIs and services. ## Submit your contribution Contribute a template by submitting a Pull Request to the [Open Source Examples Repo](https://github.com/encoredev/examples): `https://github.com/encoredev/examples` ### Submitting Starters Follow these steps to submit a **Starter**: 1. Fork the repo. 2. Create a new folder in the root directory of the repo, this is where you will place your template. — Use a short folder name as your template will be installable via the CLI, like so: `encore app create APP-NAME --example=` 3. Include a `README.md` with instructions for how to use the template. We recommend following [this format](https://github.com/encoredev/examples/blob/8c7e33243f6bfb1b2654839e996e9a924dcd309e/uptime/README.md). Once your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website. ### Submitting Bits Follow these steps to submit your **Bits**: 1. Fork the repo. 2. Create a new folder inside the `bits` folder in the repo and place your template inside it. Use a short folder name as your template will soon be installable via the CLI. 3. Include a `README.md` with instructions for how to use the template. Once your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website. ## Contribute from your own repo If you don't want to contribute code to the examples repo, but still want to be featured on the [Templates page](/templates), please contact us at [hello@encore.dev](mailto:hello@encore.dev). ## Dynamic Encore AppID In most cases, you should avoid hardcoding an `AppID` in your template's source code. Instead, use the notation `{{ENCORE_APP_ID}}`. When a developer creates an app using the template, `{{ENCORE_APP_ID}}` will be dymically replaced with their new and unique `AppID`, meaning they will not need to make any manual code adjustments. ================================================ FILE: docs/go/concepts/application-model.md ================================================ --- seotitle: Encore Application Model seodesc: How Encore understands your application using static analysis title: Encore Application Model subtitle: How Encore understands your application lang: go --- Encore works by using static analysis to understand your application. This is a fancy term for parsing and analyzing the code you write and creating a graph of how your application works. This graph closely represents your own mental model of the system: boxes and arrows that represent systems and services that communicate with other systems, pass data and connect to infrastructure. We call it the Encore Application Model. Because the Open Source framework, parser, and compiler, are all designed together, Encore can ensure 100% accuracy when creating the application model. Any deviation is caught as a compilation error. Using this model, Encore can provide tools to solve problems that normally would be up to the developer to do manually. From creating architecture diagrams and API documentation to provisioning cloud infrastructure. We're continuously expanding on Encore's capabilities and are building a new generation of developer tools that are enabled by Encore's understanding of your application. The framework, parser, and compiler that enable this are all [Open Source](https://github.com/encoredev/encore). ## Standardization brings clarity Developers make dozens of decisions when creating a backend application. Deciding how to structure the codebase, defining API schemas, picking underlying infrastructure, etc. The decisions often come down to personal preferences, not technical rationale. This creates a huge problem in the form of fragmentation! When every stack looks different, all tools have to be general purpose. When you adopt Encore, many of these stylistic decisions are already made for you. The Encore framework ensures your application follows modern best practices. And when you run your application, Encore's Open Source parser and compiler check that you're sticking to the standard. This means you're free to focus your energy on what matters: writing your application's business logic. ================================================ FILE: docs/go/concepts/benefits.md ================================================ --- seotitle: Benefits of using Encore.go seodesc: See how Encore.go helps you build backends faster using Go. title: Encore.go Benefits subtitle: How Encore.go helps you build robust distributed systems, faster. lang: go --- Using Encore.go to declare infrastructure in application code helps unlock several benefits: - **Local development with instant infrastructure**: Encore.go automatically sets up necessary infrastructure as you develop. - **Rapid feedback**: Catch issues early with type-safe infrastructure, avoiding slow deployment cycles. - **No manual configuration required**: No need for Infrastructure-as-Code. Your code is the single source of truth. - **Unified codebase**: One codebase for all environments; local, preview, and cloud. - **Cloud-agnostic by default**: Encore.go provides an abstraction layer on top of the cloud provider's APIs, so you avoid becoming locked in to a single cloud. - **Evolve infrastructure without code changes**: As requirements evolve, you can change the provisioned infrastructure without making code changes, you only need to change the infrastructure configuration which is separate from the application code. - **AI-assisted development**: Encore is built for AI coding assistants. With [Encore-specific rules and MCP integration](/docs/go/ai-integration), AI understands your architecture and can generate type-safe, pattern-consistent code and introspect your app—services, APIs, databases, and traces. ## No DevOps experience required Encore provides open source tools to help you integrate with your cloud infrastructure, enabling you to self-host your application anywhere that supports Docker containers. Learn more in the [self-host documentation](/docs/go/self-host/docker-build). You can also use [Encore Cloud](https://encore.dev/use-cases/devops-automation), which fully automates provisioning and managing infrastructure in your own cloud on AWS and GCP. This approach dramatically reduces the level of DevOps expertise required to use scalable, production-ready, cloud services like Kubernetes and Pub/Sub. And because your application code is the source of truth for infrastructure requirements, it ensures the infrastructure in all your environments are always in sync with the application's requirements. ## Simplicity without giving up flexibility Encore.go provides integrations for common infrastructure primitives, but also allows for flexibility. You can always use any cloud infrastructure, even if it's not built into Encore.go. If you use Encore's [Cloud Platform](https://encore.dev/use-cases/devops-automation), it [automates infrastructure](/docs/platform/infrastructure/infra) using your own cloud account, so you always have full access to your services from the cloud provider's console. ================================================ FILE: docs/go/develop/api-docs.md ================================================ --- seotitle: Service Catalog & Generated API Docs seodesc: See how Encore automatically generates API documentation that always stays up to date and in sync. title: Service Catalog subtitle: Automatically get a Service Catalog and complete API docs --- All developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date. To solve this, Encore uses the [Encore Application Model](/docs/go/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve. The API docs are available both in your [Local Development Dashboard](/docs/go/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud). ================================================ FILE: docs/go/develop/auth.md ================================================ --- seotitle: Adding authentication to APIs to auth users seodesc: Learn how to add authentication to your APIs and make sure you know who's calling your backend APIs. title: Authenticating users subtitle: Knowing what's what and who's who infobox: { title: "Authentication", import: "encore.dev/beta/auth", } lang: go --- Almost every application needs to know who's calling it, whether the user represents a person in a consumer-facing app or an organization in a B2B app. Encore supports both use cases in a simple yet powerful way. As described in the docs for [defining APIs](/docs/go/primitives/defining-apis), Encore offers three access levels for APIs: * `//encore:api public` – defines a public API that anybody on the internet can call. * `//encore:api private` – defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs. * `//encore:api auth` – defines a public API that anybody can call, but that requires valid authentication. When an API is defined with access level `auth`, outside calls to that API must specify an authorization header, in the form `Authorization: Bearer `. The token is passed to a designated auth handler function and the API call is allowed to go through only if the auth handler determines the token is valid. For more advanced use cases you can also customize the authentication information you want. See the section on [accepting structured auth information](#accepting-structured-auth-information) below. You can optionally send in auth data to `public` and `private` APIs, in which case the auth handler will be used. When used for `private` APIs, they are still not accessible from the outside world. ## The auth handler Encore applications can designate a special function to handle authentication, by defining a function and annotating it with `//encore:authhandler`. This annotation tells Encore to run the function whenever an incoming API call contains authentication data. The auth handler is responsible for validating the incoming authentication data and returning an `auth.UID` (a string type representing a **user id**). The `auth.UID` can be whatever you wish, but in practice it usually maps directly to the primary key stored in a user table (either defined in the Encore service or in an external service like [Firebase](/docs/go/how-to/firebase-auth) or [Auth0](/docs/go/how-to/auth0-auth)). ### With custom user data Oftentimes it's convenient for the rest of your application to easily be able to look up information about the authenticated user making the request. If that's the case, define the auth handler like so: ```go import "encore.dev/beta/auth" // Data can be named whatever you prefer (but must be exported). type Data struct { Username string // ... } // AuthHandler can be named whatever you prefer (but must be exported). //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) { // Validate the token and look up the user id and user data, // for example by calling Firebase Auth. } ``` ### Without custom user data When you don't require custom user data and it's sufficient to use `auth.UID`, simply skip it in the return type: ```go import "encore.dev/beta/auth" // AuthHandler can be named whatever you prefer (but must be exported). //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, error) { // Validate the token and look up the user id, // for example by calling Firebase Auth. } ``` ## Accepting structured auth information In the examples above the function accepts a `Bearer` token as a string argument. In that case Encore parses the `Authorization` HTTP header and passes the token to the auth handler. In cases where you have different or more complex authorization requirements, you can instead specify a data structure that specifies one or more fields to be parsed from the HTTP request. For example: ```go type MyAuthParams struct { // SessionCookie is set to the value of the "session" cookie. // If the cookie is not set it's nil. SessionCookie *http.Cookie `cookie:"session"` // ClientID is the unique id of the client, sourced from the URL query string. ClientID string `query:"client_id"` // Authorization is the raw value of the "Authorization" header // without any parsing. Authorization string `header:"Authorization"` } //encore:authhandler func AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) { // ... } ``` This example tells Encore that the application accepts authentication information via the `session` cookie, the `client_id` query string parameter, and the `Authorization` header. These fields are automatically filled in when the auth handler is called (if present in the request). You can of course combine auth params like this with custom user data (see the section above). Cookies are generally only used by browsers and are automatically added to requests made by browsers. As a result Encore does not include cookie fields in generated clients' authentication payloads or in the [Local Development Dashboard](/docs/go/observability/dev-dash). ## Handling auth errors When a token doesn't match your auth rules (for example if it's expired, the token has been revoked, or the token is invalid), you should return a non-nil error from the auth handler. Encore passes the error message on to the user when you use [Encore's built-in error package](/docs/go/primitives/api-errors), so we recommend using that with the error code `Unauthenticated` to communicate what happened. For example: ```go import "encore.dev/beta/errs" //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, error) { return "", &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } ``` Note that for security reasons you may not want to reveal too much information about why a request did not pass your auth checks. There are many subtle security considerations when dealing with authentication and we don't have time to go into all of them here. Whenever possible we recommend using a third-party auth provider instead of rolling your own authentication. ## Using auth data Once the user has been identified by the auth handler, the API handler is called as usual. If it wishes to inspect the authenticated user, it can use the `encore.dev/beta/auth` package: - `auth.Data()` returns the custom user data returned by the auth handler (if any) - `auth.UserID()` returns `(auth.UID, bool)` to get the authenticated user id (if any) For an incoming request from the outside to an API that uses the `auth` access level, these are guaranteed to be set since the API won't be called if the auth handler doesn't succeed. Encore automatically propagates the auth data when you make API calls to other Encore API endpoints. If an endpoint calls another endpoint during its processing, and the original does not have an authenticated user, the request will fail. This behavior preserves the guarantees that `auth` endpoints always have an authenticated user. ## Optional authentication While Encore always calls the auth handler for API endpoints marked as `auth`, you can also call `public` API endpoints with authentication data. This can be useful for APIs that support both a "logged in" and "logged out" experience. For example, a site like Reddit might have a `post.List` endpoint that returns the list of posts, but if you're logged in it also includes whether or not you have upvoted or downvoted each post. To support such use cases, Encore runs the auth handler for `public` API endpoints if (and only if) the request includes any authentication information (such as the `Authorization` header). In that case, the request processing behavior varies depending on the value of the `error` returned from the auth handler: * If the error is nil, the request is considered to be an authenticated request and `auth.UID()` and `auth.Data()` will include the information the auth handler returned. * If the error is non-nil and the error code is `errs.Unauthenticated` (like shown above), the request continues as an unauthenticated request, behaving exactly as if there was no authentication data provided at all. * If the error is non-nil and the error code is anything else, the request is aborted and Encore returns that error to the caller. To be able to determine if the request has an authenticated user, check the second return value from `auth.UserID()`. ## Overriding auth information Encore supports overriding the auth information for an outgoing request using the [`auth.WithContext`](https://pkg.go.dev/encore.dev/beta/auth#WithContext) function. This function returns a new context with the auth information set to the specified values. Note that this only affects the auth information passed along with the request, and not the current request being processed (if any). This function is often useful when testing APIs that use authentication. For example: ```go ctx := auth.WithContext(context.Background(), auth.UID("my-user-id"), &MyAuthData{Email: "hello@example.com"}) // ... Make an API call using `ctx` to override the auth information for that API call. ``` ================================================ FILE: docs/go/develop/config.md ================================================ --- seotitle: Configuration for environment specific changes seodesc: See how you can use configuration to define different behavior in each environment. Making it simpler to develop and test your backend application. title: Configuration subtitle: Define behavior in specific environments infobox: { title: "Configuration", import: "encore.dev/config", } lang: go --- Configuration files let you define default behavior for your application, and override it for specific environments. This allows you to make changes without affecting deployments in other environments. Encore supports configuration files written in [CUE](https://cuelang.org/), which is a superset of JSON. It adds the following: - C-style comments - Quotes may be omitted from field names without special characters - Commas at the end of fields are optional - A comma after last element in list is allowed - The outer curly braces on the file are optional - [Expressions](https://cuelang.org/docs/tutorials/tour/expressions/) such as interpolation, comprehensions and conditionals are supported. For sensitive data use Encore's [secrets management](/docs/go/primitives/secrets) functionality instead of configuration. ## Using Config Inside your service, you can call `config.Load[*SomeConfigType]()` to load the config. This must be done at the package level, and not inside a function. See more in the [package documentation](https://pkg.go.dev/encore.dev/config#Load). Here's an example implementation: ```go package mysvc import ( "encore.dev/config" ) type SomeConfigType struct { ReadOnly config.Bool // Put the system into read-only mode Example config.String } var cfg *SomeConfigType = config.Load[*SomeConfigType]() ``` The type you pass as a type parameter to this function will be used to generate a `encore.gen.cue` file in your services directory. This file will contain both the CUE definition for your configuration type, and some [metadata](#provided-meta-values) that Encore will provide to your service at runtime. This allows you to change the final value of your configuration based on the environment the application is running in. Any files ending with `.cue` in your service directory or sub-directories will be loaded by Encore and given to CUE to unify and compute a final configuration. ``` -- mysvc/encore.gen.cue -- // Code generated by encore. DO NOT EDIT. package mysvc #Meta: { APIBaseURL: string Environment: { Name: string Type: "production" | "development" | "ephemeral" | "test" Cloud: "aws" | "gcp" | "encore" | "local" } } #Config: { ReadOnly: bool // Put the system into read-only mode Example: string } #Config -- mysvc/myconfig.cue -- // Set example to "hello world" Example: "hello world" // By default we're not in read only mode ReadOnly: bool | *false // But on the old production environment, we're in read only mode if #Meta.Environment.Name == "old-prod" { ReadOnly: true } ``` Loading configuration is only supported in services and the loaded data can not be referenced from packages outside that service. ### CUE tags in Go Structs You can use the `cue` tag in your Go to specify additional constraints on your configuration. For example: ```go type FooBar { A int `cue:">100"` B int `cue:"A-50"` // If A is set, B can be inferred by CUE C int `cue:"A+B"` // Which then allows CUE to infer this too } var _ = config.Load[*FooBar]() ``` Will result in the following CUE type definition being generated: ```cue #Config: { A: int & >100 B: int & A-50 // If A is set, B can be inferred by CUE C: int & A+B // Which then allows CUE to infer this too } ``` ## Config Wrappers Encore provides type wrappers for config in the form of `config.Value[T]` and `config.Values[T]` which expand into functions of type `T` and `[]T` respectively. These functions allow you to override the default value of your configuration in your CUE files inside tests, where only code run from that test will see the override. In the future we plan to support real-time updating of configuration values on running applications, thus using these wrappers in your configuration today will future proof your code and allow you to automatically take advantage of this feature when it is available. Any type supported in API requests and responses can be used as the type for a config wrapper. However for convenience, Encore ships with the following inbuilt aliases for the config wrappers: - `config.String`, `config.Bool`, `config.Int`, `config.Uint`, `config.Int8`, `config.Int16`, `config.Int32`, `config.In64`, `config.Uint8`, `config.Uint16`, `config.Uint32`, `config.Uint64`, `config.Float32`, `config.Float64`, `config.Bytes`, `config.Time`, `config.UUID` ```go -- svc/svc.go -- type mysvc import ( "encore.dev/config" ) type Server struct { // The config wrappers do not have to be in the top level struct Enabled config.Bool Port config.Int } type SvcConfig struct { GameServerPorts config.Values[Server] } var cfg = config.Load[*SvcConfig]() func startServers() { for _, server := range cfg.GameServerPorts() { if server.Enabled() { go startServer(server.Port()) } } } func startServer(port int) { // ... } -- svc/servers.cue -- GameServerPorts: [ { Enabled: false Port: 12345 }, { Enabled: true Port: 1337 }, ] ``` ## Provided Meta Values When your application is running, Encore will provide information about that environment to your CUE files, which you can use to filter on. These fields can be found in the `encore.gen.cue` file which Encore will generate when you add a call to load config. Encore provides the following meta values: - **APIBaseURL**: The base URL of the Encore API, which can be used to make API calls to the application. - **Environment**: A struct containing information about the environment the application is running in.
   **Name**: The name of the environment
   **Type**: One of `production`, `development`, `ephemeral` or `test`.
   **Cloud**: The cloud the app is running on, which is one of `aws`, `gcp`, `encore` or `local`.
The following are useful conditionals you can use in your CUE files: ```cue // An application running due to `encore run` if #Meta.Environment.Type == "development" && #Meta.Environment.Cloud == "local" {} // An application running in a development environment in the Cloud if #Meta.Environment.Type == "development" && #Meta.Environment.Cloud != "local" {} // An application running in a production environment if #Meta.Environment.Type == "production" {} // An application running in an environment that Encore has created // for an open Pull Request on Github if #Meta.Environment.Type == "ephemeral" {} ``` ## Testing with Config Through the provided meta values, your applications configuration can have different values in tests, compared to when the application is running. This can be useful to prevent external side effects from your tests, such as emailing customers across all test. Sometimes however, you may want to test specific behaviors based on different configurations (such as disabling user signups), in this scenario using the Meta data does not give you fine enough control. To allow you to set a configuration value at a per test level, Encore provides the helper function [`et.SetCfg`](https://pkg.go.dev/encore.dev/et#SetCfg). You can use this function to set a new value only in the current test and any sub tests, while all other tests will continue to use the value defined in the CUE files. ```go -- config.cue -- // By default we want to send emails SendEmails: bool | *true // But in all tests we want to disable emails if #Meta.Environment.Type == "test" { SendEmails: false } -- signup.go -- import ( "context" "encore.dev/config" ) type Config struct { SendEmails config.Bool } var cfg = config.Load[Config]() //encore:api public func Signup(ctx context.Context, p *SignupParams) error { user := createUser(p) if cfg.SendEmails() { SendWelcomeEmail(user) } return nil } -- signup_test.go -- import ( "errors" "testing" "encore.dev/et" ) func TestSignup(t *testing.T) { err := Signup(context.Background(), &SignupParams { ... }) if err != nil { // We don't expect an error here t.Fatal(err) } if emailWasSent() { // We don't expect an email to be sent // as it's disabled for all tests t.Fatal("email was sent") } } func TestSignup_TestEmails(t *testing.T) { // For this test, we want to enable the welcome // emails so we can test that they are sent et.SetCfg(cfg.SendEmails, true) err := Signup(context.Background(), &SignupParams { ... }) if err != nil { // We don't expect an error here t.Fatal(err) } // Check the email was sent if !emailWasSent() { t.Fatal("email was not sent") } } ``` ## Useful CUE Patterns If you're new to CUE, we'd recommend checking out the [CUE documentation](https://cuelang.org/docs/) and [cuetorials](https://cuetorials.com/), however to get you started, here are some useful patterns you can use in your CUE files. ### Defaults CUE supports the concept of a default value, which it will use if no other concrete value is provided. This can be useful for when you normally want one value, but occasionally might want to provide an override in a certain scenario. A default value is specified by prefixing it with a `*`. ```cue // ReadOnlyMode is a boolean and if we don't provide a value, it // will default to false. ReadOnlyMode: bool | *false if #Meta.Environment.Name == "old-prod" { // On this environment, we want to set ReadOnlyMode to true ReadOnlyMode: true } ``` ### Validation within CUE Any field prefixed with an `_` will not be exported to the concrete configuration once evaluated by CUE and can be used to hold intermediate values. Because CUE allows you to define the same field as many times as you want, as long as the values unify, we can build complex validation logic. ```cue import ( "list" // import CUE's list package ) // Set some port numbers defaulting just to 8080 // but in development including 8443 portNumbers: [...int] | *[8080] if #Meta.Environment.Type == "development" { portNumbers: [8080, 8443] } // Port numbers must be an array and all values // are integers 1024 or above. portNumbers: [...int & >= 1024] // The ports are considered valid if they contain the port number 8080. _portsAreValid: list.Contains(portNumbers, 8080) // Ensure that the ports are valid by constraining the value to be true. // CUE will report an error if the value is false (that is if the portNumbers list // does not contain the value 8080). _portsAreValid: true ``` ### Switch Statements If statements in CUE do not have else branches, which can make it difficult to write complex conditionals, we however can use an array to emulate a switch statement, where the first value that matches the condition is returned. The following example will set `SendEmailsFrom` to a single string. ```cue SendEmailsFrom: [ // These act as individual case statements if #Meta.Environment.Type == "production" { "noreply@example.com" }, if #Meta.Environment.Name == "staging" { "staging@example.com" }, // This last value without a condition acts as the default case "dev-system@example.dev", ][0] // Return the first value which matches the condition ``` ### Using Map Keys as Values CUE allows us to extract map keys and use them as values to simplify the config we need to write and minimize duplication. ```cue // Define the type we want to use #Server: { server: string port: int & > 1024 enabled: bool | *true } // Specify that servers is a map of strings to #Server // where they key we assign the variable Name servers: [Name=string]: #Server & { // Then we union the key with the value of server server: Name } servers: { "Foo": { port: 8080 }, "Bar": { port: 8081 enabled: false }, } ``` This will result in the concrete configuration of: ```json { "servers": { "Foo": { "server": "Foo", "port": 8080, "enabled": true }, "Bar": { "server": "Bar", "port": 8081, "enabled": false } } } ``` ================================================ FILE: docs/go/develop/cors.md ================================================ --- seotitle: Handling CORS (Cross-Origin Resource Sharing) seodesc: See how you can configure CORS for your Encore application. title: CORS subtitle: Configure CORS (Cross-Origin Resource Sharing) for your Encore application lang: go --- CORS is a web security concept that defines which website origins are allowed to access your API. A deep-dive into CORS is out of scope for this documentation, but [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) provides a good overview. In short, CORS affects requests made by browsers to resources hosted on other origins (a combination of the scheme, domain, and port). ## Configuring CORS Encore provides a default CORS configuration that is suitable for many APIs. You can override these settings by specifying the `global_cors` key in the `encore.app` file, which has the following structure: ```cue { // debug enables CORS debug logging. "debug": true | false, // allow_headers allows an app to specify additional headers that should be // accepted by the app. // // If the list contains "*", then all headers are allowed. "allow_headers": [...string], // expose_headers allows an app to specify additional headers that should be // exposed from the app, beyond the default set always recognized by Encore. // // If the list contains "*", then all headers are exposed. "expose_headers": [...string], // allow_origins_without_credentials specifies the allowed origins for requests // that don't include credentials. If nil it defaults to allowing all domains // (equivalent to ["*"]). "allow_origins_without_credentials": [...string], // allow_origins_with_credentials specifies the allowed origins for requests // that include credentials. If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). "allow_origins_with_credentials": [...string], } ``` ## Allowed origins The main CORS configuration is the list of allowed origins, meaning which websites are allowed to access your API (via browsers). For this purpose, CORS makes a distinction between requests that contain authentication information (cookies, HTTP authentication, or client certificates) and those that do not. CORS applies stricter rules to authenticated requests. By default, Encore allows unauthenticated requests from all origins but disallows requests that do include authorization information from other origins. This is a good default for many APIs. This can be changed by setting the `allow_origins_without_credentials` key (see above). For convenience Encore also allows all origins when developing locally. For security reasons it's necessary to explicitly specify which origins are allowed to make authenticated requests. This is done by setting the `allow_origins_with_credentials` key (see above). ## Allowed headers and exposed headers CORS also lets you specify which headers are allowed to be sent by the client ("allowed headers"), and which headers are exposed to scripts running in the browser ("exposed headers"). Encore automatically configures headers by parsing your program using static analysis. If your API defines a request or response type that contains a header field, Encore automatically adds the header to the list of exposed and allowed headers in request types respectively. To add additional headers to these lists, you can set the `allow_headers` and `expose_headers` keys (see above). This can be useful when your application relies on custom headers in e.g. raw endpoints that aren't seen by Encore's static analysis. ================================================ FILE: docs/go/develop/env-vars.md ================================================ --- seotitle: Environment Variables Reference seodesc: Learn how to configure Encore's development environment using environment. title: Environment Variables subtitle: Configure your development environment lang: go --- Encore works out of the box without configuration, but provides several environment variables for advanced use cases such as debugging, testing, or adapting Encore to specific workflow requirements. ## Daemon & Development Dashboard These variables control how the Encore daemon operates and where it exposes its services. ### ENCORE_DAEMON_LOG_PATH Controls the location of the Encore daemon log file. **Default:** `/encore/daemon.log` **Example:** ```bash export ENCORE_DAEMON_LOG_PATH=/var/log/encore/daemon.log ``` ### ENCORE_DEVDASH_LISTEN_ADDR Overrides the listen address for the local development dashboard. **Default:** Automatically assigned by the daemon **Format:** Network address (e.g., `localhost:9400`) **Example:** ```bash export ENCORE_DEVDASH_LISTEN_ADDR=localhost:8080 encore run ``` ### ENCORE_MCPSSE_LISTEN_ADDR Overrides the listen address for the MCP SSE (Model Context Protocol Server-Sent Events) endpoint. **Default:** Automatically assigned by the daemon **Format:** Network address **Example:** ```bash export ENCORE_MCPSSE_LISTEN_ADDR=localhost:9401 ``` ### ENCORE_OBJECTSTORAGE_LISTEN_ADDR Overrides the listen address for the object storage service endpoint. **Default:** Automatically assigned by the daemon **Format:** Network address **Example:** ```bash export ENCORE_OBJECTSTORAGE_LISTEN_ADDR=localhost:9402 ``` ## Advanced Development These variables are primarily useful for advanced development scenarios, such as contributing to Encore itself or using custom builds. ### ENCORE_RUNTIMES_PATH Specifies the path to the Encore runtimes directory. **Default:** Auto-detected relative to the Encore installation (`/runtimes`) **Example:** ```bash export ENCORE_RUNTIMES_PATH=/path/to/custom/runtimes ``` ### ENCORE_GOROOT Specifies the path to the custom Encore Go runtime. **Default:** Auto-detected relative to the Encore installation (`/encore-go`) **Example:** ```bash export ENCORE_GOROOT=/path/to/custom/encore-go ``` For most users, these paths are automatically detected and don't need to be set. They are primarily useful when contributing to Encore or testing custom builds. ================================================ FILE: docs/go/develop/metadata.md ================================================ --- seotitle: Metadata API – Get data about apps, envs, and requests seodesc: See how to use Encore's Metadata API to get information about specific apps, environments, and requests. title: Metadata subtitle: Use the metadata API to get specifics about apps, environments, and requests infobox: { title: "Metadata API", import: "encore.dev", } lang: go --- While Encore tries to provide a cloud-agnostic environment, sometimes it's helpful to know more about the environment your application is running in. For this reason Encore provides an API for accessing metadata about the [application](#application-metadata) and the environment it's running in, as well as information about the [current request](#current-request) as part of the `encore.dev` package. ## Application Metadata Calling `encore.Meta()` will return an [encore.AppMetadata](https://pkg.go.dev/encore.dev/#AppMetadata) instance which contains information about the application, including: - `AppID` - the application name. - `APIBaseURL` - the URL the application API can be publicly accessed on. - `Environment` - the [environment](/docs/platform/deploy/environments) the application is currently running in. - `Build` - the revision information of the build from the version control system. - `Deploy` - the deployment ID and when this version of the app was deployed. ## Current Request `encore.CurrentRequest()` can be called from anywhere within your application and will return an [encore.Request](https://pkg.go.dev/encore.dev/#Request) instance which will provides information about why the current code is running. The [encore.Request](https://pkg.go.dev/encore.dev/#Request) type contains information about the running request, such as: - The service and endpoint being called - Path and path parameter information - When the request started This works automatically as a result of Encore's request tracking, and works even in other goroutines that were spawned during request handling. If no request is processed by the caller, which can happen if you call it during service initialization, the Type field returns None. If `CurrentRequest()` is called from a goroutine spawned during request processing it will continue to report the same request even if the request handler has already returned. This can be useful on [raw endpoints](/docs/go/primitives/raw-endpoints) with [path parameters](/docs/go/primitives/defining-apis#rest-apis) as the standard `http.Request` object passed into the raw endpoint does not provide access to the parsed path parameters, however by calling `encore.CurrentRequest().PathParams()` you can get access to the parsed path parameters. ## Example Use Cases ### Using Cloud Specific Services All the [clouds](/docs/platform/deploy/own-cloud) contain a large number of services, not all of which Encore natively supports. By using information about the [environment](/docs/platform/deploy/environments), you can define the implementation of these and use different services for each environment's provider. For instance if you are pushing audit logs into a data warehouse, when running on GCP you could use BigQuery, but when running on AWS you could use Redshift, when running locally you could simply write them to a file. ```go package audit import ( "encore.dev" "encore.dev/beta/auth" ) func Audit(ctx context.Context, action message, user auth.UID) error { switch encore.Meta().Environment.Cloud { case encore.CloudAWS: return writeIntoRedshift(ctx, action, user) case encore.CloudGCP: return writeIntoBigQuery(ctx, action, user) case encore.CloudLocal: return writeIntoFile(ctx, action, user) default: return fmt.Errorf("unknown cloud: %s", encore.Meta().Environment.Cloud) } } ``` ### Checking Environment type When implementing a signup system, you may want to skip email verification on user signups when developing the application. Using the `encore.Meta()` API, we can check the environment and decide whether to send an email or simply mark the user as verified upon signup. ```go package user import "encore.dev" //encore:api public func Signup(ctx context.Context, params *SignupParams) (*SignupResponse, error) { // ... // If this is a testing environment, skip sending the verification email switch encore.Meta().Environment.Type { case encore.EnvTest, encore.EnvDevelopment: if err := MarkEmailVerified(ctx, userID); err != nil { return nil, err } default: if err := SendVerificationEmail(ctx, userID); err != nil { return nil, err } } // ... } ``` ================================================ FILE: docs/go/develop/middleware.md ================================================ --- seotitle: Using Middleware in your backend application seodesc: See how you can use middleware in your backend application to handle cross-cutting generic functionality, like request logging, auth, or tracing. title: Middleware subtitle: Handling cross-cutting, generic functionality infobox: { title: "Middleawre", import: "encore.dev/middleware", } lang: go --- Middleware is a way to write reusable code that runs before or after (or both) the handling of API requests, often across several (or all) API endpoints. It's commonly used to implement cross-cutting concerns like [request logging](/docs/go/observability/logging), [authentication](/docs/go/develop/auth), [tracing](/docs/go/observability/tracing), and so on. One of the benefits of Encore is that all of these use cases are already handled out-of-the-box, so there's no need to use middleware for those things. Nonetheless, there are several use cases where it can be useful to write reusable functionality that applies to multiple API endpoints, and middleware is a good solution in those cases. Encore provides built-in support for middleware by defining a function with the `//encore:middleware` directive. The middleware directive takes a `target` parameter that specifies which API endpoints it applies to. ## Middleware functions A typical middleware implementation looks like this: ```go import ( "encore.dev/beta/errs" "encore.dev/middleware" ) //encore:middleware global target=all func ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response { // If the payload has a Validate method, use it to validate the request. payload := req.Data().Payload if validator, ok := payload.(interface { Validate() error }); ok { if err := validator.Validate(); err != nil { // If the validation fails, return an InvalidArgument error. err = errs.WrapCode(err, errs.InvalidArgument, "validation failed") return middleware.Response{Err: err} } } return next(req) } ``` Middleware forms a chain, allowing each middleware to introspect and process the incoming request before handing it off to the next middleware by calling the `next` function that's passed in as an argument. For the last middleware in the chain, calling `next` results in the actual API handler being called. The `req` parameter provides information about the incoming request (see [package docs](https://pkg.go.dev/encore.dev/middleware#Request)). The `next` function returns a [`middleware.Response`](https://pkg.go.dev/encore.dev/middleware#Response) object which contains the response from the API, describing whether there was an error, and on success the actual response payload. This enables middleware to also introspect and even modify the outgoing response, like this: ```go //encore:middleware target=tag:cache func CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response { data := req.Data() // Check if we have the response cached. Use the request path as the cache key. cacheKey := data.Path if cached, err := loadFromCache(cacheKey, data.API.ResponseType); err == nil && cached != nil { return middleware.Response{Payload: cached} } // Otherwise forward the request to the handler return next(req) } ``` This uses `target=tag:cache` to have the middleware only apply to APIs that have that tag. More on this below in [Targeting APIs](#targeting-apis). Middleware functions can also be defined as methods on a Dependency Injection struct declared with `//encore:service`. For example: ```go //encore:service type Service struct{} //encore:middleware target=all func (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response { // ... } ``` See the [Dependency Injection](/docs/go/how-to/dependency-injection) docs for more information. ## Middleware ordering Middleware can either be defined inside a service, in which case it only runs for APIs within that service, or it can be defined as a `global` middleware, in which case it applies to all services. For global middleware the `target` directive still applies and enables you to easily match a subset of APIs. Global middleware always run before all service-specific middleware, and then run in the order they are defined in the source code based on file name lexicographic ordering. To avoid surprises it's best to define all middleware in a file called `middleware.go` in each service, and to create a single top-level package to contain all global middleware. ## Targeting APIs The `target` directive can either be provided as `target=all` (meaning it applies to all APIs) or a list of tags, in the form `target=tag:foo,tag:bar`. Note that these tags are evaluated with `OR`, meaning the middleware applies to an API if the API has at least one of those tags. APIs can be defined with tags by adding `tag:foo` at the end of the `//encore:api` directive: ```go //encore:api public method=GET path=/user/:id tag:cache func GetUser(ctx context.Context, id string) (*User, error) { // ... } ``` ================================================ FILE: docs/go/develop/mocking.md ================================================ --- seotitle: Mocking out your APIs and services for testing seodesc: Learn how to mock out your APIs and services for testing, and how to use the built-in mocking support in Encore. title: Mocking subtitle: Testing your application in isolation infobox: { title: "Testing", import: "encore.dev/et", } lang: go --- Encore comes with built-in support for mocking out APIs and services, which makes it easier to test your application in isolation. ## Mocking Endpoints Let's say you have an endpoint that calls an external API in our `products` service: ```go //encore:api private func GetPrice(ctx context.Context, p *PriceParams) (*PriceResponse, error) { // Call external API to get the price } ``` When testing this function, you don't want to call the real external API since that would be slow and cause your tests to fail if the API is down. Instead, you want to mock out the API call and return a fake response. In Encore, you can do this by adding a mock implementation of the endpoint using the `et.MockEndpoint` function inside your test: ```go package shoppingcart import ( "context" "testing" "encore.dev/et" // Encore's test support package "your_app/products" ) func Test_Something(t *testing.T) { t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering // Create a mock implementation of pricing API which will only impact this test and any sub-tests et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) { return &products.PriceResponse{Price: 100}, nil }) // ... the rest of your test code here ... } ``` When any code within the test, or any sub-test calls the `GetPrice` API, the mock implementation will be called instead. The mock will not impact any other tests running in parallel. The function you pass to `et.MockEndpoint` must have the same signature as the real endpoint. If you want to mock out the API for all tests in the package, you can add the mock implementation to the `TestMain` function: ```go package shoppingcart import ( "context" "os" "testing" "encore.dev/et" "your_app/products" ) func TestMain(m *testing.M) { // Create a mock implementation of pricing API which will impact all tests within this package et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) { return &products.PriceResponse{Price: 100}, nil }) // Now run the tests os.Exit(m.Run()) } ``` Mocks can be changed at any time, including removing them by setting the mock implementation to `nil`. ## Mocking services As well as mocking individual APIs, you can also mock entire services. This can be useful if you want to inject a different set of dependencies into your service for testing, or a service that your code depends on. This can be done using the `et.MockService` function: ```go package shoppingcart import ( "context" "testing" "encore.dev/et" // Encore's test support package "your_app/products" ) func Test_Something(t *testing.T) { t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering // Create a instance of the products service which will only impact this test and any sub-tests et.MockService("products", &products.Service{ SomeField: "a testing value", }) // ... the rest of your test code here ... } ``` When any code within the test, or any sub-test calls the `products` service, the mock implementation will be called instead. Unlike `et.MockEndpoint`, the mock implementation does not need to have the same signature, and can be any object. The only requirement is that any of the services APIs that are called during the test must be implemented by as a receiver method on the mock object. (This also includes APIs that are defined as package level functions in the service, and are not necessarily defined as receiver methods on that services struct). To help with compile time safety on service mocking, for every service Encore will automatically generate an `Interface` interface which contains all the APIs defined in the service. This interface can be passed as a generic argument to `et.MockService` to ensure that the mock object implements all the APIs defined in the service: ```go type myMockObject struct{} func (m *myMockObject) GetPrice(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) { return &products.PriceResponse{Price: 100}, nil } func Test_Something(t *testing.T) { t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering // This will cause a compile time error if myMockObject does not implement all the APIs defined in the products service et.MockService[products.Interface]("products", &myMockObject{}) } ``` ### Automatic generation of mock objects Thanks to the generated `Interface` interface, it's possible to automatically generate mock objects for your services using either [Mockery](https://vektra.github.io/mockery/latest/) or [GoMock](https://github.com/uber-go/mock). ================================================ FILE: docs/go/develop/testing.md ================================================ --- seotitle: Automated testing for your backend application seodesc: Learn how create automated tests for your microservices backend application, and run them automatically on deploy using Go and Encore. title: Automated testing subtitle: Confidence at speed infobox: { title: "Testing", import: "encore.dev/et", } lang: go --- Go comes with excellent built-in support for automated tests. Encore builds on top of this foundation, and lets you write tests in exactly the same way. We won't cover the basics of how to write tests here, see [the official Go docs](https://golang.org/pkg/testing/) for that. Let's instead focus on the difference between testing in Encore compared to a standard Go application. The main difference is that since Encore requires an extra compilation step, you must run your tests using `encore test` instead of `go test`. This is a wrapper that compiles the Encore app and then runs `go test`. It supports all the same flags that the `go test` command does. For example, use `encore test ./...` to run tests in all sub-directories, or just `encore test` for the current directory. ## Test tracing Encore comes with built-in test tracing for local development. You only need to open Encore's local development dashboard at [localhost:9400](http://localhost:9400) to see traces for all your tests. This makes it very simple to understand the root cause for why a test is failing. ## Integration testing Since Encore removes almost all boilerplate, most of the code you write is business logic that involves databases and calling APIs between services. Such behavior is most easily tested with integration tests. When running tests, Encore automatically sets up the databases you need in a separate database cluster. They are additionally configured to skip `fsync` and to use an in-memory filesystem since durability is not a concern for automated tests. This drastically reduces the speed overhead of writing integration tests. In general, Encore applications tend to focus more on integration tests compared to traditional applications that are heavier on unit tests. This is nothing to worry about and is the recommended best practice. ### Temporary databases When Encore runs tests, by default it reuses the same database for all tests, to improve performance. However, this means that you need to take care when writing tests to ensure tests don't interfere with each other. If you instead want to have a separate database for a given test, you can use [`et.NewTestDatabase`](https://pkg.go.dev/encore.dev/et#NewTestDatabase) to create a temporary database that only exists for the duration of the test. The temporary test database is a fully-migrated database. It does not include any data written by other tests. Under the hood, when you start running tests, Encore sets up a fresh "template database" and runs the database migrations against that database. When you later call `et.NewTestDatabase`, Encore creates a new database by cloning the template database. ### Service Structs In tests, [service structs](/docs/go/primitives/service-structs) are initialized on demand when the first API call is made to that service and then that instance of the service struct for all future tests. This means your tests can run faster as they don't have to each initialize all the service struct's each time a new test starts. However, in some situations you might be storing state in the service struct that would interfere with other tests. When you have a test you want to have its own instance of the service struct, you can use the `et.EnableServiceInstanceIsolation()` function within the test to enable this for just that test, while the rest of your tests will continue to use the shared instance. ## Test-only infrastructure Encore allows tests to define infrastructure resources specifically for testing. This can be useful for testing library code that interacts with infrastructure. For example, the [x.encore.dev/pubsub/outbox](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox) package defines a test-only database that is used to do integration testing of the outbox functionality. ## Testing from your IDE ### GoLand / IntelliJ Encore has an officially supported plugin [available in the JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore). It lets you run unit tests directly from within your IDE with support for debug mode and breakpoints. ### Visual Studio Code (VS Code) There's no official VS Code plugin available yet, but we are happy to include your contribution if you build one. Reach out on [Discord](/discord) if you need help to get started. For advice on debugging when using VS Code, see the [Debugging docs](/docs/go/how-to/debug). ================================================ FILE: docs/go/develop/validation.md ================================================ --- seotitle: Request validation in your backend application seodesc: Learn how request validation works, and see how you can use Encore's built-in middleware to validate incoming requests in your backend application. title: Validation subtitle: Making sure everything's right in the world lang: go --- When receiving incoming requests it's best practice to validate the payload to make sure it meets your expectations, contains all the necessary fields, and so on. Encore provides an out-of-the-box middleware that automatically validates incoming requests if the request type implements the method `Validate() error`. If it does, Encore will call this method after deserializing the request payload, and only call your API handler (and other middleware) if the validation function returns `nil`. If the validation function returns an [`*errs.Error`](/docs/go/primitives/api-errors) that error is reported unmodified to the caller. Other errors are converted to an `*errs.Error` with code `InvalidArgument`, which results in a HTTP response with status code `400 Bad Request`. This design means that it's easy to use your validation library of choice. In the future we're looking to provide an out-of-the-box validation library for an even better developer experience. ================================================ FILE: docs/go/faq.md ================================================ --- seotitle: Frequently Asked Questions seodesc: See quick answers to common questions about Encore title: FAQ subtitle: Quick answers to common questions lang: go --- ## About the project **Is Encore Open Source?** Yes, check out the project on [GitHub](https://github.com/encoredev/encore). **Is there a community?** Yes, you're welcome to join the developer community on [Discord](https://encore.dev/discord). ## Can I use X with Encore? **Can I use Python with Encore?** Encore currently supports Go and TypeScript. Python support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026. **Can mix TypeScript and Go in one application?** Support for mixing languages in coming. Currently, if you want to use both TypeScript and Go, you need to create a separate application per language and integrate using APIs. **Can I use Azure / Digital Ocean?** Encore Cloud currently supports automating deployments to AWS and GCP. Azure support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026. If you want to use other cloud providers like Azure or Digital Ocean, you can follow the [self-hosting instructions](/docs/go/self-host/docker-build). **Can I use MongoDB / MySQL with Encore?** Encore currently has built-in support for PostgreSQL. To use another type of database, like MongoDB and MySQL, you will need to set it up and integrate as you normally would when not using Encore. **Can I use AWS lambda with Encore?** Not right now. Encore currently supports AWS Fargate and EKS (along with CloudRun and GKE on Google Cloud Platform). ## IDE Integrations **Is there an Encore plugin for Goland / IntelliJ?** Yes, Encore's official Goland plugin is available in the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore). **Is there an Encore plugin for VS Code?** Not yet, it's coming soon. ## Troubleshooting **symlink creation error on Windows** Encore currently relies on symbolic links, which may be disabled by default. A common fix for this issue is to enable "developer mode" in the Windows settings (Settings > System > For developers > Developer mode). **`node` errors** You might need to restart the Encore daemon, e.g. if your PATH has changed since installing nvm. Restart the daemon by running `encore daemon`. ================================================ FILE: docs/go/how-to/atlas-gorm.md ================================================ --- seotitle: How to use Atlas + GORM for database migrations with Encore seodesc: See how you can use Atlas to manage your database migrations in your Encore application. title: Use Atlas + GORM for database migrations lang: go --- [Atlas](https://atlasgo.io) is a popular tool for managing database migrations. [GORM](https://gorm.io/) is a popular ORM for Go. Encore provides excellent support for using them together to easily manage database schemas and migrations. Encore executes database migrations using [golang-migrate](https://github.com/golang-migrate/migrate), which Atlas supports out-of-the-box. This means that you can use Atlas to manage your Encore database migrations. The easiest way to use Atlas + GORM together is with Atlas's support for [external schemas](https://atlasgo.io/blog/2023/06/28/external-schemas-and-gorm-support). ## Setting up GORM To set up your Encore application with GORM, start by installing the GORM package and associated Postgres driver: ```shell go get -u gorm.io/gorm gorm.io/driver/postgres ``` Then, in the service that you want to use GORM for, add the `*gorm.DB` as a dependency in your service struct (create a service struct if you don't already have one). For example, if you had a service called `blog`: ```go -- blog/blog.go -- package blog import ( "encore.dev/storage/sqldb" "gorm.io/driver/postgres" "gorm.io/gorm" ) //encore:service type Service struct { db *gorm.DB } var blogDB = sqldb.NewDatabase("blog", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // initService initializes the site service. // It is automatically called by Encore on service startup. func initService() (*Service, error) { db, err := gorm.Open(postgres.New(postgres.Config{ Conn: blogDB.Stdlib(), })) if err != nil { return nil, err } return &Service{db: db}, nil } ``` Finally, create the `migrations` directory inside the `blog` directory if it doesn't already exist. This is where Atlas will put your database migrations. ## Setting up Atlas First [install Atlas](https://atlasgo.io/getting-started). Then, add an `atlas.hcl` file inside the `blog` directory: ``` -- blog/atlas.hcl -- data "external_schema" "gorm" { program = ["env", "ENCORERUNTIME_NOPANIC=1", "go", "run", "./scripts/atlas-gorm-loader.go"] } env "local" { src = data.external_schema.gorm.url migration { dir = "file://migrations" format = golang-migrate } format { migrate { diff = "{{ sql . \" \" }}" } } } ``` Next, we need to create the `atlas-gorm-loader` script referenced above. It will use the [atlas-provider-gorm](https://github.com/ariga/atlas-provider-gorm) library provided by Atlas. Create the file as follows: ``` -- blog/scripts/atlas-gorm-loader.go -- package main import ( "fmt" "io" "os" _ "ariga.io/atlas-go-sdk/recordriver" "ariga.io/atlas-provider-gorm/gormschema" "encore.app/blog" ) // Define the models to generate migrations for. var models = []any{ &blog.Post{}, &blog.Comment{}, } func main() { stmts, err := gormschema.New("postgres").Load(models...) if err != nil { fmt.Fprintf(os.Stderr, "failed to load gorm schema: %v\n", err) os.Exit(1) } io.WriteString(os.Stdout, stmts) } ``` ## Creating migrations To wrap things up, let's create a script to automate the process of generating migrations: ``` -- blog/scripts/generate-migration -- #!/bin/bash set -eu DB_NAME=blog MIGRATION_NAME=${1:-} SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # Reset the shadow database encore db reset --shadow $DB_NAME # GORM executes Go code without initializing Encore when generating migrations, # so configure the Encore runtime to be aware that this is expected. export ENCORERUNTIME_NOPANIC=1 # Generate the migration atlas migrate diff $MIGRATION_NAME --env local --dev-url "$(encore db conn-uri --shadow $DB_NAME)&search_path=public" ``` Finally let's make the script executable, and generate our first migration: ```shell $ chmod +x blog/scripts/generate-migration $ cd blog && ./scripts/generate-migration init ``` This will generate a new migration file in the `blog/migrations` directory, which will be automatically applied when running `encore run`. ================================================ FILE: docs/go/how-to/auth0-auth.md ================================================ --- seotitle: How to use Auth0 for your backend application seodesc: Learn how to use Auth0 for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Auth0. title: Use Auth0 with your app lang: go --- In this guide you will learn how to set up an Encore [auth handler](/docs/go/develop/auth#the-auth-handler) that makes use of [Auth0](https://auth0.com/) in order to add a seamless signup and login experience to your web app. For all the code and instructions of how to clone and run this example locally, see the [Auth0 Example](https://github.com/encoredev/examples/tree/main/auth0) in our examples repo. ## Communicate with Auth0 In your Encore app, install two modules: ```shell $ go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth2 ``` Create a folder and naming it `auth`, this is where our authentication related backend code will live. Next, let's set up the Auth0 `Authenticator` that will be used by our auth handler. The `Authenticator` has a method to configure and return [OAuth2](https://pkg.go.dev/golang.org/x/oauth2?utm_source=godoc) and [oidc](https://pkg.go.dev/github.com/coreos/go-oidc?utm_source=godoc) clients, and another one to verify an ID Token. Create `auth/authenticator.go` and paste the following: ```go package auth import ( "context" "crypto/rand" "encoding/base64" "encore.dev/config" "errors" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) type Auth0Config struct { ClientID config.String Domain config.String CallbackURL config.String LogoutURL config.String } var cfg = config.Load[*Auth0Config]() var secrets struct { Auth0ClientSecret string } // Authenticator is used to authenticate our users. type Authenticator struct { *oidc.Provider oauth2.Config } // New instantiates the *Authenticator. func New() (*Authenticator, error) { provider, err := oidc.NewProvider( context.Background(), "https://"+cfg.Domain()+"/", ) if err != nil { return nil, err } conf := oauth2.Config{ ClientID: cfg.ClientID(), ClientSecret: secrets.Auth0ClientSecret, RedirectURL: cfg.CallbackURL(), Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } return &Authenticator{ Provider: provider, Config: conf, }, nil } // VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken. func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { rawIDToken, ok := token.Extra("id_token").(string) if !ok { return nil, errors.New("no id_token field in oauth2 token") } oidcConfig := &oidc.Config{ ClientID: a.ClientID, } return a.Verifier(oidcConfig).Verify(ctx, rawIDToken) } func generateRandomState() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "", err } state := base64.StdEncoding.EncodeToString(b) return state, nil } ``` ## Set up the auth handler It's time to define your [auth handler](/docs/go/develop/auth) and the endpoints needed for the login and logout flow. Create the `auth/auth.go` file and paste the following: ```go package auth import ( "context" "net/url" "encore.dev/beta/auth" "encore.dev/beta/errs" "github.com/coreos/go-oidc/v3/oidc" ) // Service struct definition. // Learn more: encore.dev/docs/primitives/services-and-apis/service-structs // //encore:service type Service struct { auth *Authenticator } // initService is automatically called by Encore when the service starts up. func initService() (*Service, error) { authenticator, err := New() if err != nil { return nil, err } return &Service{auth: authenticator}, nil } type LoginResponse struct { State string `json:"state"` AuthCodeURL string `json:"auth_code_url"` } //encore:api public method=POST path=/auth/login func (s *Service) Login(ctx context.Context) (*LoginResponse, error) { state, err := generateRandomState() if err != nil { return nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } return &LoginResponse{ State: state, // add the audience to the auth code url AuthCodeURL: s.auth.AuthCodeURL(state), }, nil } type CallbackRequest struct { Code string `json:"code"` } type CallbackResponse struct { Token string `json:"token"` } //encore:api public method=POST path=/auth/callback func (s *Service) Callback( ctx context.Context, req *CallbackRequest, ) (*CallbackResponse, error) { // Exchange an authorization code for a token. token, err := s.auth.Exchange(ctx, req.Code) if err != nil { return nil, &errs.Error{ Code: errs.PermissionDenied, Message: "Failed to convert an authorization code into a token.", } } idToken, err := s.auth.VerifyIDToken(ctx, token) if err != nil { return nil, &errs.Error{ Code: errs.Internal, Message: "Failed to verify ID Token.", } } var profile map[string]interface{} if err := idToken.Claims(&profile); err != nil { return nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } return &CallbackResponse{ Token: token.Extra("id_token").(string), }, nil } type LogoutResponse struct { RedirectURL string `json:"redirect_url"` } //encore:api public method=GET path=/auth/logout func (s *Service) Logout(ctx context.Context) (*LogoutResponse, error) { logoutUrl, err := url.Parse("https://" + cfg.Domain() + "/v2/logout") if err != nil { return nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } returnTo, err := url.Parse(cfg.LogoutURL()) if err != nil { return nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } parameters := url.Values{} parameters.Add("returnTo", returnTo.String()) parameters.Add("client_id", cfg.ClientID()) logoutUrl.RawQuery = parameters.Encode() return &LogoutResponse{ RedirectURL: logoutUrl.String(), }, nil } type ProfileData struct { Email string `json:"email"` Picture string `json:"picture"` } // The `encore:authhandler` annotation tells Encore to run this function for all // incoming API call that requires authentication. // Learn more: encore.dev/docs/develop/auth#the-auth-handler // //encore:authhandler func (s *Service) AuthHandler( ctx context.Context, token string, ) (auth.UID, *ProfileData, error) { oidcConfig := &oidc.Config{ ClientID: s.auth.ClientID, } t, err := s.auth.Verifier(oidcConfig).Verify(ctx, token) if err != nil { return "", nil, &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } var profile map[string]interface{} if err := t.Claims(&profile); err != nil { return "", nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } // Extract profile data returned from the identity provider. // auth0.com/docs/manage-users/user-accounts/user-profiles/user-profile-structure profileData := &ProfileData{ Email: profile["email"].(string), Picture: profile["picture"].(string), } return auth.UID(profile["sub"].(string)), profileData, nil } // Endpoints annotated with `auth` are public and requires authentication // Learn more: encore.dev/docs/primitives/apis#access-controls // //encore:api auth method=GET path=/profile func GetProfile(ctx context.Context) (*ProfileData, error) { return auth.Data().(*ProfileData), nil } ``` ## Auth0 settings The `Authenticator` class requires some values that are specific your Auth0 application, namely the `ClientID`, `ClientSecret`, `Domain`, `CallbackURL` and `LogoutURL`. Create an Auth0 account if you haven't already. Then, in the Auth0 dashboard, create a new *Single Page Web Applications*. Next, go to the *Application Settings* section. There you will find the `Domain`, `Client ID`, and `Client Secret` that you need to communicate with Auth0. Copy these values, we will need them shortly. A callback URL is where Auth0 redirects the user after they have been authenticated. Add `http://localhost:3000/callback` to the *Allowed Callback URLs*. You will need to add more URLs to this list when you have a production or staging environments. The same goes for the logout URL (were the user will get redirected after logout). Add `http://localhost:3000/` to the *Allowed Logout URLs*. ## Config and secrets Create a [configuration file](/docs/go/develop/config) in the `auth` service and name it `auth-config.cue`. Add the following: ```cue ClientID: "" Domain: "" // An application running locally if #Meta.Environment.Type == "development" && #Meta.Environment.Cloud == "local" { CallbackURL: "http://localhost:3000/callback" LogoutURL: "http://localhost:3000/" } ``` Replace the values for the `ClientID` and `Domain` that you got from the Auth0 dashboard. The `ClientSecret` is especially sensitive and should not be hardcoded in your code/config. Instead, you should store that as an [Encore secret](/docs/go/primitives/secrets). From your terminal (inside your Encore app directory), run: ```shell $ encore secret set --prod Auth0ClientSecret ``` Now you should do the same for the development secret. The most secure way is to set up a different Auth0 application and use that for development. Depending on your security requirements you could also use the same secret for development and production. Once you have a client secret for development, set it similarly to before: ```shell $ encore secret set --dev Auth0ClientSecret ``` That's it! Encore will run your auth handler and validate the token against Auth0. ## Frontend Now that the backend is set up, we can create a frontend application that uses the login flow. Here's an example using [React](https://react.dev/) together with [React Router](https://reactrouter.com/). This example also makes use of a Encores ability to [generate request clients](/docs/go/cli/client-generation) to make the communication with our backend simple and typesafe. ```tsx -- App.tsx -- import { PropsWithChildren } from "react"; import { createBrowserRouter, Link, Outlet, redirect, RouterProvider, useRouteError, } from "react-router-dom"; import { Auth0Provider } from "./lib/auth"; import AdminDashboard from "./components/AdminDashboard.tsx"; import IndexPage from "./components/IndexPage.tsx"; import "./App.css"; import LoginStatus from "./components/LoginStatus.tsx"; // Application routes const router = createBrowserRouter([ { id: "root", path: "/", Component: Layout, errorElement: ( ), children: [ { Component: Outlet, children: [ { index: true, Component: IndexPage, }, { // Login route path: "login", loader: async ({ request }) => { const url = new URL(request.url); const searchParams = new URLSearchParams(url.search); const returnToURL = searchParams.get("returnTo") ?? "/"; if (Auth0Provider.isAuthenticated()) return redirect(returnToURL); try { const returnURL = await Auth0Provider.login(returnToURL); return redirect(returnURL); } catch (error) { throw new Error("Login failed"); } }, }, { // Callback route, redirected to from Auth0 after login path: "callback", loader: async ({ request }) => { const url = new URL(request.url); const searchParams = new URLSearchParams(url.search); const state = searchParams.get("state"); const code = searchParams.get("code"); if (!state || !code) throw new Error("Login failed"); try { const redirectURL = await Auth0Provider.validate(state, code); return redirect(redirectURL); } catch (error) { throw new Error("Login failed"); } }, }, { // Logout route path: "logout", loader: async () => { try { const redirectURL = await Auth0Provider.logout(); return redirect(redirectURL); } catch (error) { throw new Error("Logout failed"); } }, }, { element: , // Redirect to /login if not authenticated loader: async ({ request }) => { if (!Auth0Provider.isAuthenticated()) { const params = new URLSearchParams(); params.set("returnTo", new URL(request.url).pathname); return redirect("/login?" + params.toString()); } return null; }, // Protected routes children: [ { path: "admin-dashboard", Component: AdminDashboard, }, ], }, ], }, ], }, ]); export default function App() { return Loading...

} />; } function Layout({ children }: PropsWithChildren) { return (
{children ?? }
); } function ErrorBoundary() { const error = useRouteError() as Error; return (

Something went wrong

{error.message || JSON.stringify(error)}

); } -- lib/auth.ts -- import Cookies from "js-cookie"; import getRequestClient from "./getRequestClient.ts"; type RedirectURL = string; /** * Handles the backend communication for the authentication flow. */ export const Auth0Provider = { client: getRequestClient(), isAuthenticated: () => !!Cookies.get("auth-token"), async login(returnTo: RedirectURL): Promise { const response = await this.client.auth.Login(); Cookies.set("state", response.state); sessionStorage.setItem(response.state, returnTo); return response.auth_code_url; }, async logout(): Promise { const response = await this.client.auth.Logout(); Cookies.remove("auth-token"); Cookies.remove("state"); return response.redirect_url; }, async validate(state: string, authCode: string): Promise { if (state != Cookies.get("state")) throw new Error("Invalid state"); const response = await this.client.auth.Callback({ code: authCode }); Cookies.set("auth-token", response.token); const returnURL = sessionStorage.getItem(state) ?? "/"; sessionStorage.removeItem(state); return returnURL; }, }; -- components/LoginStatus.tsx -- import getRequestClient from "../lib/getRequestClient.ts"; import { useFetcher } from "react-router-dom"; import { useEffect, useState } from "react"; import { auth } from "../lib/client.ts"; import { Auth0Provider } from "../lib/auth.ts"; /** * Component displaying login/logout button and basic user information if logged in. */ function LoginStatus() { const client = getRequestClient(); const fetcher = useFetcher(); const [profile, setProfile] = useState(); const [loading, setLoading] = useState(true); // Fetch profile data if user is authenticated useEffect(() => { const getProfile = async () => { setProfile(await client.auth.GetProfile()); setLoading(false); }; if (Auth0Provider.isAuthenticated()) getProfile(); else setLoading(false); }, []); if (loading) return null; if (profile) { return (
); } const params = new URLSearchParams(); params.set("returnTo", window.location.pathname); return (
); } export default LoginStatus; -- lib/getRequestClient.ts -- import Client, { Environment, Local } from "./client.ts"; import Cookies from "js-cookie"; /** * Returns the generated Encore request client for either the local or staging environment. * If we are running the frontend locally (development) we assume that our Encore * backend is also running locally. */ const getRequestClient = () => { const token = Cookies.get("auth-token"); const env = import.meta.env.DEV ? Local : Environment("staging"); return new Client(env, { auth: token, }); }; export default getRequestClient; ``` ## Auth0 Social Identity Providers Auth0 supports multiple [social identity providers](https://auth0.com/docs/authenticate/identity-providers/social-identity-providers) (like Google and GitHub) for web applications out of the box. ================================================ FILE: docs/go/how-to/break-up-monolith.md ================================================ --- seotitle: Break a monolith into microservices seodesc: Learn how to quickly break up your backend monolith into microservices using Encore, while avoiding the common pitfalls. title: Break a monolith into microservices subtitle: Evolving your architecture as needed lang: go --- It's common to want to break out specific functionality into separate services. Perhaps you want to independently scale a specific service, or simply want to structure your codebase in smaller pieces. Encore makes it simple to evolve your system architecture over time, and enables you to deploy your application in multiple different ways without making code changes. ## How to break out a service from a monolith As a (slightly silly) example, let's imagine we have a monolith `hello` with two API endpoints `H1` and `H2`. It looks like this: ```go package hello import ( "context" ) //encore:api public path=/hello/:name func H1(ctx context.Context, name string) (*Response, error) { msg := "Hello, " + name + "!" return &Response{Message: msg}, nil } //encore:api public path=/yo/:name func H2(ctx context.Context, name string) (*Response, error) { msg := "Yo, " + name + "!" return &Response{Message: msg}, nil } type Response struct { Message string } ``` Now we're going to break out `H2` into its own separate service. Happily, all we need to do is create a new package, let's call it `yo`, and move the `H2` endpoint into it. Like so: ```go package yo import ( "context" ) //encore:api public path=/yo/:name func H2(ctx context.Context, name string) (*Response, error) { msg := "Yo, " + name + "!" return &Response{Message: msg}, nil } type Response struct { Message string } ``` On disk we now have: ``` /my-app ├── encore.app // ... and other top-level project files │ ├── hello // hello service (a Go package) │   └── hello.go // hello service code │ └── yo // yo service (a Go package) └── yo.go // yo service code ``` Encore now understands these are separate services, and when you run your app you'll see that the [Service Catalog](/docs/go/observability/service-catalog) has been automatically updated accordingly. As well as the [Flow architecture diagram](/docs/go/observability/encore-flow). ## Sharing databases between services (or not) Deciding whether to share a database between multiple services depends on your specific situation. Encore supports both options. Learn more in the [database documentation](/docs/go/primitives/share-db-between-services). ================================================ FILE: docs/go/how-to/cgo.md ================================================ --- seotitle: Build Go applications with cgo using Encore seodesc: Learn how to build Go applications with cgo using Encore title: Build with cgo lang: go --- Cgo is a feature of the Go compiler that enables Go programs to interface with libraries written in other languages using C bindings. By default, for improved portability Encore builds applications with cgo support disabled. To enable cgo for your application, add `"build": {"cgo_enabled": true}` to your `encore.app` file. For example: ```json -- encore.app -- { "id": "my-app-id", "build": { "cgo_enabled": true } } ``` With this setting Encore's build system will compile the application using an Ubuntu builder image with gcc pre-installed. ## Static linking To keep the resulting Docker images as minimal as possible, Encore compiles applications with static linking. This happens even with cgo enabled. As a result the cgo libraries you use must support static linking. In some cases, you may need to add additional linker flags to properly work with static linking of cgo libraries. See the [official cgo docs](https://pkg.go.dev/cmd/cgo) for more information on how to do this. ================================================ FILE: docs/go/how-to/clerk-auth.md ================================================ --- seotitle: How to use Clerk to authenticate users in your backend application seodesc: Learn how to use Clerk for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Clerk. title: Use Clerk with your app lang: go --- In this guide you will learn how to set up an Encore [auth handler](/docs/go/develop/auth#the-auth-handler) that makes use of [Clerk](https://clerk.com/) in order to add an integrated signup and login experience to your web app. For all the code and instructions of how to clone and run this example locally, see the [Clerk Example](https://github.com/encoredev/examples/tree/main/clerk) in our examples repo. ## Set up the auth handler In your Encore app, install the following module: ```shell $ go get github.com/clerkinc/clerk-sdk-go/clerk ``` Create a folder and naming it `auth`, this is where our authentication related backend code will live. It's time to define your [auth handler](/docs/go/develop/auth). Create `auth/auth.go` and paste the following: ```go package auth import ( "context" "encore.dev/beta/auth" "encore.dev/beta/errs" "github.com/clerkinc/clerk-sdk-go/clerk" ) var secrets struct { ClientSecretKey string } // Service struct definition. // Learn more: encore.dev/docs/primitives/services-and-apis/service-structs // //encore:service type Service struct { client clerk.Client } // initService is automatically called by Encore when the service starts up. func initService() (*Service, error) { client, err := clerk.NewClient(secrets.ClientSecretKey) if err != nil { return nil, err } return &Service{client: client}, nil } type UserData struct { ID string `json:"id"` Username *string `json:"username"` FirstName *string `json:"first_name"` LastName *string `json:"last_name"` ProfileImageURL string `json:"profile_image_url"` PrimaryEmailAddressID *string `json:"primary_email_address_id"` EmailAddresses []clerk.EmailAddress `json:"email_addresses"` } // The `encore:authhandler` annotation tells Encore to run this function for all // incoming API call that requires authentication. // Learn more: encore.dev/docs/develop/auth#the-auth-handler // //encore:authhandler func (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) { // verify the session sessClaims, err := s.client.VerifyToken(token) if err != nil { return "", nil, &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } user, err := s.client.Users().Read(sessClaims.Claims.Subject) if err != nil { return "", nil, &errs.Error{ Code: errs.Internal, Message: err.Error(), } } userData := &UserData{ ID: user.ID, Username: user.Username, FirstName: user.FirstName, LastName: user.LastName, ProfileImageURL: user.ProfileImageURL, PrimaryEmailAddressID: user.PrimaryEmailAddressID, EmailAddresses: user.EmailAddresses, } return auth.UID(user.ID), userData, nil } ``` ## Clerk credentials Create a Clerk account if you haven't already. Then, in the Clerk dashboard, create a new applications. Next, go to the *API Keys* page for your app. Copy one of the "Secret keys" (the "Publishable Key" will be used by your frontend). The `Secret key` is sensitive and should not be hardcoded in your code/config. Instead, you should store that as an [Encore secret](/docs/go/primitives/secrets). From your terminal (inside your Encore app directory), run: ```shell $ encore secret set --prod ClientSecretKey ``` Now you should do the same for the development secret. The most secure way is to create another secret key (Clerk allows you to have multiple). Once you have a client secret for development, set it similarly to before: ```shell $ encore secret set --dev ClientSecretKey ``` ## Frontend Clerk offers a [React SDK](https://clerk.com/docs/references/react/overview) for the frontend which makes it really simple to integrate a login/signup flow inside your web app as well as getting the token required to communicate with your Encore backend. You can use the `useAuth` hook from `@clerk/clerk-react` to get the token and send it to your backend. ```tsx import { useAuth } from '@clerk/clerk-react'; export default function ExternalDataPage() { const { getToken, isLoaded, isSignedIn } = useAuth(); if (!isLoaded) { // Handle loading state however you like return
Loading...
; } if (!isSignedIn) { // Handle signed out state however you like return
Sign in to view this page
; } const fetchDataFromExternalResource = async () => { const token = await getToken(); // Use token to send to Encore backend when fetching data return data; } return
...
; } ``` For a fully working backend + frontend example see the [Clerk Example](https://github.com/encoredev/examples/tree/main/clerk) in our examples repo. ================================================ FILE: docs/go/how-to/debug.md ================================================ --- seotitle: How to debug your application with Delve seodesc: Learn how to debug your Go backend application using Delve and Encore. title: Debug with Delve lang: go --- Encore makes it easy to debug your application using [Delve](https://github.com/go-delve/delve "Delve"). First, make sure you have `dlv` installed by running (Go 1.16 and later): ```shell $ go install github.com/go-delve/delve/cmd/dlv@latest ``` You have two debugger options, you can either debug by attaching to a running process or by starting the process in debug mode. ## Debug by starting the process in debug mode Run your Encore application with `encore run --debug=break`. This will launch your encore application with a headless Delve server, which will pause your application until a debugger is attached. ```shell $ encore run --debug=break API Base URL: http://localhost:4000 Dev Dashboard URL: http://localhost:9400/hello-world-cgu2 API server listening at: 127.0.0.1:2345 ``` Now it's time to attach the debugger. The instructions differ depending on how you would like to debug (in your terminal or in your editor). If instructions for your editor aren’t listed below, consult your editor for information on how to attach to a Delve server. ### Terminal debugging To debug in your terminal, run `dlv attach :2345`. You should see: ```shell $ dlv connect :2345 Type 'help' for list of commands. (dlv) ``` How to use Delve’s terminal interface for debugging is out of scope for this guide, but there are great resources available. For a good introduction, see [](https://golang.cafe/blog/golang-debugging-with-delve.html "Debugging with Delve"). ### Visual Studio Code To debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Go -> Connect to server`. Input `127.0.0.1` as host and `2345` as port. The resulting configuration should look something like this: ```json { "version": "0.2.0", "configurations": [ { "name": "Connect to server", "type": "go", "request": "attach", "mode": "remote", "remotePath": "${workspaceFolder}", "port": 2345, "host": "127.0.0.1" } ] } ``` Next, open the **Run and Debug** menu in the toolbar on the left, select `Connect to server` (the configuration you just created), and then press the green arrow. That’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ### Goland To debug with Goland, you must create a new Go Remote configuration. Press `Run | Edit Configurations`, click the `+` button, and choose `Go Remote`. Give it a name and hit `OK`. Now select the configuration you just created and press the green bug. That's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ## Debug by attaching to a running process Run your Encore application with `encore run --debug`. This will cause Encore to print the Process ID to the terminal, which you will use to attach your debugger: ```shell $ encore run --debug API Base URL: http://localhost:4000 Dev Dashboard URL: http://localhost:9400/hello-world-cgu2 Process ID: 51894 1:48PM TRC registered endpoint path=/hello/:name service=hello endpoint=Hello ``` (Your process id will differ). When your Encore application is running, it’s time to attach the debugger. The instructions differ depending on how you would like to debug (in your terminal or in your editor). If instructions for your editor aren’t listed below, consult your editor for information on how to attach a debugger to a running process. ### Terminal debugging To debug in your terminal, run `dlv attach $PID` (replace `$PID` with your Process ID from the previous step). You should see: ```shell $ dlv attach 51894 Type 'help' for list of commands. (dlv) ``` How to use Delve’s terminal interface for debugging is out of scope for this guide, but there are great resources available. For a good introduction, see [](https://golang.cafe/blog/golang-debugging-with-delve.html "Debugging with Delve"). ### Visual Studio Code To debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Go -> Attach to local process`. In the generated configuration, you should see `"processId": 0` as a field. Replace `0` with the process id from above. Next, open the **Run and Debug** menu in the toolbar on the left, select Attach to Process (the configuration you just created), and then press the green arrow. That’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ### Goland To debug with Goland, you must first install the `gops` package. Open a terminal and run the following command ```shell go get -t github.com/google/gops/ ``` Then click `Run | Attach to Process`. If a notification window appears, click the `Invoke 'go get gops'` link. Once it has completed, click `Run | Attach to Process` again. In the dialog that appears, select the process with the process ID from above. That's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ================================================ FILE: docs/go/how-to/dependency-injection.md ================================================ --- seotitle: How to use dependency injection to test your microservices app seodesc: Learn how to use dependency injection in your Go based microservices backend application using Encore. title: Dependency Injection subtitle: Simplifying testing lang: go --- Dependency Injection is a fancy name for a simple concept: when you depend on some functionality, add that dependency as a field on your struct and refer to it that way instead of directly calling it. By doing so it becomes easier to test your services by swapping out certain dependencies for other implementations (often with the use of interfaces). Encore provides built-in support for dependency injection in services through the use of the `//encore:service` directive and a **service struct**. See the [service structs docs](/docs/go/primitives/service-structs) more information on how to define service structs. As an example, consider an email service that has a SendGrid API client that is dependency injected. It might look like this: ```go package email //encore:service type Service struct { sendgridClient *sendgrid.Client } func initService() (*Service, error) { client, err := sendgrid.NewClient() if err != nil { return nil, err } return &Service{sendgridClient: client}, nil } ``` You can then define APIs as methods on this struct: ```go //encore:api private func (s *Service) Send(ctx context.Context, p *SendParams) error { // ... use s.sendgridClient to send emails ... } ``` ### Mocking dependencies If you wish to mock out the SendGrid client for testing purposes you can change the field to an interface: ```go type sendgridClient interface { SendEmail(...) // a hypothetical signature, for illustration purposes } //encore:service type Service struct { sendgridClient sendgridClient } ``` Then during your tests you can instantiate the service object by hand: ```go func TestFoo(t *testing.T) { svc := &Service{sendgridClient: &myMockClient{}} // ... } ``` ================================================ FILE: docs/go/how-to/entgo-orm.md ================================================ --- seotitle: Use ent + Atlas for database schema management with Encore. seodesc: See how you can use an ORM like ent with Atlas to handle your database schemas. title: Use ent ORM + Atlas for database schemas lang: go --- Encore has all the tools needed to support ORMs and migration frameworks out-of-the-box through [named databases](/docs/go/primitives/share-db-between-services) and [migration files](/docs/go/primitives/databases#defining-a-database-schema). Writing plain SQL might not work for your use case, or you may not want to use SQL in the first place. ORMs like [ent](https://entgo.io/) and migration frameworks like [Atlas](https://atlasgo.io/) can be used with Encore by integrating their logic with a system's database. Encore is not restrictive, it uses plain SQL migration files for its migrations. - If your ORM of choice can connect to any database using a [standard SQL driver](https://github.com/lib/pq), then it can be used with Encore. - If your migration framework can generate SQL migration files without any modifications, then it can be used with Encore. Let's take a look at how you can integrate ent with Encore, using Atlas for generating the migration files. ## Add ent schemas to a service [Install ent](https://entgo.io/docs/tutorial-setup#installation), then initialize your first schema in the service where you want to use it. For example, if you had the following app structure: ``` /my-app ├── encore.app └── user // user service ``` You can then use this command to generate a user schema along with the ent directory that will contain that schema and all future generated files: ```shell $ go run entgo.io/ent/cmd/ent@latest new --target user/ent/schema User ``` The `--target` option sets the schema directory within your Encore system. Each system should contain its own models and schemas, and its own migration files. Like you would when using plain SQL. Add the fields and edges for your new model in the generated file under `user/ent/schema/user.go`. Now, run the following command: ```shell $ go run entgo.io/ent/cmd/ent@latest generate ./user/ent/schema ``` This generates the ent client files. Run this command again whenever you change the schemas. ## Integrating ent with an Encore database Encore automates database provisioning, and automatically runs migrations in all environments. To integrate ent with Encore, we need to do three things: 1. Create the Encore database 2. Set up the ent client to use that database. 3. Generate migration files for the ent schema, using Atlas. ### Create the Encore database Create the database using [`sqldb.NewDatabase`](/docs/go/primitives/databases) in `user/user.go`: ``` -- user/user.go -- package user import "encore.dev/storage/sqldb" var userDB = sqldb.NewDatabase("user", sqldb.DatabaseConfig{ Migrations: "./migrations", }) ``` Now, create the `migrations` directory, and leave it empty for now: ```shell $ mkdir user/migrations ``` ### Connect ent to the database Next, extend the user service with a [Service Struct](/docs/go/primitives/service-structs) that creates an ent client connected to the database. Replace the contents of the `user/user.go` file with: ``` -- user/user.go -- package user import ( "encore.dev/storage/sqldb" "entgo.io/ent/dialect" entsql "entgo.io/ent/dialect/sql" "encore.app/user/ent" ) var userDB = sqldb.NewDatabase("user", sqldb.DatabaseConfig{ Migrations: "./migrations", }) //encore:service type Service struct{ ent *ent.Client } func initService() (*Service, error) { driver := entsql.OpenDB(dialect.Postgres, userDB.Stdlib()) entClient := ent.NewClient(ent.Driver(driver)) return &Service{ent: entClient}, nil } ``` Now ent is fully wired up to the Encore database, and can be used from the service struct in any API endpoint. ## Using Atlas for database migrations Finally, we'll set up Atlas to generate database migrations for the ent schema. First, make sure you [have Atlas installed](https://atlasgo.io/getting-started). Then, create the file `user/atlas.hcl` containing the following: ``` -- user/atlas.hcl -- env "local" { src = "ent://ent/schema" migration { dir = "file://migrations" format = golang-migrate } format { migrate { diff = "{{ sql . \" \" }}" } } } ``` This tells Atlas to generate migrations for the ent schema, and to output them to the `migrations` directory. Atlas works by comparing the desired ent schema with the current database schema, and generating a migration to bring the database schema in line with the ent schema. This relies on a so-called "shadow database", which is an empty database that Atlas uses to compare the ent schema against. Fortunately for us, Encore has built-in support for shadow databases. Create the file `user/scripts/generate-migration` containing the following: ``` -- user/scripts/generate-migration -- #!/bin/bash set -eu DB_NAME=user MIGRATION_NAME=${1:-} # Reset the shadow database encore db reset --shadow $DB_NAME # ent executes Go code without initializing Encore when generating migrations, # so configure the Encore runtime to be aware that this is expected. export ENCORERUNTIME_NOPANIC=1 # Generate the migration atlas migrate diff $MIGRATION_NAME --env local --dev-url "$(encore db conn-uri --shadow $DB_NAME)&search_path=public" ``` Finally, make the script executable, and generate our first migration: ```shell $ chmod +x user/scripts/generate-migration $ cd user && ./scripts/generate-migration init ``` You should see a new migration file being added to the `user/migrations` directory, containing the schema changes to create the ent models. You can now run the service with `encore run`, and everything should be ready to go! ================================================ FILE: docs/go/how-to/firebase-auth.md ================================================ --- seotitle: How to use Firebase Auth for your backend application seodesc: Learn how to use Firebase Auth for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Firebase Auth. title: Use Firebase Auth with your app lang: go --- Encore's [authentication support](/docs/go/develop/auth) provides a simple yet powerful way of dealing with various authentication scenarios. Firebase Authentication {" "}is a common solution for quickly setting up a user store and simplifying social logins. Encore makes it really easy to integrate with Firebase Authentication on the backend. For all the code and instructions of how to clone and run this example locally, see the [Firebase Auth Example](https://github.com/encoredev/examples/tree/main/firebase-auth) in our examples repo. ## Set up auth handler First, install two modules: ```shell $ go get firebase.google.com/go/v4 go4.org/syncutil ``` Next it's time to define your [authentication handler](/docs/go/develop/auth). It can live in whatever service you'd like, but it's usually easiest to create a designated `user` service. Create the `user/user.go` file and add the following skeleton code: ```go package user import ( "context" "strings" "encore.dev/beta/auth" firebase "firebase.google.com/go/v4" fbauth "firebase.google.com/go/v4/auth" "go4.org/syncutil" "google.golang.org/api/option" ) // Data represents the user's data stored in Firebase Auth. type Data struct { // Email is the user's email. Email string // Name is the user's name. Name string // Picture is the user's picture URL. Picture string } // ValidateToken validates an auth token against Firebase Auth. //encore:authhandler func ValidateToken(ctx context.Context, token string) (auth.UID, *Data, error) { panic("Not Yet Implemented") } ``` ## Initialize Firebase SDK Next, let's set up the Firebase Auth client. We'll use  `syncutil.Once`  to do it lazily the first time we need it. Add to the bottom of our file: ```go var ( fbAuth *fbauth.Client setupOnce syncutil.Once ) // setupFB ensures Firebase Auth is setup. func setupFB() error { return setupOnce.Do(func() error { opt := option.WithCredentialsJSON([]byte(secrets.FirebasePrivateKey)) app, err := firebase.NewApp(context.Background(), nil, opt) if err == nil { fbAuth, err = app.Auth(context.Background()) } return err }) } var secrets struct { // FirebasePrivateKey is the JSON credentials for calling Firebase. FirebasePrivateKey string } ``` ## Validate token against Firebase Now that we have the code to initialize Firebase Auth, we can use it from our `ValidateToken` auth handler. Update the function to look like the following: ```go func ValidateToken(ctx context.Context, token string) (auth.UID, *Data, error) { if err := setupFB(); err != nil { return "", nil, err } tok, err := fbAuth.VerifyIDToken(ctx, token) if err != nil { return "", nil, err } email, _ := tok.Claims["email"].(string) name, _ := tok.Claims["name"].(string) picture, _ := tok.Claims["picture"].(string) uid := auth.UID(tok.UID) usr := &Data{ Email: email, Name: name, Picture: picture, } return uid, usr, nil } ``` Great! We're done with the code. Now we just need to set up the secret. ## Set Firebase secret credentials If you haven't already, set up a Firebase project. Then, go to **Project settings** and navigate to **Service accounts**. Select `Go` as the language of choice and click `Generate new private key`. Download the generated key and take note where it is stored. Next, store the private key as your firebase secret. From your terminal (inside your Encore app directory), run: ```shell $ encore secret set --type prod FirebasePrivateKey < /path/to/firebase-private-key.json Successfully updated production secret FirebasePrivateKey ``` Now you should do the same for the development secret. The most secure way is to set up a different Firebase project and use that for development. Depending on your security requirements you could also use the same Firebase project, but we recommend generating a new private key for development in that case. Once you have a private key for development, set it similarly to before: ```shell $ encore secret set --type dev,local,pr FirebasePrivateKey < /path/to/firebase-private-key.json Successfully updated development secret FirebasePrivateKey ``` That's it! You can now call your Encore application and pass in Firebase tokens. Encore will run your auth handler and validate the token against Firebase Auth. ## Frontend Firebase offers a [npm package](https://www.npmjs.com/package/firebase) for your web frontend which makes it really simple to create a login/signup flow inside your web app as well as getting the token required to communicate with your Encore backend. For a fully working backend + frontend example see the [Firebase Auth Example](https://github.com/encoredev/examples/tree/main/firebase-auth) in our examples repo. ================================================ FILE: docs/go/how-to/grpc-connect.md ================================================ --- seotitle: Use Connect for gRPC/protobuf-based APIs with Encore seodesc: See how you can use the Connect protocol for gRPC communication with Encore services title: Use Connect for incoming gRPC requests lang: go --- The [Connect protocol](https://connectrpc.com/) is an HTTP/2-based protocol for RPC communication. It's conceptually similar to gRPC, but with better support for using from browsers and JavaScript clients. This guide shows how to use Encore for setting up a Connect service for external clients to use: 1. First, we'll define a simple gRPC service using Protobuf and Connect. 2. Then, we'll implement the service in Go, using [connect-go](https://connectrpc.com/docs/go/getting-started). 3. Then, we'll mount the Connect service into Encore with a raw endpoint. 4. Finally, we'll call the Connect service from cURL using its JSON mapping. ## Define a Connect service We'll largely follow the connect-go [getting started guide](https://connectrpc.com/docs/go/getting-started) with some small tweaks. Start by installing the necessary tools: ```shell $ go install github.com/bufbuild/buf/cmd/buf@latest $ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest ``` Next, inside your Encore application ([create one if you haven't already](/docs/go/quick-start)) create a new file at `greet/v1/greet.proto` with the following contents: ``` -- greet/v1/greet.proto -- syntax = "proto3"; package greet.v1; option go_package = "encore.app/gen/greet/v1;greetv1"; message GreetRequest { string name = 1; } message GreetResponse { string greeting = 1; } service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {} } ``` Next, add a `buf.gen.yaml` in the repository root, containing: ``` -- buf.gen.yaml -- version: v2 plugins: - local: protoc-gen-go out: gen opt: paths=source_relative - local: protoc-gen-connect-go out: gen opt: paths=source_relative ``` Now it's time to generate the connect-go service code. Run: ```shell $ buf lint $ buf generate ``` If all went well, you should see a new `gen` directory in the repository root containing some generated Go code: ``` gen └── greet └── v1 ├── greet.pb.go └── greetv1connect └── greet.connect.go ``` ## Implement the service Now that we have the service definition, we can implement the Connect service in Go. Add the file `greet/greet.go` with the following contents: ``` -- greet/greet.go -- package greet import ( "context" "fmt" "log" "connectrpc.com/connect" greetv1 "encore.app/gen/greet/v1" // generated by protoc-gen-go ) type GreetServer struct{} func (s *GreetServer) Greet( ctx context.Context, req *connect.Request[greetv1.GreetRequest], ) (*connect.Response[greetv1.GreetResponse], error) { log.Println("Request headers: ", req.Header()) res := connect.NewResponse(&greetv1.GreetResponse{ Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name), }) res.Header().Set("Greet-Version", "v1") return res, nil } ``` The sample code is straight from the [getting started guide](https://connectrpc.com/docs/go/getting-started); there are no Encore specific changes required here. ## Mount the service in Encore Now we'll create an Encore [service struct](/docs/go/primitives/service-structs) that initializes the Connect service, and a [raw endpoint](/docs/go/primitives/raw-endpoints) that forwards incoming requests to the Connect service. Add the file `greet/service.go` with the following contents: ``` -- greet/service.go -- package greet import ( "net/http" "encore.app/gen/greet/v1/greetv1connect" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) //encore:service type Service struct { routes http.Handler } //encore:api public raw path=/greet.v1.GreetService/*endpoint func (s *Service) GreetService(w http.ResponseWriter, req *http.Request) { s.routes.ServeHTTP(w, req) } func initService() (*Service, error) { greeter := &GreetServer{} mux := http.NewServeMux() path, handler := greetv1connect.NewGreetServiceHandler(greeter) mux.Handle(path, handler) return &Service{routes: mux}, nil } ``` That's it! We're ready to run the service and check that everything works. ## Run the service Run the service with `encore run`: ```shell $ encore run ``` Once it starts up, open a separate terminal and use `grpcurl` to call the service: ```shell # Install grpcurl if you haven't already $ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest # Call the service with grpcurl grpcurl \ -protoset <(buf build -o -) -plaintext \ -d '{"name": "Jane"}' \ localhost:4000 greet.v1.GreetService/Greet {"greeting": "Hello, Jane!"} # Or call the service with curl $ curl -H "Content-Type: application/json" -d '{"name": "Jane"}' http://localhost:4000/greet.v1.GreetService/Greet {"greeting":"Hello, Jane!"} # Expected response ``` If you see `{"greeting":"Hello, Jane!"}`, everything is working! What's more, Encore automatically traces the incoming requests, and adds request logging and captures request metrics. ================================================ FILE: docs/go/how-to/http-requests.md ================================================ --- seotitle: How to receive regular HTTP requests in your backend application seodesc: Learn how to receive regular HTTP requests in your Go based backend application using Encore. title: Receive regular HTTP requests subtitle: Dropping down in abstraction level lang: go --- Encore makes it easy to define APIs and expose them, but it works best when you are in charge of the API schema. Sometimes you need more control over the underlying HTTP request, such as to accept incoming webhooks from other services, or to use WebSockets to stream data to/from the client. For these use cases Encore lets you define **raw endpoints**. Raw endpoints operate at a lower abstraction level, giving you access to the underlying HTTP request. ## Defining raw endpoints To define a raw endpoint, change the `//encore:api` annotation and function signature like so: ```go package service import "net/http" // Webhook receives incoming webhooks from Some Service That Sends Webhooks. //encore:api public raw method=POST path=/webhook func Webhook(w http.ResponseWriter, req *http.Request) { // ... operate on the raw HTTP request ... } ``` If you're an experienced Go developer, this is just a regular Go HTTP handler. See the net/http documentation for more information on how Go HTTP handlers work. ## Reading path parameters Sometimes webhooks have information in the path that you may be interested in retrieving or validating. To do so, define the path with a path parameter, and then use [`encore.CurrentRequest`](https://pkg.go.dev/encore.dev#CurrentRequest) to access the path parameters. For example: ```go package service import ( "net/http" "encore.dev" ) //encore:api public raw method=POST path=/webhook/:id func Webhook(w http.ResponseWriter, req *http.Request) { id := encore.CurrentRequest().PathParams.Get("id") // ... Do something with id } ``` ================================================ FILE: docs/go/how-to/integrate-frontend.mdx ================================================ --- seotitle: Integrate your backend application with a frontend seodesc: Learn how to integrate your Go backend application with a frontend, using Encore's built-in frontend client generation feature. title: Integrate with a web frontend subtitle: Keep using your favorite frontend hosting provider lang: go --- Encore is not opinionated about where you host your frontend, pick the platform that suits your situation best. If your frontend and backend use different domains, often the case when using PR preview environments for your frontend, you may need to [configure CORS](#handling-cors). Take a look at our [React starter template](https://encore.dev/templates/react) for an example of deploying a frontend to [Vercel](https://vercel.com/) or the [Meeting Notes tutorial](https://encore.dev/docs/go/tutorials/meeting-notes) deployed to [GitHub Pages](https://pages.github.com/). ## Generating a request client Encore is able to generate frontend request clients (TypeScript or JavaScript). This lets you to keep the request/response types in sync without manual work and assists you in calling the APIs. Generate a client by running: ```bash $ encore gen client --output=./src/client.ts --env= ``` Adding this as a script to your `package.json` is often a good idea to be able to run it whenever a change is made to your Encore API: ```json { ... "scripts": { ... "generate-client:staging": "encore gen client --output=./src/client.ts --env=staging", "generate-client:local": "encore gen client --output=./src/client.ts --env=local" } } ``` After that you are ready to use the request client in your code. Here is an example from the [Meeting Notes tutorial](https://encore.dev/docs/tutorials/meeting-notes) for calling the `GetNote` endpoint on the `note` service in order to retrieve a specific meeting note (which has the properties `id`, `cover_url` & `text`): ```ts import Client, { Environment, Local } from "src/client.ts"; // Making request to locally running backend... const client = new Client(Local); // or to a specific deployed environment const client = new Client(Environment("staging")); // Calling APIs as typesafe functions 🌟 const response = await client.note.GetNote("note-uuid"); console.log(response.id); console.log(response.cover_url); console.log(response.text); ``` See more in the [client generation docs](/docs/develop/client-generation). ### Asynchronous state management When building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale. [TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works well with the Encore request client. Here is a simple example of using an Encore request client together with TanStack Query: ```ts import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import Client, { todo } from '../encore-client' // Create a Encore client const encoreClient = new Client(window.location.origin); // Create a react-query client const queryClient = new QueryClient() function App() { return ( // Provide the client to your App ) } function Todos() { // Access the client const queryClient = useQueryClient() // Queries const query = useQuery({ queryKey: ['todos'], queryFn: () => encoreClient.todo.List() }) // Mutations const mutation = useMutation({ mutationFn: (params: todo.AddParams) => encoreClient.todo.Add(params), onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) return (
    {query.data?.map((todo) => (
  • {todo.title}
  • ))}
) } render(, document.getElementById('root')) ``` This example assumes that we have a `todo` service with a `List` and `Add` endpoint. When adding the new todo, TanStack Query will automatically invalidate the `todos` query and refetch it. For a real-world example, take a look at the [Uptime Monitoring](https://github.com/encoredev/examples/tree/main/uptime) app which also makes use of TanStack Query's `refetchInterval` option for polling the backend. ### Testing When unit testing a component that interacts with your Encore API you can mock methods on the request client to return a value suitable for the test. This makes your test URL agnostic because you are not intercepting specific requests on the fetch layer. You also get type errors in your tests if the request client gets updated. Here is an example from the [Uptime Monitoring Starter](https://github.com/encoredev/examples/tree/main/uptime) where we are mocking a GET request method and spying on a POST request method: ```ts import { render, waitForElementToBeRemoved } from "@testing-library/react"; import App from "./App"; import { site } from "./client"; import { userEvent } from "@testing-library/user-event"; describe("App", () => { beforeEach(() => { // Return mocked data from the List (GET) endpoint jest .spyOn(site.ServiceClient.prototype, "List") .mockReturnValue(Promise.resolve({ sites: [{ id: 1, url: "test.dev" }] })); // Spy on the Add (POST) endpoint jest.spyOn(site.ServiceClient.prototype, "Add"); }); it("render sites", async () => { render(); await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); // Verify that the List endpoint has been called expect(site.ServiceClient.prototype.List).toBeCalledTimes(1); // Verify that the sites are rendered with our mocked data screen.getAllByText("test.dev"); }); it("add site", async () => { render(); await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); // Interact with the page and add 'another.com' await userEvent.click(screen.getByText("Add website")); await userEvent.type( screen.getByPlaceholderText("google.com"), "another.com", ); await userEvent.click(screen.getByText("Save")); // Verify that the Add endpoint has been called with the correct parameters expect(site.ServiceClient.prototype.Add).toHaveBeenCalledWith({ url: "another.com", }); }); }) ``` In the example above we need to mock the `List` method on `site.ServiceClient.prototype` because the request client has not yet been initialized when we're creating the mock. If you have access to the instance of the request client in your test (which could be the case if you are passing the client around in your components) you can instead do `jest.spyOn(client.site, "List")` and `expect(client.site.List).toHaveBeenCalled()` which would give you the same result. More examples of tests can be found in the [Uptime Monitoring Starter repo](https://github.com/encoredev/examples/tree/main/uptime). ## Monorepo or Multi repo Encore is not opinionated about where your frontend lives, pick the approach that fits your application best. If you use a monorepo then it is often a good idea to place your backend and frontend in separate folders. There are two approaches to moving your Encore backend to a subfolder: 1. Place your microservices together with the `encore.app` file in a subfolder. When moving `encore.app` to a subfolder you will need to configure the "Root Directory" in app settings in the [Encore Cloud dashboard](https://app.encore.cloud). 2. Place your microservices in a subfolder and keep the `encore.app` in the repo root directory. No configuration change is needed, but you will need to update the import paths if your services are calling each other. ## REST vs. GraphQL Encore allows for building backends using both REST and GraphQL, you should pick the approach that suits your use case best. Take a look at the [GraphQL tutorial](/docs/go/tutorials/graphql) for an example of building a GraphQL backend with Encore. ## Hosting a frontend on Encore for development Encore is primarily designed for backend development and does not (at the moment) support building or testing frontends in the deploy pipeline. For production use, we recommend that you deploy your frontend using Vercel, Netlify, or a similar service. For development purposes, you can create a `raw` endpoint that serves static frontend assets. It would look something like the example below (taken from the [Uptime Monitoring tutorial](https://encore.dev/docs/go/tutorials/uptime)), but keep in mind that you need to have the compiled frontend assets under version control (`dist` folder in the example below). ```go package frontend import ( "embed" "io/fs" "net/http" ) var ( //go:embed dist dist embed.FS assets, _ = fs.Sub(dist, "dist") handler = http.StripPrefix("/frontend/", http.FileServer(http.FS(assets))) ) //encore:api public raw path=/frontend/*path func Serve(w http.ResponseWriter, req *http.Request) { handler.ServeHTTP(w, req) } ``` ## Handling CORS If you are running into CORS issues when calling your Encore API from your frontend you may need to specify which origins are allowed to access your API (via browsers). Do this by specifying the `global_cors` key in the `encore.app` file, which has the following structure: ```json global_cors: { // allow_origins_without_credentials specifies the allowed origins for requests // that don't include credentials. If nil it defaults to allowing all domains // (equivalent to ["*"]). "allow_origins_without_credentials": [ "" ], // allow_origins_with_credentials specifies the allowed origins for requests // that include credentials. If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). "allow_origins_with_credentials": [ "" ] } ``` See more in the [CORS docs](/docs/go/develop/cors). ================================================ FILE: docs/go/how-to/logto-auth.md ================================================ --- seotitle: How to use Logto for your backend application seodesc: Learn how to use Logto for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Logto. title: Use Logto with your app lang: go --- [Logto](https://logto.io) is a modern Auth0 alternative that helps you build the sign-in experience and user identity within minutes. It's particularly well-suited for protecting API services built with Encore. This guide will show you how to integrate Logto with your Encore application to add authentication and authorization capabilities. You can find the complete [Logto example](https://github.com/encoredev/examples/tree/main/logto-react-sdk) in our examples repo. ## Logto settings Before we begin integrating with Encore, you'll need to set up a few things in Logto: 1. Create an account at [Logto Cloud](https://cloud.logto.io) if you don't have one yet. 2. Create an API Resource in Logto Console, this represents your Encore API service - Go to "API Resources" in Logto Console and create a new API - Set a name and API identifier (e.g., `https://api.encoreapp.com`) - Note down the API identifier on the API resource details page as we'll need it later 3. Create an application for your frontend application - Go to "Applications" in Logto Console - Create a new application according to your frontend framework (We use React as an example, but you can create any Single-Page Application (SPA) or native app) - (Optional, we'll cover this later) Integrate Logto with your frontend application according to the guide in the Logto Console. - Note down the application ID and issuer URL on the Application details page as we'll need them later ## Setup the auth handler Now let's implement the authentication in your Encore application. We'll use Encore's built-in [auth handler](/docs/go/develop/auth) to validate Logto's JWT tokens. Add these two modules in your Encore application: ```shell $ go get github.com/golang-jwt/jwt/v5 $ go get github.com/MicahParks/keyfunc/v3 ``` Create `auth/auth.go` and add the following code: ```go package auth import ( "context" "time" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/config" "github.com/MicahParks/keyfunc/v3" "github.com/golang-jwt/jwt/v5" ) // Configuration variables for authentication type LogtoAuthConfig struct { // The issuer URL Issuer config.String // URL to fetch JSON Web Key Set (JWKS) JwksUri config.String // Expected audience for the JWT ApiResourceIndicator config.String // Expected client ID in the token claims ClientId config.String } var authConfig *LogtoAuthConfig = config.Load[*LogtoAuthConfig]() // RequiredClaims defines the expected structure of JWT claims // Extends the standard JWT claims with a custom ClientID field type RequiredClaims struct { ClientID string `json:"client_id"` jwt.RegisteredClaims } // AuthHandler validates JWT tokens and extracts the user ID // Implements Encore's authentication handler interface // //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, error) { // Fetch and parse the JWKS (JSON Web Key Set) from the identity provider jwks, err := keyfunc.NewDefaultCtx(ctx, []string{authConfig.JwksUri()}) if err != nil { return "", &errs.Error{ Code: errs.Internal, Message: "failed to fetch JWKS", } } // Parse and validate the JWT token with required claims and validation options parsedToken, err := jwt.ParseWithClaims( token, &RequiredClaims{}, jwks.Keyfunc, // Expect the token to be intended for this API resource jwt.WithAudience(authConfig.ApiResourceIndicator()), // Expect the token to be issued by this issuer jwt.WithIssuer(authConfig.Issuer()), // Allow some leeway for clock skew jwt.WithLeeway(time.Minute*10), ) // Check if there were any errors during token parsing if err != nil { return "", &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } // Verify that the client ID in the token matches the expected client ID if parsedToken.Claims.(*RequiredClaims).ClientID != authConfig.ClientId() { return "", &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } // Extract the user ID (subject) from the token claims userId, err := parsedToken.Claims.GetSubject() if err != nil { return "", &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } // Return the user ID as an Encore auth.UID return auth.UID(userId), nil } ``` Create a [configuration file](https://encore.dev/docs/go/develop/config) in the auth service and name it `auth-config.cue`. Add the following: ```cue Issuer: "" JwksUri: "/jwks" ApiResourceIndicator: "" ClientId: "" ``` Replace the values with the ones you noted down from your Logto settings: - ``: The issuer URL from your Logto application endpoints (e.g., `https://your-tenant.logto.app`) - ``: The API identifier you set when creating the API resource (e.g., `https://api.encoreapp.com`) - ``: The application ID from your Logto application details page For example, your `auth-config.cue` might look like: ```cue Issuer: "https://your-tenant.logto.app" JwksUri: "https://your-tenant.logto.app/jwks" ApiResourceIndicator: "https://api.encoreapp.com" ClientId: "2gadf3mp0zotlq8j1k5x" ``` And then, you can use this auth handler to protect your API endpoints: ```go package api import ( "context" "fmt" "encore.dev/beta/auth" "encore.dev/beta/errs" ) //encore:api auth path=/api/hello func Api(ctx context.Context) (*Response, error) { userId, hasUserId := auth.UserID() if !hasUserId { return nil, &errs.Error{ Code: errs.Internal, Message: "User ID not found", } } msg := fmt.Sprintf("Hello, %s!", userId) return &Response{Message: msg}, nil } type Response struct { Message string } ``` ## Frontend We've completed our work in the Encore API service. Now we need to integrate Logto with our frontend application. You can choose the framework you are using in the [Logto Quick start](https://docs.logto.io/quick-starts) page to integrate Logto with your frontend application. In this guide we use React as an example. Check out the [Add authentication to your React application](https://docs.logto.io/quick-starts/react) guide to learn how to integrate Logto with your React application. In this example, you only need to complete up to the Integration section. After that, we'll demonstrate how the frontend application can obtain an access token from Logto to access the Encore API. First, update your `LogtoConfig` by adding the API resource used in your Encore app to the `resources` field. This tells Logto that we will be requesting access tokens for this API resource (Encore API). ```ts import { LogtoConfig } from '@logto/react'; const config: LogtoConfig = { // ...other configs resources: [''], }; ``` After updating the `LogtoConfig`, if a user is already signed in, they need to sign out and sign in again for the new `LogtoConfig` settings to take effect. Once the user is logged in, you can use the `getAccessToken` method provided by the Logto React SDK to obtain an access token for accessing specific API resources. For example, to access the Encore API, we use `https://api.encoreapp.com` as the API resource identifier. Then, add this access token to the request headers as the `Authorization` field in subsequent requests. ```ts const { getAccessToken } = useLogto(); const accessToken = await getAccessToken(''); // Add this access token to the request headers as the 'Authorization' field in subsequent requests fetch('/hello', { headers: { Authorization: `Bearer ${accessToken}`, }, }); ``` Here's the key frontend code: ```tsx -- config/logto.tsx -- import { LogtoConfig } from '@logto/react' export const config: LogtoConfig = { endpoint: '', appId: '', resources: [''], } export const appConfig = { apiResourceIndicator: '', signInRedirectUri: '', signOutRedirectUri: '', } export const encoreApiEndpoint = '' -- pages/ProtectedResource.tsx -- import { useLogto } from "@logto/react"; import { useState } from "react"; import { Navigate } from "react-router-dom"; import { appConfig, encoreApiEndpoint } from "../config/logto"; export function ProtectedResource() { const { isAuthenticated, getAccessToken } = useLogto(); const [message, setMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); const fetchProtectedResource = async () => { setIsLoading(true); setError(""); try { const accessToken = await getAccessToken(appConfig.apiResourceIndicator); const response = await fetch(`${encoreApiEndpoint}/api/hello`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setMessage(JSON.stringify(data)); } catch (error) { console.error("Error fetching protected resource:", error); setError("Failed to fetch protected resource. Please try again."); } finally { setIsLoading(false); } }; if (!isAuthenticated) { return ; } return (

Protected Resource

{message && !error && (

Response from Protected API

{message}
)} {error &&
{error}
}
); } ``` That's it, you've successfully integrated Logto with your Encore application. You can find the complete example code [here](https://github.com/encoredev/examples/tree/main/logto-react-sdk). ## Explore more If you want to use more Logto features, you can refer to the following links for more information: - Combine Logto's [Custom token claims](https://docs.logto.io/developers/custom-token-claims) to set [custom user data](/docs/go/develop/auth#with-custom-user-data) in the auth handler - Use [Logto RBAC features](https://docs.logto.io/authorization/role-based-access-control) to add authorization support to your application. The React integration tutorial also demonstrates how to add `scope` information to your Access token (note that you need to sign in again after updating Logto config) ================================================ FILE: docs/go/how-to/pubsub-outbox.md ================================================ --- seotitle: Using a transactional Pub/Sub outbox seodesc: Learn how you can use a transactional outbox with Pub/Sub to guarantee consistency between your database and Pub/Sub subscribers title: Transactional Pub/Sub outbox subtitle: Guarantee consistency between your database and Pub/Sub subscribers lang: go --- One of the hardest parts of building an event-driven application is ensuring consistency between services. A common pattern is for each service to have its own database and use Pub/Sub to notify other systems of business events. Inevitably this leads to inconsistencies since the Pub/Sub publishing is not transactional with the database writes. While there are several approaches to solving this, it's important the solution doesn't add too much complexity to what is often an already complex architecture. Perhaps the best solution in this regard is the [transactional outbox pattern](https://softwaremill.com/microservices-101/). Encore provides support for the transactional outbox pattern in the [x.encore.dev/infra/pubsub/outbox](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox) package. The transactional outbox works by binding a Pub/Sub topic to a database transaction, translating all calls to `topic.Publish` into inserting a database row in an `outbox` table. If/when the transaction later commits, the messages are picked up by a [Relay](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#Relay) that polls the `outbox` table and publishes the messages to the actual Pub/Sub topic. ## Publishing messages to the outbox To publish messages to the outbox, a topic must first be bound to the outbox. This is done using [Pub/Sub topic references](/docs/go/primitives/pubsub#using-topic-references) which allows you to retain complete type safety and the same interface as regular Pub/Sub topics, allowing existing code to continue to work without changes. In regular (non-outbox) usage the message id returned by `topic.Publish` is the same as the message id the subscriber receives when processing the message. With the outbox, this message id is not available until the transaction commits, so `topic.Publish` returns an id referencing the outbox row instead. The topic binding supports pluggable storage backends, enabling use of the outbox pattern with any transactional storage backend. Implementation are provided out-of-the-box for use with Encore's `encore.dev/storage/sqldb` package, as well as the standard library `database/sql` and `github.com/jackc/pgx/v5` drivers, but it's easy to write your own for other use cases. See the [Go package reference](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#PersistFunc) for more information. For example, to use a transactional outbox to notify subscribers when a user is created: ```go -- outbox.go -- // Create a SignupsTopic somehow. var SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */) // Create a topic ref with publisher permissions. ref := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic) // Bind it to the transactional outbox import "x.encore.dev/infra/pubsub/outbox" var tx *sqldb.Tx // somehow get a transaction ref = outbox.Bind(ref, outbox.TxPersister(tx)) // Calls to ref.Publish() will now insert a row in the outbox table. -- db_migration.sql -- -- The database used must contain the below database table: -- See https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#SQLDBStore CREATE TABLE outbox ( id BIGSERIAL PRIMARY KEY, topic TEXT NOT NULL, data JSONB NOT NULL, inserted_at TIMESTAMPTZ NOT NULL ); CREATE INDEX outbox_topic_idx ON outbox (topic, id); ``` Once the transaction commits any published messages via `ref` above will be stored in the `outbox` table. ## Consuming messages from the outbox Once committed, the messages are ready to be picked up and published to the actual Pub/Sub topic. That is done via the [Relay](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#Relay). The relay continuously polls the `outbox` table and publishes any new messages to the actual Pub/Sub topic. The relay supports pluggable storage backends, enabling use of the outbox pattern with any transactional storage backend. An implementation is provided out-of-the-box that uses Encore's built-in [SQL database support](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#SQLDBStore), but it's easy to write your own for other databases. The topics to poll must be registered with the relay, typically during service initialization. For example: ```go -- user/service.go -- package user import ( "context" "encore.dev/pubsub" "encore.dev/storage/sqldb" "x.encore.dev/infra/pubsub/outbox" ) type Service struct { signupsRef pubsub.Publisher[*SignupEvent] } // db is the database the outbox table is stored in var db = sqldb.NewDatabase(...) // Create the SignupsTopic somehow. var SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */) func initService() (*Service, error) { // Initialize the relay to poll from our database. relay := outbox.NewRelay(outbox.SQLDBStore(db)) // Register the SignupsTopic to be polled. signupsRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic) outbox.RegisterTopic(relay, signupsRef) // Start polling. go relay.PollForMessage(context.Background(), -1) return &Service{signupsRef: signupsRef}, nil } ``` ================================================ FILE: docs/go/how-to/temporal.md ================================================ --- seotitle: How to use Temporal and Encore seodesc: Learn how to use Temporal for reliable workflow execution with Encore. title: Use Temporal with Encore lang: go --- [Temporal](https://temporal.io) is a workflow orchestration system for building highly reliable systems. Encore works great with Temporal, and this guide shows you how to integrate Temporal into your Encore application. ## Set up Temporal clusters You'll need at least two Temporal clusters: one for local development and one for cloud environments. We recommend using [Temporalite](https://github.com/temporalio/temporalite) for local development, and [Temporal Cloud](https://temporal.io/cloud) for cloud environments. ## Set up Temporal Workflow Next it's time to create a Temporal Workflow. We'll base this on the Temporal [Hello World](https://learn.temporal.io/getting_started/go/hello_world_in_go/) example. Create a new Encore service named `greeting`: ```go -- greeting/greeting.go -- package greeting import ( "context" "fmt" "go.temporal.io/sdk/client" "go.temporal.io/sdk/worker" "encore.dev" ) // Use an environment-specific task queue so we can use the same // Temporal Cluster for all cloud environments. var ( envName = encore.Meta().Environment.Name greetingTaskQueue = envName + "-greeting" ) //encore:service type Service struct { client client.Client worker worker.Worker } func initService() (*Service, error) { c, err := client.Dial(client.Options{}) if err != nil { return nil, fmt.Errorf("create temporal client: %v", err) } w := worker.New(c, greetingTaskQueue, worker.Options{}) err = w.Start() if err != nil { c.Close() return nil, fmt.Errorf("start temporal worker: %v", err) } return &Service{client: c, worker: w}, nil } func (s *Service) Shutdown(force context.Context) { s.client.Close() s.worker.Stop() } ``` Next it's time to define some workflows. These need to be in the same service, so add a new `workflow` package inside the `greeting` service, containing a workflow and activity definition in separate files: ```go -- greeting/workflow/workflow.go -- package workflow import ( "time" "go.temporal.io/sdk/workflow" ) func Greeting(ctx workflow.Context, name string) (string, error) { options := workflow.ActivityOptions{ StartToCloseTimeout: time.Second * 5, } ctx = workflow.WithActivityOptions(ctx, options) var result string err := workflow.ExecuteActivity(ctx, ComposeGreeting, name).Get(ctx, &result) return result, err } -- greeting/workflow/activity.go -- package workflow import ( "context" "fmt" ) func ComposeGreeting(ctx context.Context, name string) (string, error) { greeting := fmt.Sprintf("Hello %s!", name) return greeting, nil } ``` Then, go back to the `greeting` service and register the workflow and activity: ```go -- greeting/greeting.go -- // Import the package at the top: import "encore.app/greeting/workflow" // Add these lines to `initService`, below the call to `worker.New`: w.RegisterWorkflow(workflow.Greeting) w.RegisterActivity(workflow.ComposeGreeting) ``` Now let's create an Encore API that triggers this workflow. Add a new file `greeting/greet.go`: ```go -- greeting/greet.go -- package greeting import ( "context" "encore.app/greeting/workflow" "encore.dev/rlog" "go.temporal.io/sdk/client" ) type GreetResponse struct { Greeting string } //encore:api public path=/greet/:name func (s *Service) Greet(ctx context.Context, name string) (*GreetResponse, error) { options := client.StartWorkflowOptions{ ID: "greeting-workflow", TaskQueue: greetingTaskQueue, } we, err := s.client.ExecuteWorkflow(ctx, options, workflow.Greeting, name) if err != nil { return nil, err } rlog.Info("started workflow", "id", we.GetID(), "run_id", we.GetRunID()) // Get the results var greeting string err = we.Get(ctx, &greeting) if err != nil { return nil, err } return &GreetResponse{Greeting: greeting}, nil } ``` ## Run it locally Now we're ready to test it out. Start up `temporalite` and your Encore application (in separate terminals): ```bash $ temporalite start --namespace default $ encore run ``` Now try calling it, either from the [Local Development Dashboard](/docs/go/observability/dev-dash) or using cURL: ```bash $ curl 'http://localhost:4000/greeting/Temporal' {"Greeting": "Hello Temporal!"} ``` If you see this, it works! ## Run in the cloud To run it in the cloud, you will need to use Temporal Cloud or your own, self-hosted Temporal cluster. The easiest way to automatically pick up the correct cluster address is to use Encore's [config functionality](/docs/go/develop/config). Add two new files: ``` -- greeting/config.go -- package greeting import "encore.dev/config" type Config struct { TemporalServer string } var cfg = config.Load[*Config]() -- greeting/config.cue -- package greeting TemporalServer: [ // These act as individual case statements if #Meta.Environment.Cloud == "local" { "localhost:7233" }, // TODO: configure this to match your own cluster address "my.cluster.address:7233", ][0] // Return the first value which matches the condition ``` Finally go back to `greeting/greeting.go` and update the `client.Dial` call to look like: ```go -- greeting/greeting.go -- client.Dial(client.Options{HostPort: cfg.TemporalServer}) ``` With that, Encore will automatically connect to the correct Temporal cluster, using a local cluster for local development and your cloud-hosted cluster for everything else. ================================================ FILE: docs/go/install.md ================================================ --- seotitle: Install Encore to start building seodesc: See how you can install Encore on all platforms, and get started building your next backend application in minutes. title: Installation subtitle: Install the Encore CLI to get started with local development lang: go --- If you are new to Encore, we recommend following the [quick start guide](/docs/go/quick-start). ## Install the Encore CLI To develop locally with Encore, you first need to install the Encore CLI. This is what provisions your local development environment, and runs your Local Development Dashboard complete with logs, tracing, and API documentation. To locally run Encore apps with databases, you also need to have [Docker](https://www.docker.com) installed and running. ### Optional: Add AI/LLM instructions To help AI coding assistants (Cursor, Claude Code, GitHub Copilot, etc.) understand how to use Encore, run this from your app directory: ```bash encore llm-rules init ``` This prompts you to select your tool and generates the appropriate config (e.g. `.cursorrules`, `CLAUDE.md`) and MCP setup where supported. For full details and other options, see [AI Tools Integration](/docs/go/ai-integration). ### Build from source If you prefer to build from source, [follow these instructions](https://github.com/encoredev/encore/blob/main/CONTRIBUTING.md). ## Update to the latest version Check which version of Encore you have installed by running `encore version` in your terminal. It should print something like: ```shell encore version v1.28.0 ``` If you think you're on an older version of Encore, you can easily update to the latest version by running `encore version update` from your terminal. ================================================ FILE: docs/go/migration/ai-migration.mdx ================================================ --- seotitle: Migrate to Encore.go Using an AI Agent seodesc: Learn how to use Encore's AI migration skill to automatically migrate your existing backend to Encore.go, with validation at every step. title: Migrate using AI agent lang: go --- Encore's AI migration skill analyzes your existing backend, builds a dependency-aware migration plan, and converts your code to Encore.go — one unit at a time, with validation at every step. It works with any source framework: Gin, Echo, Chi, Fiber, net/http, Django, Rails, and more. The skill has been tested with Claude Code but should work with other agents as well. ## Prerequisites Install the Encore skills package in your AI coding tool: ```bash npx add-skill encoredev/skills ``` You'll also need: - The source codebase accessible on your local machine - An Encore project to migrate into (the skill can help create one) - Your source application running locally (optional — enables HTTP comparison validation) ## Starting a migration Create a new Encore app from the "Empty app" template by running: ```bash encore app create ``` From inside your Encore app, open your AI coding tool and ask it to migrate your existing app: ``` Migrate ../path/to/existing/project to Encore.ts by using the encore-migrate skill ``` The skill walks you through four phases: **Discover**, **Plan**, **Migrate**, and **Complete**. ## How it works ### Phase 1 — Discover The AI reads your source codebase and inventories everything: API endpoints, databases, Pub/Sub topics, cron jobs, auth middleware, secrets, and tests. It groups related entities into **migration units** — typically aligned with your existing service boundaries or URL path prefixes — and presents a summary for you to review. You can adjust the groupings before moving on. Split units that are too large, merge ones that are too small, or rename them to match your domain. ### Phase 2 — Plan The AI creates a `migration-plan.md` file and a `migration-plan/` directory in your Encore project. The summary file tracks overall progress and dependency order. Each migration unit gets its own detail file listing every endpoint, database table, and test to migrate. Dependencies determine the order. Secrets and config go first, then databases, auth, leaf services, dependent services, Pub/Sub, and finally cron jobs. ### Phase 3 — Migrate The AI works through one migration unit at a time. For each entity it: 1. **Implements** the Encore equivalent — [API endpoints](/docs/go/primitives/defining-apis), [database schemas](/docs/go/primitives/databases), [infrastructure declarations](/docs/go/primitives/services) 2. **Migrates tests** from the source framework to Encore's [testing patterns](/docs/go/develop/testing) 3. **Validates** the result using up to three layers (see [Validation](#validation)) 4. **Updates the plan** files to track progress After completing a unit, it suggests the next one based on the dependency order. You can also pick a different unit or tell it to keep going through multiple units. ### Phase 4 — Complete When all units are done, the AI presents a final summary: what was migrated, what was skipped, and what needs manual attention. It suggests a final test suite run and, if your source system has a frontend, recommends reconnecting it to the new Encore backend using the [Client Generation](/docs/go/cli/client-generation) feature. ## Full-stack and monorepo support When the source codebase contains frontend code (React, Vue, Angular, Next.js, etc.), the AI identifies it and marks it as out of scope — only backend code is migrated. For full-stack frameworks like **Next.js**, **Remix**, **Nuxt**, **SvelteKit**, and **Astro**, the AI detects server-side routes (e.g., Next.js `pages/api/` or Remix `loader` functions) and asks what you want to do with them: 1. **Migrate all** server-side routes to Encore 2. **Migrate some** — you pick which ones move 3. **Keep all in the frontend framework** — only migrate standalone backend code This is useful when you want an Encore backend but prefer to keep a thin BFF or SSR data-fetching layer in your frontend framework. ## Validation Every entity is validated before it's marked as migrated. The AI uses three layers: **Test migration** — Source tests are converted to Encore's [testing patterns](/docs/go/develop/testing) and run. They must pass before the entity is marked as done. **HTTP comparison** — When both systems are running locally, the AI calls the same endpoint on both and compares the HTTP status code and response body structure. This layer is skipped for endpoints with side effects or that require auth credentials the AI can't obtain. **Verification gate** — No entity is marked as `migrated` without concrete evidence from the current session: test output, HTTP comparison results, or your explicit approval to skip. ## Resuming across sessions The migration plan is persisted to files in your Encore project, so you can close your editor and come back later. When you resume, the AI reads `migration-plan.md`, reports the current status, and suggests the next unit to work on. ``` Resume the migration ``` ``` What's left to migrate? ``` ================================================ FILE: docs/go/migration/migrate-away.md ================================================ --- title: Migrate away from Encore subtitle: If you love someone, set them free. lang: go --- _We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._ Picking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity. When designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. In practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure. ## Accommodating for your unique requirements Many software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example: - You can drop down in abstraction level in the API framework using [raw endpoints](/docs/go/primitives/raw-endpoints). - You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore ## Mitigating risk through Open Source and efficiency We believe that adopting Encore is a low-risk decision for several reasons: - There's no upfront investment needed to get the benefits - Encore apps are normal programs where less than 1% of the code is Encore-specific - All infrastructure and data is in your own cloud - It's simple to integrate with cloud services and systems not natively supported by Encore - Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes) - Everything you need to self-host your application is [Open Source and documented](/docs/go/self-host/docker-build) ## What to expect when migrating away If you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away. ### Code changes Building with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework. This means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost. ### Deployment If you are self-hosting your application, then you're already done. If you are using Encore Cloud Platform to manage deployments and want to migrate to your own solution, you can use the `encore build docker` command to produce a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility. Learn more in the [self-hosting docs](/docs/go/self-host/docker-build). ### Tell us what you need We're engineers ourselves and we understand the importance of not being constrained by a single technology. We're working every single day on making it even easier to start, and stop, using Encore. If you have specific concerns, questions, or requirements, we'd love to hear from you! Please reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts. ================================================ FILE: docs/go/observability/dev-dash.md ================================================ --- seotitle: Development dashboard for local development seodesc: Encore's Local Development Dashboard comes with build-in distributed tracing, API docs, and real-time architecture diagrams. title: Local Development Dashboard subtitle: Built-in tools for simplicity and productivity lang: go --- Encore provides an efficient local development workflow that automatically provisions [local infrastructure](/docs/platform/infrastructure/infra#local-development) and supports automated testing with dedicated test infrastructure. The local environment also comes with a built-in Local Development Dashboard to simplify development and improve productivity. It has several features to help you design, develop, and debug your application: * [Service Catalog](/docs/go/observability/service-catalog) with Automatic API Documentation * API Explorer to call your APIs * [Distributed Tracing](/docs/go/observability/tracing) for simple and powerful debugging * [Encore Flow](/docs/go/observability/encore-flow) for visualizing your microservices architecture All these features update in real-time as you make changes to your application. To access the dashboard, start your Encore application with `encore run` and it will open automatically. You can also follow the link in your terminal: ```bash $ encore run API Base URL: http://localhost:4000 Dev Dashboard URL: http://localhost:9400/hello-world-cgu2 ``` ================================================ FILE: docs/go/observability/encore-flow.md ================================================ --- seotitle: Encore Flow automatic microservices architecture diagrams seodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application. title: Flow Architecture Diagram subtitle: Visualize your cloud microservices architecture lang: go --- Flow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your microservices architecture and identify which services depend on each other and how they work together. ## Birds-eye view Having access to a zoomed out representation of your system can be invaluable in pretty much all parts of the development cycle. Flow helps you: * Track down bottlenecks before they grow into big problems. * Get new team members onboarded much faster. * Pinpoint hot paths in your system, services that might need extra attention. Services and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below the `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or subscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it: ## Highlight dependencies Hover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies. Here the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the database and requests to two of the endpoints from the `user` service as well as requests to one endpoint from the `authentication` service: ## Real-time updates Flow is accessible in the [Local Development Dashboard](/docs/go/observability/dev-dash) and, when using Encore Cloud, in the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments. When developing locally, Flow will auto update in real-time to reflect your architecture as you make code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones. For cloud environments, Flow auto-updates with each deploy. In the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service: ================================================ FILE: docs/go/observability/logging.md ================================================ --- seotitle: Use structured logging to understand your application seodesc: Learn how to use structured logging, a combination of free-form log messages and type-safe key-value pairs, to understand your backend application's behavior. title: Logging subtitle: Structured logging helps you understand your application lang: go infobox: { title: "Structured Logging", import: "encore.dev/rlog", } --- Encore offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. This enables straightforward analysis of what your application is doing, in a way that is easy for a computer to parse, analyze, and index. This makes it simple to quickly filter and search through logs. Encore’s logging is integrated with the built-in [Distributed Tracing](/docs/go/observability/tracing) functionality, and all logs are automatically included in the active trace. This dramatically simplifies debugging of your application. ## Usage First, import `encore.dev/rlog` in your package. Then simply call one of the package methods `Info`, `Error`, or `Debug`. For example: ```go rlog.Info("log message", "user_id", 12345, "is_subscriber", true) rlog.Error("something went terribly wrong!", "err", err) ``` The first parameter is the log message. After that follows zero or more key-value pairs for structured logging for context. If you’re logging many log messages with the same key-value pairs each time it can be a bit cumbersome. To help with that, use `rlog.With()` to group them into a context object, which then copies the key-value pairs into each log event: ```go ctx := rlog.With("is_subscriber", true) ctx.Info("user logged in", "login_method", "oauth") // includes is_subscriber=true ``` For more information, see the [API Documentation](https://pkg.go.dev/encore.dev/rlog). ## Live-streaming logs Encore also makes it simple to live-stream logs directly to your terminal, from any environment, by running: ``` $ encore logs --env=prod ``` ================================================ FILE: docs/go/observability/metrics.md ================================================ --- seotitle: Custom metrics in Go seodesc: Learn how to define and use custom metrics in your Go backend application with Encore. title: Metrics subtitle: Track custom metrics in your Go application infobox: { title: "Metrics", import: "encore.dev/metrics", } lang: go --- Encore provides built-in support for defining custom metrics in your Go applications. Once defined, metrics are automatically collected and displayed in the Encore Cloud Dashboard, and can be exported to third-party observability services. See the [Platform metrics documentation](/docs/platform/observability/metrics) for information about integrations with third-party services like Grafana Cloud and Datadog. ## Defining custom metrics Define custom metrics by importing the [`encore.dev/metrics`](https://pkg.go.dev/encore.dev/metrics) package and creating a new metric using one of the `metrics.NewCounter` or `metrics.NewGauge` functions. For example, to count the number of orders processed: ```go import "encore.dev/metrics" var OrdersProcessed = metrics.NewCounter[uint64]("orders_processed", metrics.CounterConfig{}) func process(order *Order) { // ... OrdersProcessed.Increment() } ``` ## Metric types Encore currently supports two metric types: counters and gauges. **Counters** measure the count of something. A counter's value must always increase, never decrease. (Note that the value gets reset to 0 when the application restarts.) Typical use cases include counting the number of requests, the amount of data processed, and so on. **Gauges** measure the current value of something. Unlike counters, a gauge's value can fluctuate up and down. Typical use cases include measuring CPU usage, the number of active instances running of a process, and so on. For information about their respective APIs, see the API documentation for [Counter](https://pkg.go.dev/encore.dev/metrics#Counter) and [Gauge](https://pkg.go.dev/encore.dev/metrics#Gauge). ### Counter example ```go import "encore.dev/metrics" var RequestsReceived = metrics.NewCounter[uint64]("requests_received", metrics.CounterConfig{}) func handleRequest() { RequestsReceived.Increment() // ... handle request } ``` ### Gauge example ```go import "encore.dev/metrics" var ActiveConnections = metrics.NewGauge[int64]("active_connections", metrics.GaugeConfig{}) func onConnect() { ActiveConnections.Add(1) } func onDisconnect() { ActiveConnections.Add(-1) } ``` ## Defining labels Encore's metrics package provides a type-safe way of attaching labels to metrics. To define labels, create a struct type representing the labels and then use `metrics.NewCounterGroup` or `metrics.NewGaugeGroup`. The Labels type must be a named struct, where each field corresponds to a single label. Each field must be of type `string`, `int`, or `bool`. ### Counter with labels ```go import "encore.dev/metrics" type Labels struct { Success bool } var OrdersProcessed = metrics.NewCounterGroup[Labels, uint64]("orders_processed", metrics.CounterConfig{}) func process(order *Order) { var success bool // ... populate success with true/false ... OrdersProcessed.With(Labels{Success: success}).Increment() } ``` ### Gauge with labels ```go import "encore.dev/metrics" type ConnectionLabels struct { Region string } var ActiveConnections = metrics.NewGaugeGroup[ConnectionLabels, int64]("active_connections", metrics.GaugeConfig{}) func onConnect(region string) { ActiveConnections.With(ConnectionLabels{Region: region}).Add(1) } ``` Each combination of label values creates a unique time series tracked in memory and stored by the monitoring system. Using numerous labels can lead to a combinatorial explosion, causing high cloud expenses and degraded performance. As a general rule, limit the unique time series to tens or hundreds at most, rather than thousands. ================================================ FILE: docs/go/observability/service-catalog.md ================================================ --- seotitle: Service Catalog & Generated API Docs seodesc: See how Encore automatically generates API documentation that always stays up to date and in sync. title: Service Catalog subtitle: Automatically get a Service Catalog and complete API docs lang: go --- All developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date. To solve this, Encore uses the [Encore Application Model](/docs/go/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve. The API docs are available both in your [Local Development Dashboard](/docs/go/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud). ================================================ FILE: docs/go/observability/tracing.md ================================================ --- seotitle: Distributed Tracing helps you understand your app seodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore. title: Distributed Tracing subtitle: Track requests across your application and infrastructure lang: go --- Distributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life. Tracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end. As opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations. You view traces in the [Local Development Dashboard](/docs/go/observability/dev-dash) and, when using Encore Cloud, you can also see traces in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments. ## Encore's tracing is more comprehensive and more performant than all other tools Unlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before: * Stack traces * Structured logging * HTTP requests * Network connection information * API calls * Database queries * etc. ## Redacting sensitive data Encore's tracing automatically captures request and response payloads to simplify debugging. For cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports redacting fields marked as containing sensitive data. See the documentation on [API Schemas](/docs/go/primitives/defining-apis#sensitive-data) for more information. ================================================ FILE: docs/go/overview.md ================================================ --- seotitle: Encore.go Introduction seodesc: Learn how Encore's Go Backend Framework works, and get to know the powerful features that help you build cloud backend applications faster. title: Encore.go subtitle: Use Encore.go to build robust backend applications and distributed systems toc: false lang: go ---

Quick Start Guide

Build your first Encore.go application in minutes
Encore.go is an open source backend framework for building distributed system. It provides a declarative approach to working with essential backend primitives like APIs, microservices, databases, queues, caches, cron jobs, and storage buckets. The framework comes with a lot of built-in tooling for a productive end-to-end developer experience: - **Local Environment Management**: Encore automatically sets up and runs your local development environment and all local infrastructure. - **Enhanced Observability**: Encore comes with tools like a [Local Development Dashboard](/docs/go/observability/dev-dash), [tracing](/docs/go/observability/tracing), and a database explorer for monitoring application behavior. - **Automatic Documentation**: Generates and maintains [up-to-date documentation](/docs/go/observability/service-catalog) for APIs and services, and created [architecture diagrams](/docs/go/observability/encore-flow) for your system. - **AI Integration:** Encore comes with built-in tools for effective AI assisted development, like [AI instructions](/docs/go/ai-integration) and an [MCP server](/docs/go/cli/mcp). - **DevOps Automation Platform (Optional)**: [Encore Cloud](https://encore.cloud) is an optional platform for automating infrastructure provisioning and DevOps processes in your cloud on AWS and GCP.

Watch a demo video

See how to build an event-driven app with Encore.

Example apps

Ready-made starter apps to inspire your development.

Join Discord

Find answers, ask questions, and chat with other Encore developers.
================================================ FILE: docs/go/primitives/api-calls.md ================================================ --- seotitle: API Calls with Encore.go seodesc: Learn how to make type-safe API calls in Go with Encore.go title: API Calls subtitle: Making API calls is as simple as making function calls lang: go --- Calling an API endpoint looks like a regular function call with Encore.go. To call an endpoint you first import the other service as a Go package using `import "encore.app/package-name"` and then call the API endpoint like a regular function. Encore will automatically generate the necessary boilerplate at compile-time. In the example below, we import the service package `hello` and call the `Ping` endpoint using a function call to `hello.Ping`. ```go import "encore.app/hello" // import service //encore:api public func MyOtherAPI(ctx context.Context) error { resp, err := hello.Ping(ctx, &hello.PingParams{Name: "World"}) if err == nil { log.Println(resp.Message) // "Hello, World!" } return err } ``` This means your development workflow is as simple as building a monolith, even if you use multiple services. You also get all the benefits of function calls, like compile-time checking of all the parameters and auto-completion in your editor, while still allowing the division of code into logical components, services, and systems. Then when building your application, Encore uses [static analysis](/docs/go/concepts/application-model) to parse all API calls and compiles them to proper API calls. ## Current Request By using Encore's [current request API](https://pkg.go.dev/encore.dev/#Request) you can get meta-information about the current request. Including the type of request, the time the request started, the service and endpoint called and the path which was called on the service. For more information, see the [metadata documentation](/docs/go/develop/metadata). ================================================ FILE: docs/go/primitives/api-errors.md ================================================ --- seotitle: API Errors – Types, Wrappers, and Codes seodesc: See how to return structured error information from your APIs using Encore's errs package, and how to build precise error messages for complex business logic. title: API Errors subtitle: Returning structured error information from your APIs infobox: { title: "API Errors", import: "encore.dev/beta/errs", } lang: go --- Encore supports returning structured error information from your APIs using the [encore.dev/beta/errs](https://pkg.go.dev/encore.dev/beta/errs) package. Errors are propagated across the network to the [generated clients](/docs/go/cli/client-generation) and can be used within your front-ends without having to build any custom marshalling code. ## The errs.Error type Structured errors are represented by the `errs.Error` type: ```go type Error struct { // Code is the error code to return. Code ErrCode `json:"code"` // Message is a descriptive message of the error. Message string `json:"message"` // Details are user-defined additional details. Details ErrDetails `json:"details"` // Meta are arbitrary key-value pairs for use within // the Encore application. They are not exposed to external clients. Meta Metadata `json:"-"` } ``` Returning an `*errs.Error` from an Encore API endpoint will result in Encore serializing this struct to JSON and returning it in the response. Additionally Encore will set the HTTP status code to match the error code (see the mapping table below). For example: ```go return &errs.Error{ Code: errs.NotFound, Message: "sprocket not found", } ``` Causes Encore to respond with a `HTTP 404` error with body: ```json { "code": "not_found", "message": "sprocket not found", "details": null } ``` ## Error Wrapping Encore applications are encouraged to always use the `errs` package to manipulate errors. It supports wrapping errors to gradually add more error information, and lets you easily define both structured error details to return to external clients, as well as internal key-value metadata for debugging and error handling. ```go func Wrap(err error, msg string, metaPairs ...interface{}) error ``` Use `errs.Wrap` to conveniently wrap an error, adding additional context and converting it to an `*errs.Error`. If `err` is nil it returns `nil`. If `err` is already an `*errs.Error` it copies the Code, Details, and Meta fields over. The variadic `metaPairs` parameter must be key-value pairs, where the key is always a `string` and the value can be any built-in type. Existing key-value pairs from the `err` are merged into the new `*Error`. ```go func WrapCode(err error, code ErrCode, msg string, metaPairs ...interface{}) error ``` `errs.WrapCode` is like `errs.Wrap` but also sets the error code. ```go func Convert(err error) error ``` `errs.Convert` converts an error to an `*errs.Error`. If the error is already an `*errs.Error` it returns it unmodified. If `err` is nil it returns nil. ## Error Codes The `errs` package defines error codes for common error scenarios. They are identical to the codes defined by `gRPC` for interoperability. The table below summarizes the error codes. You can find additional documentation about when to use them in the [package documentation](https://pkg.go.dev/encore.dev/beta/errs#ErrCode). | Code | String | HTTP Status | | -------------------- | ----------------------- | ------------------------- | | `OK` | `"ok"` | 200 OK | | `Canceled` | `"canceled"` | 499 Client Closed Request | | `Unknown` | `"unknown"` | 500 Internal Server Error | | `InvalidArgument` | `"invalid_argument"` | 400 Bad Request | | `DeadlineExceeded` | `"deadline_exceeded"` | 504 Gateway Timeout | | `NotFound` | `"not_found"` | 404 Not Found | | `AlreadyExists` | `"already_exists"` | 409 Conflict | | `PermissionDenied` | `"permission_denied"` | 403 Forbidden | | `ResourceExhausted` | `"resource_exhausted"` | 429 Too Many Requests | | `FailedPrecondition` | `"failed_precondition"` | 400 Bad Request | | `Aborted` | `"aborted"` | 409 Conflict | | `OutOfRange` | `"out_of_range"` | 400 Bad Request | | `Unimplemented` | `"unimplemented"` | 501 Not Implemented | | `Internal` | `"internal"` | 500 Internal Server Error | | `Unavailable` | `"unavailable"` | 503 Unavailable | | `DataLoss` | `"data_loss"` | 500 Internal Server Error | | `Unauthenticated` | `"unauthenticated"` | 401 Unauthorized | ## Error Building In cases where you have complex business logic, or multiple error returns, it's convenient to gradually add metadata to your error. For this purpose Encore provides `errs.Builder`. The builder lets you gradually set aspects of the error, using a chaining API design. Use `errs.B()` to get a new builder that you can start chaining with directly. When you want to return the constructed error call the `.Err() `method. For example: ```go func getBoard(ctx context.Context, boardID int64) (*Board, error) { // Construct a new error builder with errs.B() eb := errs.B().Meta("board_id", params.ID) b := &Board{ID: params.ID} err := sqldb.QueryRow(ctx, ` SELECT name, created FROM board WHERE id = $1 `, params.ID).Scan(&b.Name, &b.Created) if errors.Is(err, sqldb.ErrNoRows) { // Return a "board not found" error with code == NotFound return nil, eb.Code(errs.NotFound).Msg("board not found").Err() } else if err != nil { // Return a general error return nil, eb.Cause(err).Msg("could not get board").Err() } // ... } ``` ## Inspecting API Errors When you call another API within Encore, the returned errors are always wrapped in `*errs.Error`. You can inspect the error information either by casting to `*errs.Error`, or using the below helper methods. ```go func Code(err error) ErrCode ``` `errs.Code` returns the error code. If the error was not an `*errs.Error` it returns `errs.Unknown`. ```go func Meta(err error) Metadata type Metadata map[string]interface{} ``` `errs.Meta` returns any structured metadata present in the error. If the error was not an `*errs.Error` it returns nil. Unlike when you return error information to external clients, all the metadata is sent to the calling service, making debugging even easier. ```go func Details(err error) ErrDetails ``` `errs.Details` returns the structured error details. If the error was not an `*errs.Error` or the error lacked details, it returns nil. ================================================ FILE: docs/go/primitives/api-schemas.md ================================================ --- seotitle: API Schemas – Path, Query, and Body parameters seodesc: See how to design API schemas for your Go based backend application using Encore. title: API Schemas subtitle: How to design schemas for your APIs lang: go --- APIs in Encore are regular functions with request and response data types. These types are structs (or pointers to structs) with optional field tags, which Encore uses to encode API requests to HTTP messages. The same struct can be used for requests and responses, but the `query` tag is ignored when generating responses. All tags except `json` are ignored for nested tags, which means you can only define `header` and `query` parameters for root level fields. For example, this struct: ```go type NestedRequestResponse struct { Header string `header:"X-Header"`// this field will be read from the http header Query string `query:"query"`// this field will be read from the query string Body1 string `json:"body1"` Nested struct { Header2 string `header:"X-Header2"`// this field will be read from the body Query2 string `query:"query2"`// this field will be read from the body Body2 string `json:"body2"` } `json:"nested"` } ``` Would be unmarshalled from this request: ```output POST /example?query=a%20query HTTP/1.1 Content-Type: application/json X-Header: A header { "body1": "a body", "nested": { "Header2": "not a header", "Query2": "not a query", "body2": "a nested body" } } ``` And marshalled to this response: ```output HTTP/1.1 200 OK Content-Type: application/json X-Header: A header { "Query": "not a query", "body1": "a body", "nested": { "Header2": "not a header", "Query2": "not a query", "body2": "a nested body" } } ``` ## Path parameters Path parameters are specified by the `path` field in the `//encore:api` annotation. To specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path can be parsed as a wildcard parameter by using `*name` with a matching function parameter. ```go // GetBlogPost retrieves a blog post by id. //encore:api public method=GET path=/blog/:id/*path func GetBlogPost(ctx context.Context, id int, path string) (*BlogPost, error) { // Use id to query database... } ``` ### Fallback routes Encore supports defining fallback routes that will be called if no other endpoint matches the request, using the syntax `path=/!fallback`. This is often useful when migrating an existing backend service over to Encore, as it allows you to gradually migrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using a raw endpoint with a fallback route. For example: ```go //encore:service type Service struct { oldRouter *gin.Engine // existing HTTP router } // Route all requests to the existing HTTP router if no other endpoint matches. //encore:api public raw path=/!fallback func (s *Service) Fallback(w http.ResponseWriter, req *http.Request) { s.oldRouter.ServeHTTP(w, req) } ``` ## Headers Headers are defined by the `header` field tag, which can be used in both request and response data types. The tag name is used to translate between the struct field and http headers. In the example below, the `Language` field of `ListBlogPost` will be fetched from the `Accept-Language` HTTP header. ```go type ListBlogPost struct { Language string `header:"Accept-Language"` Author string // Not a header } ``` ### Cookies Cookies can be set in the response by using the `header` tag with the `Set-Cookie` header name. ```go type LoginResponse struct { SessionID string `header:"Set-Cookie"` } //encore:api public method=POST path=/login func Login(ctx context.Context) (*LoginResponse, error) { return &LoginResponse{SessionID: "session=123"}, nil } ```` The cookies can then be read using e.g. [structured auth data](/docs/go/develop/auth#accepting-structured-auth-information). ## Query parameters For `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default. The query parameter name defaults to the [snake-case](https://en.wikipedia.org/wiki/Snake_case) encoded name of the corresponding struct field (e.g. BlogPost becomes blog_post). The `query` field tag can be used to parse a field from the query string for other HTTP methods (e.g. POST) and to override the default parameter name. Query strings are not supported in HTTP responses and therefore `query` tags in response types are ignored. In the example below, the `PageLimit` field will be read from the `limit` query parameter, whereas the `Author` field will be parsed from the query string (as `author`) only if the method of the request is `GET`, `HEAD` or `DELETE`. ```go type ListBlogPost struct { PageLimit int `query:"limit"` // always a query parameter Author string // query if GET, HEAD or DELETE, otherwise body parameter } ``` ## Body parameters Encore will default to reading request parameters from the body (as JSON) for all HTTP methods except `GET`, `HEAD` or `DELETE`. The name of the body parameter defaults to the field name, but can be overridden by the `json` tag. Response fields will be serialized as JSON in the HTTP body unless the `header` tag is set. There is no tag to force a field to be read from the body, as some infrastructure entities do not support body content in `GET`, `HEAD` or `DELETE` requests. ```go type CreateBlogPost struct { Subject string `json:"limit"` // query if GET, HEAD or DELETE, otherwise body parameter Author string // query if GET, HEAD or DELETE, otherwise body parameter } ``` ## Supported types The table below lists the data types supported by each HTTP message location. | Type | Header | Path | Query | Body | | --------------- | ------ | ---- | ----- | ---- | | bool | X | X | X | X | | numeric | X | X | X | X | | string | X | X | X | X | | time.Time | X | X | X | X | | uuid.UUID | X | X | X | X | | json.RawMessage | X | X | X | X | | list | | | X | X | | struct | | | | X | | map | | | | X | | pointer | | | | X | ## Raw endpoints In some cases you may need to fulfill an API schema that is defined by someone else, for instance when you want to accept webhooks. This often requires you to parse custom HTTP headers and do other low-level things that Encore usually lets you skip. For these circumstances Encore lets you define raw endpoints. Raw endpoints operate at a lower abstraction level, giving you access to the underlying HTTP request. Learn more in the [raw endpoints documentation](/docs/go/primitives/raw-endpoints). ## Sensitive data Encore's built-in tracing functionality automatically captures request and response payloads to simplify debugging. That's not desirable if a request or response payload contains sensitive data, such as API keys or personally identifiable information (PII). For those use cases Encore supports marking a field as sensitive using the struct tag `encore:"sensitive"`. Encore's tracing system will automatically redact fields tagged as sensitive. This works for both individual values as well as nested fields. Note that inputs to [auth handlers](/docs/go/develop/auth) are automatically marked as sensitive and are always redacted. Raw endpoints lack a schema, which means there's no way to add a struct tag to mark certain data as sensitive. For this reason Encore supports tagging the whole API endpoint as sensitive by adding `sensitive` to the `//encore:api` annotation. This will cause the whole request and response payload to be redacted, including all request and response headers. The `encore:"sensitive"` tag is ignored for local development environments to make development and debugging with the Local Development Dashboard easier. ## Example ```go package blog // service name import ( "time" "encore.dev/types/uuid" ) type Updates struct { Author string `json:"author,omitempty"` PublishTime time.Time `json:"publish_time,omitempty"` } // BatchUpdateParams is the request data for the BatchUpdate endpoint. type BatchUpdateParams struct { Requester string `header:"X-Requester"` RequestTime time.Time `header:"X-Request-Time"` CurrentAuthor string `query:"author"` Updates *Updates `json:"updates"` MySecretKey string `encore:"sensitive"` } // BatchUpdateResponse is the response data for the BatchUpdate endpoint. type BatchUpdateResponse struct { ServedBy string `header:"X-Served-By"` UpdatedIDs []uuid.UUID `json:"updated_ids"` } //encore:api public method=POST path=/section/:sectionID/posts func BatchUpdate(ctx context.Context, sectionID string, params *BatchUpdateParams) (*BatchUpdateResponse, error) { // Update blog posts for section return &BatchUpdateResponse{ServedBy: hostname, UpdatedIDs: ids}, nil } ``` ================================================ FILE: docs/go/primitives/app-structure.md ================================================ --- seotitle: Structuring your microservices backend application seodesc: Learn how to structure your microservices backend application. See recommended app structures for monoliths, small microservices backends, and large scale microservices applications. title: App Structure subtitle: Structuring your Encore application lang: go --- Encore uses a monorepo design and it's best to use one Encore app for your entire backend application. This lets Encore build an application model that spans your entire app, necessary to get the most value out of many features like [distributed tracing](/docs/go/observability/tracing) and [Encore Flow](/docs/go/observability/encore-flow). If you have a large application, see advice on how to [structure an app with several systems](/docs/go/primitives/app-structure#large-applications-with-several-systems). It's simple to integrate Encore applications with pre-existing systems you might have, using APIs and built-in tools like [client generation](/docs/go/cli/client-generation). ## Monolith or Microservices Encore is not opinionated about monoliths vs. microservices. It does however let you build microservices applications with a monolith-style developer experience. For example, you automatically get IDE auto-complete when making [API calls between services](/docs/go/primitives/api-calls), along with cross-service type-safety. When using Encore Cloud to create an environment on AWS/GCP, Encore enables you to configure if you want to combine multiple services into one process or keep them separate. This can be useful for improved efficiency at smaller scales, and for co-locating services for increased performance. Learn more in the [environments documentation](/docs/platform/deploy/environments#process-allocation). ## Creating services To create an Encore service, you create a Go package and [define an API](/docs/go/primitives/defining-apis) within it. When using databases, you add database migrations in a subfolder `migrations` to define the structure of the database(s). Learn more in the [SQL databases docs](/docs/go/primitives/databases). On disk it might look like this: ``` /my-app ├── encore.app // ... and other top-level project files │ ├── hello // hello service (a Go package) │   ├── migrations // hello service db migration (directory) │   │ └── 1_create_table.up.sql // hello service db migration │   ├── hello.go // hello service code │   └── hello_test.go // tests for hello service │ └── world // world service (a Go package) └── world.go // world service code ``` ## Structure services using sub-packages Within a service, it's possible to have multiple sub-packages. This is a good way to define components, helper functions, or other code for your functions, should you wish to do that. You can create as many sub-packages, in any kind of nested structure within your service, as you want. To create sub-packages, you create sub-directories within a service package. Sub-packages are internal to services, they are not themselves service packages. This means sub-packages within services cannot themselves define APIs. You can however define an API in a service package that calls a function within a sub-package. For example, rather than define the entire logic for an endpoint in that endpoint's function, you can call functions from sub-packages and divide the logic in any way you want. **`hello/hello.go`** ```go package hello import ( "context" "encore.app/hello/foo" ) //encore:api public path=/hello/:name func World(ctx context.Context, name string) (*Response, error) { msg := foo.GenerateMessage(name) return &Response{Message: msg}, nil } type Response struct { Message string } ``` **`hello/foo/foo.go`** ```go package foo import ( "fmt" ) func GenerateMessage(name string) string { return fmt.Sprintf("Hello %s!", name) } ``` On disk it might look like this: ``` /my-app ├── encore.app // ... and other top-level project files │ ├── hello // hello service (a Go package) │   ├── migrations // hello service db migrations (directory) │   │ └── 1_create_table.up.sql // hello service db migration │   ├── foo // sub-package foo (directory) │   │ └── foo.go // foo code (cannot define APIs) │   ├── hello.go // hello service code │   └── hello_test.go // tests for hello service │ └── world // world service (a Go package) └── world.go // world service code ``` ## Large applications with several systems If you have a large application with several logical domains, each consisting of multiple services, it can be practical to separate these into distinct systems. Systems are not a special construct in Encore, they only help you divide your application logically around common concerns and purposes. Encore only handles services, the compiler will read your systems and extract the services of your application. As applications grow, systems help you decompose your application without requiring any complex refactoring. To create systems, create a sub-directory for each system and put the relevant service packages within it. This is all you need to do, since with Encore each service consists of a Go package. As an example, a company building a Trello app might divide their application into three systems: the **Trello** system (for the end-user facing app with boards and cards), the **User** system (for user and organization management), and the **Premium** system (for handling payments and subscriptions). On disk it might look like this: ``` /my-trello-clone ├── encore.app // ... and other top-level project files │ ├── trello // trello system (a directory) │   ├── board // board service (a Go package) │   │ └── board.go // board service code │   └── card // card service (a Go package) │ └── card.go // card service code │ ├── premium // premium system (a directory) │   ├── payment // payment service (a Go package) │   │ └── payment.go // payment service code │   └── subscription // subscription service (a Go package) │ └── subscription.go // subscription service code │ └── usr // usr system (a directory) ├── org // org service (a Go package) │   └── org.go // org service code └── user // user service (a Go package)    └── user.go // user service code ``` The only refactoring needed to divide an existing Encore application into systems is to move services into their respective subfolders. This is a simple way to separate the specific concerns of each system. What matters for Encore are the packages containing services, and the division in systems or subsystems will not change the endpoints or architecture of your application. ================================================ FILE: docs/go/primitives/caching.md ================================================ --- seotitle: Using caches in your microservices backend application seodesc: Learn how to implement caches to optimize response times and reduce cost in your microservices cloud backend. title: Caching subtitle: Optimize response times and reduce costs by avoiding re-work infobox: { title: "Caching", import: "encore.dev/storage/cache", } lang: go --- A cache is a high-speed storage layer, commonly used in distributed systems to improve user experiences by reducing latency, improving system performance, and avoiding expensive computation. For scalable systems you typically want to deploy the cache as a separate infrastructure resource, allowing you to run multiple instances of your application concurrently. Encore's built-in Caching API lets you use high-performance caches (using [Redis](https://redis.io/)) in a cloud-agnostic declarative fashion. At deployment, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra). ## Cache clusters To use caching in Encore, you must first define a *cache cluster*. Each cache cluster defined in your application will be provisioned as a separate Redis instance by Encore. This gives you fine-grained control over which service(s) should use the same cache cluster and which should have a separate one. It looks like this: ```go import "encore.dev/storage/cache" var MyCacheCluster = cache.NewCluster("my-cache-cluster", cache.ClusterConfig{ // EvictionPolicy tells Redis how to evict keys when the cache reaches // its memory limit. For typical cache use cases, cache.AllKeysLRU is a good default. EvictionPolicy: cache.AllKeysLRU, }) ``` When starting out it's recommended to use a single cache cluster that's shared between your different services. ## Keyspaces When using a cache, each cached item is stored at a particular key, which is typically an arbitrary string. If you use a cache cluster to cache different sets of data, it's important that distinct data set have non-overlapping keys. Each value stored in the cache also has a specific type, and certain cache operations can only be performed on certain types. For example, a common cache operation is to increment an integer value that is stored in the cache. If you try to apply this operation on a value that is not an integer, an error is returned. Encore provides a simple, type-safe solution to these problems through Keyspaces. In order to begin storing data in your cache, you must first define a Keyspace. Each keyspace has a Key type and a Value type. The Key type is much like a map key, in that it tells Encore where in the cache the item is stored. The Key type is combined with the Key Pattern to produce a string that is the Redis cache key. The Value type is the type of the values stored in that keyspace. For many keyspaces this is specified in the name of the constructor. For example, `NewIntKeyspace` stores `int64` values. For example, if you want to rate limit the number of requests per user ID it looks like this: ```go import ( "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/middleware" ) // RequestsPerUser tracks the number of requests per user. // The cache items expire after 10 seconds without activity. var RequestsPerUser = cache.NewIntKeyspace[auth.UID](cluster, cache.KeyspaceConfig{ KeyPattern: "requests/:key", DefaultExpiry: cache.ExpireIn(10 * time.Second), }) // RateLimitMiddleware is a global middleware that limits the number of authenticated requests // to 10 requests per 10 seconds. //encore:middleware target=all func RateLimitMiddleware(req middleware.Request, next middleware.Next) middleware.Response { if userID, ok := auth.UserID(); ok { val, err := RequestsPerUser.Increment(req.Context(), userID, 1) // NOTE: this "fails open", meaning if we can't communicate with the cache // we default to allowing the requests. // // Consider whether that's the correct behavior for your application, // or if you want to return an error to the user in that case. if err == nil && val > 10 { return middleware.Response{ Err: &errs.Error{Code: errs.ResourceExhausted, Message: "rate limit exceeded"}, } } } return next(req) } ``` As you can see, the `RequestsPerUser` defines a `KeyPattern` which is set to `"requests/:key"`. Here `:key` refers to the value of the Key type, which is the `auth.UID` value passed in. If you want the cache key to contain multiple values, you can define a struct type and pass that as the key. Then change the `KeyPattern` to specify the struct fields. For example: ```go type MyKey struct { UserID auth.UID ResourcePath string // the resource being accessed } // ResourceRequestsPerUser tracks the number of requests per user and resource. // The cache items expire after 10 seconds without activity. var ResourceRequestsPerUser = cache.NewIntKeyspace[MyKey](cluster, cache.KeyspaceConfig{ KeyPattern: "requests/:UserID/:ResourcePath", DefaultExpiry: cache.ExpireIn(10 * time.Second), }) // ... then: key := MyKey{UserID: "some-user-id", ResourcePath: "/foo"} ResourceRequestsPerUser.Increment(ctx, key, 1) ``` Encore ensures that all the struct fields are present in the `KeyPattern`, and that the placeholder values are all valid field names. That way the connection between the struct fields and the `KeyPattern` become compile-time type-safe as well. Also note that Encore ensures there are no conflicting `KeyPattern` definitions across each cache cluster. Each keyspace must define its own, non-conflicting `KeyPattern`. This way, you can feel safe that there won't be any accidental overwrites of cache values, even with multiple services sharing the same cache cluster. ## Keyspace operations Encore comes with a full suite of keyspace types, each with a wide variety of cache operations. Basic keyspace types include [strings](https://pkg.go.dev/encore.dev/storage/cache#NewStringKeyspace), [integers](https://pkg.go.dev/encore.dev/storage/cache#NewIntKeyspace), [floats](https://pkg.go.dev/encore.dev/storage/cache#NewFloatKeyspace), and [struct types](https://pkg.go.dev/encore.dev/storage/cache#NewStructKeyspace). These keyspaces all share the same set of methods (along with a few keyspace-specific ones). There are also more advanced keyspaces for storing [sets of basic types](https://pkg.go.dev/encore.dev/storage/cache#NewSetKeyspace) and [ordered lists of basic types](https://pkg.go.dev/encore.dev/storage/cache#NewListKeyspace). These keyspaces offer a different, specialized set of methods specific to set and list operations. For a list of the supported operations, see the [package documentation](https://pkg.go.dev/encore.dev/storage/cache). ## Testing When running tests, Encore spins up an in-memory cache separately for each test. This way you don't have to think about clearing the cache between tests, or worrying about whether one test affects another. Each test is automatically fully isolated. ## Local development For local development, Encore maintains a local, in-memory implementation of Redis. This implementation is designed to store a small amount of keys (currently 100). When the number of keys exceeds this value, keys are randomly purged to get below the limit. This is designed in order to simulate the ephemeral, transient nature of caches while also limiting memory use. The precise behavior for local development may change over time and should not be relied on. ================================================ FILE: docs/go/primitives/change-db-schema.md ================================================ --- seotitle: How to change your SQL database schema seodesc: Learn how to change your SQL database schema for your Go backend application, using migration files and Encore's built-in schema migration functionality. title: Change SQL database schema lang: go --- Encore database schemas are changed over time using *migration files*. Each migration file has a sequence number, and migration files are run in sequence when deploying. Encore tracks which migrations have already run and only runs new ones. To change your database schema, add a new migration file using the next available migration number. For example, if you have two migration files already, the next migration file should be named `3_something.up.sql` where `something` is a short description of what the migration does. Database migrations are applied before the application is restarted with the new code. Always make sure the old application code works with the new database schema, so that things don't break while your new code is being rolled out. ## Example Let's say you have a single migration file that creates a `todo_item` table: **`todo/migrations/1_create_table.up.sql`** ```sql CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL ); ``` And now you want to add a `created` column to track when each todo was created. Add a new file: **`todo/migrations/2_add_created_col.up.sql`** ```sql ALTER TABLE todo_item ADD created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(); ``` The next deploy Encore will notice the new migration file and run it, adding a new column. ================================================ FILE: docs/go/primitives/code-snippets.md ================================================ --- seotitle: Code snippets for using the Backend Framework's building blocks in your backend application seodesc: Learn how to build cloud-agnostic backend applications using Encore's Backend Framework. title: Code snippets subtitle: Shortcuts for building with Encore lang: go --- When you're familiar with how Encore works, you can simplify your development workflow by copy-pasting these examples. If you're looking for details on how Encore works, please refer to the relevant docs section. ## APIs ### Defining APIs ```go package hello // service name //encore:api public func Ping(ctx context.Context, params *PingParams) (*PingResponse, error) { msg := fmt.Sprintf("Hello, %s!", params.Name) return &PingResponse{Message: msg}, nil } ``` ### Defining Request and Response schemas ```go // PingParams is the request data for the Ping endpoint. type PingParams struct { Name string } // PingResponse is the response data for the Ping endpoint. type PingResponse struct { Message string } ``` ### Calling APIs ```go import "encore.app/hello" // import service //encore:api public func MyOtherAPI(ctx context.Context) error { resp, err := hello.Ping(ctx, &hello.PingParams{Name: "World"}) if err == nil { log.Println(resp.Message) // "Hello, World!" } return err } ``` **Hint:** Import the service package and call the API endpoint using a regular function call. ### Receive Webhooks ```go import "net/http" // Webhook receives incoming webhooks from Some Service That Sends Webhooks. //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { // ... operate on the raw HTTP request ... } ``` **Hint:** Like any other API endpoint, this will be exposed at:
`https://-.encr.app/service.Webhook` ## Databases ### Creating a SQL database To create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable. `sqldb.DatabaseConfig` specifies the directory containing the database migration files, which is how you define the database schema. ``` -- todo/db.go -- package todo // Create the todo database and assign it to the "tododb" variable var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Then, query the database using db.QueryRow, db.Exec, etc. -- todo/migrations/1_create_table.up.sql -- CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false -- etc... ); ``` ### Inserting data into a database One way of inserting data is with a helper function that uses the package function `sqldb.Exec`: ```go import "encore.dev/storage/sqldb" // insert inserts a todo item into the database. func insert(ctx context.Context, id, title string, done bool) error { _, err := tododb.Exec(ctx, ` INSERT INTO todo_item (id, title, done) VALUES ($1, $2, $3) `, id, title, done) return err } ``` ### Querying a database To read a single todo item in the example schema above, we can use `sqldb.QueryRow`: ```go import "encore.dev/storage/sqldb" var item struct { ID int64 Title string Done bool } err := tododb.QueryRow(ctx, ` SELECT id, title, done FROM todo_item LIMIT 1 `).Scan(&item.ID, &item.Title, &item.Done) ``` **Hint:** If `sqldb.QueryRow` does not find a matching row, it reports an error that can be checked against by importing the standard library `errors` package and calling `errors.Is(err, sqldb.ErrNoRows)`. ## Defining a Cron Job ```go import "encore.dev/cron" var _ = cron.NewJob("welcome-email", cron.JobConfig{ Title: "Send welcome emails", Every: 2 * cron.Hour, Endpoint: SendWelcomeEmail, }) //encore:api private func SendWelcomeEmail(ctx context.Context) error { // ... return nil } ``` **Hint:** Cron Jobs do not run in your local development environment. ## PubSub ### Creating a PubSub topic ```go import "encore.dev/pubsub" type SignupEvent struct { UserID int } var Signups = pubsub.NewTopic[*SignupEvent]("signups", pubsub.TopicConfig { DeliveryGuarantee: pubsub.AtLeastOnce, }) ``` **Hint:** Topics are declared as package level variables and cannot be created inside functions. Regardless of where you create a topic, it can be published and subscribed to from any service. ### Publishing an Event (Pub) ```go if _, err := Signups.Publish(ctx, &SignupEvent{UserID: id}); err != nil { return err } if err := tx.Commit(); err != nil { return err } ``` **Hint:** If you want to publish to the topic from another service, import the topic package variable (`Signups` in this example) and call publish on it from there. ### Subscribing to Events (Sub) Create a Subscription as a package level variable by calling `pubsub.NewSubscription`. ```go var _ = pubsub.NewSubscription( user.Signups, "send-welcome-email", pubsub.SubscriptionConfig[*SignupEvent] { Handler: SendWelcomeEmail, }, ) func SendWelcomeEmail(ctx context.Context, event *SignupEvent) error { ... send email ... return nil } ``` ## Defining a Cache cluster ```go import "encore.dev/storage/cache" var MyCacheCluster = cache.NewCluster("my-cache-cluster", cache.ClusterConfig{ // EvictionPolicy tells Redis how to evict keys when the cache reaches // its memory limit. For typical cache use cases, cache.AllKeysLRU is a good default. EvictionPolicy: cache.AllKeysLRU, }) ``` ## Secrets ### Defining Secrets ```go var secrets struct { GitHubAPIToken string // personal access token for deployments SomeOtherSecret string // some other secret } ``` **Hint:** The variable must be an unexported struct named `secrets`, and all the fields must be of type `string`. ### Setting secret values ```shell $ encore secret set --type ``` **Hint:** `` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. For each Secret, there can only be one secret value for each environment type. ### Using secrets ```go func callGitHub(ctx context.Context) { req, _ := http.NewRequestWithContext(ctx, "GET", "https:///api.github.com/user", nil) req.Header.Add("Authorization", "token " + secrets.GitHubAPIToken) resp, err := http.DefaultClient.Do(req) // ... handle err and resp } ``` **Hint:** Secret keys are globally unique for your whole application; if multiple services use the same secret name they both receive the same secret value at runtime. ================================================ FILE: docs/go/primitives/connect-existing-db.md ================================================ --- seotitle: How to integrate your Encore app with an existing database seodesc: Learn how to integrate your Encore Go backend application with an existing database, in any cloud you choose. title: Integrate with existing databases lang: go --- Encore automatically provision the necessary infrastructure when you create a service and add a database. However, you may want to connect to an existing database for migration or prototyping purposes. It's simple to integrate your Encore app with an existing database in these cases. ## Example Let's say you have an external database hosted by DigitalOcean that you would like to connect to. The simplest approach is to create a dedicated package that lazily instantiates a database connection pool. We can store the password using Encore's [secrets manager](/docs/go/primitives/secrets) to make it even easier. The connection string is something that looks like: ``` postgresql://user:password@externaldb-do-user-1234567-0.db.ondigitalocean.com:25010/externaldb?sslmode=require ``` So we write something like: **`pkg/externaldb/externaldb.go`** ```go package externaldb import ( "context" "fmt" "github.com/jackc/pgx/v4/pgxpool" "go4.org/syncutil" ) // Get returns a database connection pool to the external database. // It is lazily created on first use. func Get(ctx context.Context) (*pgxpool.Pool, error) { // Attempt to setup the database connection pool if it hasn't // already been successfully setup. err := once.Do(func() error { var err error pool, err = setup(ctx) return err }) return pool, err } var ( // once is like sync.Once except it re-arms itself on failure once syncutil.Once // pool is the successfully created database connection pool, // or nil when no such pool has been setup yet. pool *pgxpool.Pool ) var secrets struct { // ExternalDBPassword is the database password for authenticating // with the external database hosted on DigitalOcean. ExternalDBPassword string } // setup attempts to set up a database connection pool. func setup(ctx context.Context) (*pgxpool.Pool, error) { connString := fmt.Sprintf("postgresql://%s:%s@externaldb-do-user-1234567-0.db.ondigitalocean.com:25010/externaldb?sslmode=require", "user", secrets.ExternalDBPassword) return pgxpool.Connect(ctx, connString) } ``` Before running, remember to use `encore secrets set` to store the `ExternalDBPassword` to use. (But don't worry, Encore will remind you if you forget.) ## Other infrastructure The same pattern can easily be adapted to other infrastructure components that Encore doesn't yet provide built-in support for: - Horizontally scalable databases like Cassandra, DynamoDB, BigTable, and so on - Document or graph databases like MongoDB or Neo4j - Other cloud primitives like queues, object storage buckets, and more - Or really any cloud services or APIs you can think of In this way you can easily integrate Encore with anything you want. ================================================ FILE: docs/go/primitives/cron-jobs.md ================================================ --- seotitle: Create recurring tasks with Encore's Cron Jobs API seodesc: Learn how to create periodic and recurring tasks in your backend application using Encore's Cron Jobs API. title: Cron Jobs subtitle: Run recurring and scheduled tasks infobox: { title: "Cron Jobs", import: "encore.dev/cron", example_link: "/docs/tutorials/uptime" } lang: go --- When you need to run periodic and recurring tasks, Encore.go provides a declarative way of using Cron Jobs. When a Cron Job is defined in your application, Encore automatically calls your specified API according to the defined schedule. This eliminates the need for infrastructure maintenance, as Encore manages scheduling, monitoring, and execution of Cron Jobs. Cron Jobs do not run when developing locally or in [Preview Environments](/docs/platform/deploy/preview-environments), but you can always call the API manually to test the behavior. ## Defining a Cron Job To define a Cron Job, import the `encore.dev/cron` [package](https://pkg.go.dev/encore.dev/cron), and call the `cron.NewJob()` function and store it as a package-level variable. ### Example ```go import "encore.dev/cron" // Send a welcome email to everyone who signed up in the last two hours. var _ = cron.NewJob("welcome-email", cron.JobConfig{ Title: "Send welcome emails", Every: 2 * cron.Hour, Endpoint: SendWelcomeEmail, }) // SendWelcomeEmail emails everyone who signed up recently. // It's idempotent: it only sends a welcome email to each person once. //encore:api private func SendWelcomeEmail(ctx context.Context) error { // ... return nil } ``` The `"welcome-email"` argument to `cron.NewJob` is a unique ID you give to each Cron Job. If you later refactor the code and move the Cron Job definition to another package, we use this ID to keep track that it's the same Cron Job and not a different one. When this code gets deployed Encore will automatically register the Cron Job in Encore Cloud and begin calling the `SendWelcomeEmail` API every hour. The Encore Cloud dashboard provides a convenient user interface for monitoring and debugging Cron Job executions across all your environments via the `Cron Jobs` menu item: ![Cron Jobs UI](/assets/docs/cron.png) ## Keep in mind when using Cron Jobs - Cron Jobs do not execute during local development or in [Preview Environments](/docs/platform/deploy/preview-environments). However, you can manually invoke the API to test its behavior. - In Encore Cloud, Cron Job executions are limited to **once every hour**, with the exact minute randomized within that hour for users on the Free Tier. To enable more frequent executions or to specify the exact minute within the hour, consider [deploying to your own cloud](/docs/platform/deploy/own-cloud) or upgrading to the [Pro plan](/pricing). - Both public and private APIs are supported for Cron Jobs. - Ensure that the API endpoints used in Cron Jobs are idempotent, as they may be called multiple times under certain network conditions. - The API endpoints used in Cron Jobs must not take any request parameters. That is, their signatures must be `func(context.Context) error` or `func(context.Context) (*T, error)`. ## Cron schedules Above we used the `Every` field, which executes the Cron Job on a periodic basis. It runs around the clock each day, starting at midnight (UTC). In order to ensure a consistent delay between each run, the interval used **must divide 24 hours evenly**. For example, `10 * cron.Minute` and `6 * cron.Hour` are both allowed (since 24 hours is evenly divisible by both), whereas `7 * cron.Hour` is not (since 24 is not evenly divisible by 7). The Encore compiler will catch this and give you a helpful error at compile-time if you try to use an invalid interval. ### Cron expressions For more advanced use cases, such as running a Cron Job on a specific day of the month, or a specific week day, or similar, the `Every` field is not expressive enough. For these use cases, Encore provides full support for [Cron expressions](https://en.wikipedia.org/wiki/Cron) by using the `Schedule` field instead of the `Every` field. Cron expressions allow you to define precise schedules for your tasks, including specific days of the week, specific hours of the day, and more. Note that all times are expressed in UTC. For example: ```go // Run the monthly accounting sync job at 4am (UTC) on the 15th day of each month. var _ = cron.NewJob("accounting-sync", cron.JobConfig{ Title: "Cron Job Example", Schedule: "0 4 15 * *", Endpoint: AccountingSync, }) ``` ================================================ FILE: docs/go/primitives/database-extensions.md ================================================ --- seotitle: Pre-installed PostgreSQL extensions seodesc: See the list of pre-installed PostgreSQL extensions available when using Encore title: PostgreSQL Extensions subtitle: Pre-installed extensions infobox: { title: "SQL Databases", import: "encore.dev/storage/sqldb" } lang: go --- Encore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development, CI/CD, and for databases hosted on Encore Cloud. The docker image ships with the following PostgreSQL extensions pre-installed and available for use (via `CREATE EXTENSION`): | Extension | Version | Description | | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------- | | refint | 1.0 | functions for implementing referential integrity (obsolete) | | pg_buffercache | 1.3 | examine the shared buffer cache | | pg_freespacemap | 1.2 | examine the free space map (FSM) | | plpgsql | 1.0 | PL/pgSQL procedural language | | citext | 1.6 | data type for case-insensitive character strings | | adminpack | 2.1 | administrative functions for PostgreSQL | | moddatetime | 1.0 | functions for tracking last modification time | | amcheck | 1.3 | functions for verifying relation integrity | | seg | 1.4 | data type for representing line segments or floating-point intervals | | pg_stat_statements | 1.10 | track planning and execution statistics of all SQL statements executed | | pg_trgm | 1.6 | text similarity measurement and index searching based on trigrams | | isn | 1.2 | data types for international product numbering standards | | btree_gist | 1.7 | support for indexing common datatypes in GiST | | intarray | 1.5 | functions, operators, and index support for 1-D arrays of integers | | pg_surgery | 1.0 | extension to perform surgery on a damaged relation | | uuid-ossp | 1.1 | generate universally unique identifiers (UUIDs) | | insert_username | 1.0 | functions for tracking who changed a table | | bloom | 1.0 | bloom access method - signature file based index | | pgcrypto | 1.3 | cryptographic functions | | dblink | 1.2 | connect to other PostgreSQL databases from within a database | | tsm_system_rows | 1.0 | TABLESAMPLE method which accepts number of rows as a limit | | pg_prewarm | 1.2 | prewarm relation data | | old_snapshot | 1.0 | utilities in support of old_snapshot_threshold | | pageinspect | 1.11 | inspect the contents of database pages at a low level | | intagg | 1.1 | integer aggregator and enumerator (obsolete) | | pg_visibility | 1.2 | examine the visibility map (VM) and page-level visibility info | | cube | 1.5 | data type for multidimensional cubes | | tablefunc | 1.0 | functions that manipulate whole tables, including crosstab | | xml2 | 1.1 | XPath querying and XSLT | | fuzzystrmatch | 1.1 | determine similarities and distance between strings | | pg_walinspect | 1.0 | functions to inspect contents of PostgreSQL Write-Ahead Log | | btree_gin | 1.3 | support for indexing common datatypes in GIN | | sslinfo | 1.2 | information about SSL certificates | | tcn | 1.0 | Triggered change notifications | | hstore | 1.8 | data type for storing sets of (key, value) pairs | | dict_int | 1.0 | text search dictionary template for integers | | earthdistance | 1.1 | calculate great-circle distances on the surface of the Earth | | file_fdw | 1.0 | foreign-data wrapper for flat file access | | autoinc | 1.0 | functions for autoincrementing fields | | ltree | 1.2 | data type for hierarchical tree-like structures | | unaccent | 1.1 | text search dictionary that removes accents | | pgrowlocks | 1.2 | show row-level locking information | | tsm_system_time | 1.0 | TABLESAMPLE method which accepts time in milliseconds as a limit | | dict_xsyn | 1.0 | text search dictionary template for extended synonym processing | | pgstattuple | 1.5 | show tuple-level statistics | | postgres_fdw | 1.1 | foreign-data wrapper for remote PostgreSQL servers | | lo | 1.1 | Large Object maintenance | | postgis_sfcgal-3 | 3.4.2 | PostGIS SFCGAL functions | | address_standardizer_data_us-3 | 3.4.2 | Address Standardizer US dataset example | | address_standardizer-3 | 3.4.2 | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. | | postgis_topology-3 | 3.4.2 | PostGIS topology spatial types and functions | | postgis-3 | 3.4.2 | PostGIS geometry and geography spatial types and functions | | postgis_raster-3 | 3.4.2 | PostGIS raster types and functions | | postgis_tiger_geocoder-3 | 3.4.2 | PostGIS tiger geocoder and reverse geocoder | | vector | 0.7.0 | vector data type and ivfflat and hnsw access methods | | postgis | 3.4.2 | PostGIS geometry and geography spatial types and functions | | address_standardizer | 3.4.2 | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. | | postgis_topology | 3.4.2 | PostGIS topology spatial types and functions | | postgis_tiger_geocoder | 3.4.2 | PostGIS tiger geocoder and reverse geocoder | | address_standardizer_data_us | 3.4.2 | Address Standardizer US dataset example | | postgis_sfcgal | 3.4.2 | PostGIS SFCGAL functions | | postgis_raster | 3.4.2 | PostGIS raster types and functions | ================================================ FILE: docs/go/primitives/database-troubleshooting.md ================================================ --- seotitle: Troubleshooting SQL databases seodesc: Advice on troubleshooting SQL databases in Encore.go title: Troubleshooting Databases subtitle: Advice on troubleshooting SQL databases in Encore.go infobox: { title: "SQL Databases", import: "encore.dev/storage/sqldb" } lang: go --- When you run your application locally with `encore run`, Encore provisions local databases using [Docker](https://docker.com). If this fails with a database error, it can often be resolved by making sure you have Docker installed and running, or by restarting the Encore daemon using `encore daemon`. If this does not resolve the issue, here are steps to resolve common errors: ** Error: sqldb: unknown database ** This error is often caused by a problem with the initial migration file, such as incorrect naming or location. - Verify that you've [created the migration file](/docs/go/primitives/databases#defining-a-database-schema) correctly, then try `encore run` again. ** Error: could not connect to the database ** When you can't connect to the database in your local environment, there's likely an issue with Docker: - Make sure that you have [Docker](https://docker.com) installed and running, then try `encore run` again. - If this fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again. ** Error: Creating PostgreSQL database cluster Failed ** This means Encore was not able to create the database. Often this is due to a problem with Docker. - Check if you have permission to access Docker by running `docker images`. - Set the correct permissions with `sudo usermod -aG docker $USER` (Learn more in the [Docker documentation](https://docs.docker.com/engine/install/linux-postinstall/)) - Then log out and log back in so that your group membership is refreshed. ** Error: unable to save docker image ** This error is often caused by a problem with Docker. - Make sure that you have [Docker](https://docker.com) installed and running. - In Docker, open **Settings > Advanced** and make sure that the setting `Allow the default Docker socket to be used` is checked. - If it still fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again. ** Error: unable to add CA to cert pool ** This error is commonly caused by the presence of the file `$HOME/.postgresql/root.crt` on the filesystem. When this file is present the PostgreSQL client library will assume the database server has that root certificate, which will cause the above error. - Remove or rename the file, then try `encore run` again. ** Resetting databases ** If your local database is in a bad state (e.g. due to a incomplete migration or corrupt data), you can reset it by running: ```shell $ encore db reset ``` This drops and recreates the database, re-running all migrations from scratch. Use `--all` to reset all databases at once. ================================================ FILE: docs/go/primitives/databases.md ================================================ --- seotitle: Using SQL databases for your backend application seodesc: Learn how to use SQL databases for your backend application. See how to provision, migrate, and query PostgreSQL databases using Go and Encore. title: Using SQL databases subtitle: Provisioning, migrating, querying infobox: { title: "SQL Databases", import: "encore.dev/storage/sqldb", example_link: "/docs/tutorials/uptime" } lang: go --- Encore treats SQL databases as logical resources and natively supports **PostgreSQL** databases. ## Creating a database To create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable. Databases must be created from within an [Encore service](/docs/go/primitives/services). For example: ``` -- todo/db.go -- package todo // Create the todo database and assign it to the "tododb" variable var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Then, query the database using db.QueryRow, db.Exec, etc. -- todo/migrations/1_create_table.up.sql -- CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false -- etc... ); ``` As seen above, the `sqldb.DatabaseConfig` specifies the directory containing the database migration files, which is how you define the database schema. See the [Defining the database schema](#defining-the-database-schema) section below for more details. With this code in place, Encore will automatically create the database using [Docker](https://docker.com) when you run the command `encore run` in your local environment. Make sure Docker is installed and running on your machine before running `encore run`. If your application is already running when you define a new database, you will need to stop and restart `encore run`. This is necessary for Encore to create the new database using Docker. ## Database Migrations Encore automatically handles `up` migrations, while `down` migrations must be run manually. Each `up` migration runs sequentially, expressing changes in the database schema from the previous migration. ### Naming Conventions **File Name Format:** Migration files must start with a number followed by an underscore (`_`), and must increase sequentially. Each file name must end with `.up.sql`. **Examples:** - `1_first_migration.up.sql` - `2_second_migration.up.sql` - `3_migration_name.up.sql` You can also prefix migration files with leading zeroes for better ordering in the editor (e.g., `0001_migration.up.sql`). ### Defining the Database Schema The first migration typically defines the initial table structure. For instance, a `todo` service might create `todo/migrations/1_create_table.up.sql` with the following content: ```sql CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false ); ``` ### Migration File Structure Migration files are created in a `migrations` directory within an Encore service package. Each file is named `_.up.sql`, where `` is a sequence number for ordering and `` describes the migration. **Example Directory Structure:** ``` /my-app ├── encore.app // ... and other top-level project files │ └── todo // todo service (a Go package)    ├── migrations // todo service db migrations (directory)    │ ├── 1_create_table.up.sql // todo service db migration    │ └── 2_add_field.up.sql // todo service db migration    ├── todo.go // todo service code    └── todo_test.go // tests for todo service ``` ## Inserting data into databases Once you have created the database using `var mydb = sqldb.NewDatabase(...)` you can start inserting data into the database by calling methods on the `mydb` variable. The interface is similar to that of the Go standard library's `database/sql` package. Learn more in the [package docs](https://pkg.go.dev/encore.dev/storage/sqldb). One way of inserting data is with a helper function that uses the package function `sqldb.Exec`. For example, to insert a single todo item using the example schema above, we can use the following helper function `insert`: ``` -- todo/insert.go -- // insert inserts a todo item into the database. func insert(ctx context.Context, id, title string, done bool) error { _, err := tododb.Exec(ctx, ` INSERT INTO todo_item (id, title, done) VALUES ($1, $2, $3) `, id, title, done) return err } -- todo/db.go -- package todo // Create the todo database and assign it to the "tododb" variable var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Then, query the database using db.QueryRow, db.Exec, etc. -- todo/migrations/1_create_table.up.sql -- CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false -- etc... ); ``` ## Querying databases To query a database in your application, you similarly need to import `encore.dev/storage/sqldb` in your service package or sub-package. For example, to read a single todo item in the example schema above, we can use `sqldb.QueryRow`: ```go var item struct { ID int64 Title string Done bool } err := tododb.QueryRow(ctx, ` SELECT id, title, done FROM todo_item LIMIT 1 `).Scan(&item.ID, &item.Title, &item.Done) ``` If `QueryRow` does not find a matching row, it reports an error that can be checked against by importing the standard library `errors` package and calling `errors.Is(err, sqldb.ErrNoRows)`. Learn more in the [package docs](https://pkg.go.dev/encore.dev/storage/sqldb). ## Provisioning databases Encore automatically provisions databases to match what your application requires. When you [define a database](#creating-a-database), Encore will provision the database at your next deployment. Encore provisions databases in an appropriate way depending on the environment. When running locally, Encore creates a database cluster using [Docker](https://www.docker.com/). In the cloud, it depends on the [environment type](/docs/platform/deploy/environments#environment-types): - In `production` environments, the database is provisioned through the Managed SQL Database service offered by the chosen cloud provider. - In `development` environments, the database is provisioned as a Kubernetes deployment with a persistent disk attached. See exactly what is provisioned for each cloud provider, and each environment type, in the [infrastructure documentation](/docs/platform/infrastructure/infra). ## Connecting to databases It's often useful to be able to connect to the database from outside the backend application. For example for scripts, ad-hoc querying, or dumping data for analysis. Currently Encore does not expose user credentials for databases in the local environment or for environments on Encore Cloud. You can use a connection string to connect instead, see below. ### Using the Encore CLI Encore's CLI comes with built-in support for connecting to databases: * `encore db shell [--env=]` opens a [psql](https://www.postgresql.org/docs/current/app-psql.html) shell to the database named `` in the given environment. Leaving out `--env` defaults to the local development environment. `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with. * `encore db conn-uri [--env=]` outputs a connection string for the database named ``. When specifying a cloud environment, the connection string is temporary. Leaving out `--env` defaults to the local development environment. * `encore db proxy [--env=]` sets up a local proxy that forwards any incoming connection to the databases in the specified environment. Leaving out `--env` defaults to the local development environment. See `encore help db` for more information on database management commands. ### Using database user credentials For cloud environments on AWS/GCP you can view database user credentials (created by Encore when provisioning databases) via the Encore Cloud dashboard: * Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure** page for the appropriate environment, and locate the `USERS` section within the relevant **Database Cluster**. ## Handling migration errors When Encore applies database migrations, there's always a possibility the migrations don't apply cleanly. This can happen for many reasons: - There's a problem with the SQL syntax in the migration - You tried to add a `UNIQUE` constraint but the values in the table aren't actually unique - The existing database schema didn't look like you thought it did, so the database object you tried to change doesn't actually exist - ... and so on If that happens, Encore rolls back the migration. If it happens during a cloud deployment, the deployment is aborted. Once you fix the problem, re-run `encore run` (locally) or push the updated code (in the cloud) to try again. Encore tracks which migrations have been applied in the `schema_migrations` table: ```sql database=# \d schema_migrations Table "public.schema_migrations" Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- version | bigint | | not null | dirty | boolean | | not null | Indexes: "schema_migrations_pkey" PRIMARY KEY, btree (version) ``` The `version` column tracks which migration was last applied. If you wish to skip a migration or re-run a migration, change the value in this column. For example, to re-run the last migration, run `UPDATE schema_migrations SET version = version - 1;`. *Note that Encore does not use the `dirty` flag by default.* ================================================ FILE: docs/go/primitives/defining-apis.md ================================================ --- seotitle: Defining type-safe APIs with Encore.go seodesc: Learn how to create APIs for your cloud backend application using Go and Encore.go title: Defining Type-Safe APIs subtitle: Simplifying type-safe API development lang: go --- Encore.go enables you to create type-safe APIs from regular Go functions. To define an API, add the `//encore:api` annotation to a function in your code. This tells Encore that the function is an API endpoint and Encore will automatically generate the necessary boilerplate at compile-time. In the example below, we define the API endpoint `Ping`, in the `hello` service, which gets exposed as `hello.Ping`. ```go package hello // service name //encore:api public func Ping(ctx context.Context, params *PingParams) (*PingResponse, error) { msg := fmt.Sprintf("Hello, %s!", params.Name) return &PingResponse{Message: msg}, nil } ``` ## Access controls When you define an API, you have three options for how it can be accessed: * `//encore:api public` – defines a public API that anybody on the internet can call. * `//encore:api private` – defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs. * `//encore:api auth` – defines a public API that anybody can call, but requires valid authentication. You can optionally send in auth data to `public` and `private` APIs, in which case the auth handler will be used. When used for `private` APIs, they are still not accessible from the outside world. For more on defining APIs that require authentication, see the [authentication guide](/docs/go/develop/auth). ## API Schemas ### Request and response schemas In the example above we defined an API that uses request and response schemas. The request data is of type `PingParams` and the response data of type `PingResponse`. That means we need to define them like so: ```go package hello // service name // PingParams is the request data for the Ping endpoint. type PingParams struct { Name string } // PingResponse is the response data for the Ping endpoint. type PingResponse struct { Message string } // Ping is an API endpoint that responds with a simple response. // This is exposed as "hello.Ping". //encore:api public func Ping(ctx context.Context, params *PingParams) (*PingResponse, error) { msg := fmt.Sprintf("Hello, %s!", params.Name) return &PingResponse{Message: msg}, nil } ``` Request and response schemas are both optional. There are four different ways of defining an API: **Using both request and response data:**
`func Foo(ctx context.Context, p *Params) (*Response, error)` **Only returning a response:**
`func Foo(ctx context.Context) (*Response, error)` **With only request data:**
`func Foo(ctx context.Context, p *Params) error` **Without any request or response data:**
`func Foo(ctx context.Context) error` As you can see, two parts are always present: the `ctx context.Context` parameter and the `error` return value. The `ctx` parameter is used for *cancellation*. It lets you detect when the caller is no longer interested in the result, and lets you abort the request processing and save resources that nobody needs. [Learn more about contexts on the Go blog](https://blog.golang.org/context). The `error` return type is always required because APIs can always fail from the caller's perspective. Therefore even though our simple `Ping` API endpoint above never fails in its implementation, from the perspective of the caller perhaps the service is crashing or the network is down and the service cannot be reached. This approach is simple but very powerful. It lets Encore use [static analysis](/docs/go/concepts/application-model) to understand the request and response schemas of all your APIs, which enables Encore to automatically generate API documentation, type-safe API clients, and much more. ### Request and response data types Request and response data types are structs (or pointers to structs) with optional field tags, which Encore uses to encode API requests to HTTP messages. The same struct can be used for requests and responses, but the `query` tag is ignored when generating responses. All tags except `json` are ignored for nested tags, which means you can only define `header` and `query` parameters for root level fields. For example, this struct: ```go type NestedRequestResponse struct { Header string `header:"X-Header"`// this field will be read from the http header Query string `query:"query"`// this field will be read from the query string Body1 string `json:"body1"` Nested struct { Header2 string `header:"X-Header2"`// this field will be read from the body Query2 string `query:"query2"`// this field will be read from the body Body2 string `json:"body2"` } `json:"nested"` } ``` Would be unmarshalled from this request: ```output POST /example?query=a%20query HTTP/1.1 Content-Type: application/json X-Header: A header { "body1": "a body", "nested": { "Header2": "not a header", "Query2": "not a query", "body2": "a nested body" } } ``` And marshalled to this response: ```output HTTP/1.1 200 OK Content-Type: application/json X-Header: A header { "Query": "not a query", "body1": "a body", "nested": { "Header2": "not a header", "Query2": "not a query", "body2": "a nested body" } } ``` ### Path parameters Path parameters are specified by the `path` field in the `//encore:api` annotation. To specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path can be parsed as a wildcard parameter by using `*name` with a matching function parameter. ```go // GetBlogPost retrieves a blog post by id. //encore:api public method=GET path=/blog/:id/*path func GetBlogPost(ctx context.Context, id int, path string) (*BlogPost, error) { // Use id to query database... } ``` ### Fallback routes Encore supports defining fallback routes that will be called if no other endpoint matches the request, using the syntax `path=/!fallback`. This is often useful when migrating an existing backend service over to Encore, as it allows you to gradually migrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using a raw endpoint with a fallback route. For example: ```go //encore:service type Service struct { oldRouter *gin.Engine // existing HTTP router } // Route all requests to the existing HTTP router if no other endpoint matches. //encore:api public raw path=/!fallback func (s *Service) Fallback(w http.ResponseWriter, req *http.Request) { s.oldRouter.ServeHTTP(w, req) } ``` ### Headers Headers are defined by the `header` field tag, which can be used in both request and response data types. The tag name is used to translate between the struct field and http headers. In the example below, the `Language` field of `ListBlogPost` will be fetched from the `Accept-Language` HTTP header. ```go type ListBlogPost struct { Language string `header:"Accept-Language"` Author string // Not a header } ``` ### Cookies Cookies can be set in the response by using the `header` tag with the `Set-Cookie` header name. ```go type LoginResponse struct { SessionID string `header:"Set-Cookie"` } //encore:api public method=POST path=/login func Login(ctx context.Context) (*LoginResponse, error) { return &LoginResponse{SessionID: "session=123"}, nil } ```` The cookies can then be read using e.g. [structured auth data](/docs/go/develop/auth#accepting-structured-auth-information). ### Query parameters For `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default. The query parameter name defaults to the [snake-case](https://en.wikipedia.org/wiki/Snake_case) encoded name of the corresponding struct field (e.g. BlogPost becomes blog_post). The `query` field tag can be used to parse a field from the query string for other HTTP methods (e.g. POST) and to override the default parameter name. Query strings are not supported in HTTP responses and therefore `query` tags in response types are ignored. In the example below, the `PageLimit` field will be read from the `limit` query parameter, whereas the `Author` field will be parsed from the query string (as `author`) only if the method of the request is `GET`, `HEAD` or `DELETE`. ```go type ListBlogPost struct { PageLimit int `query:"limit"` // always a query parameter Author string // query if GET, HEAD or DELETE, otherwise body parameter } ``` When fetching data with `GET` endpoints, it's common to receive additional parameters for optional behavior, like filtering a list or changing the sort order. When you use a struct type as the last argument in the function signature, Encore automatically parses these fields from the HTTP query string (for the `GET`, `HEAD`, and `DELETE` methods). For example, if you want to have a `ListBlogPosts` endpoint: ```go type ListParams struct { Limit uint // number of blog posts to return Offset uint // number of blog posts to skip, for pagination } type ListResponse struct { Posts []*BlogPost } //encore:api public method=GET path=/blog func ListBlogPosts(ctx context.Context, opts *ListParams) (*ListResponse, error) { // Use limit and offset to query database... } ``` This could then be queried as `/blog?limit=10&offset=20`. Query parameters are more limited than structured JSON data, and can only consist of basic types (`string`, `bool`, integer and floating point numbers), [Encore's UUID types](https://pkg.go.dev/encore.dev/types/uuid#UUID), and slices of those types. ### Body parameters Encore will default to reading request parameters from the body (as JSON) for all HTTP methods except `GET`, `HEAD` or `DELETE`. The name of the body parameter defaults to the field name, but can be overridden by the `json` tag. Response fields will be serialized as JSON in the HTTP body unless the `header` tag is set. There is no tag to force a field to be read from the body, as some infrastructure entities do not support body content in `GET`, `HEAD` or `DELETE` requests. ```go type CreateBlogPost struct { Subject string `json:"limit"` // query if GET, HEAD or DELETE, otherwise body parameter Author string // query if GET, HEAD or DELETE, otherwise body parameter } ``` ### Optional types Encore supports optional types using the `option.Option[T]` type from the `encore.dev/types/option` package. This can be used in request and response schemas to indicate that the value is not always set. See the [package documentation](https://pkg.go.dev/encore.dev/types/option) for more information on usage. ### Supported types The table below lists the data types supported by each HTTP message location. | Type | Header | Path | Query | Body | | ---------------- | ------ | ---- | ----- | ---- | | bool | X | X | X | X | | numeric | X | X | X | X | | string | X | X | X | X | | time.Time | X | X | X | X | | uuid.UUID | X | X | X | X | | json.RawMessage | X | X | X | X | | option.Option[T] | X | | X | X | | pointer | X | | X | X | | list | X | | X | X | | struct | | | | X | | map | | | | X | ## Sensitive data Encore.go comes with built-in tracing functionality that automatically captures request and response payloads to simplify debugging. While helpful, that's not always desirable. For instance when a request or response payload contains sensitive data, such as API keys or personally identifiable information (PII). For those use cases Encore supports marking a field as sensitive using the struct tag `encore:"sensitive"`. Encore's tracing system will automatically redact fields tagged as sensitive. This works for both individual values as well as nested fields. Note that inputs to [auth handlers](/docs/go/develop/auth) are automatically marked as sensitive and are always redacted. Raw endpoints lack a schema, which means there's no way to add a struct tag to mark certain data as sensitive. For this reason Encore supports tagging the whole API endpoint as sensitive by adding `sensitive` to the `//encore:api` annotation. This will cause the whole request and response payload to be redacted, including all request and response headers. The `encore:"sensitive"` tag is ignored for local development environments to make development and debugging with the Local Development Dashboard easier. ### Example ```go package blog // service name import ( "time" "encore.dev/types/uuid" ) type Updates struct { Author string `json:"author,omitempty"` PublishTime time.Time `json:"publish_time,omitempty"` } // BatchUpdateParams is the request data for the BatchUpdate endpoint. type BatchUpdateParams struct { Requester string `header:"X-Requester"` RequestTime time.Time `header:"X-Request-Time"` CurrentAuthor string `query:"author"` Updates *Updates `json:"updates"` MySecretKey string `encore:"sensitive"` } // BatchUpdateResponse is the response data for the BatchUpdate endpoint. type BatchUpdateResponse struct { ServedBy string `header:"X-Served-By"` UpdatedIDs []uuid.UUID `json:"updated_ids"` } //encore:api public method=POST path=/section/:sectionID/posts func BatchUpdate(ctx context.Context, sectionID string, params *BatchUpdateParams) (*BatchUpdateResponse, error) { // Update blog posts for section return &BatchUpdateResponse{ServedBy: hostname, UpdatedIDs: ids}, nil } ``` ## REST APIs Encore has support for RESTful APIs and lets you easily define resource-oriented API URLs, parse parameters out of them, and more. To create a REST API, start by defining an endpoint and specify the `method` and `path` fields in the `//encore:api` comment. To specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter. For example, if you want to have a `GetBlogPost` endpoint that takes a numeric id as a parameter: ```go // GetBlogPost retrieves a blog post by id. //encore:api public method=GET path=/blog/:id func GetBlogPost(ctx context.Context, id int) (*BlogPost, error) { // Use id to query database... } ``` You can also combine path parameters with body payloads. For example, if you want to have an `UpdateBlogPost` endpoint: ```go // UpdateBlogPost updates an existing blog post by id. //encore:api public method=PUT path=/blog/:id func UpdateBlogPost(ctx context.Context, id int, post *BlogPost) error { // Use `post` to update the blog post with the given id. } ``` You cannot define paths that conflict with each other, including paths where the static part can be mistaken for a parameter, e.g both `/blog` and `/blog/:id` would conflict with `/:username`. As a rule of thumb, try to place path parameters at the end of the path and prefix them with the service name, e.g: ``` GET /blog/posts GET /blog/posts/:id GET /user/profile/:username GET /user/me ``` ## Custom HTTP status codes By default, Encore automatically sets appropriate HTTP status codes for your API responses. We recommend using these default status codes, but there are situations where you might need to set a custom HTTP status code, such as when porting an existing API that clients depend on for specific status codes. To set a custom HTTP status code, use the `encore:"httpstatus"` struct tag on a field in your response type: ```go type Response struct { Message string `json:"message"` Status int `encore:"httpstatus"` } //encore:api public method=GET path=/example func Example(ctx context.Context) (*Response, error) { return &Response{ Message: "Hello", Status: 201, // HTTP 201 Created }, nil } ``` The field with the `encore:"httpstatus"` tag can be an integer type and should contain a valid HTTP status code value. ================================================ FILE: docs/go/primitives/insert-test-data-db.md ================================================ --- seotitle: How to insert test data in a database seodesc: Learn how to populate your database with test data using Go and Encore, making testing your backend application much simpler. title: Insert test data in a database lang: go --- When you're developing or testing, it's often useful to seed databases with test data. This can be done is several ways depending on your use case. ## Using go:embed A straightforward way to insert test data is to conditionally insert it on startup using `go:embed` in combination with Encore's [metadata API](/docs/go/develop/metadata) control in which environments the data gets inserted. E.g. only in your local environment. ### Example Create a file with your test data named `fixtures.sql`. Then, for the service where you want to insert test data, add the following to its `.go` file in order to run on startup. ``` import ( _ "embed" "log" "encore.dev" ) //go:embed fixtures.sql var fixtures string func init() { if encore.Meta().Environment.Cloud == encore.CloudLocal { if _, err := sqldb.Exec(context.Background(), fixtures); err != nil { log.Fatalln("unable to add fixtures:", err) } } } ``` Not included in the above example is preventing adding duplicate data. This is straightforward to do by making the fixtures idempotent, or by tracking it with a database table. ## Populating databases in Encore Cloud's Preview Environments If you are using Encore Cloud's Preview Environment, it can sometimes be useful to populate new Preview Environments with test data to simplify testing. The best way to do this depends a bit on your use case, but a common way to do this is by using Encore's [webhooks](/docs/platform/integrations/webhooks) functionality, which provides notifications for when a deployment is completed and includes information about the environment in question. ================================================ FILE: docs/go/primitives/object-storage.md ================================================ --- seotitle: Using Object Storage in your backend application seodesc: Learn how you can use Object Storage to store files and unstructured data in your backend application. title: Object Storage subtitle: Simple and scalable storage APIs for files and unstructured data infobox: { title: "Object Storage", import: "encore.dev/storage/objects", } lang: go --- Object Storage is a simple and scalable solution to store files and unstructured data in your backend application. The most common implementation is Amazon S3 ("Simple Storage Service") and its semantics are universally supported by every major cloud provider. Encore.go provides a cloud-agnostic API for working with Object Storage, allowing you to store and retrieve files with ease. It has support for Amazon S3, Google Cloud Storage, as well as any other S3-compatible implementation (such as DigitalOcean Spaces, MinIO, etc.). Additionally, when you use Encore's Object Storage API you also automatically get: * Automatic tracing and instrumentation of all Object Storage operations * Built-in local development support, storing objects on the local filesystem * Support for integration testing, using a local, in-memory storage backend ## Creating a Bucket The core of Object Storage is the **Bucket**, which represents a collection of files. In Encore, buckets must be declared as package level variables, and cannot be created inside functions. Regardless of where you create a bucket, it can be accessed from any service by referencing the variable it's assigned to. When creating a bucket you can configure additional properties, like whether the objects in the bucket should be versioned. See the complete specification in the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#NewBucket). For example, to create a bucket for storing profile pictures: ```go package user import "encore.dev/storage/objects" var ProfilePictures = objects.NewBucket("profile-pictures", objects.BucketConfig{ Versioned: false, }) ``` ## Uploading files To upload a file to a bucket, use the `Upload` method on the bucket variable. It returns a writer that you can use to write the contents of the file. To complete the upload, call the `Close` method on the writer. To abort the upload, either cancel the context or call the `Abort` method on the writer. The `Upload` method additionally takes a set of options to configure the upload, like setting attributes (`objects.WithUploadAttrs`) or to reject the upload if the object already exists (`objects.WithPreconditions`). See the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.Upload) for more details. ```go package user import ( "context" "io" "net/http" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/storage/objects" ) var ProfilePictures = objects.NewBucket("profile-pictures", objects.BucketConfig{}) //encore:api auth raw method=POST path=/upload-profile-picture func UploadProfilePicture(w http.ResponseWriter, req *http.Request) { // Store the user's profile picture with their user id as the key. userID, _ := auth.UserID() key := string(userID) // We store the profile writer := ProfilePictures.Upload(req.Context(), key) _, err := io.Copy(writer, req.Body) if err != nil { // If something went wrong with copying data, abort the upload and return an error. writer.Abort() errs.HTTPError(w, err) return } if err := writer.Close(); err != nil { errs.HTTPError(w, err) return } // All good! Return a 200 OK. w.WriteHeader(http.StatusOK) } ``` ## Downloading files To download a file from a bucket, use the `Download` method on the bucket variable. It returns a reader that you can use to read the contents of the file. The `Download` method additionally takes a set of options to configure the download, like downloading a specific version if the bucket is versioned (`objects.WithVersion`). See the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.Download) for more details. For example, to download the user's profile picture and serve it: ```go package user import ( "context" "io" "net/http" "encore.dev" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/storage/objects" ) var ProfilePictures = objects.NewBucket("profile-pictures", objects.BucketConfig{}) //encore:api public raw method=GET path=/profile-picture/:userID func ServeProfilePicture(w http.ResponseWriter, req *http.Request) { userID := encore.CurrentRequest().PathParams.Get("userID") reader := ProfilePictures.Download(req.Context(), userID) // Did we encounter an error? if err := reader.Err(); err != nil { errs.HTTPError(w, err) return } // Assuming all images are JPEGs. w.Header().Set("Content-Type", "image/jpeg") io.Copy(w, reader) } ``` ## Listing objects To list objects in a bucket, use the `List` method on the bucket variable. It returns an iterator of `(error, *objects.ListEntry)` pairs that you can use to easily iterate over the objects in the bucket using a `range` loop. For example, to list all profile pictures: ```go for err, entry := range ProfilePictures.List(ctx, &objects.Query{}) { if err != nil { // Handle error } // Do something with entry } ``` The `*objects.Query` type can be used to limit the number of objects returned, or to filter them to a specific key prefix. See the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.List) for more details. ## Deleting objects To delete an object from a bucket, use the `Remove` method on the bucket variable. For example, to delete a profile picture: ```go err := ProfilePictures.Remove(ctx, "my-user-id") if err != nil && !errors.Is(err, objects.ErrObjectNotFound) { // Handle error } ``` ## Retrieving object attributes You can retrieve information about an object using the `Attrs` method on the bucket variable. It returns the attributes of the object, like its size, content type, and ETag. For example, to get the attributes of a profile picture: ```go attrs, err := ProfilePictures.Attrs(ctx, "my-user-id") if errors.Is(err, objects.ErrObjectNotFound) { // Object not found } else if err != nil { // Some other error } // Do something with attrs ``` For convenience there is also `Exists` which returns a boolean indicating whether the object exists. ```go exists, err := ProfilePictures.Exists(ctx, "my-user-id") if err != nil { // Handle error } else if !exists { // Object does not exist } ``` ## Using Public Buckets Encore supports creating public buckets where objects can be accessed directly via HTTP/HTTPS without authentication. This is useful for serving static assets like images, videos, or other public files. To create a public bucket, set `Public: true` in the `BucketConfig`: ```go var PublicAssets = objects.NewBucket("public-assets", objects.BucketConfig{ Public: true, }) ``` Once configured as public, you can get the public URL for any object using the `PublicURL` method: ```go // Get the public URL for an object url := PublicAssets.PublicURL("path/to/image.jpg") // The URL can be used directly or shared publicly fmt.Println(url) // e.g. https://assets.example.com/path/to/image.jpg ``` When self-hosting, see how to configure public buckets in the [infrastructure configuration docs](/docs/ts/self-host/configure-infra). When deploying with Encore Cloud it will automatically configure the bucket to be publicly accessible and [configure CDN](/docs/platform/infrastructure/infra#production-infrastructure) for optimal content delivery. ### Using bucket references Encore uses static analysis to determine which services are accessing each bucket, and what operations each service is performing. That information is used to provision infrastructure correctly, render architecture diagrams, and configure IAM permissions. This means that `*objects.Bucket` variables can't be passed around however you'd like, as it makes static analysis impossible in many cases. To work around these restrictions Encore allows you to get a "reference" to a bucket that can be passed around any way you want by calling `objects.BucketRef`. To ensure Encore still is aware of which permissions each service needs, the call to `objects.BucketRef` must be made from within a service. Additionally, it must pre-declare the permissions it needs; those permissions are then assumed to be used by the service. It looks like this (using the `ProfilePictures` topic above): ```go ref := objects.BucketRef[objects.Downloader](ProfilePictures) // ref is of type objects.Downloader, which allows downloading. ``` Encore provides permission interfaces for each operation that can be performed on a bucket: * `objects.Downloader` for downloading objects * `objects.Uploader` for uploading objects * `objects.Lister` for listing objects * `objects.Attrser` for getting object attributes * `objects.Remover` for removing objects * `objects.SignedDownloader` for generating signed download URLs for objects * `objects.SignedUploader` for generating signed upload URLs for objects If you need multiple permissions they can be combined by creating an interface that embeds the permissions you need. ```go type myPerms interface { objects.Downloader objects.Uploader } ref := objects.BucketRef[myPerms](ProfilePictures) ``` For convenience Encore provides an `objects.ReadWriter` interface that gives complete read-write access with all the permissions above. See the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#BucketRef) for more details. ## Signed Upload URLs You can use `SignedUploadURL` to create signed URLs to allow clients to upload content directly into the bucket over the internet. The URL is always restricted to one filename, and has a set expiration date. Anyone in possession of the URL can upload data under this filename without any additional authentication. ```go url, err := ProfilePictures.SignedUploadURL(ctx, "my-user-id", objects.WithTTL(time.Duration(7200)*time.Second)) // Pass url to client ``` The client can now `PUT` to this URL with the content as a binary payload. ```bash curl -X PUT --data-binary @/home/me/dog-wizard.jpeg "https://storage.googleapis.com/profile-pictures/my-user-id/?x-goog-signature=b7a1<...>" ``` ### Why signed upload URLs? Signed URLs are an alternative to accepting the content payload directly in your API. Content upload requests are sometimes inconvenient to handle well: they can be long running and very large. With signed URLs, the content flows directly into the storage bucket, and only object IDs and metadata go through your API service. The trade-off is that the upload flow becomes more complex from a client point of view. ## Signed Download URLs You can use `SignedDownloadURL` to create signed URLs to allow clients to download content directly from the bucket, even if it's private. The URL is always restricted to one filename, and has a set expiration date. Anyone in possession of the URL can download the file without any additional authentication. ```go url, err := Documents.SignedDownloadURL(ctx, "letter-1234", objects.WithTTL(time.Duration(7200)*time.Second)) // Pass url to client ``` ### Why signed download URLs? Similar to the upload case, signed download URLs is a way to avoid handing large files or bulk traffic through your API. With signed URLs, the content flows directly from the storage bucket, and only object IDs and metadata go through your API service. Note: unless the content is private, prefer serving urls with `PublicURL()` over signed URLs. Public URLs go over CDN, which is typically significantly more performant and cost effective. ================================================ FILE: docs/go/primitives/pubsub.md ================================================ --- seotitle: Using PubSub in your backend application seodesc: Learn how you can use PubSub as an asynchronous message queue in your backend application, a great approach for decoupling services for better reliability. title: Pub/Sub subtitle: Decoupling services and building asynchronous systems infobox: { title: "Pub/Sub Messaging", import: "encore.dev/pubsub", example_link: "/docs/tutorials/uptime" } lang: go --- Publishers & Subscribers (Pub/Sub) let you build systems that communicate by broadcasting events asynchronously. This is a great way to decouple services for better reliability and responsiveness. Encore's Backend Framework lets you use Pub/Sub in a cloud-agnostic declarative fashion. At deployment, Encore automatically [provisions the required infrastructure](/docs/platform/infrastructure/infra). ## Creating a Topic The core of Pub/Sub is the **Topic**, a named channel on which you publish events. Topics must be declared as package level variables, and cannot be created inside functions. Regardless of where you create a topic, it can be published to from any service, and subscribed to from any service. When creating a topic, it must be given an event type, a unique name, and a configuration to define its behaviour. See the complete specification in the [package documentation](https://pkg.go.dev/encore.dev/pubsub#NewTopic). For example, to create a topic with events about user signups: ```go package user import "encore.dev/pubsub" type SignupEvent struct{ UserID int } var Signups = pubsub.NewTopic[*SignupEvent]("signups", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) ``` ### At-least-once delivery The above example configures the topic to ensure that, for each subscription, events will be delivered _at least once_. This means that if the topic believes the event was not processed, it will attempt to deliver the message again. **Therefore, all subscription handlers should be [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning).** This helps ensure that if the handler is called two or more times, from the outside there's no difference compared to calling it once. This can be achieved using a database to track if you have already performed the action that the event is meant to trigger, or ensuring that the action being performed is also idempotent in nature. ### Exactly-once delivery Topics can also be configured to deliver events _exactly once_ by setting the `DeliveryGuarantee` field to `pubsub.ExactlyOnce`. This enables stronger guarantees on the infrastructure level to minimize the likelihood of message re-delivery. However, there are still some rare circumstances when a message might be redelivered. For example, if a networking issue causes the acknowledgement of successful processing the message to be lost before the cloud provider receives it (the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)). As such, if correctness is critical under all circumstances, it's still advisable to design your subscription handlers to be idempotent. By enabling exactly-once delivery on a topic the cloud provider enforces certain throughput limitations: - AWS: 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)). - GCP: At least 3,000 messages per second across all topics in the region (can be higher on the region see [GCP PubSub Quotas](https://cloud.google.com/pubsub/quotas#quotas)). Exactly-once delivery does not perform message deduplication on the publishing side. If `Publish` is called twice with the same message, the message will be delivered twice. ### Ordered Topics Topics are unordered by default, meaning that messages can be delivered in any order. This allows for better throughput on the topic as messages can be processed in parallel. However, in some cases, messages must be delivered in the order they were published for a given entity. To create an ordered topic, configure the topic's `OrderingAttribute` to match the `pubsub-attr` tag on one of the top-level fields of the event type. This field ensures that messages delivered to the same subscriber are delivered in the order of publishing for that specific field value. Messages with a different value on the ordering attribute are delivered in an unspecified order. To maintain topic order, messages with the same ordering key aren't delivered until the earliest message is processed or dead-lettered, potentially causing delays due to [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking). Mitigate processing issues by ensuring robust logging and alerts, and appropriate subscription retry policies. The `OrderingAttribute` currently has no effect in local environments. #### Throughput limitations Each cloud provider enforces certain throughput limitations for ordered topics: - **AWS:** 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)) - **GCP:** 1 MBps for each ordering key (See [GCP Pub/Sub Resource Limits](https://cloud.google.com/pubsub/quotas#resource_limits)) #### Ordered topic example ```go package example import ( "context" "encore.dev/pubsub" ) type CartEvent struct { ShoppingCartID int `pubsub-attr:"cart_id"` Event string } var CartEvents = pubsub.NewTopic[*CartEvent]("cart-events", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, OrderingAttribute: "cart_id", }) func Example(ctx context.Context) error { // These are delivered in order as they all have the same shopping cart ID CartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: "item_added"}) CartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: "checkout_started"}) CartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: "checkout_completed"}) // This event may be delivered at any point as it has a different shopping cart ID CartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 2, Event: "item_added"}) } ``` ## Publishing events To publish an **Event**, call `Publish` on the topic passing in the event object (which is the type specified in the `pubsub.NewTopic[Type]` constructor). For example: ```go messageID, err := Signups.Publish(ctx, &SignupEvent{UserID: id}) if err != nil { return err } // If we get here the event has been successfully published, // and all registered subscribers will receive the event. // The messageID variable contains the unique id of the message, // which is also provided to the subscribers when processing the event. ``` By defining the `Signups` topic variable as an exported variable you can also publish to the topic from other services in the same way. ### Using topic references Encore uses static analysis to determine which services are publishing messages to what topics. That information is used to provision infrastructure correctly, render architecture diagrams, and configure IAM permissions. This means that `*pubsub.Topic` variables can't be passed around however you'd like, as it makes static analysis impossible in many cases. To work around these restrictions Encore allows you to get a reference to a topic that can be passed around any way you want. It looks like this (using the `Signups` topic above): ```go signupRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](Signups) // signupRef is of type pubsub.Publisher[*SignupEvent], which allows publishing. ``` The difference between a **TopicRef** and a **Topic** is that topic references need to pre-declare what permissions are needed. Encore then assumes that all the permissions you declare are used. For example, if you declare a **TopicRef** with the `pubsub.Publisher` permission (as seen above) Encore assumes that the service will publish messages to the topic and provisions the infrastructure to support that. Note that a **TopicRef** must be declared _within a service_, but the reference itself can be freely passed around to library code, be dependency injected into [service structs](/docs/go/how-to/dependency-injection), and so on. ## Subscribing to Events To **Subscribe** to events, you create a Subscription as a package level variable by calling the [`pubsub.NewSubscription`](https://pkg.go.dev/encore.dev/pubsub#NewSubscription) function. Each subscription needs: - the topic to subscribe to - a name which is unique for the topic - a configuration object with at least a `Handler` function to process the events - a configuration object Here's an example of how you create a subscription to a topic: ```go package email import ( "encore.dev/pubsub" "user" ) var _ = pubsub.NewSubscription( user.Signups, "send-welcome-email", pubsub.SubscriptionConfig[*SignupEvent]{ Handler: SendWelcomeEmail, }, ) func SendWelcomeEmail(ctx context.Context, event *SignupEvent) error { // send email... return nil } ``` Subscriptions can be in the same service as the topic is declared, or in any other service of your application. Each subscription to a single topic receives the events independently of any other subscriptions to the same topic. This means that if one subscription is running very slowly, it will grow a backlog of unprocessed events. However, any other subscriptions will still be processing events in real-time as they are published. The `ctx` passed to the handler function is cancelled when the `AckDeadline` for the subscription is reached. This is the time when the message is considered to have timed out and can be redelivered to another subscriber. The timeout defaults to 30 seconds if you don't explicitly configure `AckDeadline`. ### Method-based handlers When using [service structs](/docs/go/primitives/service-structs) for dependency injection it's common to want to define the subscription handler as a method on the service struct, to be able to access the injected dependencies. The pubsub package provides the `pubsub.MethodHandler` function for this purpose: ```go //encore:service type Service struct { /* ... */ } func (s *Service) SendWelcomeEmail(ctx context.Context, event *SignupEvent) error { // ... } var _ = pubsub.NewSubscription( user.Signups, "send-welcome-email", pubsub.SubscriptionConfig[*SignupEvent]{ Handler: pubsub.MethodHandler((*Service).SendWelcomeEmail), }, ) ``` Note that `pubsub.MethodHandler` only allows referencing methods on the service struct type, not any other type. ### Subscription configuration When creating a subscription you can configure behavior such as message retention and retry policy, using the `SubscriptionConfig` type. See the [package documentation](https://pkg.go.dev/encore.dev/pubsub#SubscriptionConfig) for the complete configuration options. The `SubscriptionConfig` struct fields must be defined as compile-time constants, and cannot be defined in terms of function calls. This is necessary for Encore to understand the exact requirements of the subscription, in order to provision the correct infrastructure upon deployment. ### Error Handling If a subscription function returns an error, the event being processed will be retried, based on the retry policy [configured on that subscription](https://pkg.go.dev/encore.dev/pubsub#SubscriptionConfig). After the `MaxRetries` is hit, the event will be placed into a dead-letter queue (DLQ) for that subscriber. This allows the subscription to continue processing events until the bug which caused the event to fail can be fixed. Once fixed, the messages on the dead-letter queue can be manually released to be processed again by the subscriber. ## Testing Pub/Sub Encore uses a special testing implementation of Pub/Sub topics. When running tests, topics are aware of which test is running. This gives you the following guarantees: - Your subscriptions will not be triggered by events published. This allows you to test the behaviour of publishers independently of side effects caused by subscribers. - Message ID's generated on publish are deterministic (based on the order of publishing), thus your assertions can make use of that fact. - Each test is isolated from other tests, meaning that events published in one test will not impact other tests (even if you use parallel testing). Encore provides a helper function, [`et.Topic`](https://pkg.go.dev/encore.dev/et#Topic), to access the testing topic. You can use this object to extract the events that have been published to it during a test. Here's an example implementation: ```go package user import ( "testing" "encore.dev/et" "github.com/stretchr/testify/assert" ) func Test_Register(t *testing.T) { t.Parallel() ... Call Register() and assert changes to the database ... // Get all published messages on the Signups topic from this test. msgs := et.Topic(Signups).PublishedMessages() assert.Len(t, msgs, 1) } ``` ## Ensuring consistency between services Ensuring consistency between services in event-driven applications can be challenging, especially when database writes and Pub/Sub publishing are not transactional. This can lead to inconsistencies between services. To address this issue without adding excessive complexity, consider using a transactional outbox pattern. For more information on implementing this pattern with Encore, see the [Pub/Sub Outbox guide](/docs/primitives/pubsub-outbox). ## The benefits of Pub/Sub Pub/Sub is a powerful building block in a backend application. It can be used to improve app reliability by reducing the blast radius of faulty components and bottlenecks. It can also be used to increase the speed of response to the user, and even helps reduce cognitive overhead for developers by inverting the dependencies between services. For those not familiar with Pub/Sub, lets take a look at an example API in a user registration service. The behavior we want to implement is that upon registration, we send a welcome email to the user and create a record of the signup in our analytics system. Now let's see how we could implement this only using APIs, compared to how a Pub/Sub implementation might look. ### An API only approach Using API calls between services, we might design a system which looks like this when the user registers:
1. The `user` service starts a database transaction and records the user in its database. 2. The `user` service makes a call to the `email` service to send a welcome email. 3. The `email` service then calls an email provider to actually send the email. 4. Upon success, the `email` service replies to the `user` service that the request was processed. 5. The `user` service then calls the `analytics` service to record the signup. 6. The `analytics` service the writes to the data warehouse to record the information. 7. The `analytics` service then replies to the `user` service that the request was processed. 8. The `user` service commits the database transaction. 9. The `user` service then can reply to the user to say the registration was successful.
Notice how we have to wait for everything to complete before we can reply to the user to tell then we've registered them. This means that if our email provider takes 3 seconds to send the email, we've now taken 3 seconds to respond to the user, when in reality once the user was written to the database, we could have responded to the user instantly at that point to confirm the registration. Another downside to this approach is if our data warehouse is currently broken and reporting errors, our system will also report errors whenever anybody tries to signup! Given analytics is purely internal and doesn't impact users, why should the analytics system being down impact user signup? ### A Pub/Sub approach A more ideal solution would be if we could decouple the behaviour of emailing the user and recording our analytics, such that the user service only has to record the user in its own database and let the user know they are registered - without worrying about the downstream impacts. Thankfully, this is exactly what [Pub/Sub topics](https://pkg.go.dev/encore.dev/pubsub#Topic) allow us to do.
In this example, when a user registers we: 1. The `user` service starts a database transaction and records the user in its database. 2. Publish a signup event to the `signups` topic. 3. Commit the transaction and reply to the user to say the registration was successful. At this point the user is free to continue interacting with the application and we've isolated the registration behaviour from the rest of the application. In parallel, the `email` and `analytics` services will receive the signup event from the `signups` topic and will then perform their respective tasks. If either service returns an error, the event will automatically be backed off and retried until the service is able to process the event successfully, or reaches the maximum number of attempts and is placed into the deadletter queue (DLQ).
Notice how in this version, the processing time of the two other services did not impact the end user and in fact the `user` service is not even aware of the `email` and `analytics` services. This means that new systems which need to know about new users signing up can be added to the application, without the need to change the `user` service or impacting its performance. ================================================ FILE: docs/go/primitives/raw-endpoints.md ================================================ --- seotitle: Raw Endpoints seodesc: Learn how to create raw API endpoints for your cloud backend application using Go and Encore.go title: Raw Endpoints subtitle: Drop down in abstraction to access the raw HTTP request lang: go --- Sometimes you need to operate a lower abstraction than Encore.go normally provides. For example, you might want to access the underlying HTTP request, often useful for things like accepting webhooks. Encore.go has you covered using "raw endpoints". To define a raw endpoint, change the `//encore:api` annotation and function signature like so: ```go package service import "net/http" // Webhook receives incoming webhooks from Some Service That Sends Webhooks. //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { // ... operate on the raw HTTP request ... } ``` Like any other Encore API endpoint, once deployed this will be exposed at the URL:
`https://-.encr.app/service.Webhook`. Just like regular endpoints, raw endpoints support the use of `:id` and `*wildcard` segments. Experienced Go developers will have already noted this is just a regular Go HTTP handler. (See the net/http documentation for how Go HTTP handlers work.) Learn more about receiving webhooks and using WebSockets in the [receiving regular HTTP requests guide](/docs/go/how-to/http-requests). ================================================ FILE: docs/go/primitives/secrets.md ================================================ --- seotitle: Securely storing API keys and secrets seodesc: Learn how to store API keys, and secrets, securely for your backend application. Encore's built in vault makes it simple to keep your app secure. title: Storing Secrets and API keys subtitle: Simply storing secrets securely lang: go --- Wouldn't it be nice to store secret values like API keys, database passwords, and private keys directly in the source code? Of course, we can’t do that – it's horrifyingly insecure! (Unfortunately, it's also [very common](https://www.ndss-symposium.org/ndss-paper/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/).) Encore's built-in secrets manager makes it simple to store secrets in a secure way and lets you use them in your program like regular variables. ## Using secrets in your application To use a secret in your application, first define it directly in your code by creating an unexported struct named `secrets`, where all fields are of type `string`. For example: ```go var secrets struct { SSHPrivateKey string // ed25519 private key for SSH server GitHubAPIToken string // personal access token for deployments // ... } ``` When you've defined secrets in your program, the Encore compiler will check that they are set before running or deploying your application. If a secret is not set, you will get a compilation error notifying you that a secret value is missing. Once you've provided values for all secrets, you can just use them in your application like a regular variable. For example: ```go func callGitHub(ctx context.Context) { req, _ := http.NewRequestWithContext(ctx, "GET", "https:///api.github.com/user", nil) req.Header.Add("Authorization", "token " + secrets.GitHubAPIToken) resp, err := http.DefaultClient.Do(req) // ... handle err and resp } ``` Secret keys are globally unique for your whole application. If multiple services use the same secret name they both receive the same secret value at runtime. ## Storing secret values ### Using the Encore Cloud dashboard The simplest way to set up secrets is with the Secrets Manager in the Encore Cloud dashboard. Open your app in [app.encore.cloud](https://app.encore.cloud), go to **Settings** in the main navigation, and then click on **Secrets** in the settings menu. From here you can create secrets, save secret values, and configure different values for different environments. ### Using the CLI If you prefer, you can also set up secrets from the CLI using:
`encore secret set --type ` `` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`. For example `encore secret set --type prod SSHPrivateKey` sets the secret value for production environments,
and `encore secret set --type dev,preview,local GitHubAPIToken` sets the secret value for development, preview, and local environments. In some cases, it can be useful to define a secret for a specific environment instead of an environment type. You can do so with `encore secret set --env `. Secret values for specific environments take precedence over values for environment types. ### Environment settings Each secret can only have one secret value for each environment type. For example: If you have a secret value that's shared between `development`, `preview` and `local`, and you want to override the value for `local`, you must first edit the existing secret and remove `local` using the Secrets Manager in the [Encore Cloud dashboard](https://app.encore.cloud). You can then add a new secret value for `local`. The end result should look something like the picture below. ## How it works: Where secrets are stored When you store a secret Encore stores it encrypted using Google Cloud Platform's [Key Management Service](https://cloud.google.com/security-key-management) (KMS). - **Production / Your own cloud:** When you deploy to production using your own cloud account on GCP or AWS, Encore provisions a secrets manager in your account (using either KMS or AWS Secrets Manager) and replicates your secrets to it. The secrets are then injected into the container using secret environment variables. - **Local:** For local secrets Encore automatically replicates them to developers' machines when running `encore run`. - **Development / Encore Cloud:** Environments on Encore's development cloud (running on GCP under the hood) work the same as self-hosted GCP environments, using GCP Secrets Manager. ### Overriding local secrets When setting secrets via the `encore secret set` command, they are automatically synced to all developers working on the same application, courtesy of Encore Cloud. In some cases, however, you want to override a secret only for your local machine. This can be done by creating a file named `.secrets.local.cue` in the root of your Encore application, next to the `encore.app` file. The file contains key-value pairs of secret names to secret values. For example: ```cue GitHubAPIToken: "my-local-override-token" SSHPrivateKey: "custom-ssh-private-key" ``` ================================================ FILE: docs/go/primitives/service-structs.md ================================================ --- seotitle: Service Structs seodesc: Learn how to use service structs to define APIs as methods. title: Service structs lang: go --- Encore lets you define a type, called a service struct, to represent your running service. This lets you define an initialization function (similar to the `main` function in regular Go programs). You can also define API endpoints as methods on the service struct type, enabling you to use [dependency injection](/docs/go/how-to/dependency-injection) for testing purposes. It works by defining a struct type of your choice (typically called `Service`) and declaring it with `//encore:service`. Then, you can define a special function named `initService` (or `initWhatever` if you named the type `Whatever`) that gets called by Encore to initialize your service when it starts up. It looks like this: ```go //encore:service type Service struct { // Add your dependencies here } func initService() (*Service, error) { // Write your service initialization code here. } //encore:api public func (s *Service) MyAPI(ctx context.Context) error { // ... } ``` ## Calling APIs defined on service structs When using a service struct like above, Encore will create a file named `encore.gen.go` in your service directory. This file contains package-level functions for the APIs defined as methods on the service struct. In the example above, you would see: ```go // Code generated by encore. DO NOT EDIT. package email import "context" // These functions are automatically generated and maintained by Encore // to simplify calling them from other services, as they were implemented as methods. // They are automatically updated by Encore whenever your API endpoints change. func Send(ctx context.Context, p *SendParams) error { // The implementation is elided here, and generated at compile-time by Encore. return nil } ``` These functions are generated in order to allow other services to keep calling your APIs as package-level functions, in the same way as before: `email.Send(...)`. This means other services do not need to care about whether you're using Dependency Injection internally. You must always use these generated package-level functions for making API calls. Encore will automatically generate these files and keep them up to date whenever your code changes. There is no need to manually invoke anything to regenerate this code. Encore adds all `encore.gen.go` files to your `.gitignore` since you typically don't want to commit them to your repository; doing so ends up creating a lot of unnecessary merge conflicts. However, in some cases when running third-party linters in a CI/CD environment it can be helpful to generate these wrappers to make the linter happy. You can do that by invoking `encore gen wrappers`. ## Graceful Shutdown When defining a service struct, Encore supports notifying your service when it's time to gracefully shut down. This works by having your service struct implement the method `func (s *Service) Shutdown(force context.Context)`. If that method exists, Encore will call it when it's time to begin gracefully shutting down. Initially the shutdown is in "graceful mode", which means that you have a few seconds to complete ongoing work. The provided `force` context is canceled when the graceful shutdown window is over, and it's time to forcefully shut down. How much time you have from when `Shutdown` is called to when forceful shutdown begins depends on the cloud provider and the underlying infrastructure. Typically it's in the range 5-30 seconds. Encore automatically handles graceful shutdown of all Encore-managed functionality, such as HTTP servers, database connection pools, Pub/Sub message receivers, distributed tracing recorders, and so on. The graceful shutdown functionality is provided if you have additional, non-Encore-related resources that need graceful shutdown. Note that graceful shutdown in Encore is *cooperative*: Encore will wait indefinitely for your `Shutdown` method to return. If your `Shutdown` method does not return promptly after the `force` context is closed, the underlying infrastructure at your cloud provider will typically force-kill your service, which can lead to lingering connections and other such issues. In summary, when your `Shutdown(force context.Context)` function is called: - Immediately begin gracefully shutting down - When the `force` context is canceled, you should forcefully shut down the resources that haven't yet completed their shutdown - Wait until the shutdown is complete before returning from the `Shutdown` function ================================================ FILE: docs/go/primitives/services.md ================================================ --- seotitle: Defining services with Encore.go seodesc: Learn how to create microservices and define APIs for your cloud backend application using Go and Encore. The easiest way of building cloud backends. title: Defining Services subtitle: Simplifying (micro-)service development lang: go --- Encore.go makes it simple to build applications with one or many services, without needing to manually handle the typical complexity of developing microservices. ## Defining a service With Encore.go you define a service by [defining at least one API](/docs/go/primitives/defining-apis) within a regular Go package. Encore recognizes this as a service, and uses the package name as the service name. On disk it might look like this: ``` /my-app ├── encore.app // ... and other top-level project files │ ├── hello // hello service (a Go package) │   ├── hello.go // hello service code │   └── hello_test.go // tests for hello service │ └── world // world service (a Go package) └── world.go // world service code ``` This means building a microservices architecture is as simple as creating multiple Go packages within your application. See the [app structure documentation](/docs/go/primitives/app-structure) for more details. ## Service Initialization Under the hood Encore automatically generates a `main` function that initializes all your infrastructure resources when the application starts up. This means you don't write a `main` function for your Encore application. If you want to customize the initialization behavior of your service, you can define a service struct and define custom initialization logic with that. See the [service struct docs](/docs/go/primitives/service-structs) for more info. ================================================ FILE: docs/go/primitives/share-db-between-services.md ================================================ --- seotitle: How to share SQL databases between services seodesc: Learn how to share a SQL database between multiple Go backend services using Encore. title: Share SQL databases between services lang: go --- By default, each service in an Encore app has its own database. This approach has many benefits: - Which database is used and how it works is abstracted away from other services - The database is more isolated, making changes to it smaller and safer - By making the services more independent your application becomes more reliable by being able to more gracefully handle partial outages, such as if your database is temporarily overloaded or offline. But like everything else in software engineering, there are trade-offs involved, and sometimes it's simpler and more reliable to use a single database that's accessed by multiple services. Encore makes this easy to do. Each database in Encore is defined within a service. That service's name becomes the name of the database. Other services can then access that database by creating a database reference with `sqldb.Named("dbname")`. ## Example Let's say you have a simple `todo` service, with only one table: **`todo/migrations/1_create_table.up.sql`** ```sql CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT FALSE ); ``` You want to create a `report` service that produces various reports for internal business processes, but for simplicity you decide it makes sense to directly access the `todo` database. All that's needed is to define the `todoDB` variable like so: **`report/report.go`** ```go package report import ( "context" "encore.dev/storage/sqldb" ) // todoDB connects to the "todo" service's database. var todoDB = sqldb.Named("todo") type ReportResponse struct { Total int } // CountCompletedTodos generates a report with the number of completed todo items. //encore:api method=GET path=/report/todo func CountCompletedTodos(ctx context.Context) (*ReportResponse, error) { var report ReportResponse err := todoDB.QueryRow(ctx,` SELECT COUNT(*) FROM todo_item WHERE completed = TRUE `).Scan(&report.Total) return &report, err } ``` With that, Encore understands that the `report` service depends on the `todo` service's database, and orchestrates the necessary connections to make that happen. And like everything else with Encore, it works exactly the same regardless of where it's running: for local development as well as in the cloud. ================================================ FILE: docs/go/quick-start.mdx ================================================ --- seotitle: Quick Start Guide – Learn how to build backends with Encore.go seodesc: See how you to build and ship a cloud based backend application using Go and Encore. Install Encore and build a REST API in just a few minutes. title: Quick Start Guide subtitle: Build your first Encore.go app in 5 minutes lang: go --- In this short guide, you'll learn key concepts and experience the Encore workflow. It should only take about 5 minutes to complete and by the end you'll have an API running in Encore's free development Cloud (Encore Cloud). To make it easy to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Install the Encore CLI To develop with Encore, you need the Encore CLI. It provisions your local environment, and runs your local development dashboard complete with tracing and API documentation. 🥐 Install by running the appropriate command for your system: ## 2. Create your app 🥐 Create your app by running: ```shell $ encore app create ``` If this is your first time using Encore, you’ll be prompted to create a free Encore Cloud account. This enables Encore to manage things like secrets and fully automate cloud deployments (which you’ll use later in the tutorial). 🥐 Select `Go` as your app’s language. 🥐 Choose a starter template. Pick `Hello World` and continue. Optional: Install AI instructions to improve how tools like Cursor and Claude Code work with Encore. After selecting your template, choose the AI instructions for the tool you plan to use. 🥐 Pick a name for your app. Encore will now create your app in a folder named after your app. ### Let's take a look at the code Part of what makes Encore different is the simple developer experience when building distributed systems. Let's look at the code to better understand how to build applications with Encore. 🥐 Open the `hello.go` file in your code editor. It's located in the folder: `your-app-name/hello/`. You should see this: ```go -- hello/hello.go -- // Service hello implements a simple hello world REST API. package hello import ( "context" ) // This is a simple REST API that responds with a personalized greeting. // //encore:api public path=/hello/:name func World(ctx context.Context, name string) (*Response, error) { msg := "Hello, " + name + "!" return &Response{Message: msg}, nil } type Response struct { Message string } ``` As you can see, it's all standard Go code except for a few lines specific to Encore's Backend Framework. One such element is the API annotation: ``` //encore:api public path=/hello/:name ``` This annotation is all that's needed for Encore to understand that the Go package `hello` is a service, and the `World` function is a public API endpoint. To create more services and endpoints, you simply create new Go packages and define endpoints using the `//encore:api` annotation. _If you're curious, you can read more about [defining APIs](/docs/go/primitives/defining-apis)._ Encore.go provides several other declarative ways of using backend primitives, such as databases, Pub/Sub, and scheduled tasks. All defined in your application code. ## 3. Start your app & Explore the Local Development Dashboard 🥐 Run your app locally: ```shell $ cd your-app-name # replace with the app name you picked $ encore run ``` You should see this: That means your local development environment is up and running! Encore takes care of setting up all the necessary infrastructure for your applications, even including databases and Pub/Sub. ### Open the Local Development Dashboard You can now start using your [Local Development Dashboard](/docs/go/observability/dev-dash). 🥐 Open [http://localhost:9400](http://localhost:9400) in your browser to access it. The Local Development Dashboard is a powerful tool to help you move faster when you're developing new features. It comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful observability features like [distributed tracing](/docs/go/observability/tracing). Through the Local Development Dashboard you also have access to [Encore Flow](/docs/go/observability/encore-flow), a visual representation of your microservice architecture that updates in real-time as you develop your application. ### Call your API 🥐 While you keep the app running, call your API from the API Explorer: You can also open a separate terminal to call your API endpoint: ```shell $ curl http://localhost:4000/hello/world {"Message": "Hello, world!"} ``` If you see this JSON response, you've successfully made an API call to your very first Encore application. Well done, you're on your way! ### Review a trace of the request You can now take a look at the trace for the request you just made by clicking on it in the right column in the local dashboard. With such a simple API, there's not much to it, just a simple request and response. However, just imagine how powerful it is to have tracing when you're developing a more complex system with multiple services, Pub/Sub, and databases. (Learn more about Encore's tracing capabilities in the [tracing docs](/docs/go/observability/tracing).) ## 4. Make a code change Let's put our mark on this API and make our first code change. 🥐 Head back to your code editor and look at the `hello.go` file again. If you can't come up a creative change yourself, why not simply change the "Hello" message to a more sassy "Howdy"? 🥐 Once you've made your change, save the file. When you save, the daemon run by the Encore CLI instantly detects the change and automatically recompiles your application and reloads your local development environment. The output where you're running your app will look something like this: ```output Changes detected, recompiling... Reloaded successfully. TRC registered endpoint endpoint=World path=/hello/:name service=hello TRC listening for incoming HTTP requests ``` 🥐 Test your change by calling your API again. ```shell $ curl http://localhost:4000/hello/world {"Message": "Howdy, world!"} ``` Great job, you made a change and your app was reloaded automatically. Now you're ready to head to the cloud! ## 5. Deploy your app ### Generating Docker image You can either deploy by generating a Docker image for you app using: ```shell $ encore build docker MY-IMAGE:TAG ``` This will compile your application using the host machine and then produce a Docker image containing the compiled application. You can now deploy this anywhere you like. Learn more in the [self-host docs](/docs/go/self-host/docker-build). ### Deploy using Encore Cloud Optionally, you can use [Encore Cloud](https://encore.dev/use-cases/devops-automation) to automatically deploy your application. It comes with built-in free development hosting, and for production offers fully automated deployment to your own cloud on AWS or GCP. 🥐 To deploy, simply push your changes to Encore: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore Cloud will now build and test your app, provision the needed infrastructure, and deploy your application to a staging environment. After triggering the deployment, you will see a URL where you can view its progress in the Encore Cloud dashboard. It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` 🥐 Open the URL to access the Encore Cloud dashboard and check the progress of your deployment. You can now use the Cloud Dashboard to view production [traces](/docs/go/observability/tracing), [connect your cloud account](/docs/platform/deploy/own-cloud), [integrate with GitHub](/docs/platform/integrations/github), and much more. ## What's next? - Check out the [REST API tutorial](/docs/go/tutorials/rest-api) to learn how to create endpoints, use databases, and more. - Join the friendly community on [Discord](/discord) to ask questions and meet other Encore developers. ================================================ FILE: docs/go/self-host/ci-cd.md ================================================ --- seotitle: Integrate with your CI/CD pipeline seodesc: Learn how to integrate Encore.go with your CI/CD pipeline. title: Integrate with your CI/CD pipeline lang: go --- Encore seamlessly integrates with any CI/CD pipeline through its CLI tools. You can automate Docker image creation using the `encore build` command as part of your deployment workflow. ## Integrating with CI/CD Platforms While every CI/CD pipeline is unique, integrating Encore follows a straightforward process. Here are the key steps: 1. Install the Encore CLI in your CI environment 2. Use `encore build docker` to create Docker images 3. Push the images to your container registry 4. Deploy to your infrastructure If your app is linked with Encore Cloud, you'll need to authenticate the CLI in your CI environment using an [auth key](/docs/platform/integrations/auth-keys). Generate one from **App Settings > Auth Keys** in the Encore Cloud dashboard, store it as a CI secret, and run `encore auth login --auth-key=` before building. Refer to your CI/CD platform's documentation for more details on how to integrate CLI tools like `encore build`. ### GitHub actions example This example shows how to build, push, and deploy an Encore Docker image to DigitalOcean using GitHub Actions. The DigitalOcean application is set up re-deploy the application every time an image with the tag `latest` is uploaded. ```yaml name: Build, Push and Deploy a Encore Docker Image to DigitalOcean on: push: branches: [ main ] permissions: contents: read packages: write jobs: build-push-deploy-image: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Download Encore CLI script uses: sozo-design/curl@v1.0.2 with: args: --output install.sh -L https://encore.dev/install.sh - name: Install Encore CLI run: bash install.sh - name: Authenticate with Encore run: /home/runner/.encore/bin/encore auth login --auth-key=${{ secrets.ENCORE_AUTH_KEY }} - name: Log in to DigitalOcean container registry run: docker login registry.digitalocean.com -u my-email@gmail.com -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Build Docker image run: /home/runner/.encore/bin/encore build docker myapp - name: Tag Docker image run: docker tag myapp registry.digitalocean.com//:latest - name: Push Docker image run: docker push registry.digitalocean.com//:latest ``` ## Building Docker Images The `encore build docker` command provides several options to customize your builds: ```bash # Build specific services and gateways encore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG # Customize the base image encore build docker --base=node:18-alpine MY-IMAGE:TAG # Build for a specific architecture (useful when CI and deploy targets differ) encore build docker --arch=arm64 MY-IMAGE:TAG ``` The image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image. ```bash docker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG ``` Learn more about the `encore build docker` command in the [build Docker images](/docs/go/self-host/docker-build) guide. Continue to learn how to [configure infrastructure](/docs/go/self-host/configure-infra). ================================================ FILE: docs/go/self-host/configure-infra.md ================================================ --- title: Configure Infrastructure seotitle: Configure Infrastructure seodesc: Learn how to configure infrastructure resources for your Encore app. lang: go --- If you are using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration. The `build` command lets you provide this by specifying a path to a config file using the `--config` flag. ```bash encore build docker --config path/to/infra-config.json MY-IMAGE:TAG ``` The configuration file should be a JSON file using the [Encore Infra Config](https://encore.dev/schemas/infra.schema.json) schema. This supports configuring things like: - How to access infrastructure resources (what provider to use, what credentials to use, etc.) - How to call other services over the network ("service discovery"), most notably their base URLs. - Observability configuration (where to export metrics, etc.) - Metadata about the environment the application is running in, to power Encore's metadata APIs - The values for any application-defined secrets. This configuration is necessary for the application to behave correctly. ## Example Here's an example configuration file you can use. ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "metadata": { "app_id": "my-app", "env_name": "my-env", "env_type": "production", "cloud": "gcp", "base_url": "https://my-app.com" }, "sql_servers": [ { "host": "my-db-host:5432", "databases": { "my-db": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ], "service_discovery": { "myservice": { "base_url": "https://myservice:8044" } }, "redis": { "my-redis": { "database_index": 0, "auth": { "type": "acl", "username": "encoreredis", "password": {"$env": "REDIS_PASSWORD"} }, "host": "my-redis-host", } }, "metrics": { "type": "prometheus", "remote_write_url": "https://my-remote-write-url" }, "graceful_shutdown": { "total": 30 }, "auth": [ { "type": "key", "id": 1, "key": {"$env": "SVC_TO_SVC_KEY"} } ], "secrets": { "AppSecret": {"$env": "APP_SECRET"} }, "pubsub": [ { "type": "gcp_pubsub", "project_id": "my-project", "topics": { "my-topic": { "name": "gcp-topic-name", "subscriptions": { "encore-subscription": { "name": "gcp-subscription-name" } } } } } ], "object_storage": [ { "type": "gcs", "buckets": { "my-gcs-bucket": { "name": "my-gcs-bucket", } } } ] } ``` ## Configuring Infrastructure To use infrastructure resources, additional configuration must be added so that Encore is aware of how to access each infrastructure resource. See below for examples of each type of infrastructure resource. ### 1. Basic Environment Metadata Configuration ```json { "metadata": { "app_id": "my-encore-app", "env_name": "production", "env_type": "production", "cloud": "aws", "base_url": "https://api.myencoreapp.com" } } ``` - `app_id`: The ID of your Encore application. - `env_name`: The environment name, such as `production`, `staging`, or `development`. - `env_type`: Specifies the type of environment (`production`, `test`, `development`, or `ephemeral`). - `cloud`: The cloud provider hosting the infrastructure (e.g., `aws`, `gcp`, or `azure`). - `base_url`: The base URL for services in the environment. ### 2. Graceful Shutdown Configuration ```json { "graceful_shutdown": { "total": 30, "shutdown_hooks": 10, "handlers": 20 } } ``` - `total`: The total time allowed for the shutdown process in seconds. - `shutdown_hooks`: The time allowed for executing shutdown hooks. - `handlers`: The time allocated for processing request handlers during the shutdown. ### 3. Authentication Methods Configuration Private endpoints will not require authentication if no authentication methods are specified. This is typically fine when services are deployed on a private network such as a VPC. But sometimes you might need to connect to other services over the public internet, in which case you'll want to ensure private endpoints are only accessible to other backend services. To do that you can configure authentication methods. Encore currently supports authentication through a shared key, which you can specify in your infrastructure configuration file. ```json { "auth": [ { "type": "key", "id": 1, "key": { "$env": "SERVICE_API_KEY" } } ] } ``` - `type`: The authentication method type (e.g., `key`). - `id`: The ID associated with the authentication method. - `key`: The authentication key, which can be set using an environment variable reference. ### 4. Service Discovery Configuration Service discovery is used to access other services over the network. You can configure service discovery in the infrastructure configuration file. If you export all services into the same docker image, you don't need to configure service discovery as it will be automatically configured when the services are started. ```json { "service_discovery": { "myservice": { "base_url": "https://myservice.myencoreapp.com", "auth": [ { "type": "key", "id": 1, "key": { "$env": "MY_SERVICE_API_KEY" } } ] } } } ``` - `myservice`: This is the name of the service as it is declared in your Encore app. - `base_url`: The base URL for the service. - `auth`: Authentication methods used for accessing the service. If no authentication methods are specified, the service will use the auth methods defined in the `auth` section. ### 5. Metrics Configuration Similarly to cloud infrastructure resources, Encore supports configurable metrics exports: * Prometheus * DataDog * GCP Cloud Monitoring * AWS CloudWatch This is configured by setting the metrics field. Below are examples for each of the supported metrics providers: #### 5.1. Prometheus Configuration ```json { "metrics": { "type": "prometheus", "collection_interval": 15, "remote_write_url": { "$env": "PROMETHEUS_REMOTE_WRITE_URL" } } } ``` #### 5.2. Datadog Configuration ```json { "metrics": { "type": "datadog", "collection_interval": 30, "site": "datadoghq.com", "api_key": { "$env": "DATADOG_API_KEY" } } } ``` #### 5.3. GCP Cloud Monitoring Configuration ```json { "metrics": { "type": "gcp_cloud_monitoring", "collection_interval": 60, "project_id": "my-gcp-project", "monitored_resource_type": "gce_instance", "monitored_resource_labels": { "instance_id": "1234567890", "zone": "us-central1-a" }, "metric_names": { "cpu_usage": "compute.googleapis.com/instance/cpu/usage_time" } } } ``` #### 5.4. AWS CloudWatch Configuration ```json { "metrics": { "type": "aws_cloudwatch", "collection_interval": 60, "namespace": "MyAppMetrics" } } ``` ### 6. SQL Database Configuration The SQL databases you've declared in your Encore app must be configured in the infrastructure configuration file. There must be exactly one database configuration for each declared database. You can configure multiple SQL servers if needed. ```json { "sql_servers": [ { "host": "db.myencoreapp.com:5432", "tls_config": { "disabled": false, "ca": "---BEGIN CERTIFICATE---\n...", "disable_tls_hostname_verification": false, "disable_ca_verification": false }, "databases": { "my-database": { "name": "my-postgres-db-name", "max_connections": 100, "min_connections": 10, "username": "db_user", "password": { "$env": "DB_PASSWORD" } } } } ] } ``` - `my-database`: This is the name of the database as it is declared in your Encore app. - `name`: The name of the database on the database server. Defaults to the declared Encore name. - `host`: SQL server host, optionally including the port. - `tls_config`: TLS configuration for secure connections. If the server uses TLS with a non-system CA root, or requires a client certificate, specify the appropriate fields as PEM-encoded strings. Otherwise, they can be left empty. - `databases`: List of databases, each with connection settings. ### 7. Secrets Configuration #### 7.1. Using Direct Secrets You can set the secret value directly in the configuration file, or use an environment variable reference to set the secret value. ```json { "secrets": { "API_TOKEN": "embedded-secret-value", "DB_PASSWORD": { "$env": "DB_PASSWORD" } } } ``` - `API_TOKEN`: This is the name of a secret as it is declared in your Encore app. #### 7.2. Using Environment Reference As an alternative, you can use an environment variable reference to set the secret value. The env variable should be set in the environment where the application is running. The content of the environment variable should be a JSON string where each key is the secret name and the value is the secret value. ```json { "secrets": { "$env": "SECRET_JSON" } } ``` ### 8. Redis Configuration ```json { "redis": { "my-redis": { "host": "redis.myencoreapp.com:6379", "database_index": 0, "auth": { "type": "auth", "auth_string": { "$env": "REDIS_AUTH_STRING" } }, "max_connections": 50, "min_connections": 5 } } } ``` - `my-redis`: This is the name of the redis resource as it is declared in your Encore app. - `host`: Redis server host, optionally including the port. - `auth`: Authentication configuration for the Redis server. - `key_prefix`: Prefix applied to all keys. ### 9. Pub/Sub Configuration Encore currently supports the following Pub/Sub providers: - `nsq` for [NSQ](https://nsq.io/) - `gcp` for [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) - `aws` for AWS [SNS](https://aws.amazon.com/sns/) + [SQS](https://aws.amazon.com/sqs/) - `azure` for [Azure Service Bus](https://azure.microsoft.com/en-us/products/service-bus) The configuration for each provider is different. Below are examples for each provider. #### 9.1. GCP Pub/Sub ```json { "pubsub": [ { "type": "gcp_pubsub", "project_id": "my-gcp-project", "topics": { "my-topic": { "name": "my-topic", "project_id": "my-gcp-project", "subscriptions": { "my-subscription": { "name": "my-subscription", "push_config": { "id": "my-push", "service_account": "service-account@my-gcp-project.iam.gserviceaccount.com" } } } } } } ] } ``` - `my-topic`: This is the name of the topic as it is declared in your Encore app. - `my-subscription`: This is the name of the subscription as it is declared in your Encore app. - `project_id`: The default GCP project ID. This can be overridden by setting the `project_id` field in the topic or subscription. - `name`: The name of the topic or subscription. - `push_config/id`: The id will be appended to `/__encore/pubsub/push/` to form the full push path of your service, e.g. `/__encore/pubsub/push/`. This is the path your service expects to receive push messages on. - `push_config/service_account`: The service account configured for the push subscription. #### 9.2. AWS SNS/SQS ```json { "pubsub": [ { "type": "aws_sns_sqs", "topics": { "my-topic": { "arn": "arn:aws:sns:us-east-1:123456789012:my-topic", "subscriptions": { "my-queue": { "url": "https://sqs.eu-east-1.amazonaws.com/123456789012/my-queue" } } } } } ] } ``` - `my-topic`: This is the name of the topic as it is declared in your Encore app. - `my-queue`: This is the name of the queue as it is declared in your Encore app. - `arn`: The ARN of the SNS topic. - `url`: The URL of the SQS queue. #### 9.3. NSQ Configuration ```json { "pubsub": [ { "type": "nsq", "hosts": "nsq.myencoreapp.com:4150", "topics": { "my-topic": { "name": "my-topic", "subscriptions": { "my-subscription": { "name": "my-subscription" } } } } } ] } ``` - `my-topic`: This is the name of the topic as it is declared in your Encore app. - `my-subscription`: This is the name of the subscription as it is declared in your Encore app. ### 10. Object Storage Configuration Encore currently supports the following object storage providers: - `gcs` for [Google Cloud Storage](https://cloud.google.com/storage) - `s3` for [AWS S3](https://aws.amazon.com/s3/) or a custom S3-compatible provider #### 10.1. GCS Configuration ```json { "object_storage": [ { "type": "gcs", "buckets": { "my-gcs-bucket": { "name": "my-gcs-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `my-gcs-bucket`: This is the name of the bucket as it is declared in your Encore app. - `name`: The full name of the GCS bucket. - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. #### 10.2. S3 Configuration ```json { "object_storage": [ { "type": "s3", "region": "us-east-1", "buckets": { "my-s3-bucket": { "name": "my-s3-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `my-s3-bucket`: This is the name of the bucket as it is declared in your Encore app. - `region`: The AWS region where the bucket is located. - `name`: The full name of the S3 bucket. - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. #### 10.3. Custom S3 Provider Configuration You can also configure a custom S3 provider by specifying the endpoint, access key id, and secret access key. Custom S3 providers are useful if you are using a S3-compatible storage provider such as [Cloudflare R2](https://developers.cloudflare.com/r2/). ```json { "object_storage": [ { "type": "s3", "region": "auto", "endpoint": "https://...", "access_key_id": "...", "secret_access_key": { "$env": "BUCKET_SECRET_ACCESS_KEY" }, "buckets": { "my-custom-bucket": { "name": "my-custom-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `my-custom-bucket`: This is the name of the bucket as it is declared in your Encore app. - `region`: The region where the bucket is located. - `name`: The full name of the bucket - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. This guide covers typical infrastructure configurations. Adjust according to your specific requirements to optimize your Encore app's infrastructure setup. ================================================ FILE: docs/go/self-host/deploy-to-digital-ocean-wip.md ================================================ --- seotitle: How to deploy an Encore app to DigitalOcean seodesc: Learn how to deploy an Encore application to DigitalOcean's App Platform using Docker. title: Deploy to DigitalOcean lang: go --- If you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to DigitalOcean's App Platform using Docker. ### Prerequisites 1. **DigitalOcean Account**: Make sure you have a DigitalOcean account. If not, you can [sign up here](https://www.digitalocean.com/). 2. **Docker Installed**: Ensure Docker is installed on your local machine. You can download it from the [Docker website](https://www.docker.com/get-started). 3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/go/install). 4. **DigitalOcean CLI (Optional)**: You can install the DigitalOcean CLI for more flexibility and automation, but it’s not necessary for this tutorial. ### Step 1: Create an Encore App 1. **Create a New Encore App**: - If you haven’t already, create a new Encore app using the Encore CLI. - You can use the following command to create a new app: ```bash encore app create myapp ``` - Select the `Hello World` template. - Follow the prompts to create the app. 2. **Build a Docker image**: - Build the Encore app to generate the docker image for deployment: ```bash encore build docker myapp ``` ### Step 2: Push the Docker Image to a Container Registry To deploy your Docker image to DigitalOcean, you need to push it to a container registry. DigitalOcean supports its own container registry, but you can also use DockerHub or other registries. Here’s how to push the image to DigitalOcean’s registry: 1. **Create a DigitalOcean Container Registry**: - Go to the [DigitalOcean Control Panel](https://cloud.digitalocean.com/registries) and create a new container registry. - Follow the instructions to set it up. 2. **Login to DigitalOcean's registry**: Use the login command provided by DigitalOcean, which will look something like this: ```bash doctl registry login ``` You’ll need the DigitalOcean CLI for this, which can be installed from [DigitalOcean CLI documentation](https://docs.digitalocean.com/reference/doctl/how-to/install/). 3. **Tag your Docker image**: Tag your image to match the registry’s URL. ```bash docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` 4. **Push your Docker image to the registry**: ```bash docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` ### Step 3: Deploy the Docker Image to DigitalOcean App Platform 1. **Navigate to the App Platform**: Go to [DigitalOcean's App Platform](https://cloud.digitalocean.com/apps). 2. **Create a New App**: - Click on **"Create App"**. - Choose the **"DigitalOcean Container Registry"** option. 3. **Select the Docker Image Source**: - Select the image you pushed earlier. 4. **Configure the App Settings**: - **Set up scaling options**: Configure the number of containers, CPU, and memory settings. - **Environment variables**: Add any environment variables your application might need. - **Choose the region**: Pick a region close to your users for better performance. 5. **Deploy the App**: - Click **"Next"**, review the settings, and click **"Create Resources"**. - DigitalOcean will take care of provisioning the infrastructure, pulling the Docker image, and starting the application. ### Step 4: Monitor and Manage the App 1. **Access the Application**: - Once deployed, you will get a public URL to access your application. - Test the app to ensure it’s running as expected, e.g. ```bash curl https://myapp.ondigitalocean.app/hello/world ``` 2. **View Logs and Metrics**: - Go to the **"Runtime Logs"** tab in the App Platform to view logs - Go to the **"Insights"** tab to view performance metrics. 3. **Manage Scaling and Deployment Settings**: - You can change the app configuration, such as scaling settings, deployment region, or environment variables. ### Step 5: Add a Database to Your App DigitalOcean’s App Platform provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a managed database for your app: 1. **Navigate to the DigitalOcean Control Panel**: - Go to [DigitalOcean Control Panel](https://cloud.digitalocean.com/). - Click on **"Databases"** in the left-hand sidebar. 2. **Create a New Database Cluster**: - Click **"Create Database Cluster"**. - Choose **PostgreSQL** - Select the **database version**, **data center region**, and **cluster configuration** (e.g., development or production settings based on your needs). - **Name the database** and configure other settings if necessary, then click **"Create Database Cluster"**. 3. **Configure the Database Settings**: - Once the database is created, go to the **"Connection Details"** tab of the database dashboard. - Copy the **connection string** or individual settings (host, port, username, password, database name). You will need these details to connect your app to the database. - Download the **CA certificate** 4. **Create a Database** - Connect to the database using the connection string provided by DigitalOcean. ```bash psql -h mydb.db.ondigitalocean.com -U doadmin -d mydb -p 25060 ``` - Create a database ```sql CREATE DATABASE mydb; ``` - Create a table ```sql CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(50) ); INSERT INTO users (name) VALUES ('Alice'); ``` 5. **Declare a Database in your Encore app**: - Open your Encore app’s codebase. - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases)) ```typescript const mydb = new SQLDatabase("mydb", { migrations: "./migrations", }); export const getUser = api( { expose: true, method: "GET", path: "/names/:id" }, async ({id}: {id:number}): Promise<{ id: number; name: string }> => { return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string }; } ); ``` 6. **Create an Encore Infrastructure config** - Create a file named `infra.config.json` in the root of your Encore app. - Add the **CA certificate** and the connection details to the file: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "mydb.db.ondigitalocean.com:25060", "tls_config": { "ca": "-----BEGIN CERTIFICATE-----\n..." }, "databases": { "mydb": { "username": "doadmin", "password": {"$env": "DB_PASSWORD"} } } }] } ``` 7. **Set Up Environment Variables (Optional)**: - Go to the DigitalOcean App Platform dashboard. - Select your app. - In the **"Settings"** section, go to **"App-Level Environment Variables"** - Add the database password as an encrypted environment variable called `DB_PASSWORD`. 8. **Build and push the Docker image**: - Build the Docker image with the updated configuration. ```bash encore build docker --config infra.config.json myapp ``` - Tag and push the Docker image to the DigitalOcean container registry. ```bash docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` 9. **Test the Database Connection**: - Redeploy the app on DigitalOcean to apply the changes. - Test the database connection by calling the API ```bash curl https://myapp.ondigitalocean.app/names/1 ``` ### Troubleshooting Tips - **Deployment Failures**: Check the build logs for any errors. Make sure the Docker image is correctly tagged and pushed to the registry. - **App Not Accessible**: Verify that the correct port is exposed in the Dockerfile and the App Platform configuration. - **Database Connection Issues**: Ensure the database connection details are correct and the database is accessible from the app. ### Conclusion That’s it! You’ve successfully deployed an Encore app to DigitalOcean’s App Platform using Docker. You can now scale your app, monitor its performance, and manage it easily through the DigitalOcean dashboard. If you encounter any issues, refer to the DigitalOcean documentation or the Encore community for help. Happy coding! ================================================ FILE: docs/go/self-host/self-host.md ================================================ --- seotitle: Build Docker Images seodesc: Learn how to build Docker images for your Encore application, which can be self-hosted on your own infrastructure. title: Build Docker Images lang: go --- Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. This can be a good choice if Encore Cloud isn't a good fit for your use case, or if you want to [migrate away](/docs/go/migration/migrate-away). ## Building your own Docker image To build your own Docker image, use `encore build docker MY-IMAGE:TAG` from the CLI. This will compile your application using the host machine and then produce a Docker image containing the compiled application. The base image defaults to `scratch` for GO apps and `node:slim` for TS, but can be customized with `--base`. This is exactly the same code path that Encore's CI system uses to build Docker images, ensuring compatibility. By default, all your services are included and started by the Docker image. If you want to specify specific services and gateways to include, you can use the `--services` and `--gateways` flags. ```bash encore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG ``` You can target a specific architecture with `--arch` (useful when your build machine differs from your deploy target): ```bash encore build docker --arch=arm64 MY-IMAGE:TAG ``` To provide an [infrastructure configuration](/docs/go/self-host/configure-infra) file at build time, use `--config`: ```bash encore build docker --config=infra-config.json MY-IMAGE:TAG ``` The image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image. ```bash docker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG ``` Congratulations, you've built your own Docker image! 🎉 Continue to learn how to [configure infrastructure](/docs/go/self-host/configure-infra). ================================================ FILE: docs/go/tutorials/booking-system.mdx ================================================ --- title: Building a Booking System subtitle: Learn how to build your own appointment booking system with both user facing and admin functionality seotitle: How to build an Appointment Booking System in Go seodesc: Learn how to build an appointment booking tool using Go and Encore. Get your entire application running in the cloud in 30 minutes! lang: go --- In this tutorial we'll build a booking system with a user facing UI (see available slots and book appointments) and an admin dashboard (manage scheduled appointments and set availability). You will learn how to: * Create API endpoints using Encore (both public and authenticated). * Working with PostgreSQL databases using [sqlc](https://sqlc.dev/) and [pgx](https://github.com/jackc/pgx). * Scrub sensitive user data from traces. * Work with dates and times in Go. * Authenticate requests using an auth handler. * Send emails using a SendGrid integration. [Demo version of the app](https://prod-booking-system-teti.encr.app/frontend) The final result will look like this: If you want to skip ahead you can view the final project here: [https://github.com/encoredev/examples/tree/main/booking-system](https://github.com/encoredev/examples/tree/main/booking-system) ## 1. Create your Encore application To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. Make sure you have [Docker](https://docker.com) installed and running, it is used by Encore to run PostgreSQL databases locally. 🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use. ```shell $ encore app create booking-system --example=github.com/encoredev/example-booking-system/tree/starting-point ``` 🥐 Check that your frontend works: ```shell $ cd booking-system $ encore run ``` Then visit [http://localhost:4000/frontend/](http://localhost:4000/frontend/) to see the frontend. It won't function yet, since we haven't yet built the backend, so let's do just that! When we're done we'll have a backend with this [architecture](/docs/go/observability/encore-flow): ## 2. Create booking service Let's start by creating the functionality to view bookable slots. With Encore you define a service by [defining one or more APIs](/docs/go/primitives/defining-apis) within a regular Go package. Encore recognizes this as a service, and uses the package name as the service name. When deploying, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra) for each service. We already have a Go package named `booking`, let's turn that into an Encore service. 🥐 Inside the `booking` folder, create a file named `slots.go`. ```shell $ touch booking/slots.go ``` 🥐 Add an Encore API endpoint named `GetBookableSlots` that takes a date as input. The endpoint will return a list of bookable slots from the supplied date and six days forward (so that we can show a week view calendar in the UI). ```go -- booking/slots.go -- // Service booking keeps track of bookable slots in the calendar. package booking import ( "context" "github.com/jackc/pgx/v5/pgtype" "time" ) const DefaultBookingDuration = 1 * time.Hour type BookableSlot struct { Start time.Time `json:"start"` End time.Time `json:"end"` } type SlotsParams struct{} type SlotsResponse struct{ Slots []BookableSlot } //encore:api public method=GET path=/slots/:from func GetBookableSlots(ctx context.Context, from string) (*SlotsResponse, error) { fromDate, err := time.Parse("2006-01-02", from) if err != nil { return nil, err } const numDays = 7 var slots []BookableSlot for i := 0; i < numDays; i++ { date := fromDate.AddDate(0, 0, i) daySlots, err := bookableSlotsForDay(date) if err != nil { return nil, err } slots = append(slots, daySlots...) } return &SlotsResponse{Slots: slots}, nil } func bookableSlotsForDay(date time.Time) ([]BookableSlot, error) { // 09:00 availStartTime := pgtype.Time{ Valid: true, Microseconds: int64(9*3600) * 1e6, } // 17:00 availEndTime := pgtype.Time{ Valid: true, Microseconds: int64(17*3600) * 1e6, } availStart := date.Add(time.Duration(availStartTime.Microseconds) * time.Microsecond) availEnd := date.Add(time.Duration(availEndTime.Microseconds) * time.Microsecond) // Compute the bookable slots in this day, based on availability. var slots []BookableSlot start := availStart for { end := start.Add(DefaultBookingDuration) if end.After(availEnd) { break } slots = append(slots, BookableSlot{ Start: start, End: end, }) start = end } return slots, nil } ``` The availability is currently hardcoded to be 09:00 - 17:00 for each day. Later we'll add the functionality to set it for each day of the week. We are also returning time slots that have already passed. Don't worry, we'll come back and fix it later on. 🥐 Let's try it! Open up the Local Development Dashboard running at [http://localhost:9400](http://localhost:9400) and try calling the `booking.GetBookableSlots` endpoint, passing in `2024-12-01`. If you prefer to use the terminal instead run `curl http://localhost:4000/slots/2024-12-01` in a new terminal instead. Either way you should see the response: ```json { "Slots": [ { "start": "2024-12-01T09:00:00Z", "end": "2024-12-01T10:00:00Z" }, { "start": "2024-12-01T10:00:00Z", "end": "2024-12-01T11:00:00Z" }, { "start": "2024-12-01T11:00:00Z", "end": "2024-12-01T12:00:00Z" }, ... ] } ``` ## 3. Book an appointment Next, we want to make it possible to book an appointment. We'll need a database to store the bookings in. Encore makes it really simple to [create and use databases](/docs/go/primitives/databases) (both for local and cloud environments), but for this example we will also make use of [sqlc](https://sqlc.dev/) that will compile our SQL queries into type-safe Go code that we can use in our application. 🥐 Let's create a SQL database for our booking service and the required sqlc scaffolding. Create the following file structure: ``` /my-app └── booking // booking service (a Go package) ├── db // (New) db related files (directory) │ ├── migrations // (New) db migrations (directory) │ │ └── 1_create_tables.up.sql // (New) db migration schema │ └── query.sql // (New) SQL queries ├── sqlc.yaml // (New) sqlc config file ├── slots.go // booking service code └── helpers.go // booking service code ``` 🥐 Naming of the database migration file is important, it must look something like: `1_.up.sql`. Add the following contents to the migration file: ```sql -- booking/db/migrations/1_create_tables.up.sql -- CREATE TABLE booking ( id BIGSERIAL PRIMARY KEY, start_time TIMESTAMP NOT NULL, end_time TIMESTAMP NOT NULL, email TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); ``` 🥐 Next, install the sqlc library: ```shell $ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest ``` 🥐 Next, we need to configure sqlc. Add the following contents to `sqlc.yaml`: ```yaml -- booking/sqlc.yaml -- version: "2" sql: - engine: "postgresql" queries: "db/query.sql" schema: "./db/migrations" gen: go: package: "db" out: "db" sql_package: "pgx/v5" ``` This instructs sqlc to generate Go code from the queries in `db/query.sql` and models from the schemas in the `db/migrations` folder. 🥐 Let's create our first SQL queries. Add the following contents to `db/query.sql`: ```sql -- name: InsertBooking :one INSERT INTO booking (start_time, end_time, email) VALUES ($1, $2, $3) RETURNING *; -- name: ListBookingsBetween :many SELECT * FROM booking WHERE start_time >= $1 AND end_time <= $2; -- name: ListBookings :many SELECT * FROM booking; -- name: DeleteBooking :exec DELETE FROM booking WHERE id = $1; ``` 🥐 It's time for sqlc to shine! Run the following command in your terminal: ```shell $ cd booking $ sqlc generate ``` Three files should now have been generated inside the `db` folder: `query.sql.go`, `db.go` and `models.go`. These files contain generated Go code and should not be manually edited. We will be adding more queries to `db/query.sql` later and then re-run `sqlc generate` to update the generated Go code. Now let's create an endpoint that makes use of one of these queries. 🥐 Create `booking/booking.go` with the contents: ```go -- booking/booking.go -- package booking import ( "context" "time" "encore.app/booking/db" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "encore.dev/beta/errs" "encore.dev/storage/sqldb" ) var ( bookingDB = sqldb.NewDatabase("booking", sqldb.DatabaseConfig{ Migrations: "./db/migrations", }) pgxdb = sqldb.Driver[*pgxpool.Pool](bookingDB) query = db.New(pgxdb) ) type Booking struct { ID int64 `json:"id"` Start time.Time `json:"start"` End time.Time `json:"end"` Email string `encore:"sensitive"` } type BookParams struct { Start time.Time `json:"start"` Email string `encore:"sensitive"` } //encore:api public method=POST path=/booking func Book(ctx context.Context, p *BookParams) error { eb := errs.B() now := time.Now() if p.Start.Before(now) { return eb.Code(errs.InvalidArgument).Msg("start time must be in the future").Err() } tx, err := pgxdb.Begin(ctx) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to start transaction").Err() } defer tx.Rollback(context.Background()) // committed explicitly on success _, err = query.InsertBooking(ctx, db.InsertBookingParams{ StartTime: pgtype.Timestamp{Time: p.Start, Valid: true}, EndTime: pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true}, Email: p.Email, }) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to insert booking").Err() } if err := tx.Commit(ctx); err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to commit transaction").Err() } return nil } ``` We are now using the generated type-safe `query.InsertBooking` function to make the database operation. Notice the `encore:"sensitive"` tag on the `Email` field. This tells Encore to scrub this field so that the data is not viewable in the traces for deployed environments. This is useful for fields that contain [sensitive data](/docs/go/primitives/defining-apis#sensitive-data) such as email addresses, passwords, etc. 🥐 Restart `encore run` to cause the database to be created, and then call the `booking.Book` endpoint: ```shell $ curl -X POST 'http://localhost:4000/booking' -d '{"start": "2024-12-11T09:00:00Z", "email": "test@example.com"}' ``` Congratulations, you have now booked your first appointment! ## 4. Authentication To provide an admin dashboard for our booking system, we need to add authentication to our application so that we can have protected endpoints. Keep in mind, in this tutorial we'll only include a very basic implementation. 🥐 Let's start by creating a new service named `user`: ```shell $ mkdir user $ touch user/auth.go ``` 🥐 Add the following contents to `user/auth.go`: ```go -- user/auth.go -- // Service user authenticates users. package user import ( "context" "encore.dev/beta/auth" "encore.dev/beta/errs" ) type Data struct { Email string } type AuthParams struct { Authorization string `header:"Authorization"` } //encore:authhandler func AuthHandler(ctx context.Context, p *AuthParams) (auth.UID, *Data, error) { if p.Authorization != "" { return "test", &Data{}, nil } return "", nil, errs.B().Code(errs.Unauthenticated).Msg("no auth header").Err() } ``` This function is our [auth handler](/docs/go/develop/auth#the-auth-handler). An Encore applications can designate a special function to handle authentication, by defining a function and annotating it with `//encore:authhandler`. This annotation tells Encore to run the function whenever an incoming API call contains authentication data. The auth handler is responsible for validating the incoming authentication data and returning an `auth.UID` (a string type representing a user id). The `auth.UID` can be whatever you wish, but in practice it usually maps directly to the primary key stored in a user table (either defined in the Encore service or in an external service like Firebase or Okta). In order to keep this example simple, we'll just approve any request containing a token that is not empty. Next we will implement some of our auth endpoints and make use of our newly created auth handler. ## 5. Setting availability Right now the availability is hardcoded to 9:00 - 17:00. Let's add the functionality to let our admin users customize this. Let's start by adding another migration file, this time to create an `availability` table. 🥐 Create a file called `2_add_availability.up.sql` inside the `booking/db/migrations` folder. Add the following contents to that file: ```sql -- booking/db/migrations/2_add_availability.up.sql -- CREATE TABLE availability ( weekday SMALLINT NOT NULL PRIMARY KEY, -- Sunday=0, Monday=1, etc. start_time TIME NULL, -- null indicates not available end_time TIME NULL -- null indicates not available ); -- Add some placeholder availability to get started INSERT INTO availability (weekday, start_time, end_time) VALUES (0, '09:30', '17:00'), (1, '09:00', '17:00'), (2, '09:00', '18:00'), (3, '08:30', '18:00'), (4, '09:00', '17:00'), (5, '09:00', '17:00'), (6, '09:30', '16:30'); ``` 🥐 We can now add two queries to `booking/db/query.sql` so that we can store and retrieve availability: ```sql -- booking/db/query.sql -- -- name: GetAvailability :many SELECT * FROM availability ORDER BY weekday; -- name: UpdateAvailability :exec INSERT INTO availability (weekday, start_time, end_time) VALUES (@weekday, @start_time, @end_time) ON CONFLICT (weekday) DO UPDATE SET start_time = @start_time, end_time = @end_time; ``` 🥐 Run `sqlc generate` to update the generated Go code. 🥐 Create a new file in the `booking` service named `availability.go`: ```shell $ touch booking/availability.go ``` 🥐 Add the following to that file: ```go -- booking/availability.go -- package booking import ( "context" "errors" "fmt" "encore.app/booking/db" "github.com/jackc/pgx/v5/pgtype" "encore.dev/beta/errs" "encore.dev/rlog" ) type Availability struct { Start *string `json:"start" encore:"optional"` End *string `json:"end" encore:"optional"` } type GetAvailabilityResponse struct { Availability []Availability } //encore:api public method=GET path=/availability func GetAvailability(ctx context.Context) (*GetAvailabilityResponse, error) { rows, err := query.GetAvailability(ctx) if err != nil { return nil, err } availability := make([]Availability, 7) for _, row := range rows { day := row.Weekday if day < 0 || day > 6 { rlog.Error("invalid week day in availability table", "row", row) continue } // These never fail start, _ := row.StartTime.TimeValue() end, _ := row.EndTime.TimeValue() availability[day] = Availability{ Start: timeToStr(start), End: timeToStr(end), } } return &GetAvailabilityResponse{Availability: availability}, nil } type SetAvailabilityParams struct { Availability []Availability } //encore:api auth method=POST path=/availability func SetAvailability(ctx context.Context, params SetAvailabilityParams) error { eb := errs.B() tx, err := pgxdb.Begin(ctx) if err != nil { return err } defer tx.Rollback(context.Background()) // committed explicitly on success qry := query.WithTx(tx) for weekday, a := range params.Availability { if weekday > 6 { return eb.Code(errs.InvalidArgument).Msgf("invalid weekday %d", weekday).Err() } start, err1 := strToTime(a.Start) end, err2 := strToTime(a.End) if err := errors.Join(err1, err2); err != nil { return eb.Cause(err).Code(errs.InvalidArgument).Msg("invalid start/end time").Err() } else if start.Valid != end.Valid { return eb.Code(errs.InvalidArgument).Msg("both start/stop must be set, or both null").Err() } else if start.Valid && start.Microseconds > end.Microseconds { return eb.Code(errs.InvalidArgument).Msg("start must be before end").Err() } err = qry.UpdateAvailability(ctx, db.UpdateAvailabilityParams{ Weekday: int16(weekday), StartTime: start, EndTime: end, }) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to update availability").Err() } } err = tx.Commit(ctx) return errs.WrapCode(err, errs.Unavailable, "failed to commit transaction") } ``` This file contains two endpoints, a setter and a getter. The `SetAvailability` endpoint is protected by the `auth` middleware which means that the user must be authenticated in order to call it. The `GetAvailability` endpoint is public and can be called without authentication. 🥐 Let's set the availability for each day of the week. Open the Development Dashboard at [http://localhost:9400](http://localhost:9400) and select the `booking.SetAvailability` endpoint in the API Explorer. For the request body, paste the following: ```json { "Availability": [{ "start": "09:30", "end": "17:00" },{ "start": "09:00", "end": "17:00" },{ "start": "09:00", "end": "18:00" },{ "start": "08:30", "end": "18:00" },{ "start": "09:00", "end": "17:00" },{ "start": "09:00", "end": "17:00" },{ "start": "09:30", "end": "16:30" }] } ``` Don't leave the auth token empty, it will cause the auth handler to reject the request. You can use any value for the auth token. Now try retrieving the availability by calling the `booking.GetAvailability` endpoint through the API Explorer in the Development Dashboard. 🥐 Add the following functions inside the `booking` package, and import the `slices` package: ```go func listBookingsBetween( ctx context.Context, start, end time.Time, ) ([]*Booking, error) { rows, err := query.ListBookingsBetween(ctx, db.ListBookingsBetweenParams{ StartTime: pgtype.Timestamp{Time: start, Valid: true}, EndTime: pgtype.Timestamp{Time: end, Valid: true}, }) if err != nil { return nil, err } var bookings []*Booking for _, row := range rows { bookings = append(bookings, &Booking{ ID: row.ID, Start: row.StartTime.Time, End: row.EndTime.Time, Email: row.Email, }) } return bookings, nil } func filterBookableSlots( slots []BookableSlot, now time.Time, bookings []*Booking, ) []BookableSlot { // Remove slots for which the start time has already passed. slots = slices.DeleteFunc(slots, func(s BookableSlot) bool { // Has the slot already passed? if s.Start.Before(now) { return true } // Is there a booking that overlaps with this slot? for _, b := range bookings { if b.Start.Before(s.End) && b.End.After(s.Start) { return true } } return false }) return slots } ``` We'll use these functions to figure out which slots are bookable, and which are not, to avoid double bookings. 🥐 Now we can update the `Book` endpoint inside `booking.go` and make use of these new functions: ```go HL booking/booking.go 15:27 -- booking/booking.go -- //encore:api public method=POST path=/booking func Book(ctx context.Context, p *BookParams) error { eb := errs.B() now := time.Now() if p.Start.Before(now) { return eb.Code(errs.InvalidArgument).Msg("start time must be in the future").Err() } tx, err := pgxdb.Begin(ctx) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to start transaction").Err() } defer tx.Rollback(context.Background()) // committed explicitly on success // Get the bookings for this day. startOfDay := time.Date(p.Start.Year(), p.Start.Month(), p.Start.Day(), 0, 0, 0, 0, p.Start.Location()) bookings, err := listBookingsBetween(ctx, startOfDay, startOfDay.AddDate(0, 0, 1)) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to list bookings").Err() } // Is this slot bookable? slot := BookableSlot{Start: p.Start, End: p.Start.Add(DefaultBookingDuration)} if len(filterBookableSlots([]BookableSlot{slot}, now, bookings)) == 0 { return eb.Code(errs.InvalidArgument).Msg("slot is unavailable").Err() } _, err = query.InsertBooking(ctx, db.InsertBookingParams{ StartTime: pgtype.Timestamp{Time: p.Start, Valid: true}, EndTime: pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true}, Email: p.Email, }) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to insert booking").Err() } if err := tx.Commit(ctx); err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to commit transaction").Err() } return nil } ``` 🥐 Inside `slots.go`, update the `GetBookableSlots` endpoint and the `bookableSlotsForDay` functions to look like this: ```go HL booking/slots.go 7:12 HL booking/slots.go 18:23 HL booking/slots.go 29:36 HL booking/slots.go 39:48 -- booking/slots.go -- //encore:api public method=GET path=/slots/:from func GetBookableSlots(ctx context.Context, from string) (*SlotsResponse, error) { fromDate, err := time.Parse("2006-01-02", from) if err != nil { return nil, err } availabilityResp, err := GetAvailability(ctx) if err != nil { return nil, err } availability := availabilityResp.Availability const numDays = 7 var slots []BookableSlot for i := 0; i < numDays; i++ { date := fromDate.AddDate(0, 0, i) weekday := int(date.Weekday()) if len(availability) <= weekday { break } daySlots, err := bookableSlotsForDay(date, &availability[weekday]) if err != nil { return nil, err } slots = append(slots, daySlots...) } // Get bookings for the next 7 days. activeBookings, err := listBookingsBetween(ctx, fromDate, fromDate.AddDate(0, 0, numDays)) if err != nil { return nil, err } slots = filterBookableSlots(slots, time.Now(), activeBookings) return &SlotsResponse{Slots: slots}, nil } func bookableSlotsForDay(date time.Time, avail *Availability) ([]BookableSlot, error) { if avail.Start == nil || avail.End == nil { return nil, nil } availStartTime, err1 := strToTime(avail.Start) availEndTime, err2 := strToTime(avail.End) if err := errors.Join(err1, err2); err != nil { return nil, err } availStart := date.Add(time.Duration(availStartTime.Microseconds) * time.Microsecond) availEnd := date.Add(time.Duration(availEndTime.Microseconds) * time.Microsecond) // Compute the bookable slots in this day, based on availability. var slots []BookableSlot start := availStart for { end := start.Add(DefaultBookingDuration) if end.After(availEnd) { break } slots = append(slots, BookableSlot{ Start: start, End: end, }) start = end } return slots, nil } ``` ## 6. Managing scheduled bookings To display the scheduled bookings in the admin dashboard, we need to add the functionality to list all bookings. While we're at it, we'll also make it possible to delete bookings. 🥐 Add two new endpoints to `booking/booking.go`: ```go -- booking/booking.go -- type ListBookingsResponse struct { Booking []*Booking `json:"bookings"` } //encore:api auth method=GET path=/booking func ListBookings(ctx context.Context) (*ListBookingsResponse, error) { rows, err := query.ListBookings(ctx) if err != nil { return nil, err } var bookings []*Booking for _, row := range rows { bookings = append(bookings, &Booking{ ID: row.ID, Start: row.StartTime.Time, End: row.EndTime.Time, Email: row.Email, }) } return &ListBookingsResponse{Booking: bookings}, nil } //encore:api auth method=DELETE path=/booking/:id func DeleteBooking(ctx context.Context, id int64) error { return query.DeleteBooking(ctx, id) } ``` That's it! We now have all the backend endpoints in place to be able to supply the frontend with data. 🎉 ## 7. Running the React frontend The frontend should now be working as expected. 🥐 Go to [http://localhost:4000/frontend/](http://localhost:4000/frontend/) and try out your new booking system. The frontend is built using [React](https://react.dev/) and [Tailwind CSS](https://tailwindcss.com/). It uses Encore's ability to generate type-safe [request clients](https://encore.dev/docs/go/cli/client-generation). This means you don't need to manually keep the request/response objects in sync on the frontend. To generate a client: ```bash $ encore gen client --output=./src/client.ts --env= ``` While you're developing, you are going to want to run this command quite often (whenever you make a change to your endpoints) so having it as an `npm` script is a good idea. Take a look at the scripts in the `package.json` file: ```json { ... "scripts": { ... "gen": "encore gen client --output=./src/lib/client.ts --env=staging", "gen:local": "encore gen client --output=./src/lib/client.ts --env=local" }, } ``` For this frontend we use the request client together with [TanStack Query](https://tanstack.com/query/latest). When building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale. [TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works great with the Encore request client. See our the docs page about [integrating with a web frontend](/docs/how-to/integrate-frontend) to learn more. ## 8. Deploy to Encore's development cloud Let's deploy the project to Encore's free development cloud. Encore comes with built-in CI/CD, and the deployment process is as simple as a `git push`. (You can also integrate with GitHub to activate per Pull Request Preview Environments, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).) 🥐 Now, let's deploy your app to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment. 🥐 When the deploy has finished, you can try out your booking system by going to `https://staging-$APP_ID.encr.app/frontend/`. *You now have an Appointment Booking System running in the cloud, well done!* ## 8. Sending confirmation emails using SendGrid In order for the users to get a confirmation email when they book an appointment we need to add an email integration. Conveniently for us, there is a ready to use SendGrid integration as an [Encore Bit](https://github.com/encoredev/examples?tab=readme-ov-file#bits). 🥐 [Follow the instructions](https://github.com/encoredev/examples/tree/main/bits/sendgrid) to add the SendGrid integration to your project. Next, we need to call our new `sendgrid` service when an appointment is booked. 🥐 Add a call to `sendgrid.Send` in the `Book` endpoint: ```go HL booking/booking.go 41:59 -- booking/booking.go -- //encore:api public method=POST path=/booking func Book(ctx context.Context, p *BookParams) error { eb := errs.B() now := time.Now() if p.Start.Before(now) { return eb.Code(errs.InvalidArgument).Msg("start time must be in the future").Err() } tx, err := pgxdb.Begin(ctx) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to start transaction").Err() } defer tx.Rollback(context.Background()) // committed explicitly on success // Get the bookings for this day. startOfDay := time.Date(p.Start.Year(), p.Start.Month(), p.Start.Day(), 0, 0, 0, 0, p.Start.Location()) bookings, err := listBookingsBetween(ctx, startOfDay, startOfDay.AddDate(0, 0, 1)) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to list bookings").Err() } // Is this slot bookable? slot := BookableSlot{Start: p.Start, End: p.Start.Add(DefaultBookingDuration)} if len(filterBookableSlots([]BookableSlot{slot}, now, bookings)) == 0 { return eb.Code(errs.InvalidArgument).Msg("slot is unavailable").Err() } _, err = query.InsertBooking(ctx, db.InsertBookingParams{ StartTime: pgtype.Timestamp{Time: p.Start, Valid: true}, EndTime: pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true}, Email: p.Email, }) if err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to insert booking").Err() } if err := tx.Commit(ctx); err != nil { return eb.Cause(err).Code(errs.Unavailable).Msg("failed to commit transaction").Err() } // Send confirmation email using SendGrid formattedTime := pgtype.Timestamp{Time: p.Start, Valid: true}.Time.Format("2006-01-02 15:04") _, err = sendgrid.Send(ctx, &sendgrid.SendParams{ From: sendgrid.Address{ Name: "", Email: "", }, To: sendgrid.Address{ Email: p.Email, }, Subject: "Booking Confirmation", Text: "Thank you for your booking!\nWe look forward to seeing you soon at " + formattedTime, Html: "", }) if err != nil { return err } return nil } ``` The `From` email used when sending emails needs to go through the SendGrid verification process before it can be used. You can read more about it here: https://sendgrid.com/docs/ui/sending-email/sender-verification/ The default behaviour of the SendGrid integration is to only send emails on production environments. You can create production environments through the Encore Cloud Dashboard. ## 9. Deploy your finished Booking System Now you're ready to deploy your finished Booking System, complete with a SendGrid integration. 🥐 As before, deploying your app to the cloud is as simple as running: ```shell $ git add -A . $ git commit -m 'Add sendgrid integration' $ git push encore ``` ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ================================================ FILE: docs/go/tutorials/graphql.mdx ================================================ --- title: Building a GraphQL API subtitle: Learn how to build a GraphQL API using Encore.go seotitle: How to build a GraphQL API using Encore.go seodesc: Learn how to build a microservices backend in Go, powered by GraphQL and Encore. lang: go --- Encore has great support for GraphQL with its type-safe approach to building APIs. Encore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)). The best way to use GraphQL with Encore is using [gqlgen](https://gqlgen.com/), which has similar goals as Encore (type-safe APIs, minimal boilerplate, code generation, etc). The final code will look like this:
## 1. Create your Encore application This tutorial uses the [REST API](/docs/go/tutorials/rest-api) tutorial as a starting point. You can either follow that tutorial first, or you can create a new Encore application using the `url-shortener` template by running: ```shell $ encore app create --example=url-shortener ``` ## 2. Initialize gqlgen To get started, initialize gqlgen by creating a `tools.go` file in the application root: ```go -- tools.go -- //go:build tools package tools import ( _ "github.com/99designs/gqlgen" _ "github.com/99designs/gqlgen/graphql/introspection" ) ``` Then run `go mod tidy` to download the dependencies. Next, create a `gqlgen.yml` file in the application root containing: ``` -- gqlgen.yml -- # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - graphql/*.graphqls # Where should the generated server code go? exec: filename: graphql/generated/generated.go package: generated # Where should any generated models go? model: filename: graphql/model/models_gen.go package: model # Where should the resolver implementations go? resolver: layout: follow-schema dir: graphql package: graphql # gqlgen will search for any type names in the schema in these go packages # if they match it will use them, otherwise it will generate them. autobind: - "encore.app/url" # This section declares type mapping between the GraphQL and go type systems # # The first line in each type will be used as defaults for resolver arguments and # modelgen, the others will be allowed when binding to fields. Configure them to # your liking models: ID: model: - github.com/99designs/gqlgen/graphql.ID - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Int: model: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 ``` ## 3. Create Encore service Now it's time to create our Encore service that will provide the GraphQL API. First generate the gqlgen boilerplate: ```shell $ mkdir -p graphql/generated graphql/model $ echo "package model" > graphql/model/model.go $ go run github.com/99designs/gqlgen generate ``` This will create a bunch of files in the `graphql` directory. Next, create a `graphql/service.go` file containing: ```go -- graphql/service.go -- // Service graphql exposes a GraphQL API. package graphql import ( "net/http" "encore.app/graphql/generated" "encore.dev" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" ) //go:generate go run github.com/99designs/gqlgen generate //encore:service type Service struct { srv *handler.Server playground http.Handler } func initService() (*Service, error) { srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{}})) pg := playground.Handler("GraphQL Playground", "/graphql") return &Service{srv: srv, playground: pg}, nil } //encore:api public raw path=/graphql func (s *Service) Query(w http.ResponseWriter, req *http.Request) { s.srv.ServeHTTP(w, req) } //encore:api public raw path=/graphql/playground func (s *Service) Playground(w http.ResponseWriter, req *http.Request) { // Disable playground in production if encore.Meta().Environment.Type == encore.EnvProduction { http.Error(w, "Playground disabled", http.StatusNotFound) return } s.playground.ServeHTTP(w, req) } ``` This creates an Encore service that exposes the `/graphql` and `/graphql/playground` endpoints. It also adds a `//go:generate` directive that lets you re-run the gqlgen code generation by running `go generate ./graphql`. ## 4. Add GraphQL schema Now it's time to define the GraphQL schema. Create a `graphql/schema.graphqls` file containing: ``` -- graphql/url.graphqls -- type Query { urls: [URL!]! get(id: ID!): URL! } type Mutation { shorten(input: String!): URL! } type URL { id: ID! # shortened id url: String! # full URL } ``` Then, re-run the code generation to generate the resolver stubs: ```shell $ go generate ./graphql ``` The stubs will be written to `graphql/url.resolvers.go` and will contain a bunch of unimplemented resolver methods that look something like this: ```go // Shorten is the resolver for the shorten field. func (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) { panic(fmt.Errorf("not implemented: Shorten - shorten")) } ``` ## 5. Implement resolvers Now, modify the resolvers to call the `url` service. Since the GraphQL API uses the same types (thanks to the `autobind` directive in `gqlgen.yml`) as the Encore API exposes, we can just call the endpoints directly. Implement the resolvers in `graphql/url.resolvers.go` like this: ```go -- graphql/url.resolvers.go -- // Shorten is the resolver for the shorten field. func (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) { return url.Shorten(ctx, &url.ShortenParams{URL: input}) } // Urls is the resolver for the urls field. func (r *queryResolver) Urls(ctx context.Context) ([]*url.URL, error) { resp, err := url.List(ctx) if err != nil { return nil, err } return resp.URLs, nil } // Get is the resolver for the get field. func (r *queryResolver) Get(ctx context.Context, id string) (*url.URL, error) { return url.Get(ctx, id) } ``` As you can see, the resolvers are just thin wrappers around the Encore API endpoints themselves. ## 6. Trying it out With that, the GraphQL API is done! Try it out by running `encore run` and opening up [the playground](http://localhost:4000/graphql/playground). Enter the query: ```graphql mutation { shorten(input: "https://encore.dev") { id } } ``` You should get back an id like `MnTWA8Jo`. Pass the id you got (it will be something different) to a `get` query: ```graphql query { get(id: "") { url } } ``` And you should get back `https://encore.dev`. ## 7. Deploy ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/go/self-host/configure-infra) your Docker image. 🥐 Build a Docker image by running `encore build docker graphql:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Deploy to Encore Cloud Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes The final step before you deploy is to commit all changes to the project repo. Push your changes and deploy your application to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment. ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ## Conclusion We've now built a GraphQL API gateway that forwards requests to the application's underlying Encore services in a type-safe way with minimal boilerplate. Note that the concepts discussed here are general and can be easily adapted to any GraphQL schema. Whenever you make a change to the schema or configuration, re-run `go generate ./graphql` to regenerate the GraphQL boilerplate. And for more information on how to use `gqlgen`, see the [gqlgen documentation](https://gqlgen.com/). ================================================ FILE: docs/go/tutorials/incident-management-tool.md ================================================ --- seotitle: How to build an Incident Management Tool with Go seodesc: Learn how to build an incident management tool like PagerDuty using Go and Encore. Get a working app running in the cloud in 30 minutes! title: Building an Incident Management Tool subtitle: Set up your own PagerDuty from zero-to-production in just 30 minutes social_card: /assets/docs/incident-og-image.png lang: go --- In this tutorial, we're going to walk through together how to build our very own Incident Management Tool like [Incident.io](https://incident.io) or [PagerDuty](https://pagerduty.com). We can then have our own on call schedule that can be rotated between many users, and have incidents come and be assigned according to the schedule! ![Slack Incident Management Tool](/assets/docs/incident-slack-example.png "Incident Management Tool") In about 30 minutes, your application will be able to support: - Creating users, as well as schedules for when users will be on call - Creating incidents, and reminders for unacknowledged incidents on Slack every 10 minutes - Auto-assign incidents which are unassigned (when the next user is on call) _ Sounds good? Let's dig in! _ Or if you'd rather watch a video of this tutorial, you can do that below. View full project on [GitHub](https://github.com/encoredev/example-app-oncall) To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create your Encore application 🥐 Create a new Encore application by running `encore app create`, select `Empty app` as the template and name it `oncall-tutorial`. ## 2. Integrate with Slack 🥐 Follow [this guide to create your own Incoming Webhook](https://api.slack.com/messaging/webhooks) for your Slack workspace. Incoming webhooks cannot read messages, and can only post to a specific channel of your choice. Creating a Slack app 🥐 Once you have your Webhook URL which starts with `https://hooks.slack.com/services/...` then copy and paste that and run the following commands to save these as secrets. We recommend having a different webhook/channel for development and production. ```shell $ encore secret set --type dev,local,pr SlackWebhookURL $ encore secret set --type prod SlackWebhookURL ``` 🥐 Next, let's create our `slack` service that contains the logic for calling the Webhook URL in order to post notifications to our Slack. To do this we need to implement our code in `slack/slack.go`: ```go // Service slack calls a webhook to post notifications to Slack. package slack import ( "bytes" "context" "encoding/json" "encore.dev/beta/errs" "io" "net/http" ) type NotifyParams struct { Text string `json:"text"` } //encore:api private func Notify(ctx context.Context, p *NotifyParams) error { eb := errs.B() reqBody, err := json.Marshal(p) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, "POST", secrets.SlackWebhookURL, bytes.NewReader(reqBody)) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { body, _ := io.ReadAll(resp.Body) return eb.Code(errs.Unavailable).Msgf("notify slack: %s: %s", resp.Status, body).Err() } return nil } var secrets struct { SlackWebhookURL string } ``` The `slack` service can be reused across any of your Encore apps. All you need is the `slack/slack.go` code and the `SlackWebhookURL` secret to be defined. Then you can call the following method signature anywhere in your app: ```go slack.Notify(context, &slack.NotifyParams{ Text: "Send a Slack notification" }) ``` ## 3. Create a service to manage users With an Incident Management Tool (or usually any tool, for that matter) we need a service for users. This will allow us to figure out who we should assign incoming incidents to! To get started, we need to create a `users` service with the following resources: | # | Type | Description / Filename | | --- | ------------------------------------ | ---------------------------------------------------------------------------------------- | | #1 | SQL Migration | Our PostgreSQL schema for scheduling data
`users/migrations/1_create_users.up.sql` | | #2 | HTTP Endpoint
`POST /users` | Create a new User
`users/users.go` | | #3 | HTTP Endpoint
`GET /users/:id` | Get an existing User
`users/users.go` | With #1, let's design our database schema for a User in our system. For now let's store a first and last name as well as a Slack handle in case we need to notify them about any incidents which may have been assigned to them or acknowledged by them. 🥐 Let's create our migration file in `users/migrations/1_create_users.up.sql`: ```sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, slack_handle VARCHAR(255) NOT NULL ); ``` 🥐 Then, we need to write our code to implement the HTTP endpoints listed in #2 (for creating a user) and #3 (for listing a user) belonging in `users/users.go`. Let's split them out into three sections: our structs (i.e. data models) and methods. ```go // Service users manages users and assigns incidents. package users import ( "context" "encore.dev/storage/sqldb" ) // This is a Go struct representing our PostgreSQL schema for `users` type User struct { Id int32 FirstName string LastName string SlackHandle string } // Define a database named 'users', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. var db = sqldb.NewDatabase("users", sqldb.DatabaseConfig{ Migrations: "./migrations", }) //encore:api public method=POST path=/users func Create(ctx context.Context, params CreateParams) (*User, error) { user := User{} err := db.QueryRow(ctx, ` INSERT INTO users (first_name, last_name, slack_handle) VALUES ($1, $2, $3) RETURNING id, first_name, last_name, slack_handle `, params.FirstName, params.LastName, params.SlackHandle).Scan(&user.Id, &user.FirstName, &user.LastName, &user.SlackHandle) if err != nil { return nil, err } return &user, nil } // This is what JSON params our POST /users endpoint will accept type CreateParams struct { FirstName string LastName string SlackHandle string } //encore:api public method=GET path=/users/:id func Get(ctx context.Context, id int32) (*User, error) { user := User{} err := db.QueryRow(ctx, ` SELECT id, first_name, last_name, slack_handle FROM users WHERE id = $1 `, id).Scan(&user.Id, &user.FirstName, &user.LastName, &user.SlackHandle) if err != nil { return nil, err } return &user, nil } ``` 🥐 Next, type `encore run` in your Terminal and in a separate window run the command under **cURL Request** (feel free to edit the values!) to create our first user: ```bash curl -d '{ "FirstName":"Katy", "LastName":"Smith", "SlackHandle":"katy" }' http://localhost:4000/users # Example JSON response # { # "Id":1, # "FirstName":"Katy", # "LastName":"Smith", # "SlackHandle":"katy" # } ``` Fantastic, we now have a user system in our app! Next we need a list of start and end times of each scheduled rotation so we know who to assign incoming incidents to (as well as notify them on Slack!) ## 4. Add scheduling A good incident management tool should be able to spread the workload of diagnosing and fixing incidents across multiple users in a team. Being able to know who the correct person to assign an incident to is very important; our incidents might not get resolved quickly otherwise! In order to achieve this, let's create a new service called `schedules`: | # | Type | Description / Filename | | --- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ | | #1 | SQL Migration | Our PostgreSQL schema for user data
`schedules/migrations/1_create_schedules.up.sql` | | #2 | HTTP Endpoint
`GET /schedules` | Get list of schedules between time range
`schedules/schedules.go` | | #3 | HTTP Endpoint
`POST /users/:id/schedules` | Create a new Schedule
`schedules/schedules.go` | | #4 | HTTP Endpoint
`GET /scheduled/:timestamp` | Get Schedule at specific time
`schedules/schedules.go` | For the SQL migration in #1, we need to create both a table and an index. For every rotation let's need a new entry containing the user who it is for as well as the start and end times of the scheduled rotation. 🥐 Let's create our migration file in `schedules/migrations/1_create_schedules.up.sql`: ```sql CREATE TABLE schedules ( id BIGSERIAL PRIMARY KEY, user_id INTEGER NOT NULL, start_time TIMESTAMP NOT NULL, end_time TIMESTAMP NOT NULL ); CREATE INDEX schedules_range_index ON schedules (start_time, end_time); ``` Table indexes are used to optimize lookups without having to search every row in the table. In this case, looking up rows against both `start_time` and `end_time` will be faster _with the index_ as the dataset grows. [Learn more about PostgreSQL indexes here](https://www.tutorialspoint.com/postgresql/postgresql_indexes.htm). 🥐 Next, let's implement the HTTP endpoints for #2 (listing schedules), #3 (creating a schedule) and #4 (getting the schedule/user at a specific time) in `schedules/schedules.go`: ```go // Service schedules implements schedules to answer who should be assigned to an incident. package schedules import ( "context" "errors" "time" "encore.app/users" "encore.dev/beta/errs" "encore.dev/storage/sqldb" ) // Define a database named 'schedules', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. var db = sqldb.NewDatabase("schedules", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // This struct holds multiple Schedule structs type Schedules struct { Items []Schedule } // This is a Go struct representing our PostgreSQL schema for `schedules` type Schedule struct { Id int32 User users.User Time TimeRange } // As we use time ranges in our schedule, we created a generic TimeRange struct type TimeRange struct { Start time.Time End time.Time } //encore:api public method=POST path=/users/:userId/schedules func Create(ctx context.Context, userId int32, timeRange TimeRange) (*Schedule, error) { eb := errs.B().Meta("userId", userId, "timeRange", timeRange) // check for existing overlapping schedules if schedule, err := ScheduledAt(ctx, timeRange.Start.String()); schedule != nil && err == nil { return nil, eb.Code(errs.InvalidArgument).Cause(err).Msg("schedule already exists within this start timestamp").Err() } if schedule, err := ScheduledAt(ctx, timeRange.End.String()); schedule != nil && err == nil { return nil, eb.Code(errs.InvalidArgument).Cause(err).Msg("schedule already exists within this end timestamp").Err() } // check user exists user, err := users.Get(ctx, userId) if err != nil { return nil, eb.Code(errs.Unavailable).Cause(err).Msg("failed to get user").Err() } schedule := Schedule{User: *user, Time: TimeRange{}} err = db.QueryRow( ctx, `INSERT INTO schedules (user_id, start_time, end_time) VALUES ($1, $2, $3) RETURNING id, start_time, end_time`, userId, timeRange.Start, timeRange.End, ).Scan(&schedule.Id, &schedule.Time.Start, &schedule.Time.End) if err != nil { return nil, eb.Code(errs.Unavailable).Cause(err).Msg("failed to insert schedule").Err() } return &schedule, nil } //encore:api public method=GET path=/scheduled func ScheduledNow(ctx context.Context) (*Schedule, error) { return scheduled(ctx, time.Now()) } //encore:api public method=GET path=/scheduled/:timestamp func ScheduledAt(ctx context.Context, timestamp string) (*Schedule, error) { eb := errs.B().Meta("timestamp", timestamp) parsedtime, err := time.Parse(time.RFC3339, timestamp) if err != nil { return nil, eb.Code(errs.InvalidArgument).Msg("timestamp is not in a valid format").Err() } return scheduled(ctx, parsedtime) } func scheduled(ctx context.Context, timestamp time.Time) (*Schedule, error) { eb := errs.B().Meta("timestamp", timestamp) schedule, err := RowToSchedule(ctx, db.QueryRow(ctx, ` SELECT id, user_id, start_time, end_time FROM schedules WHERE start_time <= $1 AND end_time >= $1 `, timestamp.UTC())) if errors.Is(err, db.ErrNoRows) { return nil, eb.Code(errs.NotFound).Msg("no schedule found").Err() } if err != nil { return nil, err } return schedule, nil } //encore:api public method=GET path=/schedules func ListByTimeRange(ctx context.Context, timeRange TimeRange) (*Schedules, error) { rows, err := db.Query(ctx, ` SELECT id, user_id, start_time, end_time FROM schedules WHERE start_time >= $1 AND end_time <= $2 ORDER BY start_time ASC `, timeRange.Start, timeRange.End) if err != nil { return nil, err } defer rows.Close() var schedules []Schedule for rows.Next() { schedule, err := RowToSchedule(ctx, rows) if err != nil { return nil, err } schedules = append(schedules, *schedule) } return &Schedules{Items: schedules}, nil } //encore:api public method=DELETE path=/schedules func DeleteByTimeRange(ctx context.Context, timeRange TimeRange) (*Schedules, error) { schedules, err := ListByTimeRange(ctx, timeRange) if err != nil { return nil, err } _, err = db.Exec(ctx, `DELETE FROM schedules WHERE start_time >= $1 AND end_time <= $2`, timeRange.Start, timeRange.End) if err != nil { return nil, err } return schedules, err } // Helper function to convert a Row object to to Schedule func RowToSchedule(ctx context.Context, row interface { Scan(dest ...interface{}) error }) (*Schedule, error) { var schedule = &Schedule{Time: TimeRange{}} var userId int32 err := row.Scan(&schedule.Id, &userId, &schedule.Time.Start, &schedule.Time.End) if err != nil { return nil, err } user, err := users.Get(ctx, userId) if err != nil { return nil, err } schedule.User = *user return schedule, nil } ``` 🥐 Next, type `encore run` in your Terminal and in a separate window run the command under **cURL Request** (also feel free to edit the values!) to create our first schedule against the user we created earlier: ```bash curl -d '{ "Start":"2023-11-28T10:00:00Z", "End":"2023-11-30T10:00:00Z" }' "http://localhost:4000/users/1/schedules" # Example JSON response # { # "Id":1, # "User":{ # "Id":1, # "FirstName":"Katy", # "LastName":"Smith", # "SlackHandle":"katy" # }, # "Time":{ # "Start":"2023-11-28T10:00:00Z", # "End":"2023-11-30T10:00:00Z" # } # } ``` ## 5. Create a service to manage incidents So we have users, and we know who is available to be notified (or if nobody should be notified) at any given time with the introduction of the `schedules` service. The only thing we're missing is the ability to report, assign and acknowledge incidents! The flow we're going to implement is: an incoming incident will arrive, let's either unassign or auto-assign it based on the `schedules` service, and incidents have to be acknowledged. If they are not acknowledged, they will continue to be notified on Slack every 10 minutes until it has. To start with, we need to create a new `incidents` service with the following resources: | # | Type | Description / Filename | | --- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | #1 | SQL Migration | Our PostgreSQL schema for storing incidents
`incidents/migrations/1_create_incidents.up.sql` | | #2 | HTTP Endpoint
`GET /incidents` | Get list of all unacknowledged incidents
`incidents/incidents.go` | | #3 | HTTP Endpoint
`PUT /incidents/:id/acknowledge` | Acknowledge an incident
`incidents/incidents.go` | | #4 | HTTP Endpoint
`GET /scheduled/:timestamp` | Get
`incidents/incidents.go` | For the SQL migration in #1, we need to create the table for our incidents. We need to have a one-to-many relationship between an user and an incident. That is, an incident can only be assigned to a single user but a single user can be assigned to many incidents. 🥐 Let's create our migration file in `incidents/migrations/1_create_incidents.up.sql`: ```sql CREATE TABLE incidents ( id BIGSERIAL PRIMARY KEY, assigned_user_id INTEGER, body TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), acknowledged_at TIMESTAMP ); ``` 🥐 Next, our code belonging in `incidents/incidents.go` for being able to support incidents is below: ```go // Service incidents reports, assigns and acknowledges incidents. package incidents import ( "context" "encore.app/schedules" "encore.app/slack" "encore.app/users" "encore.dev/beta/errs" "encore.dev/storage/sqldb" "fmt" "time" ) // Define a database named 'incidents', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. var db = sqldb.NewDatabase("incidents", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // This struct holds multiple Incidents structs type Incidents struct { Items []Incident } // This is a Go struct representing our PostgreSQL schema for `incidents` type Incident struct { Id int32 Body string CreatedAt time.Time Acknowledged bool AcknowledgedAt *time.Time Assignee *users.User } //encore:api public method=GET path=/incidents func List(ctx context.Context) (*Incidents, error) { rows, err := db.Query(ctx, ` SELECT id, assigned_user_id, body, created_at, acknowledged_at FROM incidents WHERE acknowledged_at IS NULL `) if err != nil { return nil, err } return RowsToIncidents(ctx, rows) } //encore:api public method=PUT path=/incidents/:id/acknowledge func Acknowledge(ctx context.Context, id int32) (*Incident, error) { eb := errs.B().Meta("incidentId", id) rows, err := db.Query(ctx, ` UPDATE incidents SET acknowledged_at = NOW() WHERE acknowledged_at IS NULL AND id = $1 RETURNING id, assigned_user_id, body, created_at, acknowledged_at `, id) if err != nil { return nil, err } incidents, err := RowsToIncidents(ctx, rows) if err != nil { return nil, err } if incidents.Items == nil { return nil, eb.Code(errs.NotFound).Msg("no incident found").Err() } incident := &incidents.Items[0] _ = slack.Notify(ctx, &slack.NotifyParams{ Text: fmt.Sprintf("Incident #%d assigned to %s %s <@%s> has been acknowledged:\n%s", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body), }) return incident, err } //encore:api public method=POST path=/incidents func Create(ctx context.Context, params *CreateParams) (*Incident, error) { // check who is on-call schedule, err := schedules.ScheduledNow(ctx) incident := Incident{} if schedule != nil { incident.Assignee = &schedule.User } var row *db.Row if schedule != nil { // Someone is on-call row = db.QueryRow(ctx, ` INSERT INTO incidents (assigned_user_id, body) VALUES ($1, $2) RETURNING id, body, created_at `, &schedule.User.Id, params.Body) } else { // Nobody is on-call row = db.QueryRow(ctx, ` INSERT INTO incidents (body) VALUES ($1) RETURNING id, body, created_at `, params.Body) } if err = row.Scan(&incident.Id, &incident.Body, &incident.CreatedAt); err != nil { return nil, err } var text string if incident.Assignee != nil { text = fmt.Sprintf("Incident #%d created and assigned to %s %s <@%s>\n%s", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body) } else { text = fmt.Sprintf("Incident #%d created and unassigned\n%s", incident.Id, incident.Body) } _ = slack.Notify(ctx, &slack.NotifyParams{Text: text}) return &incident, nil } type CreateParams struct { Body string } // Helper to take a db.Rows instance and convert it into a list of Incidents func RowsToIncidents(ctx context.Context, rows *db.Rows) (*Incidents, error) { eb := errs.B() defer rows.Close() var incidents []Incident for rows.Next() { var incident = Incident{} var assignedUserId *int32 if err := rows.Scan(&incident.Id, &assignedUserId, &incident.Body, &incident.CreatedAt, &incident.AcknowledgedAt); err != nil { return nil, eb.Code(errs.Unknown).Msgf("could not scan: %v", err).Err() } if assignedUserId != nil { user, err := users.Get(ctx, *assignedUserId) if err != nil { return nil, eb.Code(errs.NotFound).Msgf("could not retrieve user for incident %v", assignedUserId).Err() } incident.Assignee = user } incident.Acknowledged = incident.AcknowledgedAt != nil incidents = append(incidents, incident) } return &Incidents{Items: incidents}, nil } ``` Fantastic! We have an _almost_ working application. The main two things we're missing are: 1. For unacknowledged incidents, we need to post a reminder on Slack every 10 minutes until they have been acknolwedged. 2. Whenever a user is currently on call, we should assign all previously unassigned incidents to them. 🥐 To achieve this, we'll need to create two [Cron Jobs](http://localhost:3000/docs/develop/cron-jobs) which thankfully Encore makes incredibly simple. So let's go ahead and create the first one for reminding us every 10 minutes of incidents we haven't acknowledged. Go ahead and add the code below to our `incidents/incidents.go` file: ```go // Track unacknowledged incidents var _ = cron.NewJob("unacknowledged-incidents-reminder", cron.JobConfig{ Title: "Notify on Slack about incidents which are not acknowledged", Every: 10 * cron.Minute, Endpoint: RemindUnacknowledgedIncidents, }) //encore:api private func RemindUnacknowledgedIncidents(ctx context.Context) error { incidents, err := List(ctx) // we never query for acknowledged incidents if err != nil { return err } if incidents == nil { return nil } var items = []string{"These incidents have not been acknowledged yet. Please acknowledge them otherwise you will be reminded every 10 minutes:"} for _, incident := range incidents.Items { var assignee string if incident.Assignee != nil { assignee = fmt.Sprintf("%s %s (<@%s>)", incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle) } else { assignee = "Unassigned" } items = append(items, fmt.Sprintf("[%s] [#%d] %s", assignee, incident.Id, incident.Body)) } if len(incidents.Items) > 0 { _ = slack.Notify(ctx, &slack.NotifyParams{Text: strings.Join(items, "\n")}) } return nil } ``` And for our second cronjob, when someone goes on call we need to automatically assign the previously unassigned incidents to them. We don't have a HTTP endpoint for assigning incidents so we need to implement a `PUT /incidents/:id/assign` endpoint. 🥐 So let's also add that endpoint as well as the cronjob code to our `incidents/incidents.go` file: ```go //encore:api public method=PUT path=/incidents/:id/assign func Assign(ctx context.Context, id int32, params *AssignParams) (*Incident, error) { eb := errs.B().Meta("params", params) rows, err := db.Query(ctx, ` UPDATE incidents SET assigned_user_id = $1 WHERE acknowledged_at IS NULL AND id = $2 RETURNING id, assigned_user_id, body, created_at, acknowledged_at `, params.UserId, id) if err != nil { return nil, err } incidents, err := RowsToIncidents(ctx, rows) if err != nil { return nil, err } if incidents.Items == nil { return nil, eb.Code(errs.NotFound).Msg("no incident found").Err() } incident := &incidents.Items[0] _ = slack.Notify(ctx, &slack.NotifyParams{ Text: fmt.Sprintf("Incident #%d is re-assigned to %s %s <@%s>\n%s", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body), }) return incident, err } type AssignParams struct { UserId int32 } var _ = cron.NewJob("assign-unassigned-incidents", cron.JobConfig{ Title: "Assign unassigned incidents to user on-call", Every: 1 * cron.Minute, Endpoint: AssignUnassignedIncidents, }) //encore:api private func AssignUnassignedIncidents(ctx context.Context) error { // if this fails, we don't have anyone on call so let's skip this schedule, err := schedules.ScheduledNow(ctx) if err != nil { return err } incidents, err := List(ctx) // we never query for acknowledged incidents if err != nil { return err } for _, incident := range incidents.Items { if incident.Assignee != nil { continue // this incident has already been assigned } _, err := Assign(ctx, incident.Id, &AssignParams{UserId: schedule.User.Id}) if err == nil { rlog.Info("OK assigned unassigned incident", "incident", incident, "user", schedule.User) } else { rlog.Error("FAIL to assign unassigned incident", "incident", incident, "user", schedule.User, "err", err) return err } } return nil } ``` 🥐 Next, call `encore run` in your Terminal and in a separate window run the command under **cURL Request** (also feel free to edit the values!) to trigger our first incident. Most likely we won't have an assigned user unless you have scheduled a time that overlaps with right now in the last cURL request for creating a schedule: ```bash curl -d '{ "Body":"An unexpected error happened on example-website.com on line 38. It needs addressing now!" }' http://localhost:4000/incidents # Example JSON response # { # "Id":1, # "Body":"An unexpected error happened on example-website.com on line 38. It needs addressing now!", # "CreatedAt":"2022-09-28T15:09:00Z", # "Acknowledged":false, # "AcknowledgedAt":null, # "Assignee":null # } ``` ## 6. Try your app and deploy Congratulations! Our application looks ready for others to try - we have our `users`, `schedules` `incidents` and `slack` services along with 3 database tables and 2 cronjobs. Even better that all of the deployment and maintenance is taken care by Encore! 🥐 To try out your application, type `encore run` in your Terminal and run the following cURL commands: ```bash # Step 1: Create a User and copy the User ID to your clipboard curl -d '{ "FirstName":"Katy", "LastName":"Smith", "SlackHandle":"katy" }' http://localhost:4000/users # Step 2: Create a schedule for the user we just created curl -d '{ "Start":"2022-09-28T10:00:00Z", "End":"2022-09-29T10:00:00Z" }' "http://localhost:4000/users/1/schedules" # Step 3: Trigger an incident curl -d '{ "Body":"An unexpected error happened on example-website.com on line 38. It needs addressing now!" }' http://localhost:4000/incidents # Step 4: Acknowledge the Incident curl -X PUT "http://localhost:4000/incidents/1/acknowledge" ``` And if you don't acknowledge incoming incidents on Step 4, you will be reminded on Slack every 10 minutes: ![Being reminded on Slack about unacknowledged incidents](/assets/docs/incident-slack-reminder-example.png) ### Deploy to the cloud 🥐 Push your changes and deploy your application to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment. ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ### Architecture Diagram Take a look at the [Encore Flow](/docs/go/observability/encore-flow) diagram that was automatically generated for our new application too! ![Being reminded on Slack about unacknowledged incidents](/assets/docs/incident-flow-diagram.png) ### GitHub Repository 🥐 Check out the `example-app-oncall` repository on GitHub for this example which includes additional features and tests: [https://github.com/encoredev/example-app-oncall](https://github.com/encoredev/example-app-oncall) Alternatively, you can clone our example application by running this in your Terminal: ```shell $ encore app create --example https://github.com/encoredev/example-app-oncall ``` ### Feedback 🥐 We'd love to hear your thoughts about this tutorial and learn about what you're building next. Let us know by [tweeting your experience](https://twitter.com/encoredotdev), blog about it, or talk to us about it on [Discord](https://encore.dev/discord). ================================================ FILE: docs/go/tutorials/meeting-notes.mdx ================================================ --- title: Building a Meeting Notes app subtitle: Learn how to set up a web app backend (with database) in less than 100 lines of code seotitle: How to build a Meeting Notes app in Go & React seodesc: Learn how to set up a free & production-ready web app backend in Go (with database) in less than 100 lines lang: go --- In this tutorial, we will create a backend in less than 100 lines of code. The backend will: - Store data in a cloud SQL database - Make API calls to a third-party service - Deploy to the cloud and be publicly available The example app we will build is a markdown meeting notes app BUT it’s trivial to replace the specifics if you have another idea in mind (again, less than 100 lines of code). **[Demo version of the app](https://encoredev.github.io/meeting-notes)** This is the end result:
## Create your Encore application Create a new app from the meeting-notes example. This will start you off with everything described in this tutorial: ```shell $ encore app create my-app --example=meeting-notes ``` Before running the project locally, make sure you have [Docker](https://www.docker.com/products/docker-desktop/) installed and running. Docker is needed for Encore to create databases for locally running projects. Also, if you want to try the photo search functionality then you will need an API key from [pexels.com/api/](https://www.pexels.com/api/) (more on that below) To run the backend locally: ```shell $ cd you-app-name # replace with the app name you picked $ encore run ``` You should see the following: That means your local development backend is up and running! Encore takes care of setting up all the necessary infrastructure for your application, including databases. Encore also starts the local development dashboard which is a tool to help you move faster when you're developing new features. To start the front-end, run the following commands in another terminal window: ```shell $ cd you-app-name/frontend $ npm install $ npm run dev ``` You can now open http://localhost:5173/example-meeting-notes/ in your browser 🔥 ## Storing and retrieving from an SQL database Let's take a look at the backend code. There are essentially only three files of interest, let's start by looking at `note.go`. This file contains two endpoints and one interface, all standard Go code except for a few lines specific to Encore. The `Note` type represents our data structure: ```go type Note struct { ID string `json:"id"` Text string `json:"text"` CoverURL string `json:"cover_url"` } ``` Every note will have an `ID` (uuid that is created on the frontend), `Text` (Markdown text content), and `CoverURL` (background image URL). The `SaveNote` function handles storing a meeting note: ```go //encore:api public method=POST path=/note func SaveNote(ctx context.Context, note *Note) (*Note, error) { // Save the note to the database. // If the note already exists (i.e. CONFLICT), we update the notes text and the cover URL. _, err := sqldb.Exec(ctx, ` INSERT INTO note (id, text, cover_url) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET text=$2, cover_url=$3 `, note.ID, note.Text, note.CoverURL) // If there was an error saving to the database, then we return that error. if err != nil { return nil, err } // Otherwise, we return the note to indicate that the save was successful. return note, nil } ``` The comment above the function tells Encore that this is a public endpoint that should be reachable by POST on `/note`. The second argument to the function (`Note`) is the POST body and the function returns a `Note` and an `error` (a `nil` error means a 200 response). The `GetNote` function takes care of fetching a meeting note from our database given an `id`: ```go //encore:api public method=GET path=/note/:id func GetNote(ctx context.Context, id string) (*Note, error) { note := &Note{ID: id} // We use the note ID to query the database for the note's text and cover URL. err := sqldb.QueryRow(ctx, ` SELECT text, cover_url FROM note WHERE id = $1 `, id).Scan(¬e.Text, ¬e.CoverURL) // If the note doesn't exist, we return an error. if err != nil { return nil, err } // Otherwise, we return the note. return note, nil } ``` Here we have a public GET endpoint with a dynamic path parameter which is the `id` of the meeting note to fetch. The second argument, in this case, is the dynamic path parameter, a request to this endpoint will look like `/note/123-abc` where `id` will be set to `123-abc`. Both `SaveNote` and `GetNote` makes use of a SQL database table named `note`, let's look at how that table is defined. ## Defining a SQL database To create a SQL database using Encore we first create a folder named `migrations` and inside that folder a migration file named `1_create_tables.up.sql`. The file name is important (it must look something like `1_name.up.sql`). Our migration file is only five lines long and looks like this: ```sql CREATE TABLE note ( id TEXT PRIMARY KEY, text TEXT, cover_url TEXT ); ``` When recognizing this file, Encore will create a `note` table with three columns `id`, `text` and `cover_url`. The `id` is the primary key, used to identify specific meeting notes. ## Making requests to a third-party API Let's look at how we can use an Encore endpoint to proxy requests to a third-party service (in this example photo service [pexels.com](http://www.pexels.com/) but the idea would be the same for any other third-party API). The file `pexels.go` only has one endpoint, `SearchPhoto`: ```go //encore:api public method=GET path=/images/:query func SearchPhoto(ctx context.Context, query string) (*SearchResponse, error) { // Create a new http client to proxy the request to the Pexels API. URL := "https://api.pexels.com/v1/search?query=" + query client := &http.Client{} req, _ := http.NewRequest("GET", URL, nil) // Add authorization header to the req with the API key. req.Header.Set("Authorization", secrets.PexelsApiKey) // Make the request, and close the response body when we're done. res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode >= 400 { return nil, fmt.Errorf("Pexels API error: %s", res.Status) } // Decode the data into the searchResponse struct. var searchResponse *SearchResponse err = json.NewDecoder(res.Body).Decode(&searchResponse) if err != nil { return nil, err } return searchResponse, nil } ``` Again a GET endpoint with a dynamic path parameter which this time represents the query text we want to send to the Pexels API. The type we use to decode the response from the Pexels API looks like this: ```go type SearchResponse struct { Photos []struct { Id int `json:"id"` Src struct { Medium string `json:"medium"` Landscape string `json:"landscape"` } `json:"src"` Alt string `json:"alt"` } `json:"photos"` } ``` We get a lot more data from Pexels but here we only pick the fields that we want to propagate to our frontend. [Pexels API](https://www.pexels.com/api/) requires an API key, as most open APIs do. The API key is added as a header to the requests (from the `SearchPhoto` function above): ```go req.Header.Set("Authorization", secrets.PexelsApiKey) ``` Here we could have hardcoded the API key but that would have made it readable for everyone with access to our repo. Instead, we made use of Encore's built-in [secrets management](https://encore.dev/docs/go/primitives/secrets). To set this secret, run the following command in your project folder and follow the prompt: ```shell encore secret set --type dev,prod,local,pr PexelsApiKey ``` ## Creating a request client Encore is able to generate frontend [request clients](https://encore.dev/docs/go/cli/client-generation) (TypeScript or JavaScript). This means that you do not need to manually keep the request/response objects in sync on the frontend, huge time saver. To generate a client run: ```shell $ encore gen client --output=./src/client.ts --env= ``` You are going to want to run this command quite often (whenever you make a change to your endpoints) so having it as an `npm` script is a good idea: ```json { ... "scripts": { ... "generate-client:staging": "encore gen client --output=./src/client.ts --env=staging", "generate-client:local": "encore gen client --output=./src/client.ts --env=local" }, } ``` After that you are ready to use the request client in your code. Here is an example of calling the `GetNote` endpoint: ```tsx import Client, { Environment, Local } from "src/client.ts"; // Making request to locally running backend... const client = new Client(Local); // or to a specific deployed environment const client = new Client(Environment("staging")); // Calling APIs as typesafe functions 🌟 const response = await client.note.GetNote("note-uuid"); console.log(response.id); console.log(response.cover_url); console.log(response.text); ``` ## Deploying the backend to the cloud It’s deploy time! To get your backend deployed in the cloud all you need to do is to commit your code and push it to the `encore` remote: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` When running `git push encore` you will get a link to the Encore Cloud dashboard where you can view the deploy for your app and after about a minute you have a backend running in the cloud ☁️ ## Hosting the frontend The frontend can be deployed to any static site hosting platform. The example project is pre-configured to deploy the frontend to [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site). Take a look at `.github/workflows/node.yml` to see the GitHub actions workflow being triggered on new commits to the repo: ```yaml name: Build and Deploy on: [push] permissions: contents: write jobs: build-and-deploy: concurrency: ci-${{ github.ref }} runs-on: ubuntu-latest defaults: run: working-directory: frontend steps: - name: Checkout 🛎️ uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "16.15.1" - name: Install and Build 🔧 run: | npm install npm run build - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@v4.3.3 with: branch: gh-pages folder: frontend/dist ``` The interesting part is towards the bottom where we build the frontend code and make use of the [github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) step to automatically make a new commit with the compiled frontend code to a `gh-pages` branch. **Steps to deploy to GitHub pages:** 1. Create a repo on GitHub 2. In the `vite.config.js` file, set the `base` property to the name of your repo: ```yaml base: "/my-repo-name/", ``` 1. Push your code to GitHub and wait for the GitHub actions workflow to finish. 2. Go to _Settings_ → _Pages_ for your repo on GitHub and set _Branch_ to `gh-pages`. ## Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ## Wrapping up You’ve learned how to build and deploy a Go backend using Encore, store data in an SQL database, and make API calls to an external service. All of this in under 100 lines of code. ================================================ FILE: docs/go/tutorials/rest-api.mdx ================================================ --- seotitle: How to build a REST API seodesc: Learn how to build and ship a REST API in just a few minutes, Encore.go. Go from zero to running API with this tutorial. title: Building a REST API subtitle: Learn how to build a URL shortener with a REST API and PostgreSQL database lang: go --- In this tutorial you will create a REST API for a URL Shortener service. In a few short minutes, you'll learn how to: * Create REST APIs with Encore * Use PostgreSQL databases * Use the local development dashboard to test your app * Create and run tests This is the end result:
Deploy to Encore Deploy this app to a free dev environment
To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create a service and endpoint If you haven't already, create a new application by running `encore app create` and select `Empty app` as the template. If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial). Now let's create a new `url` service. 🥐 In your application's root folder, create a new folder `url` and create a new file `url.go` that looks like this: ```go -- url/url.go -- // Service url takes URLs, generates random short IDs, and stores the URLs in a database. package url import ( "context" "crypto/rand" "encoding/base64" ) type URL struct { ID string // short-form URL id URL string // complete URL, in long form } type ShortenParams struct { URL string // the URL to shorten } // Shorten shortens a URL. //encore:api public method=POST path=/url func Shorten(ctx context.Context, p *ShortenParams) (*URL, error) { id, err := generateID() if err != nil { return nil, err } return &URL{ID: id, URL: p.URL}, nil } // generateID generates a random short ID. func generateID() (string, error) { var data [6]byte // 6 bytes of entropy if _, err := rand.Read(data[:]); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(data[:]), nil } ``` This sets up the `POST /url` endpoint (see the `//encore:api` annotation on the `Shorten` function). 🥐 Let’s see if it works! Start your app by running `encore run`. You should see this: ```output Your API is running at: http://localhost:4000 Development Dashboard URL: http://localhost:9400 4:19PM TRC registered endpoint path=/url service=url endpoint=Shorten ``` 🥐 Next, call your endpoint from the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view a trace of the response. It should look like this: You can also call it from the terminal: ```shell $ curl http://localhost:4000/url -d '{"URL": "https://encore.dev"}' ``` And you should see this: ```json { "ID": "5cJpBVRp", "URL": "https://encore.dev" } ``` It works! There’s just one problem... Right now, we’re not actually storing the URL anywhere. That means we can generate shortened IDs but there’s no way to get back to the original URL! We need to store a mapping from the short ID to the complete URL. ## 2. Save URLs in a database Fortunately, Encore makes it really easy to set up a PostgreSQL database to store our data. To do so, we first define a **database schema**, in the form of a migration file. 🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `1_create_tables.up.sql`. The file name format is important (it must start with `1_` and end in `.up.sql`). 🥐 Add the following contents to the file: ```sql -- url/migrations/1_create_tables.up.sql -- CREATE TABLE url ( id TEXT PRIMARY KEY, original_url TEXT NOT NULL ); ``` 🥐 Next, go back to the `url/url.go` file and import the `encore.dev/storage/sqldb` package by modifying the import statement to become: ```go HL url/url.go 5:5 -- url/url.go -- import ( "context" "crypto/rand" "encoding/base64" "encore.dev/storage/sqldb" ) ``` 🥐 Then let's define our database object by adding the following to `url/url.go`: ```go -- url/url.go -- // Define a database named 'url', using the database // migrations in the "./migrations" folder. var db = sqldb.NewDatabase("url", sqldb.DatabaseConfig{ Migrations: "./migrations", }) ``` 🥐 Now, to insert data into our database, let’s create a helper function `insert`: ```go -- url/url.go -- // insert inserts a URL into the database. func insert(ctx context.Context, id, url string) error { _, err := db.Exec(ctx, ` INSERT INTO url (id, original_url) VALUES ($1, $2) `, id, url) return err } ``` 🥐 Lastly, we can update our `Shorten` function to insert into the database: ```go -- url/url.go -- func Shorten(ctx context.Context, p *ShortenParams) (*URL, error) { id, err := generateID() if err != nil { return nil, err } else if err := insert(ctx, id, p.URL); err != nil { return nil, err } return &URL{ID: id, URL: p.URL}, nil } ``` Before running your application, make sure you have [Docker](https://www.docker.com) installed and running. It's required to locally run Encore applications with databases. 🥐 Next, start the application again with `encore run` and Encore automatically sets up your database. (In case your application won't run, check the [databases troubleshooting guide](/docs/develop/databases#troubleshooting).) You can verify that the database was created by opening the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should something like this: Infra tab in local development dashboard 🥐 Now let's call the API again from the local development dashboard, or from the terminal: ```shell $ curl http://localhost:4000/url -d '{"URL": "https://encore.dev"}' ``` 🥐 Finally, let's verify that it was saved in the database. You can do this by checking the trace in the local development dashboard, or you can run `encore db shell url` from the app root directory and inputting `select * from url;`: ```shell $ encore db shell url psql (13.1, server 11.12) Type "help" for help. url=# select * from url; id | original_url ----------+-------------------- zr6RmZc4 | https://encore.dev (1 row) ``` That was easy! ## 3. Add endpoint to retrieve URLs To complete our URL shortener API, let’s add the endpoint to retrieve a URL given its short id. 🥐 Add this endpoint to `url/url.go`: ```go -- url/url.go -- // Get retrieves the original URL for the id. //encore:api public method=GET path=/url/:id func Get(ctx context.Context, id string) (*URL, error) { u := &URL{ID: id} err := db.QueryRow(ctx, ` SELECT original_url FROM url WHERE id = $1 `, id).Scan(&u.URL) return u, err } ``` Encore uses the `path=/url/:id` syntax to represent a path with a parameter. The `id` name corresponds to the parameter name in the function signature. In this case it is of type `string`, but you can also use other built-in types like `int` or `bool` if you want to restrict the values. 🥐 We can make sure it works by reviewing the endpoint in the Service Catalog in the local development dashboard, where we can call it using the `id` you got in the previous step: You can also call it directly from the terminal: ```shell $ curl http://localhost:4000/url/zr6RmZc4 ``` You should now see this: ```json { "ID": "zr6RmZc4", "URL": "https://encore.dev" } ``` It works! That's how you build REST APIs and use PostgreSQL databases in Encore. ## 4. Add a test Before deployment, it is good practice to have tests to assure that the service works properly. Such tests including database access are easy to write. We've prepared a test to check that the whole cycle of shortening the URL, storing and then retrieving the original URL works. 🥐 Save this in a separate file `url/url_test.go`. ```go -- url/url_test.go -- package url import ( "context" "testing" ) // TestShortenAndRetrieve - test that the shortened URL is stored and retrieved from database. func TestShortenAndRetrieve(t *testing.T) { testURL := "https://github.com/encoredev/encore" sp := ShortenParams{URL: testURL} resp, err := Shorten(context.Background(), &sp) if err != nil { t.Fatal(err) } wantURL := testURL if resp.URL != wantURL { t.Errorf("got %q, want %q", resp.URL, wantURL) } firstURL := resp gotURL, err := Get(context.Background(), firstURL.ID) if err != nil { t.Fatal(err) } if *gotURL != *firstURL { t.Errorf("got %v, want %v", *gotURL, *firstURL) } } ``` 🥐 Now run `encore test ./...` to verify that it's working. If you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests. ## 5. Deploy ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration. Our URL shortener makes use of a PostgreSQL database, so we'll need to supply a [runtime configuration](/docs/go/self-host/configure-infra) so that our app knows how to connect to the database in the cloud. 🥐 Create a new file `infra-config.json` in the root of your project with the following contents: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "my-db-host:5432", "databases": { "url": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ] } ``` The values in this configuration are just examples, you will need to replace them with the correct values for your database. 🥐 Build a Docker image by running `encore build docker url-shortener:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Encore Cloud (free) Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes The final step before you deploy is to commit all changes to the project repo. 🥐 Commit the new files to the project's git repo and trigger a deploy to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, and connect your own AWS or GCP account to use for production deployment. *Now you have a fully fledged backend running in the cloud, well done!* ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) 🥐 A great next step is to [integrate with GitHub](/docs/platform/integrations/github). Once you've linked with GitHub, Encore will automatically start building and running tests against your Pull Requests. ## What's next Now that you know how to build a backend with a database, you're ready to let your creativity flow and begin building your next great idea! We're excited to hear what you're going to build with Encore, join the pioneering developer community on [Discord](/discord) and share your story. ================================================ FILE: docs/go/tutorials/slack-bot.md ================================================ --- seotitle: Tutorial – How to build a Slack bot seodesc: Learn how to build a Slack bot with Enore.go, and get it running in the cloud in just a few minutes. title: Building a Slack bot subtitle: Learn how to build a Slack bot with an Encore backend lang: go --- In this tutorial you will create a Slack bot that brings the greatness of the `cowsay` utility to Slack! ![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png "Slack bot") This is the end result:
Deploy to Encore Deploy this app to a free dev environment
To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create your Encore application 🥐 Create a new Encore application by running `encore app create` and select `Empty app` as the template. **Take a note of your app id, we'll need it in the next step.** ## 2. Create a Slack app 🥐 The first step is to create a new Slack app: 1. Head over to [Slack's API site](https://api.slack.com/apps) and create a new app. 2. When prompted, choose to create the app **from an app manifest**. 3. Choose a workspace to install the app in. 🥐 Enter the following manifest (replace `$APP_ID` in the URL below with your app id from above): ```yaml _metadata: major_version: 1 display_information: name: Encore Bot description: Cowsay for the cloud age. features: slash_commands: - command: /cowsay # Replace $APP_ID below url: https://staging-$APP_ID.encr.app/cowsay description: Say things with a flair! usage_hint: your message here should_escape: false bot_user: display_name: encore-bot always_online: true oauth_config: scopes: bot: - commands - chat:write - chat:write.public settings: org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false ``` Once created, we're ready to move on with implementing our Encore endpoint! ## 3. Implement the Slack endpoint Since Slack sends custom HTTP headers that we need to pay attention to, we're going to use a raw endpoint in Encore. For more information on this check out Slack's documentation on [Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands). 🥐 In your Encore app, create a new directory named `slack` and create a file `slack/slack.go` with the following contents: ```go -- slack/slack.go -- // Service slack implements a cowsaw Slack bot. package slack import ( "encoding/json" "fmt" "net/http" ) // cowart is the formatting string for printing the cow art. const cowart = "Moo! %s" //encore:api public raw path=/cowsay func Cowsay(w http.ResponseWriter, req *http.Request) { text := req.FormValue("text") data, _ := json.Marshal(map[string]string{ "response_type": "in_channel", "text": fmt.Sprintf(cowart, text), }) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write(data) } ``` Let's try it out locally. 🥐 Start your app with `encore run` and then call it in another terminal: ```shell $ curl http://localhost:4000/cowsay -d 'text=Eat your greens!' {"response_type":"in_channel","text":"Moo! Eat your greens!"} ``` Looks great! 🥐 Next, let's deploy it to the cloud: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Once deployed, we're ready to try our Slack command! 🥐 Head over to the workspace you installed the app in and run `/cowsay Hello there`. You should see something like this: ![Cowsay](https://encore.dev/assets/docs/cowsay-wip.png "Cowsay (Work in Progress)") And just like that we have a fully working Slack integration. ## 4. Secure the webhook endpoint In order to get up and running quickly we ignored one important aspect for a production-ready Slack app: verifying that the webhook requests are actually coming from Slack. Let's do that now! The Slack documentation covers this really well on the [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack) page. In short, what we need to do is: 1. Save a shared secret that Slack provides us 2. Use the secret to verify that the request comes from Slack, using HMAC (Hash-based Message Authentication Code). ### Save the shared secret Let's define a secret using Encore's secrets management functionality. 🥐 Add this to your `slack.go` file: ```go -- slack/slack.go -- var secrets struct { SlackSigningSecret string } ``` 🥐 Head over to the configuration section for your Slack app (go to [Your Apps](https://api.slack.com/apps) → select your app → Basic Information). 🥐 Copy the **Signing Secret** and then run `encore secret set --type prod SlackSigningSecret` and paste the secret. 🥐 For development you will also want to set `encore secret set --type dev,local,pr SlackSigningSecret`. You can use the same secret value or a placeholder value. ### Compute the HMAC Go makes computing HMAC very straightforward, but it's still a fair amount of code. 🥐 Add a few more imports to your file, so that it reads: ```go -- slack/slack.go -- import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strconv" "strings" "time" "encore.dev/beta/errs" "encore.dev/rlog" ) ``` 🥐 Next, we'll add the `verifyRequest` function: ```go -- slack/slack.go -- // verifyRequest verifies that a request is coming from Slack. func verifyRequest(req *http.Request) (body []byte, err error) { eb := errs.B().Code(errs.InvalidArgument) body, err = ioutil.ReadAll(req.Body) if err != nil { return nil, eb.Cause(err).Err() } // Compare timestamps to prevent replay attack ts := req.Header.Get("X-Slack-Request-Timestamp") threshold := int64(5 * 60) n, _ := strconv.ParseInt(ts, 10, 64) if diff := time.Now().Unix() - n; diff > threshold || diff < -threshold { return body, eb.Msg("message not recent").Err() } // Compare HMAC signature sig := req.Header.Get("X-Slack-Signature") prefix := "v0=" if !strings.HasPrefix(sig, prefix) { return body, eb.Msg("invalid signature").Err() } gotMac, _ := hex.DecodeString(sig[len(prefix):]) mac := hmac.New(sha256.New, []byte(secrets.SlackSigningSecret)) fmt.Fprintf(mac, "v0:%s:", ts) mac.Write(body) expectedMac := mac.Sum(nil) if !hmac.Equal(gotMac, expectedMac) { return body, eb.Msg("bad mac").Err() } return body, nil } ``` As you can see, this function needs to consume the whole HTTP body in order to compute the HMAC. This breaks the use of `req.FormValue("text")` that we used earlier, since it relies on reading the HTTP body. That's the reason we're returning the body from `verifyRequest`, so that we can parse the form values from that directly instead. We're now ready to verify the signature. 🥐 Update the `Cowsay` function to look like this: ```go -- slack/slack.go -- //encore:api public raw path=/cowsay func Cowsay(w http.ResponseWriter, req *http.Request) { body, err := verifyRequest(req) if err != nil { errs.HTTPError(w, err) return } q, _ := url.ParseQuery(string(body)) text := q.Get("text") data, _ := json.Marshal(map[string]string{ "response_type": "in_channel", "text": fmt.Sprintf(cowart, text), }) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write(data) } ``` ## 5. Put it all together and deploy Finally we're ready to put it all together. 🥐 Add the `cowart` like so: ```go -- slack/slack.go -- const cowart = ` ________________________________________ < %- 38s > ---------------------------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ` ``` 🥐 Finally, let's commit our changes and deploy it: ```shell $ git add -A . $ git commit -m 'Verify webhook requests and improve art' $ git push encore ``` 🥐 Once deployed, head back to Slack and run `/cowsay Hello there`. If everything is set up correctly, you should see: ![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png "Slack bot") And there we go, a production-ready Slack bot in less than 100 lines of code. Well done! ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ================================================ FILE: docs/go/tutorials/uptime.md ================================================ --- title: Building an Uptime Monitor subtitle: Learn how to build an event-driven uptime monitoring system seotitle: How to build an event-driven Uptime Monitoring System using Encore.go seodesc: Learn how to build an event-driven uptime monitoring tool using Go and Encore. Get your application running in the cloud in 30 minutes! lang: go --- Want to be notified when your website goes down so you can fix it before your users notice? You need an uptime monitoring system. Sounds daunting? Don't worry, we'll build it with Encore in 30 minutes! The app will use an event-driven architecture and the final result will look like this:
Deploy to Encore Deploy this app to a free dev environment
## 1. Create your Encore application To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. 🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use. ```shell $ encore app create uptime --example=github.com/encoredev/example-app-uptime/tree/starting-point ``` If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial). When we're done we'll have a backend with an event-driven architecture, as seen below in the [automatically generated diagram](/docs/go/observability/encore-flow) where white boxes are services and black boxes are Pub/Sub topics: ## 2. Create monitor service Let's start by creating the functionality to check if a website is currently up or down. Later we'll store this result in a database so we can detect when the status changes and send alerts. 🥐 Create an Encore service named `monitor` containing a file named `ping.go`. ```shell $ mkdir monitor $ touch monitor/ping.go ``` 🥐 Add an Encore API endpoint named `Ping` that takes a URL as input and returns a response indicating whether the site is up or down. ```go -- monitor/ping.go -- // Service monitor checks if a website is up or down. package monitor import ( "context" "net/http" "strings" ) // PingResponse is the response from the Ping endpoint. type PingResponse struct { Up bool `json:"up"` } // Ping pings a specific site and determines whether it's up or down right now. // //encore:api public path=/ping/*url func Ping(ctx context.Context, url string) (*PingResponse, error) { // If the url does not start with "http:" or "https:", default to "https:". if !strings.HasPrefix(url, "http:") && !strings.HasPrefix(url, "https:") { url = "https://" + url } // Make an HTTP request to check if it's up. req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return &PingResponse{Up: false}, nil } resp.Body.Close() // 2xx and 3xx status codes are considered up up := resp.StatusCode < 400 return &PingResponse{Up: up}, nil } ``` 🥐 Let's try it! Run `encore run` in your terminal and you should see the service start up. Then open up the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and try calling the `monitor.ping` endpoint from the API Explorer, passing in `google.com` as the URL. You can then see the response, logs, and view a trace of the request. It will look something like this: If you prefer to use the terminal instead run `curl http://localhost:4000/ping/google.com` in a new terminal instead. Either way you should see the response: ```json {"up": true} ``` You can also try with `httpstat.us/400` and `some-non-existing-url.com` and it should respond with `{"up": false}`. (It's always a good idea to test the negative case as well.) ### Add a test 🥐 Let's write an automated test so we don't break this endpoint over time. Create the file `monitor/ping_test.go` with the content: ```go -- monitor/ping_test.go -- package monitor import ( "context" "testing" ) func TestPing(t *testing.T) { ctx := context.Background() tests := []struct { URL string Up bool }{ {"encore.dev", true}, {"google.com", true}, // Test both with and without "https://" {"httpbin.org/status/200", true}, {"https://httpbin.org/status/200", true}, // 4xx and 5xx should considered down. {"httpbin.org/status/400", false}, {"https://httpbin.org/status/500", false}, // Invalid URLs should be considered down. {"invalid://scheme", false}, } for _, test := range tests { resp, err := Ping(ctx, test.URL) if err != nil { t.Errorf("url %s: unexpected error: %v", test.URL, err) } else if resp.Up != test.Up { t.Errorf("url %s: got up=%v, want %v", test.URL, resp.Up, test.Up) } } } ``` 🥐 Run `encore test ./...` to check that it all works as expected. You should see something like: ```shell $ encore test ./... 9:38AM INF starting request endpoint=Ping service=monitor test=TestPing 9:38AM INF request completed code=ok duration=71.861792 endpoint=Ping http_code=200 service=monitor test=TestPing [... lots more lines ...] PASS ok encore.app/monitor 1.660 ``` And if you open the local development dashboard at [localhost:9400](http://localhost:9400), you can also see traces for the tests. ## 3. Create site service Next, we want to keep track of a list of websites to monitor. Since most of these APIs will be simple "CRUD" (Create/Read/Update/Delete) endpoints, let's build this service using [GORM](https://gorm.io/), an ORM library that makes building CRUD endpoints really simple. 🥐 Let's create a new service named `site` with a SQL database. To do so, create a new directory `site` in the application root with `migrations` folder inside that folder: ```shell $ mkdir site $ mkdir site/migrations ``` 🥐 Add a database migration file inside that folder, named `1_create_tables.up.sql`. The file name is important (it must look something like `1_.up.sql`). Add the following contents: ```sql -- site/migrations/1_create_tables.up.sql -- CREATE TABLE sites ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL ); ``` 🥐 Next, install the GORM library and PostgreSQL driver: ```shell $ go get -u gorm.io/gorm gorm.io/driver/postgres ``` Now let's create the `site` service itself. To do this we'll use Encore's support for [dependency injection](https://encore.dev/docs/go/how-to/dependency-injection) to inject the GORM database connection. 🥐 Create `site/service.go` with the contents: ```go -- site/service.go -- // Service site keeps track of which sites to monitor. package site import ( "encore.dev/storage/sqldb" "gorm.io/driver/postgres" "gorm.io/gorm" ) //encore:service type Service struct { db *gorm.DB } // Define a database named 'site', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. var db = sqldb.NewDatabase("site", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // initService initializes the site service. // It is automatically called by Encore on service startup. func initService() (*Service, error) { db, err := gorm.Open(postgres.New(postgres.Config{ Conn: db.Stdlib(), })) if err != nil { return nil, err } return &Service{db: db}, nil } ``` 🥐 With that, we're now ready to create our CRUD endpoints. Create the following files: ```go -- site/get.go -- package site import "context" // Site describes a monitored site. type Site struct { // ID is a unique ID for the site. ID int `json:"id"` // URL is the site's URL. URL string `json:"url"` } // Get gets a site by id. // //encore:api public method=GET path=/site/:siteID func (s *Service) Get(ctx context.Context, siteID int) (*Site, error) { var site Site if err := s.db.Where("id = $1", siteID).First(&site).Error; err != nil { return nil, err } return &site, nil } -- site/add.go -- package site import "context" // AddParams are the parameters for adding a site to be monitored. type AddParams struct { // URL is the URL of the site. If it doesn't contain a scheme // (like "http:" or "https:") it defaults to "https:". URL string `json:"url"` } // Add adds a new site to the list of monitored websites. // //encore:api public method=POST path=/site func (s *Service) Add(ctx context.Context, p *AddParams) (*Site, error) { site := &Site{URL: p.URL} if err := s.db.Create(site).Error; err != nil { return nil, err } return site, nil } -- site/list.go -- package site import "context" type ListResponse struct { // Sites is the list of monitored sites. Sites []*Site `json:"sites"` } // List lists the monitored websites. // //encore:api public method=GET path=/site func (s *Service) List(ctx context.Context) (*ListResponse, error) { var sites []*Site if err := s.db.Find(&sites).Error; err != nil { return nil, err } return &ListResponse{Sites: sites}, nil } -- site/delete.go -- package site import "context" // Delete deletes a site by id. // //encore:api public method=DELETE path=/site/:siteID func (s *Service) Delete(ctx context.Context, siteID int) error { return s.db.Delete(&Site{ID: siteID}).Error } ``` 🥐 Now make sure you have [Docker](https://docker.com) installed and running, and then restart `encore run` to cause the `site` database to be created by Encore. You can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), and then use the Service Catalog to call the `site.Add` endpoint. Or you can call `site.Add` from the terminal: ```shell $ curl -X POST 'http://localhost:4000/site' -d '{"url": "https://encore.dev"}' { "id": 1, "url": "https://encore.dev" } ``` ## 4. Record uptime checks In order to notify when a website goes down or comes back up, we need to track the previous state it was in. 🥐 To do so, let's add a database to the `monitor` service as well. Create the directory `monitor/migrations` and the file `monitor/migrations/1_create_tables.up.sql`: ```sql -- monitor/migrations/1_create_tables.up.sql -- CREATE TABLE checks ( id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, up BOOLEAN NOT NULL, checked_at TIMESTAMP WITH TIME ZONE NOT NULL ); ``` We'll insert a database row every time we check if a site is up. 🥐 Add a new endpoint `Check` to the `monitor` service, that takes in a Site ID, pings the site, and inserts a database row in the `checks` table. For this service we'll use Encore's [`sqldb` package](https://encore.dev/docs/go/primitives/databases#querying-databases) instead of GORM (in order to showcase both approaches). ```go -- monitor/check.go -- package monitor import ( "context" "encore.app/site" "encore.dev/storage/sqldb" ) // Check checks a single site. // //encore:api public method=POST path=/check/:siteID func Check(ctx context.Context, siteID int) error { site, err := site.Get(ctx, siteID) if err != nil { return err } result, err := Ping(ctx, site.URL) if err != nil { return err } _, err = db.Exec(ctx, ` INSERT INTO checks (site_id, up, checked_at) VALUES ($1, $2, NOW()) `, site.ID, result.Up) return err } // Define a database named 'monitor', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. var db = sqldb.NewDatabase("monitor", sqldb.DatabaseConfig{ Migrations: "./migrations", }) ``` 🥐 Restart `encore run` to cause the `monitor` database to be created. We can again verify that the database was created in the Flow diagram, and also see the dependency between the `monitor` service and the `site` service that we just added. We can then call the `monitor.Check` endpoint using the id `1` that we got in the last step, and view the trace where we see the database interactions. It will look something like this: 🥐 You can also inspect the database using `encore db shell ` to make sure everything worked: ```shell $ encore db shell monitor psql (14.4, server 14.2) Type "help" for help. monitor=> SELECT * FROM checks; id | site_id | up | checked_at ----+---------+----+------------------------------- 1 | 1 | t | 2022-10-21 09:58:30.674265+00 ``` If that's what you see, everything's working great! ### Add a cron job to check all sites We now want to regularly check all the tracked sites so we can respond in case any of them go down. We'll create a new `CheckAll` API endpoint in the `monitor` service that will list all the tracked sites and check all of them. 🥐 Let's extract some of the functionality we wrote for the `Check` endpoint into a separate function, like so: ```go -- monitor/check.go -- // Check checks a single site. // //encore:api public method=POST path=/check/:siteID func Check(ctx context.Context, siteID int) error { site, err := site.Get(ctx, siteID) if err != nil { return err } return check(ctx, site) } func check(ctx context.Context, site *site.Site) error { result, err := Ping(ctx, site.URL) if err != nil { return err } _, err = db.Exec(ctx, ` INSERT INTO checks (site_id, up, checked_at) VALUES ($1, $2, NOW()) `, site.ID, result.Up) return err } ``` Now we're ready to create our new `CheckAll` endpoint. 🥐 Create the new `CheckAll` endpoint inside `monitor/check.go`: ```go -- monitor/check.go -- import "golang.org/x/sync/errgroup" // CheckAll checks all sites. // //encore:api public method=POST path=/checkall func CheckAll(ctx context.Context) error { // Get all the tracked sites. resp, err := site.List(ctx) if err != nil { return err } // Check up to 8 sites concurrently. g, ctx := errgroup.WithContext(ctx) g.SetLimit(8) for _, site := range resp.Sites { site := site // capture for closure g.Go(func() error { return check(ctx, site) }) } return g.Wait() } ``` This uses [an errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) to check up to 8 sites concurrently, aborting early if we encounter any error. (Note that a website being down is not treated as an error.) 🥐 Run `go get golang.org/x/sync/errgroup` to install that dependency. 🥐 Now that we have a `CheckAll` endpoint, define a [cron job](https://encore.dev/docs/go/primitives/cron-jobs) to automatically call it every 1 hour (since this is an example, we don't need to go too crazy and check every minute): ```go -- monitor/check.go -- import "encore.dev/cron" // Check all tracked sites every 1 hour. var _ = cron.NewJob("check-all", cron.JobConfig{ Title: "Check all sites", Endpoint: CheckAll, Every: 1 * cron.Hour, }) ``` Cron jobs are not triggered when running the application locally but work when deploying the application to a cloud environment. The frontend needs a way to list all sites and display if they are up or down. 🥐 Add a file in the `monitor` service and name it `status.go`. Add the following code: ```go -- monitor/status.go -- package monitor import ( "context" "time" ) // SiteStatus describes the current status of a site // and when it was last checked. type SiteStatus struct { Up bool `json:"up"` CheckedAt time.Time `json:"checked_at"` } // StatusResponse is the response type from the Status endpoint. type StatusResponse struct { // Sites contains the current status of all sites, // keyed by the site ID. Sites map[int]SiteStatus `json:"sites"` } // Status checks the current up/down status of all monitored sites. // //encore:api public method=GET path=/status func Status(ctx context.Context) (*StatusResponse, error) { rows, err := db.Query(ctx, ` SELECT DISTINCT ON (site_id) site_id, up, checked_at FROM checks ORDER BY site_id, checked_at DESC `) if err != nil { return nil, err } defer rows.Close() result := make(map[int]SiteStatus) for rows.Next() { var siteID int var status SiteStatus if err := rows.Scan(&siteID, &status.Up, &status.CheckedAt); err != nil { return nil, err } result[siteID] = status } if err := rows.Err(); err != nil { return nil, err } return &StatusResponse{Sites: result}, nil } ``` Now try visiting http://localhost:4000/frontend in your browser again. This time you should see a working frontend that lists all sites and their current status. ## 5. Deploy To try out your uptime monitor for real, let's deploy it to the cloud. ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/go/self-host/configure-infra) your Docker image. 🥐 Create a new file `infra-config.json` in the root of your project with the following contents: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "my-db-host:5432", "databases": { "monitor": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} }, "site": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ] } ``` The values in this configuration are just examples, you will need to replace them with the correct values for your database. 🥐 Build a Docker image by running `encore build docker uptime:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Encore Cloud (free) Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes Encore comes with built-in CI/CD, and the deployment process is as simple as a `git push`. (You can also integrate with GitHub to activate per Pull Request Preview Environments, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).) 🥐 Now, let's deploy your app to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From the Cloud Dashboard you can also see metrics, trigger Cron Jobs, see traces, and later connect your own AWS or GCP account to use for deployment. 🥐 When the deploy has finished, you can try out your uptime monitor by going to `https://staging-$APP_ID.encr.app/frontend`. *You now have an Uptime Monitor running in the cloud, well done!* ## 6. Publish Pub/Sub events when a site goes down Hold on, an uptime monitoring system isn't very useful if it doesn't actually notify you when a site goes down. To do so let's add a [Pub/Sub topic](https://encore.dev/docs/go/primitives/pubsub) on which we'll publish a message every time a site transitions from being up to being down, or vice versa. 🥐 Define the topic using Encore's Pub/Sub package in a new file, `monitor/alerts.go`: ```go -- monitor/alerts.go -- package monitor import "encore.dev/pubsub" // TransitionEvent describes a transition of a monitored site // from up->down or from down->up. type TransitionEvent struct { // Site is the monitored site in question. Site *site.Site `json:"site"` // Up specifies whether the site is now up or down (the new value). Up bool `json:"up"` } // TransitionTopic is a pubsub topic with transition events for when a monitored site // transitions from up->down or from down->up. var TransitionTopic = pubsub.NewTopic[*TransitionEvent]("uptime-transition", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) ``` Now let's publish a message on the `TransitionTopic` if a site's up/down state differs from the previous measurement. 🥐 Create a `getPreviousMeasurement` function to report the last up/down state: ```go -- monitor/alerts.go -- import ( "encore.dev/storage/sqldb" "errors" "context" ) // getPreviousMeasurement reports whether the given site was // up or down in the previous measurement. func getPreviousMeasurement(ctx context.Context, siteID int) (up bool, err error) { err = db.QueryRow(ctx, ` SELECT up FROM checks WHERE site_id = $1 ORDER BY checked_at DESC LIMIT 1 `, siteID).Scan(&up) if errors.Is(err, sqldb.ErrNoRows) { // There was no previous ping; treat this as if the site was up before return true, nil } else if err != nil { return false, err } return up, nil } ``` 🥐 Now add a function to conditionally publish a message if the up/down state differs: ```go -- monitor/alerts.go -- import "encore.app/site" func publishOnTransition(ctx context.Context, site *site.Site, isUp bool) error { wasUp, err := getPreviousMeasurement(ctx, site.ID) if err != nil { return err } if isUp == wasUp { // Nothing to do return nil } _, err = TransitionTopic.Publish(ctx, &TransitionEvent{ Site: site, Up: isUp, }) return err } ``` 🥐 Finally modify the `check` function to call this function: ```go -- monitor/check.go -- func check(ctx context.Context, site *site.Site) error { result, err := Ping(ctx, site.URL) if err != nil { return err } // Publish a Pub/Sub message if the site transitions // from up->down or from down->up. if err := publishOnTransition(ctx, site, result.Up); err != nil { return err } _, err = db.Exec(ctx, ` INSERT INTO checks (site_id, up, checked_at) VALUES ($1, $2, NOW()) `, site.ID, result.Up) return err } ``` Now the monitoring system will publish messages on the `TransitionTopic` whenever a monitored site transitions from up->down or from down->up. It doesn't know or care who actually listens to these messages. The truth is right now nobody does. So let's fix that by adding a Pub/Sub subscriber that posts these events to Slack. ## 7. Send Slack notifications when a site goes down 🥐 Start by creating a Slack service containing the following: ```go -- slack/slack.go -- package slack import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" ) type NotifyParams struct { // Text is the Slack message text to send. Text string `json:"text"` } // Notify sends a Slack message to a pre-configured channel using a // Slack Incoming Webhook (see https://api.slack.com/messaging/webhooks). // //encore:api private func Notify(ctx context.Context, p *NotifyParams) error { reqBody, err := json.Marshal(p) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, "POST", secrets.SlackWebhookURL, bytes.NewReader(reqBody)) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("notify slack: %s: %s", resp.Status, body) } return nil } var secrets struct { // SlackWebhookURL defines the Slack webhook URL to send // uptime notifications to. SlackWebhookURL string } ``` 🥐 Now go to a Slack community of your choice where you have the permission to create a new Incoming Webhook. 🥐 Once you have the Webhook URL, set it as an Encore secret: ```shell $ encore secret set --type dev,local,pr SlackWebhookURL Enter secret value: ***** Successfully updated development secret SlackWebhookURL. ``` 🥐 Test the `slack.Notify` endpoint by calling it via cURL: ```shell $ curl 'http://localhost:4000/slack.Notify' -d '{"Text": "Testing Slack webhook"}' ``` You should see the *Testing Slack webhook* message appear in the Slack channel you designated for the webhook. 🥐 When it works it's time to add a Pub/Sub subscriber to automatically notify Slack when a monitored site goes up or down. Add the following: ```go -- slack/slack.go -- import ( "encore.dev/pubsub" "encore.app/monitor" ) var _ = pubsub.NewSubscription(monitor.TransitionTopic, "slack-notification", pubsub.SubscriptionConfig[*monitor.TransitionEvent]{ Handler: func(ctx context.Context, event *monitor.TransitionEvent) error { // Compose our message. msg := fmt.Sprintf("*%s is down!*", event.Site.URL) if event.Up { msg = fmt.Sprintf("*%s is back up.*", event.Site.URL) } // Send the Slack notification. return Notify(ctx, &NotifyParams{Text: msg}) }, }) ``` ## 8. Deploy your finished Uptime Monitor Now you're ready to deploy your finished Uptime Monitor, complete with a Slack integration. ### Self-hosting Because we have added more infrastructure to our app, we need to [update the configuration](/docs/go/self-host/configure-infra) in our `infra-config.json` to include the new Pub/Sub topic and subscription as well as how we should set the `SlackWebhookURL` secret. 🥐 Update your `ìnfra-config.json` to reflect the new infrastructure. 🥐 Build a Docker image by running `encore build docker uptime:v2.0`. 🥐 Upload the Docker image to the cloud provider and run it. ### Encore Cloud (free) 🥐 As before, deploying your app to the cloud is as simple as running: ```shell $ git add -A . $ git commit -m 'Add slack integration' $ git push encore ``` ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ## Conclusion We've now built a fully functioning uptime monitoring system. If we may say so ourselves (and we may; it's our documentation after all) it's pretty remarkable how much we've accomplished in such little code: * We've built three different services (`site`, `monitor`, and `slack`) * We've added two databases (to the `site` and `monitor` services) for tracking monitored sites and the monitoring results * We've added a cron job for automatically checking the sites every hour * We've set up a Pub/Sub topic to decouple the monitoring system from the Slack notifications * We've added a Slack integration, using secrets to securely store the webhook URL, listening to a Pub/Sub subscription for up/down transition events All of this in just a bit over 300 lines of code. It's time to lean back and take a sip of your favorite beverage, safe in the knowledge you'll never be caught unaware of a website going down suddenly. ================================================ FILE: docs/menu.cue ================================================ #Menu: #RootMenu | #SubMenu #RootMenu: { kind: "rootmenu" items: [...#MenuItem] } #SubMenu: { kind: "submenu" // Menu title to display when this submenu is active. title: string // ID for the submenu, used for tracking active menu in frontend. id: string // Additional presentation options for the menu item. presentation?: #Presentation back: { // Text to display in the back button. text: string // Path to the page to navigate to when the back button is clicked. path: string } items: [...#MenuItem] } // Represents an item in a menu. #MenuItem: #SectionMenuItem | #BasicMenuItem | #NavMenuItem | #AccordionMenuItem #SectionMenuItem: { // Represents a menu section that can't be navigated to. kind: "section" // The text to display in the menu. text: string // Menu items to show for this section. items: [...#MenuItem] } #BasicMenuItem: { // Represents a basic page that can be navigated to. kind: "basic" // The text to display in the menu. text: string // The URL path to the page. path: string // The file to render when viewing this page. file: string // Inline menu to show when viewing this page. inline_menu?: [...#MenuItem] // hidden, if true, indicates the page exists but is hidden in the menu. // It can be navigated to directly, and will be show as "next page"/"prev page" // in the per-page navigation. hidden?: true } #NavMenuItem: { // Represents a page that can be navigated to, that has a menu // that replaces the navigation when viewing this page. kind: "nav" // The text to display in the menu. text: string // The URL path to the page. path: string // The file to render when viewing this page. file: string // The items to display in the submenu. submenu: #SubMenu // Additional presentation options for the menu item. presentation?: #Presentation } #Presentation: { // Icon to display next to the menu item. icon?: string style: "card" | *"basic" } #AccordionMenuItem: { kind: "accordion" text: string // If the accordion is open by default. defaultExpanded: bool | *false // The items to display in the accordion. accordion: [...#MenuItem] } // The root object is a #RootMenu. #RootMenu { items: [ { kind: "nav" text: "Encore.go" path: "/go" file: "go/overview" submenu: #EncoreGO presentation: { icon: "golang" style: "card" } }, { kind: "nav" text: "Encore.ts" path: "/ts" file: "ts/overview" submenu: #EncoreTS presentation: { icon: "typescript" style: "card" } }, { kind: "nav" text: "Encore Cloud" path: "/platform" file: "platform/overview" submenu: #EncorePlatform presentation: { icon: "typescript" style: "card" } }, ] } #EncoreGO: #SubMenu & { title: "Encore.go" id: "go" presentation: { icon: "golang" } back: { text: "" path: "" } items: [ { kind: "section" text: "Get Started" items: [{ kind: "basic" text: "Installation" path: "/go/install" file: "go/install" }, { kind: "basic" text: "Quick Start" path: "/go/quick-start" file: "go/quick-start" }, { kind: "basic" text: "AI Integration" path: "/go/ai-integration" file: "go/ai-integration" }, { kind: "basic" text: "FAQ" path: "/go/faq" file: "go/faq" }] }, { kind: "section" text: "Concepts" items: [{ kind: "basic" text: "Benefits" path: "/go/concepts/benefits" file: "go/concepts/benefits" }, { kind: "basic" text: "Application Model" path: "/go/concepts/application-model" file: "go/concepts/application-model" }] }, { kind: "section" text: "Tutorials" items: [{ kind: "basic" text: "Building a REST API" path: "/go/tutorials/rest-api" file: "go/tutorials/rest-api" }, { kind: "basic" text: "Building an Uptime Monitor" path: "/go/tutorials/uptime" file: "go/tutorials/uptime" }, { kind: "basic" text: "Building a GraphQL API" path: "/go/tutorials/graphql" file: "go/tutorials/graphql" }, { kind: "basic" text: "Building a Slack bot" path: "/go/tutorials/slack-bot" file: "go/tutorials/slack-bot" }, { kind: "basic" text: "Building a Meeting Notes app" path: "/go/tutorials/meeting-notes" file: "go/tutorials/meeting-notes" }, { kind: "basic" text: "Building a Booking System" path: "/go/tutorials/booking-system" file: "go/tutorials/booking-system" }, { kind: "basic" text: "Building an Incident Management tool" path: "/go/tutorials/incident-management-tool" file: "go/tutorials/incident-management-tool" hidden: true }] }, { kind: "section" text: "Primitives" items: [{ kind: "basic" text: "App Structure" path: "/go/primitives/app-structure" file: "go/primitives/app-structure" }, { kind: "basic" text: "Services" path: "/go/primitives/services" file: "go/primitives/services" }, { kind: "accordion" text: "APIs" accordion: [{ kind: "basic" text: "Defining APIs" path: "/go/primitives/defining-apis" file: "go/primitives/defining-apis" }, { kind: "basic" text: "API Calls" path: "/go/primitives/api-calls" file: "go/primitives/api-calls" }, { kind: "basic" text: "Raw Endpoints" path: "/go/primitives/raw-endpoints" file: "go/primitives/raw-endpoints" }, { kind: "basic" text: "Service Structs" path: "/go/primitives/service-structs" file: "go/primitives/service-structs" }, { kind: "basic" text: "API Errors" path: "/go/primitives/api-errors" file: "go/primitives/api-errors" }] }, { kind: "accordion" text: "Databases" accordion: [{ kind: "basic" text: "Using SQL databases" path: "/go/primitives/databases" file: "go/primitives/databases" }, { kind: "basic" text: "Change SQL database schema" path: "/go/primitives/change-db-schema" file: "go/primitives/change-db-schema" }, { kind: "basic" text: "Integrate with existing databases" path: "/go/primitives/connect-existing-db" file: "go/primitives/connect-existing-db" }, { kind: "basic" text: "Insert test data in a database" path: "/go/primitives/insert-test-data-db" file: "go/primitives/insert-test-data-db" }, { kind: "basic" text: "Share databases between services" path: "/go/primitives/share-db-between-services" file: "go/primitives/share-db-between-services" }, { kind: "basic" text: "PostgreSQL Extensions" path: "/go/primitives/databases/extensions" file: "go/primitives/database-extensions" }, { kind: "basic" text: "Troubleshooting" path: "/go/primitives/databases/troubleshooting" file: "go/primitives/database-troubleshooting" }] }, { kind: "basic" text: "Object Storage" path: "/go/primitives/object-storage" file: "go/primitives/object-storage" }, { kind: "basic" text: "Cron Jobs" path: "/go/primitives/cron-jobs" file: "go/primitives/cron-jobs" }, { kind: "basic" text: "Pub/Sub" path: "/go/primitives/pubsub" file: "go/primitives/pubsub" }, { kind: "basic" text: "Caching" path: "/go/primitives/caching" file: "go/primitives/caching" }, { kind: "basic" text: "Secrets" path: "/go/primitives/secrets" file: "go/primitives/secrets" }, { kind: "basic" text: "Code Snippets" path: "/go/primitives/code-snippets" file: "go/primitives/code-snippets" }] }, { kind: "section" text: "Development" items: [{ kind: "basic" text: "Authentication" path: "/go/develop/auth" file: "go/develop/auth" }, { kind: "basic" text: "Configuration" path: "/go/develop/config" file: "go/develop/config" }, { kind: "basic" text: "CORS" path: "/go/develop/cors" file: "go/develop/cors" }, { kind: "basic" text: "Metadata" path: "/go/develop/metadata" file: "go/develop/metadata" }, { kind: "basic" text: "Middleware" path: "/go/develop/middleware" file: "go/develop/middleware" }, { kind: "basic" text: "Testing" path: "/go/develop/testing" file: "go/develop/testing" }, { kind: "basic" text: "Mocking" path: "/go/develop/testing/mocking" file: "go/develop/mocking" }, { kind: "basic" text: "Validation" path: "/go/develop/validation" file: "go/develop/validation" }, { kind: "basic" text: "Environment Variables" path: "/go/develop/env-vars" file: "go/develop/env-vars" }] }, { kind: "section" text: "CLI" items: [{ kind: "basic" text: "CLI Reference" path: "/go/cli/cli-reference" file: "go/cli/cli-reference" }, { kind: "basic" text: "Client Generation" path: "/go/cli/client-generation" file: "go/cli/client-generation" }, { kind: "basic" text: "Infra Namespaces" path: "/go/cli/infra-namespaces" file: "go/cli/infra-namespaces" }, { kind: "basic" text: "CLI Configuration" path: "/go/cli/config-reference" file: "go/cli/config-reference" }, { kind: "basic" text: "Telemetry" path: "/go/cli/telemetry" file: "go/cli/telemetry" }, { kind: "basic" text: "MCP" path: "/go/cli/mcp" file: "go/cli/mcp" }] }, { kind: "section" text: "Observability" items: [{ kind: "basic" text: "Development Dashboard" path: "/go/observability/dev-dash" file: "go/observability/dev-dash" }, { kind: "basic" text: "Distributed Tracing" path: "/go/observability/tracing" file: "go/observability/tracing" }, { kind: "basic" text: "Flow Architecture Diagram" path: "/go/observability/encore-flow" file: "go/observability/encore-flow" }, { kind: "basic" text: "Service Catalog" path: "/go/observability/service-catalog" file: "go/observability/service-catalog" }, { kind: "basic" text: "Logging" path: "/go/observability/logging" file: "go/observability/logging" }, { kind: "basic" text: "Metrics" path: "/go/observability/metrics" file: "go/observability/metrics" }] }, { kind: "section" text: "Self Hosting" items: [ { kind: "basic" text: "CI/CD" path: "/go/self-host/ci-cd" file: "go/self-host/ci-cd" }, { kind: "basic" text: "Build Docker Images" path: "/go/self-host/docker-build" file: "go/self-host/self-host" }, { kind: "basic" text: "Configure Infrastructure" path: "/go/self-host/configure-infra" file: "go/self-host/configure-infra" }] }, { kind: "section" text: "How to guides" items: [{ kind: "basic" text: "Break a monolith into microservices" path: "/go/how-to/break-up-monolith" file: "go/how-to/break-up-monolith" }, { kind: "basic" text: "Integrate with a web frontend" path: "/go/how-to/integrate-frontend" file: "go/how-to/integrate-frontend" }, { kind: "basic" text: "Use Temporal with Encore" path: "/go/how-to/temporal" file: "go/how-to/temporal" }, { kind: "basic" text: "Build with cgo" path: "/go/how-to/cgo" file: "go/how-to/cgo" }, { kind: "basic" text: "Debug with Delve" path: "/go/how-to/debug" file: "go/how-to/debug" }, { kind: "basic" text: "Receive regular HTTP requests & Use websockets" path: "/go/how-to/http-requests" file: "go/how-to/http-requests" }, { kind: "basic" text: "Use Atlas + GORM for database migrations" path: "/go/how-to/atlas-gorm" file: "go/how-to/atlas-gorm" }, { kind: "basic" text: "Use the ent ORM for migrations" path: "/go/how-to/entgo-orm" file: "go/how-to/entgo-orm" }, { kind: "basic" text: "Use Connect for gRPC communication" path: "/go/how-to/grpc-connect" file: "go/how-to/grpc-connect" }, { kind: "basic" text: "Use a Pub/Sub Transactional Outbox" path: "/go/how-to/pubsub-outbox" file: "go/how-to/pubsub-outbox" }, { kind: "basic" text: "Use Dependency Injection" path: "/go/how-to/dependency-injection" file: "go/how-to/dependency-injection" }, { kind: "basic" text: "Use Auth0 Authentication" path: "/go/how-to/auth0-auth" file: "go/how-to/auth0-auth" }, { kind: "basic" text: "Use Clerk Authentication" path: "/go/how-to/clerk-auth" file: "go/how-to/clerk-auth" }, { kind: "basic" text: "Use Firebase Authentication" path: "/go/how-to/firebase-auth" file: "go/how-to/firebase-auth" }, { kind: "basic" text: "Use Logto Authentication" path: "/go/how-to/logto-auth" file: "go/how-to/logto-auth" }] }, { kind: "section" text: "Migration guides" items: [{ kind: "basic" text: "Migrate using AI agent" path: "/go/migration/ai-migration" file: "go/migration/ai-migration" }, { kind: "basic" text: "Migrate away from Encore" path: "/go/migration/migrate-away" file: "go/migration/migrate-away" }] }, { kind: "section" text: "Community" items: [{ kind: "basic" text: "Get Involved" path: "/go/community/get-involved" file: "go/community/get-involved" }, { kind: "basic" text: "Contribute" path: "/go/community/contribute" file: "go/community/contribute" }, { kind: "basic" text: "Open Source" path: "/go/community/open-source" file: "go/community/open-source" }, { kind: "basic" text: "Principles" path: "/go/community/principles" file: "go/community/principles" }, { kind: "basic" text: "Submit Template" path: "/go/community/submit-template" file: "go/community/submit-template" }] }, ] } #EncoreTS: #SubMenu & { title: "Encore.ts" id: "ts" presentation: { icon: "typescript" } back: { text: "" path: "" } items: [ { kind: "section" text: "Get started" items: [{ kind: "basic" text: "Installation" path: "/ts/install" file: "ts/install" }, { kind: "basic" text: "Quick Start" path: "/ts/quick-start" file: "ts/quick-start" }, { kind: "basic" text: "AI Integration" path: "/ts/ai-integration" file: "ts/ai-integration" }, { kind: "basic" text: "FAQ" path: "/ts/faq" file: "ts/faq" }] }, { kind: "section" text: "Concepts" items: [{ kind: "basic" text: "Benefits" path: "/ts/concepts/benefits" file: "ts/concepts/benefits" }, { kind: "basic" text: "Application Model" path: "/ts/concepts/application-model" file: "ts/concepts/application-model" }, { kind: "basic" text: "Hello World" path: "/ts/concepts/hello-world" file: "ts/concepts/hello-world" }] }, { kind: "section" text: "Tutorials" items: [{ kind: "basic" text: "Building a REST API" path: "/ts/tutorials/rest-api" file: "ts/tutorials/rest-api" }, { kind: "basic" text: "Building an Uptime Monitor" path: "/ts/tutorials/uptime" file: "ts/tutorials/uptime" }, { kind: "basic" text: "Building a GraphQL API" path: "/ts/tutorials/graphql" file: "ts/tutorials/graphql" }, { kind: "basic" text: "Building a Slack bot" path: "/ts/tutorials/slack-bot" file: "ts/tutorials/slack-bot" }] }, { kind: "section" text: "Primitives" items: [{ kind: "basic" text: "App Structure" path: "/ts/primitives/app-structure" file: "ts/primitives/app-structure" }, { kind: "basic" text: "Services" path: "/ts/primitives/services" file: "ts/primitives/services" }, { kind: "accordion" text: "APIs" accordion: [{ kind: "basic" text: "Defining APIs" path: "/ts/primitives/defining-apis" file: "ts/primitives/defining-apis" }, { kind: "basic" text: "Validation" path: "/ts/primitives/validation" file: "ts/primitives/validation" }, { kind: "basic" text: "API Calls" path: "/ts/primitives/api-calls" file: "ts/primitives/api-calls" }, { kind: "basic" text: "Raw Endpoints" path: "/ts/primitives/raw-endpoints" file: "ts/primitives/raw-endpoints" }, { kind: "basic" text: "GraphQL" path: "/ts/primitives/graphql" file: "ts/primitives/graphql" }, { kind: "basic" text: "Streaming APIs" path: "/ts/primitives/streaming-apis" file: "ts/primitives/streaming-apis" }, { kind: "basic" text: "API Errors" path: "/ts/primitives/errors" file: "ts/primitives/errors" }, { kind: "basic" text: "Static Assets" path: "/ts/primitives/static-assets" file: "ts/primitives/static-assets" }, { kind: "basic" text: "Cookies" path: "/ts/primitives/cookies" file: "ts/primitives/cookies" }, { kind: "basic" text: "Types" path: "/ts/primitives/types" file: "ts/primitives/types" }] }, { kind: "basic" text: "Databases" path: "/ts/primitives/databases" file: "ts/primitives/databases" }, { kind: "basic" text: "PostgreSQL Extensions" path: "/ts/primitives/databases-extensions" file: "ts/primitives/database-extensions" }, { kind: "basic" text: "Object Storage" path: "/ts/primitives/object-storage" file: "ts/primitives/object-storage" }, { kind: "basic" text: "Cron Jobs" path: "/ts/primitives/cron-jobs" file: "ts/primitives/cron-jobs" }, { kind: "basic" text: "Pub/Sub" path: "/ts/primitives/pubsub" file: "ts/primitives/pubsub" }, { kind: "basic" text: "Caching" path: "/ts/primitives/caching" file: "ts/primitives/caching" }, { kind: "basic" text: "Secrets" path: "/ts/primitives/secrets" file: "ts/primitives/secrets" }] }, { kind: "section" text: "Development" items: [{ kind: "basic" text: "Authentication" path: "/ts/develop/auth" file: "ts/develop/auth" }, { kind: "accordion" text: "ORMs" accordion: [{ kind: "basic" text: "Overview" path: "/ts/develop/orms" file: "ts/develop/orms/overview" }, { kind: "basic" text: "Knex.js" path: "/ts/develop/orms/knex" file: "ts/develop/orms/knex" }, { kind: "basic" text: "Prisma" path: "/ts/develop/orms/prisma" file: "ts/develop/orms/prisma" }, { kind: "basic" text: "Drizzle" path: "/ts/develop/orms/drizzle" file: "ts/develop/orms/drizzle" }, { kind: "basic" text: "Sequelize" path: "/ts/develop/orms/sequelize" file: "ts/develop/orms/sequelize" }] }, { kind: "basic" text: "Metadata" path: "/ts/develop/metadata" file: "ts/develop/metadata" }, { kind: "basic" text: "Testing" path: "/ts/develop/testing" file: "ts/develop/testing" }, { kind: "basic" text: "Debugging" path: "/ts/develop/debug" file: "ts/develop/debug" }, { kind: "basic" text: "Middleware" path: "/ts/develop/middleware" file: "ts/develop/middleware" }, { kind: "basic" text: "Multithreading" path: "/ts/develop/multithreading" file: "ts/develop/multithreading" }, { kind: "basic" text: "Running Scripts" path: "/ts/develop/running-scripts" file: "ts/develop/running-scripts" }, { kind: "basic" text: "Environment Variables" path: "/ts/develop/env-vars" file: "ts/develop/env-vars" }, { kind: "accordion" text: "Monorepo" accordion: [{ kind: "basic" text: "Turborepo" path: "/ts/develop/monorepo/turborepo" file: "ts/develop/monorepo/turborepo" }, { kind: "basic" text: "Nx" path: "/ts/develop/monorepo/nx" file: "ts/develop/monorepo/nx" }] }] }, { kind: "section" text: "Resources" items: [{ kind: "accordion" text: "Integrations" accordion: [{ kind: "basic" text: "Better Auth" path: "/ts/develop/integrations/better-auth" file: "ts/develop/integrations/better-auth" }, { kind: "basic" text: "Polar" path: "/ts/develop/integrations/polar" file: "ts/develop/integrations/polar" }, { kind: "basic" text: "Resend" path: "/ts/develop/integrations/resend" file: "ts/develop/integrations/resend" }] }] }, { kind: "section" text: "CLI" items: [{ kind: "basic" text: "CLI Reference" path: "/ts/cli/cli-reference" file: "ts/cli/cli-reference" }, { kind: "basic" text: "Client Generation" path: "/ts/cli/client-generation" file: "ts/cli/client-generation" }, { kind: "basic" text: "Infra Namespaces" path: "/ts/cli/infra-namespaces" file: "ts/cli/infra-namespaces" }, { kind: "basic" text: "CLI Configuration" path: "/ts/cli/config-reference" file: "ts/cli/config-reference" }, { kind: "basic" text: "Telemetry" path: "/ts/cli/telemetry" file: "ts/cli/telemetry" }, { kind: "basic" text: "MCP" path: "/ts/cli/mcp" file: "ts/cli/mcp" }] }, { kind: "section" text: "Frontend" items: [{ kind: "basic" text: "Hosting" path: "/ts/frontend/hosting" file: "ts/frontend/hosting" }, { kind: "basic" text: "CORS" path: "/ts/frontend/cors" file: "ts/frontend/cors" }, { kind: "basic" text: "Request Client" path: "/ts/frontend/request-client" file: "ts/frontend/request-client" }, { kind: "basic" text: "Template Engine" path: "/ts/frontend/template-engine" file: "ts/frontend/template-engine" }, { kind: "basic" text: "Mono vs Multi Repo" path: "/ts/frontend/mono-vs-multi-repo" file: "ts/frontend/mono-vs-multi-repo" }] }, { kind: "section" text: "Observability" items: [{ kind: "basic" text: "Development Dashboard" path: "/ts/observability/dev-dash" file: "ts/observability/dev-dash" }, { kind: "basic" text: "Logging" path: "/ts/observability/logging" file: "ts/observability/logging" }, { kind: "basic" text: "Distributed Tracing" path: "/ts/observability/tracing" file: "ts/observability/tracing" }, { kind: "basic" text: "Flow Architecture Diagram" path: "/ts/observability/flow" file: "ts/observability/flow" }, { kind: "basic" text: "Service Catalog" path: "/ts/observability/service-catalog" file: "ts/observability/service-catalog" }, { kind: "basic" text: "Metrics" path: "/ts/observability/metrics" file: "ts/observability/metrics" }] }, { kind: "section" text: "Self Hosting" items: [ { kind: "basic" text: "CI/CD" path: "/ts/self-host/ci-cd" file: "ts/self-host/ci-cd" }, { kind: "basic" text: "Build Docker Images" path: "/ts/self-host/build" file: "ts/self-host/build" }, { kind: "basic" text: "Configure Infrastructure" path: "/ts/self-host/configure-infra" file: "ts/self-host/configure-infra" }, { kind: "basic" text: "Deploy to DigitalOcean" path: "/ts/self-host/deploy-digitalocean" file: "ts/self-host/deploy-to-digital-ocean" }, { kind: "basic" text: "Deploy to Railway" path: "/ts/self-host/deploy-railway" file: "ts/self-host/deploy-to-railway" }] }, { kind: "section" text: "How to guides" items: [{ kind: "basic" text: "Handle file uploads" path: "/ts/how-to/file-uploads" file: "ts/how-to/file-uploads" }, { kind: "basic" text: "Use NestJS with Encore" path: "/ts/how-to/nestjs" file: "ts/how-to/nestjs" }] }, { kind: "section" text: "Migration guides" items: [{ kind: "basic" text: "Migrate using AI agent" path: "/ts/migration/ai-migration" file: "ts/migration/ai-migration" }, { kind: "basic" text: "Migrate away from Encore" path: "/ts/migration/migrate-away" file: "ts/migration/migrate-away" }, { kind: "basic" text: "Migrate from Express.js" path: "/ts/migration/express-migration" file: "ts/migration/express-migration" }] }, { kind: "section" text: "Community" items: [{ kind: "basic" text: "Get Involved" path: "/ts/community/get-involved" file: "ts/community/get-involved" }, { kind: "basic" text: "Contribute" path: "/ts/community/contribute" file: "ts/community/contribute" }, { kind: "basic" text: "Open Source" path: "/ts/community/open-source" file: "ts/community/open-source" }, { kind: "basic" text: "Principles" path: "/ts/community/principles" file: "ts/community/principles" }, { kind: "basic" text: "Submit Template" path: "/ts/community/submit-template" file: "ts/community/submit-template" }] }, ] } #EncorePlatform: #SubMenu & { title: "Encore Cloud" id: "platform" presentation: { icon: "" } back: { text: "" path: "" } items: [ { kind: "section" text: "Concepts" items: [{ kind: "basic" text: "Introduction" path: "/platform/introduction" file: "platform/introduction" }, { kind: "basic" text: "AI Integration" path: "/platform/ai-integration" file: "platform/ai-integration" }] }, { kind: "section" text: "Deployment" items: [{ kind: "basic" text: "Deploying & CI/CD" path: "/platform/deploy/deploying" file: "platform/deploy/deploying" }, { kind: "basic" text: "Connect your cloud account" path: "/platform/deploy/own-cloud" file: "platform/deploy/own-cloud" }, { kind: "basic" text: "Environments" path: "/platform/deploy/environments" file: "platform/deploy/environments" }, { kind: "basic" text: "Preview Environments" path: "/platform/deploy/preview-environments" file: "platform/deploy/preview-environments" }, { kind: "basic" text: "Application Security" path: "/platform/deploy/security" file: "platform/deploy/security" }] }, { kind: "section" text: "Infrastructure" items: [{ kind: "basic" text: "Provisioning & Environments" path: "/platform/infrastructure/infra" file: "platform/infrastructure/infra" }, { kind: "basic" text: "Infrastructure Configuration" path: "/platform/infrastructure/configuration" file: "platform/infrastructure/configuration" }, { kind: "accordion" text: "GCP Infrastructure" accordion: [{ kind: "basic" text: "Overview" path: "/platform/infrastructure/gcp" file: "platform/infrastructure/gcp" }, { kind: "basic" text: "Import Cloud SQL" path: "/platform/infrastructure/gcp/import-cloud-sql" file: "platform/infrastructure/import-cloud-sql" }, { kind: "basic" text: "Import Project" path: "/platform/infrastructure/gcp/import-project" file: "platform/infrastructure/import-project" }, { kind: "basic" text: "Configure Network" path: "/platform/infrastructure/configure-network" file: "platform/infrastructure/configure-network" }] }, { kind: "accordion" text: "AWS Infrastructure" accordion: [{ kind: "basic" text: "Overview" path: "/platform/infrastructure/aws" file: "platform/infrastructure/aws" },{ kind: "basic" text: "Import RDS Database" path: "/platform/infrastructure/aws/import-rds" file: "platform/infrastructure/import-rds" }, { kind: "basic" text: "Configure Network" path: "/platform/infrastructure/configure-network" file: "platform/infrastructure/configure-network" }] }, { kind: "accordion" text: "Kubernetes deployment" accordion: [{ kind: "basic" text: "Deploying to a new cluster" path: "/platform/infrastructure/kubernetes" file: "platform/infrastructure/kubernetes" }, { kind: "basic" text: "Import an existing cluster" path: "/platform/infrastructure/import-kubernetes-cluster" file: "platform/infrastructure/import-kubernetes-cluster" }, { kind: "basic" text: "Configure kubectl" path: "/platform/infrastructure/configure-kubectl" file: "platform/infrastructure/configure-kubectl" }] }, { kind: "basic" text: "Neon Postgres" path: "/platform/infrastructure/neon" file: "platform/infrastructure/neon" }, { kind: "basic" text: "Cloudflare R2" path: "/platform/infrastructure/cloudflare" file: "platform/infrastructure/cloudflare" }, { kind: "basic" text: "Managing database users" path: "/platform/infrastructure/manage-db-users" file: "platform/infrastructure/manage-db-users" }] }, { kind: "section" text: "Observability" items: [{ kind: "basic" text: "Metrics" path: "/platform/observability/metrics" file: "platform/observability/metrics" }, { kind: "basic" text: "Distributed Tracing" path: "/platform/observability/tracing" file: "platform/observability/tracing" }, { kind: "basic" text: "Flow Architecture Diagram" path: "/platform/observability/encore-flow" file: "platform/observability/encore-flow" }, { kind: "basic" text: "Service Catalog" path: "/platform/observability/service-catalog" file: "platform/observability/service-catalog" }] }, { kind: "section" text: "Integrations" items: [{ kind: "basic" text: "GitHub" path: "/platform/integrations/github" file: "platform/integrations/github" }, { kind: "basic" text: "Custom Domains" path: "/platform/integrations/custom-domains" file: "platform/integrations/custom-domains" }, { kind: "basic" text: "Webhooks" path: "/platform/integrations/webhooks" file: "platform/integrations/webhooks" }, { kind: "basic" text: "OAuth Clients" path: "/platform/integrations/oauth-clients" file: "platform/integrations/oauth-clients" }, { kind: "basic" text: "Auth Keys" path: "/platform/integrations/auth-keys" file: "platform/integrations/auth-keys" }, { kind: "basic" text: "API Reference" path: "/platform/integrations/api-reference" file: "platform/integrations/api-reference" }, { kind: "basic" text: "Terraform" path: "/platform/integrations/terraform" file: "platform/integrations/terraform" }] }, { kind: "section" text: "Migration guides" items: [{ kind: "basic" text: "Migrate to Encore" path: "/platform/migration/migrate-to-encore" file: "platform/migration/migrate-to-encore" }, { kind: "basic" text: "Migrate away from Encore" path: "/platform/migration/migrate-away" file: "platform/migration/migrate-away" }] }, { kind: "section" text: "Management & Billing" items: [{ kind: "basic" text: "Security & Compliance" path: "/platform/management/compliance" file: "platform/management/compliance" }, { kind: "basic" text: "Plans & billing" path: "/platform/management/billing" file: "platform/management/billing" }, { kind: "basic" text: "Telemetry" path: "/platform/management/telemetry" file: "platform/management/telemetry" }, { kind: "basic" text: "Roles & Permissions" path: "/platform/management/permissions" file: "platform/management/permissions" }, { kind: "basic" text: "Usage limits" path: "/platform/management/usage" file: "platform/management/usage" }] }, { kind: "section" text: "Other" items: [ { kind: "accordion" text: "Product comparisons" accordion: [{ kind: "basic" text: "Encore vs. Heroku" path: "/platform/other/vs-heroku" file: "platform/other/vs-heroku" }, { kind: "basic" text: "Encore vs. Supabase / Firebase" path: "/platform/other/vs-supabase" file: "platform/other/vs-supabase" }, { kind: "basic" text: "Encore vs. Terraform / Pulumi" path: "/platform/other/vs-terraform" file: "platform/other/vs-terraform" }] }] }, ] } ================================================ FILE: docs/platform/ai-integration.md ================================================ --- seotitle: AI-Powered Development with Encore Cloud seodesc: Learn how Encore Cloud enables AI agents to provision infrastructure in AWS/GCP with automatic guardrails, preview environments, and more. title: AI Integration subtitle: AI agents that can provision real infrastructure in your cloud lang: platform --- Encore Cloud supercharges AI-powered development by letting AI agents provision real infrastructure in your AWS or GCP account with automatic guardrails. When you connect your cloud account to [Encore Cloud](https://encore.cloud), AI-generated code that declares databases, pub/sub topics, cron jobs, and other [primitives](/docs/ts/primitives) gets automatically provisioned with production-ready defaults: proper networking, IAM permissions, and security configurations. ## How It Works 1. **AI writes infrastructure code** using Encore's declarative primitives 2. **Push to GitHub** to trigger a deployment 3. **Encore Cloud provisions** the infrastructure in your AWS/GCP account with automatic guardrails 4. **Preview environments** let you test AI-generated changes in isolation This enables fast iterative development: AI generates code, you push it and validate in preview environments, then deploy seamlessly with automatic infrastructure provisioning. ## Infrastructure with Guardrails When AI declares infrastructure using Encore primitives, Encore Cloud provisions it in your cloud with automatic guardrails: - **Databases**: Proper networking, encryption at rest, automated backups - **Pub/Sub**: Dead letter queues, retry policies, proper IAM roles - **Secrets**: Encrypted storage, access controls - **Services**: Load balancing, health checks, auto-scaling AI doesn't need to know the intricacies of AWS or GCP. It just declares what it needs, and Encore Cloud handles the cloud-specific configuration. You stay in control: review infrastructure changes in pull requests, approve or deny resource additions, and use [infrastructure configuration](/docs/platform/infrastructure/configuration) to customize defaults per environment. ## Preview Environments [Preview environments](/docs/platform/deploy/preview-environments) are perfect for testing AI-generated changes. Each pull request gets its own isolated environment with real infrastructure. This means you can: - Let AI generate features and immediately test them with real databases and services - Review AI-generated infrastructure changes before they hit production - Catch issues early in isolated environments ## Production Observability Encore Cloud provides [distributed tracing](/docs/platform/observability/tracing) and [metrics](/docs/platform/observability/metrics) across all your environments. You can: - Analyze traces to debug issues across services - Inspect timing to find bottlenecks - Compare behavior between preview and production environments ## Connecting Your Cloud To deploy your Encore app to your cloud: 1. [Sign up for Encore Cloud](https://app.encore.dev) 2. [Connect your AWS or GCP account](/docs/platform/deploy/own-cloud) 3. Push your Encore app to deploy Encore Cloud provisions infrastructure in your cloud account based on the primitives declared in your code. You maintain full control and ownership of your infrastructure. ## What AI Can Provision With Encore Cloud connected, AI-generated code can provision: | Resource | AWS | GCP | |----------|-----|-----| | Databases | RDS (PostgreSQL) | Cloud SQL | | Pub/Sub | SNS + SQS | Cloud Pub/Sub | | Object Storage | S3 | Cloud Storage | | Cron Jobs | CloudWatch Events | Cloud Scheduler | | Secrets | Secrets Manager | Secret Manager | | Caching | ElastiCache | Memorystore | All resources are provisioned with security best practices like least-privilege IAM policies, private networking, and encryption. See the [AWS](/docs/platform/infrastructure/aws) and [GCP](/docs/platform/infrastructure/gcp) infrastructure docs for specifics. ## Learn More - [Connect Your Cloud Account](/docs/platform/deploy/own-cloud) - [Preview Environments](/docs/platform/deploy/preview-environments) - [Infrastructure Configuration](/docs/platform/infrastructure/configuration) - [Framework AI Integration (TypeScript)](/docs/ts/ai-integration) - [Framework AI Integration (Go)](/docs/go/ai-integration) ================================================ FILE: docs/platform/deploy/deploying.md ================================================ --- seotitle: Deploying your Encore application is as simple as git push seodesc: Learn how to deploy your backend application built with Encore with a single command, while Encore manages your entire CI/CD process. title: Deploying Applications with Encore Cloud subtitle: Encore Cloud automates the deployment and infrastructure provisioning process lang: platform --- Encore Cloud simplifies deploying your application, making it as simple as pushing to a git repository, removing the need for manual steps. ## Deploying your application ### Step 1: Create account & application Before deploying, ensure that you have an **Encore Cloud account** and have created an **Encore application**. You can create both an account and an application by running the following command: ```shell $ encore app create ``` You will be asked to create a free Encore Cloud account first, and then proceed to create a new Encore application. #### Already created an application locally? Follow these steps if you've already created an app and want to link it to an account on Encore Cloud: **1. Ensure you are logged in with the CLI** ```bash encore auth signup # If you haven't created an Encore Cloud account encore auth login # If you've already created an Encore Cloud account ``` **2. Link your local app to Encore Cloud** Run this command from you application's root folder: ```bash encore app init ``` **3. Set up Encore's git remote to enable pushing directly to Encore Cloud** Run this command from you application's root folder: ```bash git remote add encore encore:// ``` ### Step 2: Integrate with GitHub (Optional) When creating an Encore application, Encore will automatically create a new Encore managed git repository. If you are just trying out Encore Cloud, you can use this and skip the rest of this step. For production applications we recommend integrating with GitHub instead of using the built-in Encore managed git: #### **Connecting your GitHub account** Open your app in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > GitHub**. Click the **Connect Account to GitHub** button, which will open GitHub where you can grant access either to the relevant repositorie(s). [See the full docs](/docs/platform/integrations/github) on integrating with GitHub to learn how to configure different repository structures. Once connected to GitHub, pushing code will trigger deployments automatically. Encore Cloud Pro users get [Preview Environments](/docs/platform/deploy/preview-environments) for each pull request. ### Step 3: Connect your AWS / GCP account (Optional) Deploy to your own cloud on AWS or GCP by connecting your cloud account to Encore Cloud. If you're just trying out Encore Cloud, skip this step to deploy to a free development environment using Encore Cloud's hosting, subject to [fair use limits](/docs/platform/management/usage). #### **Connecting your cloud account** Open your app in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**. Learn more in the [connecting your cloud docs](/docs/platform/deploy/own-cloud). ### Step 4: Push to deploy Deploy your application by pushing your code to the connected Git repository. - **Using Encore Cloud's managed git**: ```shell $ git add -A . $ git commit -m 'Commit message' $ git push encore ``` - **If you have connected your GitHub account:** ```shell $ git add -A . $ git commit -m 'Commit message' $ git push origin ``` This will trigger Encore Cloud's deployment process, consisting of the following phases: * A build & test phase * An infrastructure provisioning phase * A deployment phase Once you've pushed your code, you can monitor the progress in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > Deployments**. ## Configuring deploy trigger When using GitHub, you can configure Encore Cloud to automatically trigger deploys when you push to a specific branch name. To configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name` and hit save. ### Integrating using Encore Cloud's API You can trigger deployments using Encore Cloud's API, learn more in the [API reference](/docs/platform/integrations/api-reference). ## Configuring custom build settings If you want, you can override certain aspects of the CI/CD process in the `encore.app` file: * The Docker base image to use when deploying * Whether to build with Cgo enabled * Whether to bundle the source code in the docker image (useful for [Sentry stack traces](https://docs.sentry.io/platforms/go/usage/serverless/)) Below are the available build settings configurable in the `encore.app` file, with their default values: ```cue { "build": { // Enables cgo when building the application and running tests // in Encore's CI/CD system. "cgo_enabled": false, // Docker-related configuration "docker": { // The Docker base image to use when deploying the application. // It must be a publicly accessible image. It defaults to "scratch" for go apps // and "node:24-trixie" for typescript apps. "base_image": "scratch", // Whether to bundle the source code in the docker image. // The source code will be copied into /workspace as part // of the build process. This is primarily useful for tools like // Sentry that need access to the source code to generate stack traces. "bundle_source": false, // The working directory to start the docker image in. // If empty it defaults to "/workspace" if the source code is bundled, and to "/" otherwise. "working_dir": "" } // Build hooks allow you to run custom commands during the build process. // They can be specified as a string (e.g. "cmd1 && cmd2") or as an object // with a command and optional environment variables. "hooks": { // Runs before the Encore build, but after dependencies are fetched (e.g. npm install). "prebuild": "", // Or as an object: // "prebuild": {"command": "my-command", "env": {"MY_VAR": "value"}}, // Runs after the Encore build has finished. "postbuild": "" // Or as an object: // "postbuild": {"command": "my-command", "env": {"MY_VAR": "value"}} } } } ``` ================================================ FILE: docs/platform/deploy/environments.md ================================================ --- seotitle: Environments – Creating local, preview, and prod environments seodesc: Learn how to create all the environments you need for your backend application, local, preview, testing and production. Here's how you keep them in sync! title: Creating & configuring environments subtitle: Get the environments you need, without the work lang: platform --- Encore automatically sets up and manages different environments for your application (local, preview, testing, and production). Each environment is: - Fully isolated - Automatically provisioned - Always in sync with your codebase - Configured with appropriate infrastructure for its purpose ## Environment Types Encore has four types of environments: - `production` - `development` - `preview` - `local` Some environment types differ in how infrastructure is provisioned: - `local` is provisioned by Encore's Open Source CLI using local versions of infrastructure. - `preview` environments are provisioned in Encore Cloud hosting and are optimized to be cost-efficient and fast to provision. - `production` and `development` environments are provisioned by Encore Cloud, either in your [cloud account](/docs/platform/deploy/own-cloud) or using Encore Cloud's free development hosting. Both environment types offer the same infrastructure options when deployed using your own cloud account. Environment type is also used for [Secrets management](/docs/ts/primitives/secrets), allowing you to configure different secrets for different environment types. Therefore, you can easily configure different secrets for your `production` and `development` environments. ## Creating environments 1. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud) 2. Go to **Environments** > **Create env** 3. Configure your environment: - Name your environment - Choose type: **Production** or **Development** (see [Environment Types](#environment-types)) - Set deploy trigger: Git branch or manual - Configure infrastructure approval: automatic or manual - Select cloud provider - Choose process allocation: single or separate processes ![Creating an environment](/assets/docs/createenv.png "Creating an environment") ### Configuring deploy trigger When using GitHub, you can configure Encore Cloud to automatically trigger deploys when you push to a specific branch name. To configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name` and hit save. ### Configuring infrastructure approval For some environments you may want to enforce infrastructure approval before deploying. You can configure this in the **Settings** > **Infrastructure Approval** section for your environment. When infrastructure approval is enabled, an application **Admin** will need to manually approve the infrastructure changes before the deployment can proceed. ### Configuring process allocation Encore Cloud offers flexible process allocation options: - **Single process**: All services run in one process (simpler, lower cost) - **Separate processes**: Each service runs independently (better isolation, independent scaling) Choose your preferred deployment model when creating each environment. You can use different models for production and development environments without changing any code. ## Setting a Primary environment Every Encore app has a configurable Primary environment that serves as the default for: - App insights in the Encore Cloud dashboard - API documentation - CLI functionality (like API client generation) **Configuring your Primary environment:** 1. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud) 2. Navigate to **Settings** > **General** > **Primary Environment** 3. Select your desired environment from the dropdown 4. Click **Update** ================================================ FILE: docs/platform/deploy/own-cloud.md ================================================ --- seotitle: Connect your cloud account to deploy to any cloud seodesc: Learn how to deploy your backend application to all the major cloud providers (AWS or GCP) using Encore. title: Connect your cloud account subtitle: Whatever cloud you prefer is fine by us lang: platform --- Encore Cloud lets you deploy your application to any of the major cloud providers, using your own cloud account. This lets you use Encore to improve your experience and productivity, while keeping the reliability of a major cloud provider. Each [environment](/docs/platform/deploy/environments) can be configured to use a different cloud provider, and you can have as many environments as you wish. This also lets you easily deploy a hybrid or multi-cloud application, as you see fit. Encore Cloud will provision infrastructure in your cloud account, but for safety reasons Encore Cloud does not automatically destroy infrastructure once it's no longer required. To do this, you need to manually approve the deletion of the infrastructure in your Encore Cloud dashboard. This means if you disconnect your app from your cloud provider, or delete the environment within Encore, you need to explicitly approve the deletion of the infrastructure in your Encore Cloud dashboard. ## Google Cloud Platform (GCP) Encore Cloud provides a GCP Service Account for each Encore Cloud application, letting you grant Encore Cloud access to provision all the necessary infrastructure directly in your own GCP Organization account. To find your app's Service Account email and configure GCP deployments, head over to the Connect Cloud page by going to the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**. ![Connect GCP account](/assets/docs/connectgcp.png "Connect GCP account") ### Troubleshooting **I can't access/edit the `Policy for Domain restricted sharing` page** To edit Organization policies, you need to have the `Organization Policy Administrator` role. If you don't have this role, you can ask your GCP Organization Administrator to grant you the necessary permissions. If you're a GCP Organization Administrator, you can grant yourself the necessary permissions by following the steps below: 1. Go to the [IAM & Admin page](https://console.cloud.google.com/iam-admin/iam) in the GCP Console. 2. Find your user account in the list of members. 3. Click the pencil icon to edit your user account. 4. Add the `Organization Policy Administrator` role to your user account. 5. Click Save. **I can't grant access to the Encore Cloud service account** If you're unable to grant access to the Encore Cloud service account, you may have failed to add Encore Cloud to your `Domain restricted sharing` policy. Make sure you've followed all the steps in the Connect Cloud page to add Encore Cloud to the policy. If you're using several GCP accounts, make sure you're logged in with the correct account and that the correct organization is selected in the GCP Console. **Encore Cloud returns "Could not find Organization ID"** If you see this error message, it means that Encore Cloud was unable to connect to your GCP Organization. Make sure you've followed all the steps in the Connect Cloud page to grant Encore Cloud access to your GCP Organization. If you're using several GCP accounts, make sure you're logged in with the correct account and that the correct organization is selected in the GCP Console. Still having issues? Drop us an email at [support@encore.dev](mailto:support@encore.dev) or chat with us in the [Encore Discord](https://encore.dev/discord. ## Amazon Web Services (AWS) To configure your Encore Cloud app to deploy to your AWS account, head over to the Connect Cloud page by going to the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**. Follow the instructions to create an IAM Role, and then connect the role with Encore Cloud. [Learn more in the AWS docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user.html). ![Connect AWS account](/assets/docs/connectaws.png "Connect AWS account") For your security, make sure to check `Require external ID` and specify the external ID provided in the instructions. After connecting your app to AWS, you will be asked to choose which region you want Encore Cloud to provision resources in. [Learn more about AWS regions here](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/). ================================================ FILE: docs/platform/deploy/preview-environments.md ================================================ --- seotitle: Preview Environments – Temporary dev environments per Pull Request seodesc: Learn how to use Encore to activate automatic Preview Environments for every Pull Request to simplify testing and collaborating. title: Preview Environments subtitle: Accelerate development with isolated test environments for each Pull Request lang: platform --- When using [Encore Cloud Pro](https://encore.cloud/pricing), you automatically get ephemeral Preview Environments for each Pull Request. Preview Environments are free, fully-managed development environments that run on Encore Cloud. They let you test changes without managing infrastructure or incurring cost. See the [infra docs](/docs/platform/infrastructure/infra#preview-environments) if you're curious about exactly how Preview Environments are provisioned. ## Using Preview Environments To use Preview Environments, you first need to [connect your application to GitHub](/docs/platform/integrations/github). Preview Environments are named after the pull request, for example PR #72 creates a Preview Environment named `pr:72` with the API base url `https://pr72-$APP_ID.encr.app`. You can also view the environment in the Encore Cloud dashboard, where the url will be `https://app.encore.cloud/$APP_ID/envs/pr:72`. ![Preview environment linked in GitHub](/assets/docs/ghpreviewenv.png "Preview environment linked in GitHub") ## Populate databases with test data automatically Preview Environments can automatically come with pre-populated test data thanks to Neon's database branching feature. Here's how it works: 1. Your main database (typically in staging) contains your test data 2. When a Preview Environment is created, it gets a fresh database that's an exact copy of your main database 3. This happens automatically - no manual data copying needed! #### Setup instructions 1. Go to [Encore Cloud dashboard](https://app.encore.cloud) 2. Select your app > App Settings > Preview Environments 3. Choose which environment's database to copy from (e.g., staging) 4. Save your changes **Note:** This feature requires using Neon as your database provider, which is: - Default for Encore Cloud environments - Optional for AWS and GCP environments ## Frontend Collaboration Preview Environments make it really easy to collaborate and test changes with your frontend. Just update your frontend API client to point to the `pr:#` environment. This is a one-line change since your API client always specifies the environment name, e.g. `https://-.encr.app/`. If your pull request makes changes to the API, you can [generate a new API client](/docs/ts/cli/client-generation) for the new backend API using `encore gen client --env=pr:72 --lang=typescript my-app` ================================================ FILE: docs/platform/deploy/security.md ================================================ --- seotitle: Security – How Encore keeps your backend application secure seodesc: Encore applications come with built-in security best practises. See how Encore keeps your application secure by default. title: Application Security subtitle: Encore Cloud makes strong security the default path lang: platform --- ## Built on industry experience The security practices in Encore Cloud are built on our team's decades of experience designing and operating sensitive systems at companies like Google, Spotify, and Monzo. ## Security by Default Encore Cloud is designed to make security effortless rather than burdensome: - **Zero-config security**: Focus on building features while Encore Cloud automatically implements security best practices - **Built-in secrets management**: Safely handle sensitive data using the built-in [secrets management system](/docs/ts/primitives/secrets) - **Automated IAM management**: Encore Cloud automatically manages IAM policies based on the principle of least privilege ## Security features When Encore Cloud deploys your application and infrastructure, it takes care of implementing security best practices: - **Strong encryption**: All communication uses mutual TLSv1.3 - **Secure databases**: Database access is encrypted with certificate validation and strong security credentials - **Cloud security**: Automatic provisioning with security best practices specific to each cloud provider - Learn more about [Google Cloud Platform (GCP)](/docs/platform/infrastructure/gcp) - Learn more about [Amazon Web Services (AWS)](/docs/platform/infrastructure/aws) ================================================ FILE: docs/platform/infrastructure/aws.md ================================================ --- seotitle: AWS Infrastructure on Encore Cloud seodesc: A comprehensive guide to how Encore Cloud provisions and manages AWS infrastructure for your applications title: AWS Infrastructure subtitle: Understanding your application's AWS infrastructure lang: platform --- Encore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary AWS infrastructure. This guide provides a detailed look at the components involved and how they work together to support your applications. ## Core Infrastructure Components ### Networking Architecture Networking is a critical aspect of cloud infrastructure, ensuring secure and efficient communication between different parts of your application. Encore Cloud creates an isolated [Virtual Private Cloud (VPC)][aws-vpc] for each environment, which serves as a secure network boundary. The network architecture is designed with reliability and security in mind. Each VPC spans across two Availability Zones (AZs), providing redundancy and fault tolerance. If one AZ experiences issues, your application can continue running in the other AZ, significantly reducing the risk of downtime. This multi-AZ setup is crucial for maintaining high availability in production environments. Within the VPC, Encore Cloud implements a three-tier architecture that carefully separates different components of your application into distinct subnet layers. This separation of concerns enhances both security and performance by controlling traffic flow between layers and limiting potential attack vectors. Each tier is configured with specific security groups and network ACLs to enforce these boundaries, creating a robust and secure networking foundation for your application. #### Subnet Tiers 1. **Public Subnet** The public subnet contains several key components that manage external traffic flow. At the forefront is the Application Load Balancer (ALB), which serves as the entry point for all incoming traffic to your application. The ALB intelligently distributes requests across your application instances, ensuring optimal performance and reliability. To enable outbound communication, the subnet includes an Internet Gateway that allows your application components to securely connect to external services and APIs. Working alongside it is a NAT Gateway, which provides a secure pathway for resources in private subnets (like your compute instances) to access the internet while remaining protected from direct external access. This NAT Gateway acts as an intermediary, translating private IP addresses to public ones for outbound traffic while maintaining the security of your internal resources. 2. **Compute Subnet** The compute subnet is where your application's containers run, regardless of whether you're using Fargate or EKS as your container orchestration platform. This subnet is carefully isolated and configured to only accept incoming traffic from the Application Load Balancer in the public subnet. This strict traffic control ensures that your application containers can only be accessed through proper channels, protecting them from unauthorized direct access while still allowing legitimate requests to flow through seamlessly. 3. **Storage Subnet** (provisioned as needed) The storage subnet is a dedicated network segment designed to host your application's databases and caching systems. To maintain the highest level of security, this subnet operates in complete isolation from the internet, with no direct inbound or outbound connectivity. Access to resources within the storage subnet is strictly limited to traffic originating from the compute subnet, creating a secure enclave for your data layer. This architecture ensures that your sensitive data remains protected while still being readily accessible to your application's services running in the compute tier. ### Container Management Encore Cloud provisions an [Elastic Container Registry (ECR)][aws-ecr] to store your application's Docker images. The registry is seamlessly integrated with your chosen compute platform and provides robust security features. Access to images is tightly controlled through comprehensive access controls, ensuring only authorized users and services can pull or push container images. Additionally, ECR automatically scans all images for known security vulnerabilities as they are pushed to the registry, helping you maintain a secure application environment by identifying potential risks before deployment. ### Secrets Management Managing sensitive information securely is crucial. Encore Cloud uses [AWS Secrets Manager][aws-secrets] to store and manage secrets, such as API keys and database credentials. Through deep integration with AWS Secrets Manager, Encore Cloud automatically injects secrets directly into your service's environment variables at runtime, making them easily accessible while maintaining strict security controls. All secrets are encrypted both at rest and in transit using industry-standard encryption algorithms, providing comprehensive protection for your sensitive data. The system implements fine-grained access controls, where each service is given precisely scoped permissions to access only the specific secrets it needs. This ensures that even if one service is compromised, the blast radius is contained and other secrets remain secure. ## Compute Options Encore Cloud provisions one of two compute platforms for running your application containers, based on your choice: ### AWS Fargate When using Fargate, Encore Cloud configures: - **Task Definitions** Task definitions are meticulously configured to ensure optimal performance and reliability of your services. Each service's container settings are fine-tuned based on its specific requirements, including memory allocation, CPU utilization, and networking parameters. Comprehensive health check configurations monitor the service's status, enabling quick detection and recovery from any issues. Environment variables are securely injected from AWS Secrets Manager at runtime, providing your services with the credentials and configuration they need while maintaining security. The task definitions are also integrated with AWS Service Discovery, enabling automatic service registration and allowing for seamless service-to-service communication within your application. - **Fargate Services** Fargate services are configured with sophisticated deployment strategies that ensure zero downtime during updates. When deploying new versions of your services, Encore Cloud orchestrates a rolling update process where new tasks are gradually introduced while old ones are removed, maintaining consistent availability throughout the deployment. Each service is automatically integrated with Application Load Balancer target groups, enabling intelligent request routing and load distribution. The load balancer continuously monitors the health of your service instances and automatically routes traffic only to healthy targets. To ensure smooth service startup, appropriate health check grace periods are configured. This gives your services adequate time to initialize and warm up before receiving traffic, preventing premature health check failures during deployment or scaling events. - **IAM Configuration** Encore Cloud implements a comprehensive IAM security model by creating unique execution roles for each task definition. These roles are automatically configured with precisely scoped permissions that enable secure access to required AWS services. The execution roles allow containers to pull images from ECR and write operational logs to CloudWatch for monitoring and debugging. They also grant access to assigned AWS resources like S3 buckets and SQS queues that the service needs to interact with. Additionally, the roles are configured to securely retrieve secrets from AWS Secrets Manager at runtime, enabling safe storage and access of sensitive configuration data. This granular permission model follows security best practices by providing each service with the minimum privileges required for operation. - **Network Integration** Fargate tasks are strategically placed within private compute subnets, ensuring they remain isolated from direct internet access while maintaining the ability to communicate with other application components. The associated security groups are configured with precise rules that govern network traffic. These rules allow inbound traffic exclusively from the Application Load Balancer, ensuring that your services can only be accessed through the properly configured entry point. For outbound connectivity, the security groups permit traffic to flow to your databases and caching layers, enabling your services to interact with these essential backend resources while maintaining a secure network boundary. ### Amazon EKS When using EKS, Encore Cloud configures: - **Cluster Setup** Encore Cloud configures the core networking components required for cluster operation. The VPC CNI (Container Network Interface) is configured to enable pod networking within the cluster, allowing pods to communicate efficiently using the underlying AWS VPC networking capabilities. This includes setting up IP address management and network policy enforcement. The cluster's internal DNS resolution is handled through CoreDNS, which is configured for optimal service discovery and name resolution within the cluster. CoreDNS settings are tuned to provide fast and reliable DNS lookups while maintaining reasonable cache sizes and query limits. - **Kubernetes Resources** Encore Cloud automatically manages all necessary Kubernetes resources for your application. Each service in your application is deployed as a separate Kubernetes Deployment, allowing for independent scaling and lifecycle management. These deployments are configured with appropriate resource requests, limits, and health checks to ensure reliable operation. For authentication and authorization, Encore Cloud implements IAM Roles for Service Accounts (IRSA), providing secure access to AWS services. Each service gets its own service account with precisely scoped IAM roles, following the principle of least privilege. For sensitive data like API keys and credentials, Encore Cloud uses Kubernetes Secrets, which are encrypted at rest and only accessible to authorized services. To enable network connectivity, Encore Cloud creates Kubernetes Service resources for each of your application's services, providing stable networking endpoints for inter-service communication. - **Load Balancer Integration** Encore Cloud manages the complete load balancer integration for your EKS cluster. The AWS Load Balancer Controller is automatically installed and configured to handle ingress traffic for your application. This controller works in conjunction with the Application Load Balancer (ALB) to provide intelligent traffic routing and SSL/TLS termination. The ALB Ingress Controller is configured to automatically create and manage Application Load Balancers based on your application's needs. It handles the creation and configuration of target groups, ensuring traffic is properly distributed across your service pods. The controller also manages the lifecycle of these resources, automatically cleaning up unused resources to prevent waste. Target group binding is automatically configured to map your Kubernetes services to the appropriate ALB target groups. This ensures that traffic is correctly routed to the right pods and that health checks are properly configured to maintain high availability. For secure communication, Encore Cloud automatically manages SSL/TLS certificates through AWS Certificate Manager. These certificates are automatically provisioned, renewed, and attached to your load balancers, ensuring all external traffic to your application is encrypted. The system also handles certificate rotation and updates transparently, maintaining secure communication without manual intervention. - **Monitoring Setup** Encore Cloud automatically aggregates and sends metrics to your configured metrics destination, providing you with real-time visibility into your application's performance. In addition to metrics, Encore Cloud configures the CloudWatch Logs agent to capture and forward all container logs. The logs are structured and organized by service name, making it easy to search and analyze application behavior. Log streams are automatically created for each container, and log retention policies are configured to help manage storage costs while maintaining necessary historical data. - **Service Accounts** Encore Cloud implements a comprehensive service account management system that ensures secure and controlled access to resources. Each service in your application receives its own dedicated Kubernetes service account, providing a unique identity for authentication and authorization purposes. To enable secure interaction with AWS services, Encore Cloud maps each Kubernetes service account to a corresponding IAM role using IAM Roles for Service Accounts (IRSA). This mapping allows pods to securely authenticate with AWS services without storing long-lived credentials. The IAM roles are automatically configured with the minimum required permissions for each service's needs. This includes access to service-specific S3 buckets for object storage operations, permissions to publish and subscribe to SQS queues and SNS topics, ability to retrieve secrets from AWS Secrets Manager, and secure access to assigned database instances. These permissions are continuously updated as your application evolves, ensuring services always have the access they need while maintaining strong security boundaries. All of these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs. ## Managed Services ### Databases Encore Cloud provisions [Amazon RDS][aws-rds] for PostgreSQL databases, providing a robust and scalable database solution. Each database runs the latest PostgreSQL version to ensure compatibility with modern features while maintaining up-to-date security patches. The databases are provisioned on auto-scaling capable instances, starting with db.m5.large configurations that can seamlessly scale up as your application's needs grow. To protect your data, Encore Cloud configures automated daily backups with a 7-day retention period. Security is paramount, so databases are strategically placed within private subnets and protected by comprehensive access controls. This network isolation combined with strict security rules ensures your data remains secure while still being accessible to your application's services. #### Database Access Database access is managed through a comprehensive security model. At its core, Encore Cloud deploys [Emissary](https://github.com/encoredev/emissary), a secure socks proxy that enables safe database migrations while maintaining strict access controls. Each service in your application is assigned its own dedicated database role, providing granular control over data access and ensuring services can only interact with the data they need. For enhanced security, all databases are placed in private subnets, completely isolated from direct internet access. This multi-layered approach creates a secure foundation for your application's data access needs while maintaining operational flexibility. ### Pub/Sub Encore Cloud implements a robust messaging system using [SQS][aws-sqs] and [SNS][aws-sns]. The system automatically configures dead-letter queues to capture failed messages, enabling thorough analysis and debugging of messaging issues. Each service in your application receives precisely scoped IAM permissions to publish and consume messages, ensuring secure communication between components. Encore Cloud fully manages the creation and configuration of subscriptions and topics, streamlining the setup and ongoing maintenance of your messaging infrastructure while maintaining optimal performance and reliability. ### Object Storage Encore Cloud leverages [S3][aws-s3] for object storage, providing a comprehensive solution for your application's storage needs. When you declare storage requirements in your application, Encore Cloud automatically provisions dedicated S3 buckets with unique names to ensure global uniqueness across AWS. Each service in your application receives precisely scoped permissions to perform storage operations, following the principle of least privilege. For public buckets, Encore Cloud automatically integrates with CloudFront to create a global content delivery network, significantly improving access speeds for your users worldwide. Each bucket is assigned its own unique domain name, making it simple to manage and access stored content while maintaining a clear organizational structure. ### Caching Encore Cloud uses [ElastiCache for Redis][aws-redis] to provide a high-performance caching solution. The service starts with cache.m6g.large instances that can automatically scale up as your application's needs grow. To ensure maximum reliability, caches are configured with Multi-AZ replication across availability zones, providing both high availability and fault tolerance. In the event of any failures, automatic failover capabilities ensure your application experiences no disruption in service. Security is maintained through Redis Access Control Lists (ACLs), which provide fine-grained control over who can access your cache and what operations they can perform. The entire system is configured for high availability, with monitoring and alerting in place to maintain optimal performance and uptime. This comprehensive setup ensures your application's caching layer remains fast, secure, and always available. ### Cron Jobs Encore Cloud provides a streamlined approach to scheduled tasks that prioritizes security and simplicity. Each cron job is executed through authenticated API requests that are cryptographically signed to verify their authenticity. The system performs rigorous source verification to ensure all scheduled tasks originate exclusively from Encore Cloud's cron functionality, preventing unauthorized execution attempts. This elegant implementation requires no additional infrastructure components, making it both cost-effective and easy to maintain while ensuring your scheduled tasks run reliably and securely. [aws-vpc]: https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html [aws-fargate]: https://aws.amazon.com/fargate/ [aws-eks]: https://aws.amazon.com/eks/ [aws-secrets]: https://aws.amazon.com/secrets-manager/ [aws-rds]: https://aws.amazon.com/rds/postgresql/ [aws-sqs]: https://aws.amazon.com/sqs/ [aws-sns]: https://aws.amazon.com/sns/ [aws-s3]: https://aws.amazon.com/s3/ [aws-redis]: https://aws.amazon.com/elasticache/redis/ [aws-ecr]: https://aws.amazon.com/ecr/ [aws-alb]: https://aws.amazon.com/elasticloadbalancing/application-load-balancer/ ================================================ FILE: docs/platform/infrastructure/cloudflare.md ================================================ --- seotitle: Cloudflare R2 Infrastructure on Encore Cloud seodesc: A comprehensive guide to how Encore Cloud provisions and manages Cloudflare R2 infrastructure for your applications title: Cloudflare R2 Buckets lang: platform --- Encore Cloud simplifies the process of using Cloudflare R2 for object storage by automatically provisioning and managing the necessary infrastructure. This guide provides setup instructions and details on how Encore Cloud manages your Cloudflare R2 infrastructure. ## Setup Process ### 1. Cloudflare Account Connection To connect your Cloudflare account to Encore Cloud: 1. Create a Cloudflare API token using the **Create Additional Tokens** button in the Cloudflare dashboard 2. Add the the following permissions: - Zone > Zone: Read - Zone > DNS: Edit - Account > Workers R2 Storage: Edit 3. Add the token in the Encore Cloud dashboard: - Navigate to App Settings > Integrations > Cloudflare - Click "Connect Account" - Provide an account name and your API token ### 2. Environment Configuration When creating a new environment: 1. Select your preferred cloud provider 2. Choose "Cloudflare R2" as the object storage provider 3. Configure the following R2-specific settings: - Token: Your Cloudflare API token - Account: Your Cloudflare account - Zone: The domain zone for public bucket URLs - Region: Your preferred R2 storage region ## Managed Features ### Bucket Management Encore Cloud provides comprehensive bucket management capabilities that adapt to your application's needs. When you define storage requirements in your application, Encore Cloud automatically provisions the necessary R2 buckets with appropriate configurations. Each bucket is created with carefully configured policies and access controls to ensure secure yet efficient access to your stored objects. ### Public Access Configuration When working with public buckets, Encore Cloud handles all aspects of public access configuration automatically. Each bucket is assigned a unique subdomain that is automatically provisioned and configured in your DNS settings. The bucket is seamlessly integrated with Cloudflare's global CDN network, ensuring fast content delivery worldwide. Encore Cloud also configures optimal caching rules to maximize performance while maintaining appropriate cache invalidation policies. This comprehensive setup ensures your public content is served efficiently and securely through Cloudflare's infrastructure. ### Security Controls Encore Cloud implements a comprehensive multi-layered security model to protect your R2 storage. At the bucket level, fine-grained access controls ensure that only authorized services can perform specific operations on each bucket. Each service in your application receives its own unique set of credentials, preventing any unauthorized cross-service access. These credentials are securely distributed to the appropriate services through Encore Cloud's built-in secrets management system, which handles the entire credential lifecycle. All these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs. [cloudflare-r2]: https://developers.cloudflare.com/r2/ [cloudflare-cdn]: https://developers.cloudflare.com/cdn/ ================================================ FILE: docs/platform/infrastructure/configuration.md ================================================ --- seotitle: Infrastructure Configuration seodesc: Learn how you can configure infrastructure provisioned using Encore Cloud title: Infrastructure Configuration subtitle: How to configure infrastructure when using Encore Cloud lang: platform --- Encore Cloud provides a powerful and flexible approach to infrastructure management, ensuring that your cloud resources are efficiently provisioned, according to enterprise best practices. Unlike traditional Infrastructure-as-Code (IaC) tools, when using Encore's declarative infrastructure framework, you do not define any cloud service specifics in code. This ensures your code is cloud-agnostic and portable across clouds, and can be deployed using different infrastructure for each environment according to your priorities (cost, performance, etc.). Infrastructure configuration is made in the Encore Cloud dashboard, which provides a controlled workflow, role-based access controls, and auditable history of changes. Encore Cloud provisions and manages infrastructure by using your cloud provider's APIs. Learn more in the [Infrastructure](/docs/platform/infrastructure/infra) documentation. ## Infrastructure settings when creating a new environment When creating a new environment, you can decide the following: - Which cloud provider to use (AWS or GCP) - Which compute hardware to use (e.g. AWS Fargate, GCP Cloud Run, Kubernetes) - If using Kubernetes, should a new cluster be created or should an existing cluster be used? - Which Kubernetes provider to use (GKE or EKS) - Which database to use (e.g. AWS RDS, GCP CloudSQL, Neon Serverless Postgres) - Which process allocation strategy to use (more on this below) ## Ongoing infrastructure configuration ### Configuration UI in Encore Cloud After creating an environment, you can continue to configure the infrastructure via the Encore Cloud dashboard. The dashboard exposes the most common configuration options, and provides a controlled workflow for making changes, including audit logs and role-based access controls. #### Process allocation configuration Encore provides a powerful configuration option called process allocation. This enables you to configure how microservices should be deployed on the compute hardware; either deploying all services in one process or one process per service. All without any code changes. It's often recommended to deploy all services in one process in order to reduce costs and minimize response times between services. (But it depends on your use case.) Deploying each service as its own process will improve scalability and decrease blast radius if things go wrong. This is only recommended for production environments. ### Manual configuration in your cloud provider's console Manual configuration is relevant in cases where some configuration options are not yet available in the Encore Cloud dashboard, or you may want to make changes manually. Handily, you have full access to make changes directly in your cloud provider's console. Encore Cloud tries very hard to ensure that any manual changes made in the cloud provider's console are not overwritten. Therefore it only makes the minimum necessary modifications to infrastructure when deploying new changes, using the following strategies: - **PATCH-style updates:** Resources are updated using compare-and-set and similar techniques, modifying only the attributes that require changes. - **Avoid full syncs:** Unlike Terraform, Encore Cloud updates only the specific resources necessary to accomplish an infrastructure change rather than performing a complete infrastructure refresh. These behaviors ensure an efficient and predictable workflow, minimizing unintended changes and reducing deployment times, and means that you can safely use your cloud provider's console to modify the provisioned resources. This behavior also makes Encore Cloud well-suited for environments where infrastructure is partially managed outside of Encore Cloud, enabling you to deploy Encore applications alongside existing infrastructure (more on this below). ## Working with Existing Infrastructure One of Encore Cloud’s strengths is its ability to work seamlessly with existing infrastructure. Since it does not enforce a full sync approach, it can: - Integrate with pre-existing cloud resources without overwriting manual changes - Deploy to existing Kubernetes clusters - Co-exist with other IaC tools like Terraform and CloudFormation. Encore Cloud also provides a Terraform Provider to simplify integration with existing Terraform-managed infrastructure. Learn more in the [Terraform Provider](/docs/platform/integrations/terraform) documentation. ================================================ FILE: docs/platform/infrastructure/configure-kubectl.md ================================================ --- seotitle: Configure kubectl to access your Encore Kubernetes cluster seodesc: Learn how to configure kubectl to access your Encore Kubernetes cluster. title: Configure kubectl lang: platform --- Encore Cloud automatically provisions and manages Kubernetes clusters for you, but sometimes it's useful to manually inspect clusters using the [kubectl](https://kubernetes.io/docs/reference/kubectl/) cli. To do this, you need to configure `kubectl` to connect and authenticate through encore. You can do this by running the following command in your app directory: ```shell encore kubernetes configure -e ``` Where `` is the name of the environment you want to configure `kubectl` for. This will configure `kubectl` to use `encore` to authenticate the cluster and proxy your traffic to the correct cluster. You can now use `kubectl` as you normally would, for example: ```shell kubectl get pods ``` ================================================ FILE: docs/platform/infrastructure/configure-network.md ================================================ --- seotitle: How to configure custom network settings for your Encore environment seodesc: Learn how to configure IP ranges when connecting your Encore application to existing networks. title: Configure network settings subtitle: Customizing IP ranges for network peering lang: platform --- # Overview When deploying applications with Encore Cloud, a network is automatically provisioned with default settings. However, if you plan to peer your Encore network with an existing network, you can manually configure the IP range for your environment. ## Benefits Configuring custom network settings allows you to: - Connect your Encore application to existing networks via peering - Prevent IP range conflicts with other networks in your organization - Plan your network topology with predictable addressing ## Configuring network settings Follow these steps to configure custom network settings: 1. Navigate to **Create Environment** in the Encore Cloud dashboard 2. Select the AWS or GCP cloud provider 3. Expand the **Network** section 4. Enter your desired IP range - The range must be at least a /16 block to reserve enough IPs for your application to grow - Choose a range that doesn't conflict with your existing networks Once configured, Encore will use your specified IP range instead of assigning a random private network. ## Default network behavior By default, Encore will reserve a randomly assigned /16 block in one of the private IP ranges. This is suitable for most deployments that don't require network peering. ================================================ FILE: docs/platform/infrastructure/gcp.md ================================================ --- seotitle: GCP Infrastructure on Encore Cloud seodesc: A comprehensive guide to how Encore Cloud provisions and manages GCP infrastructure for your applications title: GCP Infrastructure subtitle: Understanding your application's GCP infrastructure lang: platform --- Encore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary GCP infrastructure. This page provides an overview of the components involved and how they work together to support your applications. _Example of Encore project deployment alongside existing legacy systems on GCP:_ ## Core Infrastructure Components ### Networking Architecture To ensure maximum security and isolation, Encore Cloud provisions a dedicated GCP Project for each environment. This project isolation prevents any potential cross-environment access and enables granular control over resources and permissions. Within each project, all resources are deployed into a private network configuration, where they can only communicate with other resources inside the VPC. This private networking approach significantly reduces the attack surface by preventing direct access from the public internet, with traffic only flowing through designated ingress points. ### Container Management Encore Cloud provisions a [Google Container Registry (GCR)][gcp-gcr] to store your application's Docker images. The registry implements comprehensive access controls to ensure only authorized users and services can access and manage container images. Through integration with GCP's Identity and Access Management (IAM), each service is granted the minimum required permissions needed to pull its container images. Additionally, GCR performs automated vulnerability scanning on all container images. As new images are pushed to the registry, they are automatically analyzed for known security vulnerabilities in the operating system and application dependencies. This proactive scanning helps identify potential security issues early in the deployment pipeline, allowing you to maintain a secure application environment. ### Secrets Management Encore Cloud's integration with Secret Manager provides comprehensive security and seamless access to sensitive data. All secrets are automatically injected as environment variables into your services, eliminating the need for manual configuration while maintaining security. The secrets are protected using industry-standard encryption both when stored and during transmission between services. To ensure maximum security, Secret Manager implements strict access controls - each service can only access the specific secrets it needs, and all access attempts are logged and audited. ## Compute Options Encore Cloud provisions one of two compute platforms for running your application containers, based on your choice: ### Google Cloud Run When using Cloud Run, Encore Cloud configures: **Service Deployments** Each service is configured with optimized container settings and health check configurations to ensure reliable operation. Environment variables are automatically injected from Secret Manager to securely provide configuration values. Service discovery integration enables seamless communication between services. **Cloud Run Services** Cloud Run services are configured with zero-downtime deployment strategies, ensuring your application remains available during updates. Each service is integrated with a load balancer to distribute traffic efficiently across instances. Health check grace periods are configured to allow containers adequate time to start up before receiving traffic, preventing premature termination of healthy instances. **IAM Configuration** Each deployment receives its own dedicated service account to ensure proper isolation and security. These service accounts are automatically configured with the minimum required permissions needed for operation. This includes access to pull container images from Google Container Registry, write application logs to Cloud Logging, and interact with assigned GCP resources like Cloud Storage buckets and Pub/Sub topics. The service accounts are also granted permission to read secrets from Secret Manager, enabling secure access to sensitive configuration values. This automated permission management ensures your services have exactly the access they need while following security best practices. ### Google Kubernetes Engine When using GKE, Encore Cloud configures: - **Cluster Setup** Encore Cloud provisions either GKE Autopilot clusters or standard GKE clusters with managed node pools, both configured to run in private subnets for enhanced security. With Autopilot, GKE automatically manages the underlying infrastructure, while with standard clusters Encore Cloud configures and maintains optimized node pools based on your workload requirements. In both cases, the nodes are placed in private subnets to ensure they're not directly accessible from the internet, with all traffic flowing through the load balancer. - **Kubernetes Resources** Encore Cloud automatically creates and manages all necessary Kubernetes resources for your application. Each Encore service is deployed as a Kubernetes Deployment, ensuring reliable operation and scaling capabilities. These deployments are backed by service accounts configured with appropriate IAM roles to access GCP resources securely. Sensitive configuration data is stored as Kubernetes Secrets and automatically mounted into the appropriate pods. To enable network connectivity, Encore Cloud provisions Kubernetes Service and Ingress resources that integrate with the Google Cloud Load Balancer, providing secure external access to your application endpoints. - **Load Balancer Integration** Encore Cloud integrates with Google Cloud Load Balancer to provide secure and reliable access to your applications. The load balancer is configured to distribute traffic across your services while handling SSL/TLS termination. All traffic is automatically encrypted using managed SSL/TLS certificates that are provisioned and renewed automatically. This ensures your application endpoints remain secure and accessible through HTTPS without requiring manual certificate management. - **Monitoring Setup** Encore Cloud sets up comprehensive monitoring for your GKE clusters by configuring both metrics collection and log management. Container metrics are automatically collected from each pod and exported to your configured monitoring service, providing detailed insights into resource usage, performance, and application behavior. Additionally, all container logs are seamlessly forwarded to Cloud Logging, enabling centralized log aggregation and analysis. This integrated monitoring approach gives you full visibility into your application's health and performance within the Google Cloud ecosystem. - **Service Accounts** Encore Cloud implements a comprehensive service account management system that ensures secure and controlled access to GCP resources. Each service in your application receives its own dedicated service account, providing fine-grained access control and isolation between services. These service accounts are automatically configured with IAM roles that map precisely to the GCP services your application needs to interact with. The permission configuration is handled dynamically based on your application's declared resource usage. For example, if your service needs to access a GCS bucket, Encore Cloud automatically grants the minimum required permissions for those specific storage operations. Similarly, when your service needs to publish or subscribe to Pub/Sub topics, connect to databases, or retrieve secrets, the appropriate IAM roles are configured automatically. This automated permission management ensures that each service operates under the principle of least privilege, having access only to the resources it explicitly needs to function. This significantly enhances your application's security posture by minimizing the potential impact of any security breach. All of these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs. ## Managed Services ### Databases Encore Cloud provisions [GCP Cloud SQL][gcp-cloudsql] for PostgreSQL databases, providing a robust and scalable database solution: Encore Cloud provisions Cloud SQL instances running the latest PostgreSQL version, ensuring you have access to the newest features and security updates. Each instance starts with the smallest available configuration to optimize costs, while maintaining the ability to automatically scale up resources as your application's needs grow. Data protection is a key priority, with automated daily backups retained for 7 days and point-in-time recovery capabilities. This allows you to restore your database to any moment within the retention period if needed. Security is enforced through strategic placement of databases in private subnets, isolating them from direct internet access. Strict access controls ensure that only authorized services and users can connect to the database instances. ### Pub/Sub Encore Cloud implements a robust messaging system using [GCP Pub/Sub][gcp-pubsub]. The system is designed with reliability and security in mind, automatically configuring dead-letter topics to capture and preserve any failed messages for later analysis and debugging. Each service in your application receives precisely scoped IAM permissions for publishing and consuming messages, ensuring secure communication between components while maintaining the principle of least privilege. Encore Cloud fully manages all subscriptions and topics, handling the complex setup and ongoing maintenance of your messaging infrastructure, allowing you to focus on your application logic rather than infrastructure management. ### Object Storage Encore Cloud leverages [Google Cloud Storage][gcp-gcs] for object storage needs. When you declare storage buckets in your application, Encore Cloud automatically provisions them with unique names in GCP. Each service that interacts with storage is configured with precisely scoped permissions, ensuring secure access to only the buckets and operations it requires. For public buckets, Encore Cloud integrates with Cloud CDN to optimize content delivery, with each bucket accessible through a unique URL. This comprehensive setup provides secure, efficient, and easily manageable object storage capabilities for your application. ### Caching Encore Cloud uses [GCP Memorystore for Redis][gcp-redis] to provide a high-performance caching solution. Each Redis instance starts with the smallest available configuration to optimize costs while maintaining the ability to automatically scale up resources as your application's caching needs grow. The instances are configured in a high-availability setup to ensure your cache remains available and performant even during infrastructure updates or zone outages. Access to the cache is secured through Redis authentication, with credentials automatically managed and rotated by Encore Cloud to maintain a strong security posture. ### Cron Jobs Encore Cloud provides a streamlined approach to scheduled task execution that prioritizes both simplicity and security. Each cron job is executed through authenticated API requests that are cryptographically signed, ensuring that only legitimate, verified requests can trigger your scheduled tasks. The system includes robust source verification that validates all requests originate from Encore Cloud's trusted cron infrastructure. This elegant implementation requires no additional infrastructure components, making it both cost-effective and easy to maintain while providing the reliability and security needed for production workloads. [gcp-vpc]: https://cloud.google.com/vpc [gcp-cloudrun]: https://cloud.google.com/run [gcp-gke]: https://cloud.google.com/kubernetes-engine [gcp-secrets]: https://cloud.google.com/secret-manager [gcp-pubsub]: https://cloud.google.com/pubsub [gcp-gcs]: https://cloud.google.com/storage [gcp-cloudsql]: https://cloud.google.com/sql [gcp-redis]: https://cloud.google.com/memorystore [gcp-gcr]: https://cloud.google.com/container-registry ================================================ FILE: docs/platform/infrastructure/import-cloud-sql.md ================================================ --- seotitle: How to deploy your Encore application with an existing Cloud SQL instance seodesc: Learn how to easily import your existing Cloud SQL instance and connect your Encore application to it. title: Import an existing Cloud SQL instance subtitle: Using your pre-existing database instead of provisioning a new one lang: platform --- # Overview When deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including database instances. However, if you already have a Cloud SQL instance, you can connect your Encore application directly to this existing database. ## Benefits Using an existing Cloud SQL instance allows you to: - Maintain data continuity with your existing systems - Preserve specific database configurations - Utilize familiar database setups without migration ## Importing a Cloud SQL instance Follow these steps to import your existing Cloud SQL instance: 1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud) 2. Select the GCP cloud provider 3. Choose **Import Existing Cloud SQL Instance** 4. Add permissions for the Encore Service Account: - Copy the `Encore GCP Service Account` from the cloud dashboard - Go to your project's IAM page in the GCP Console - Grant the `Owner` role to the `Encore GCP Service Account` 5. Return to the Encore Cloud dashboard 6. Specify your database's `GCP Project ID` and `Cloud SQL Instance Name` 7. Click the `Resolve` button to validate the instance Once validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically connect your application to your imported Cloud SQL instance rather than provisioning a new database. ## Mapping existing databases to your Encore app To access an existing database in your Encore application, you need to specify the name of the existing database when you declare the database in your app. For example, if you have an existing database called `mydb` you can create a reference to it like so: ```typescript const db = new SQLDatabase("mydb"); ``` ```go sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{ Migrations: "./migrations", }) ``` ## Applying migrations to existing databases Encore uses a table called `schema_migrations` in the public namespace to keep track of which migrations have been applied. If you import an existing database without that table, Encore will create it for you and apply your migrations in order. If the table already exists, Encore expects it to contain exactly two columns: ``` version bigint dirty boolean ``` If the table exists but has a different schema, you will not be able to import it with Encore at this time. If the table exists with an existing entry, Encore will apply all higher versions in your `migrations` directory to the database. ================================================ FILE: docs/platform/infrastructure/import-kubernetes-cluster.md ================================================ --- seotitle: How to deploy your Encore application to an existing Kubernetes cluster seodesc: Learn how to easily import your existing Kubernetes cluster and deploy your Encore application into it. title: Import an existing Kubernetes cluster subtitle: Deploying to your pre-existing cluster instead of provisioning a new one lang: platform --- When you deploy your application to your own cloud, Encore Cloud can provision infrastructure for it in many different ways – including setting up a Kubernetes cluster. If you already have a Kubernetes cluster, Encore Cloud can deploy your Encore application into this pre-existing cluster. This is often useful if you want to integrate your Encore application with other parts of your system that are not built using Encore. Kubernetes imports are supported on GCP, AWS support is coming soon. ## Importing a cluster To import your cluster, go to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud), select **Kubernetes: Existing GKE Cluster** as the compute platform, and then specify your cluster's `Project ID`, `Region`, and `Cluster Name`. When you deploy to this environment, Encore Cloud will use your imported cluster as the compute instance. ================================================ FILE: docs/platform/infrastructure/import-project.md ================================================ --- seotitle: How to deploy your Encore application to an existing GCP project seodesc: Learn how to easily import your existing GCP project and connect your Encore application to it. title: Import an existing GCP project subtitle: Using your pre-existing GCP project instead of provisioning a new one lang: platform --- # Overview When deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including new GCP projects. However, if you already have a GCP project, you can deploy your Encore application directly to this existing project. ## Benefits Using an existing GCP project allows you to: - Keep all your infrastructure in a single project - Maintain existing IAM policies and permissions - Utilize existing billing settings and quotas - Consolidate resources for easier management ## Importing a GCP project Follow these steps to import your existing GCP project: 1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud) 2. Select the GCP cloud provider 3. Choose **Import Project** 4. Add permissions for the Encore Service Account: - Copy the `Encore GCP Service Account` from the cloud dashboard - Go to your project's IAM page in the GCP Console - Grant the `Owner` role to the `Encore GCP Service Account` 5. Return to the Encore Cloud dashboard 6. Enter your `Project ID` 7. Click the `Resolve` button to validate the project Once validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically deploy your application to your imported GCP project rather than provisioning a new one. ================================================ FILE: docs/platform/infrastructure/import-rds.md ================================================ --- seotitle: How to deploy your Encore application with an existing AWS RDS instance seodesc: Learn how to easily import your existing AWS RDS instance and connect your Encore application to it. title: Import an existing AWS RDS instance subtitle: Using your pre-existing database instead of provisioning a new one lang: platform --- # Overview When deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including database instances. However, if you already have an AWS RDS instance, you can connect your Encore application directly to this existing database. ## Benefits Using an existing AWS RDS instance allows you to: - Maintain data continuity with your existing systems - Preserve specific database configurations - Utilize familiar database setups without migration ## Importing an AWS RDS instance Follow these steps to import your existing AWS RDS instance: 1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud) 2. Select the AWS cloud provider 3. Pick the `AWS Region` your database resides in 3. Choose **Import Existing RDS Instance** 4. Specify your database's `RDS Instance Name` 5. Click the `Resolve` button to validate the instance Once validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically connect your application to your imported AWS RDS instance rather than provisioning a new database. ## Mapping existing databases to your Encore app To access an existing database in your Encore application, you need to specify the name of the existing database when you declare the database in your app. For example, if you have an existing database called `mydb` you can create a reference to it like so: ```typescript const db = new SQLDatabase("mydb"); ``` ```go sqldb.NewDatabase("mydb", sqldb.DatabaseConfig{ Migrations: "./migrations", }) ``` ## Applying migrations to existing databases Encore uses a table called `schema_migrations` in the public namespace to keep track of which migrations have been applied. If you import an existing database without that table, Encore will create it for you and apply your migrations in order. If the table already exists, Encore expects it to contain exactly two columns: ``` version bigint dirty boolean ``` If the table exists but has a different schema, you will not be able to import it with Encore at this time. If the table exists with an existing entry, Encore will apply all higher versions in your `migrations` directory to the database. ================================================ FILE: docs/platform/infrastructure/infra.md ================================================ --- seotitle: Cloud Infrastructure Provisioning seodesc: Learn how to provision appropriate cloud infrastructure depending on the environment type for AWS and GCP. title: Infrastructure provisioning & Environments subtitle: How Encore Cloud provisions infrastructure for your application lang: platform --- Encore Cloud automatically provisions all necessary infrastructure, in all environments and across all major cloud providers, without requiring application code changes. You simply [connect your cloud account](/docs/platform/deploy/own-cloud) and create an environment. ## How it works This is powered by Encore's open source [backend framework](/docs/ts), which lets you declare infrastructure resources (databases, caches, queues, scheduled jobs, etc.) as type-safe objects in application code. At compile time, Encore parses the application code to generate an [Application Model](/docs/ts/concepts/application-model), and Encore Cloud uses this meta data to create an infrastructure graph with a high-resolution definition of the infrastructure your application requires. Encore Cloud then uses this graph to provision and manage the necessary infrastructure in your cloud account (using AWS and GCP APIs), and in development and preview environments hosted by Encore Cloud. The approach removes the need for infrastructure configuration files and avoids creating cloud-specific dependencies in your application. Having an end-to-end integration between application code and infrastructure also enables Encore Cloud to keep environments in sync and track cloud infrastructure, giving you an up-to-date view of your infrastructure to avoid unnecessary cloud costs. ## Environment types By default, Encore Cloud provisions infrastructure using contextually appropriate objectives for each environment type. You retain control over the infrastructure in your cloud account, and can configure it directly both via the Encore Cloud dashboard and your cloud provider's console. Encore Cloud takes care of syncing your changes. | | Local | Encore Cloud Hosting | GCP / AWS | | ---------------------- | ------------------ | -------------------------- | ---------------------------------- | | **Environment types:** | Development | Preview, Development | Development, Production | | **Objectives:** | Provisioning speed | Provisioning speed, Cost\* | Reliability, Security, Scalability | \*Encore Cloud Hosting is free to use, subject to Fair Use guidelines and usage limits. [Learn more](/docs/platform/management/usage) ## Development Infrastructure Encore Cloud provisions infrastructure resources differently for each type of development environment. | | Local | Preview / Development (Encore Cloud Hosting) | GCP / AWS | | ------------------- | --------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------- | | **SQL Databases:** | Docker | Encore Cloud Managed (Kubernetes), [Neon](/docs/deploy/neon) | [See production](/docs/deploy/infra#production-infrastructure) | | **Pub/Sub:** | In-memory ([NSQ](https://nsq.io)) | GCP Pub/Sub | [See production](/docs/deploy/infra#production-infrastructure) | | **Caches:** | In-memory (Redis) | In-memory (Redis) | [See production](/docs/deploy/infra#production-infrastructure) | | **Cron Jobs:** | Disabled | [Encore Cloud Managed](/docs/primitives/cron-jobs) | [See production](/docs/deploy/infra#production-infrastructure) | | **Object Storage:** | Local Disk | Encore Cloud Managed | [See production](/docs/deploy/infra#production-infrastructure) | ### Local Development For local development Encore Cloud provisions a combination of Docker and in-memory infrastructure components. SQL Databases are provisioned using [Docker](https://docker.com). For Pub/Sub and Caching the infrastructure is run in-memory. When running tests, a separate SQL Database cluster is provisioned that is optimized for high performance (using an in-memory filesystem and fsync disabled) at the expense of reduced reliability. To avoid surprises during development, Cron Jobs are not triggered in local environments. They can always be triggered manually by calling the API directly from the [development dashboard](/docs/ts/observability/dev-dash). The application code itself is compiled and run natively on your machine (without Docker). ### Preview Environments When you've [connected your application to GitHub](/docs/platform/integrations/github), Encore Cloud automatically provisions a temporary [Preview Environment](/docs/platform/deploy/preview-environments) for each Pull Request. Preview Environments are created in Encore Cloud Hosting, and are optimized for provisioning speed and cost-effectiveness. The Preview Environment is automatically destroyed when the Pull Request is merged or closed. Preview Environments are named after the pull request, so PR #72 will create an environment named `pr:72`. ### Encore Cloud Hosting Encore Cloud Hosting is a simple, zero-configuration hosting solution provided by Encore. It's perfect for development environments and small-scale use that do not require any specific SLAs. It's also a great way to evaluate Encore Cloud without needing to connect your cloud account. Encore Cloud Hosting is not designed for business-critical use and does not offer reliability guarantees for persistent storage like SQL Databases. Other infrastructure primitives like Pub/Sub and Caching are provisioned with small-scale use in mind. [Learn more about the usage limitations](/docs/platform/management/usage) ## Production Infrastructure Encore Cloud provisions production infrastructure resources using best-practice guidelines and services for each respective cloud provider. | | GCP | AWS | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **Networking:** | [VPC](/docs/platform/infrastructure/gcp#networking-architecture) | [VPC](/docs/platform/infrastructure/aws#networking-architecture) | | **Compute:** | [Cloud Run](/docs/platform/infrastructure/gcp#google-cloud-run), [GKE](/docs/platform/infrastructure/gcp#google-kubernetes-engine) | [Fargate ECS](/docs/platform/infrastructure/aws#aws-fargate), [EKS](/docs/platform/infrastructure/aws#aws-eks) | | **SQL Databases:** | [GCP Cloud SQL](/docs/platform/infrastructure/gcp#databases), [Neon](/docs/platform/infrastructure/neon) | [Amazon RDS](/docs/platform/infrastructure/aws#databases), [Neon](/docs/platform/infrastructure/neon) | | **Pub/Sub:** | [GCP Pub/Sub](/docs/platform/infrastructure/gcp#pubsub) | [Amazon SQS & Amazon SNS](/docs/platform/infrastructure/aws#pubsub) | | **Object Storage:** | [GCS/Cloud CDN](/docs/platform/infrastructure/gcp#object-storage) | [Amazon S3/CloudFront](/docs/platform/infrastructure/aws#object-storage) | | **Caches:** | [GCP Memorystore (Redis)](/docs/platform/infrastructure/gcp#caching) | [Amazon ElastiCache (Redis)](/docs/platform/infrastructure/aws#caching) | | **Cron Jobs:** | Encore Cloud Managed | Encore Cloud Managed | Encore Cloud Managed | | **Secrets:** | [Secret Manager](/docs/platform/infrastructure/gcp#secrets-management) | [AWS Secrets Manager](/docs/platform/infrastructure/aws#se) | ### Configuration With Encore you do not define any cloud service specifics in the application code. This means that after deploying, you can safely use your cloud provider's console to modify the provisioned resources, or use the built-in configuration UI in the Encore Cloud dashboard. Learn more in the [Infrastructure Configuration](/docs/platform/infrastructure/configuration) documentation. ================================================ FILE: docs/platform/infrastructure/kubernetes.md ================================================ --- seotitle: How to deploy your Encore application to a new Kubernetes cluster seodesc: Learn how to automatically deploy your Encore application to a new Kubernetes cluster. title: Kubernetes deployment subtitle: Deploying your app to a new Kubernetes cluster lang: platform --- # Deploying Encore Apps to Kubernetes Encore Cloud gives you flexibility in where you run your applications. You have two options for Kubernetes deployments: 1. **Deploy to a new cluster**: Encore Cloud can automatically provision and manage a new Kubernetes cluster in your cloud account on AWS or GCP. 2. **Use an existing cluster**: Deploy to your pre-existing Kubernetes cluster ([see instructions here](/docs/platform/infrastructure/import-kubernetes-cluster)) All infrastructure provisioning is automated, and configuration is managed through the [Encore Cloud Dashboard](https://app.encore.cloud), keeping your application code clean and infrastructure-agnostic. ## Deploying to a new Kubernetes cluster **1. Connect your cloud account:** Ensure your cloud account (Google Cloud Platform or AWS) is connected to Encore Cloud. ([See docs](/docs/platform/deploy/own-cloud)) **2. Create environment:** Open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to **Environments**, then click on **Create Environment**. Next, select your cloud (AWS or GCP) and then specify Kubernetes as the compute platform. Encore Cloud supports deploying to GKE on GCP, and EKS Fargate on AWS. You can also configure if you want to allocate all services in one process or run one process per service. **3. Push your code:** To deploy, commit and push your code to the branch you configured as the deployment trigger. You can also trigger a manual deploy from the Cloud Dashboard by going to the **Environment Overview** page and clicking on **Deploy**. **4. Automatic deployment by Encore Cloud:** Once you've triggered the deploy, Encore Cloud will automatically provision and deploy the necessary infrastructure on Kubernetes, per your environment configuration in the Cloud Dashboard. You can monitor the status of your deploy and view your environment's details through the Encore Cloud Dashboard. **5. Accessing your cluster with kubectl:** You can access your cluster using the `kubectl` CLI tool. [See the docs](/docs/platform/infrastructure/configure-kubectl) for how to do this. ## Infrastructure Overview Encore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary Kubernetes components. Here's an overview of the components Encore Cloud manages and how they work together to support your applications. ### Namespace Management Encore Cloud creates a unique namespace for each environment deployed to your Kubernetes cluster, ensuring complete isolation between different environments of your application. ### Secrets Management Encore Cloud provides comprehensive secrets management through deep integration with Kubernetes Secrets. Application secrets that you configure in Encore Cloud are automatically stored as Kubernetes Secrets and made available to your services at runtime. This includes both application-specific secrets that you define, as well as infrastructure secrets like database credentials that Encore Cloud manages automatically. Service accounts are automatically bound to the appropriate secrets they need access to, ensuring each service can only access the secrets it requires. This follows the principle of least privilege and helps maintain a strong security posture. ### Ingress Configuration Encore Cloud provisions and manages ingress for your applications through a cloud provider-specific ingress controller. The ingress controller is automatically configured to handle incoming traffic and route it securely to your application's Encore Gateway service. It manages TLS certificates automatically to ensure all traffic is encrypted, and provides fine-grained control over which services are accessible from the public internet. The controller configuration is optimized for your specific cloud provider to ensure the best possible performance and reliability. ## Service Management ### Deployments Encore Cloud manages the deployment configuration for each service in your application. Each service is deployed as a separate Kubernetes deployment, allowing for independent scaling and management. The deployment configurations are automatically generated and optimized based on your service's requirements. For each service, Encore Cloud configures the pod specifications with appropriate resource requests and limits, health checks, and container settings. Runtime configurations like environment variables and command arguments are automatically set based on your application's needs. The container orchestration is handled seamlessly, with Encore Cloud managing pod scheduling, updates, and scaling to ensure your services run reliably and efficiently. ### Network Configuration Encore Cloud provides a comprehensive networking setup through Kubernetes Service resources. Each service in your application gets assigned a unique cluster IP address, enabling reliable internal communication between services. This IP allocation works in conjunction with Kubernetes' built-in service discovery mechanism, allowing services to locate and communicate with each other using consistent internal DNS names. The internal service routing ensures that requests are efficiently distributed across all available pods for each service, providing automatic load balancing and failover capabilities. ### Identity and Access Encore Cloud provides comprehensive service identity management through Kubernetes service accounts. Each pod is assigned its own dedicated service account, which handles authentication with the Kubernetes API and enables secure access to resources. These service accounts are automatically bound to the specific secrets and permissions required by each service. For cloud provider integration, Encore Cloud maps the service accounts to appropriate IAM roles, enabling secure access to cloud resources like databases and object storage. Following the principle of least privilege, Encore Cloud configures the minimum required permissions for each service account, ensuring services can only access the resources they explicitly need. All these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs. ================================================ FILE: docs/platform/infrastructure/manage-db-users.md ================================================ --- seotitle: Managing database user credentials seodesc: Learn how to manage user credentials for databases created by Encore. title: Managing database user credentials lang: platform --- Encore Cloud provisions your databases automatically, meaning you don't need to manually create database users. However, in some use cases you need access to the database user credentials, so Encore Cloud makes it simple to view them. As an application **Admin**, open the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Infrastructure** page for the relevant environment. In the section for the relevant **Database Cluster**, you will find a **Users** sub-section which lists your database users. Click on the "eye" icon next to each username to decrypt the password. Note that databases hosted in [Encore Cloud](/docs/platform/infrastructure/infra#encore-cloud) currently do not expose usernames and passwords. To connect to an Encore Cloud-hosted database, use [`encore db shell`](/docs/ts/primitives/databases#connecting-to-databases). `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with. Do not change or remove the database users created by Encore, as this will prevent Encore Cloud from maintaining and handling connections to the databases in your application. ================================================ FILE: docs/platform/infrastructure/neon.md ================================================ --- seotitle: Neon Postgres Database seodesc: Learn how to configure your environment to provision a Neon Postgres database. title: Use Neon Postgres lang: platform --- [Neon](https://neon.tech/) is a serverless database provider that offers a fully managed and autoscalable Postgres database. You can configure Encore Cloud to provision a Neon Postgres database instead of the default offering for all supported cloud providers. ## Connect your Neon account To start using Neon with Encore Cloud, you need to add your Neon API key to your Encore Cloud application. You can sign up for a Neon account at [neon.tech](https://neon.tech/). Once you have an account, you can find your API key in the [Neon Console](https://neon.tech/docs/manage/api-keys) Then, head over to the Neon settings page by going to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > App Settings > Integrations > Neon. Click the "Connect Account" button, give it a name, and enter your API key. ## Creating environments using Neon Neon organizes databases in projects. A project consist of a main branch and any number of feature branches. [Branches](https://neon.tech/docs/introduction/branching) in Neon are similar to branches in git, letting you to create a new branch for each feature or bug fix, to test your changes in isolation. When configuring your Encore Cloud environment to use Neon, you can choose which project and branch to use. To get started, head to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > Environments > Create Environment. In the Database section, select `Neon database`. ### Create a new Neon project and branch If you're starting off a blank slate, you can let Encore Cloud create a new Neon project and branch for you. Select `New Neon project` and choose a Neon account and region. We recommend picking a region close to your compute and that you use the suggested project and branch names, but you're free to choose any configuration you like. ### Branch from an existing Encore Cloud environment If you already have an Encore Cloud environment with Neon, you can branch your database from that environment. Simply select `Branch from Encore environment` and choose the environment you want to branch from. This option will be disabled if you don't have any environments using Neon. ### Branch from an existing Neon branch You can also choose to manually select a Neon branch to branch from. This is useful if you have an existing Neon project, but it's not currently being used by any Encore Cloud environments. Select `Branch from Neon project`, then choose the account, project and branch you want to use. ### Import an existing Neon branch The final option is to import an existing Neon branch. This is useful if you have an existing database you want to use. Be wary that this option will not create a new branch but operate on the existing data. Select `Import Neon branch`, then choose the account, project and branch you want to use. **Note:** You may need to manually adjust the roles, commonly you need to change the database owner to the `db__admin` role to enable execution of migrations. See more in the [Roles](#roles) section below. ## Edit your Neon environment Once the environment is created, you can edit the Neon settings by going to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > Environments > (Select your environment) > Infrastructure. Here you can view and edit your Neon account resources. As a safety precaution, we've disabled editing of imported resources to prevent accidental changes to shared data. ### Neon project The retention history specifies how long Neon will keep changes to your data. The default is 1 day, but depending on your Neon plan, you can increase this to up to 30 days. ### Neon endpoint Each branch is assigned a unique endpoint which essentially is the serverless compute handling your database. You can edit the endpoint to set the CPU limits and the suspend timeout. The suspend timeout is the time Neon will wait before suspending the compute when it's not in use. The default is 5 minutes, but you can increase this to up to a week (depending on your Neon plan). ## Use Neon for Preview Environments Neon is a great choice for [Preview Environments](/docs/platform/deploy/preview-environments) as it allows you to branch off a populated database and test your changes in isolation. To configure which branch to use for Preview Environments, head to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > App Settings > Preview Environments and select the environment with the database you want to branch from. Hit save and you're all done. Keep in mind that you can only branch from environments that use Neon as the database provider; this is the default for Encore Cloud environments, but is a configurable option when creating AWS and GCP environments. ## Roles Encore Cloud automatically implements a structured role hierarchy that ensures a secure, scalable, and efficient management of databases. Below is an explanation of how roles are created, utilized, and managed. ### Role hierarchy #### 1. Initial Superuser Role - **Role Name:** `encore_platform` - **Access level:** This role has full privileges and is the foundational user for setting up the role hierarchy. - **Purpose:** The role creates and configures the subsequent roles and then steps back from day-to-day operations. #### 2. Global Roles Three core roles are created to define access levels across all databases: - `encore_reader` - **Access level:** Provides read-only access. - **Use Case:** Reading data without modifying it. - `encore_writer` - **Access level:** Allows read and write access. - **Use Case:** Performing data manipulations and inserts. - `encore_admin` - **Access level:** Grants administrative privileges for global database operations. - **Use Case:** Overseeing configurations, managing schemas, and handling elevated tasks. These global roles are used by Encore's CLI when using the `encore db shell` command. Learn more in the [CLI docs](/docs/ts/primitives/databases#using-the-encore-cli). #### 3. Database-Specific Roles For each database within the Neon integration, specific roles are created to provide fine-grained control: - `db__reader`: Read-only access to the main database. - `db__writer`: Read and write access to the main database. - `db__admin`: Administrative privileges specific to the main database. #### 4. Service-Specific Roles For each service in your application, a dedicated role is generated in the format `svc_`. This role is granted the necessary `db__writer` role for each database the service accesses. This ensures that each service has the appropriate level of access to perform its operations while maintaining security and separation of concerns. **Example:** A service named `orders` that writes to the `main` database is assigned the `svc_orders` role, which is granted the `db_main_writer` role. ### Role Setup Workflow - **1. Superuser Creation:** the `encore_platform` superuser role is created upon integration setup. - **2. Global Role Creation:** The `encore_reader`, `encore_writer`, and `encore_admin` roles are established to provide general access control. - **3. Database-Specific Roles:** For each database, roles are created in the format `db__` to manage access specific to that database. - **4. Service-Specific Roles:** For each service, roles are created in the format `svc_` and are granted the necessary writer roles for the databases used by each service. ### Viewing credentials To view database credentials, open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure page** for the appropriate **Environment**, and locate the **USERS** section within the relevant **Database Cluster**. ### Best Practices Encore Cloud automatically manages roles according to these security best practices: - **Role Ownership:** Ensures critical operations, such as migrations, are executed by roles with appropriate permissions (e.g., `db__admin`). - **Access Control:** Assigns the least privilege necessary for each task. Uses specific database roles (e.g., `db__reader`) to restrict access. - **Consistency:** Maintains consistent naming conventions (`db__`) for ease of management and troubleshooting. ### Integrating with existing Neon databases If you are integrating with an existing Neon database, you may need to manually adjust the roles to work with Encore Cloud's role structure. Commonly, the adjustment needed is changing the database owner to the `db__admin` role to enable execution of migrations. ================================================ FILE: docs/platform/integrations/api-reference.md ================================================ --- seotitle: Encore Cloud API Reference seodesc: Learn how to use the Encore Cloud API. title: Encore Cloud API Reference lang: platform --- Encore Cloud provides an API for programmatic access to control certain parts of the platform. We're working on expanding the set of features available over the API. Please reach out to us [on Discord](https://encore.dev/discord) if you have use cases where additional API functionality would be useful. The Base URL for the Encore Cloud API is `https://api.encore.cloud`. ## Authentication All API calls require valid authentication, which is provided by sending an access token in the `Authorization` header, in the format `Authorization: Bearer ${ACCESS_TOKEN}`. You can retrieve an API access token from the OAuth Token endpoint, using an OAuth Client. An API access token expires after one hour. For continuous access, shortly before an API access token expires, request a new API access token from Encore Cloud's OAuth token endpoint. OAuth client libraries in popular programming languages can handle the API access token generation and renewal. See the [OAuth Clients](/docs/platform/integrations/oauth-clients) for more information on creating OAuth Clients. ## OAuth **Method**: `POST`
**Path**: `/api/oauth/token` #### Query Parameters | Parameter | Description | | ----------------- | -------------------------------------------------------------- | | **client_id** | The client id of the OAuth Client to generate a token for. | | **client_secret** | The client secret of the OAuth Client to generate a token for. | #### Response The API responds with a 2xx status code on successful creation of an API access token. ```typescript type Token = { // The access token itself. "access_token": string; // The access token expires after 1 hour (3600 seconds). "expires_in": 3600; // The actor the token belongs to, in this case the OAuth2 client id. actor: string; // Indicates the access token should be passed as a "Bearer" token in the Authorization header. "token_type": "Bearer"; } ``` ## Rollouts Encore Cloud's deployment system consists of several phases: * A build phase * An infrastructure provisioning phase * A deployment phase These phases are combined into a unified entity called a *Rollout*. A rollout represents the coordinated process of rolling out a specific version of an Encore application. We use the term *rollout* to disambiguate from the *deployment phase*, which specifically refers to the last phase of the rollout process (where the version is being deployed onto the provisioned infrastructure). ### The Rollout Object The Rollout object represents the state of a rollout. ```typescript // The representation of a rollout. type Rollout = { // Unique id of the rollout. id: string; // The current status of the rollout. status: "pending" | "queued" | "running" | "completed"; // What the conclusion was of the rollout (when status is "completed"). // If the status is not "completed" the conclusion is "pending". conclusion: "pending" | "canceled" | "failure" | "success"; // When the rollout was queued, started, and completed. queued_at: Date | null; started_at: Date | null; completed_at: Date | null; // Information about the various rollout phases. // See type definitions below. build: RolloutPhase; infra: RolloutPhase; deploy: RolloutPhase; } // Common structure of the various rollout phases. type RolloutPhase = { // Unique id of the phase. id: string; // The current status of the rollout phase. status: Status; // What the conclusion was of the phase. conclusion: Conclusion; // When the phase was queued, started, and completed. queued_at: Date | null; started_at: Date | null; completed_at: Date | null; } // The current status and conclusion of a build. // If the status is not "completed" the conclusion is "unknown". type BuildStatus = "queued" | "running" | "completed"; type BuildConclusion = "unknown" | "canceled" | "failure" | "success"; // The current status and conclusion of an infra change. // The "proposed" status means the change is awaiting human approval. // The "rejected" conclusion means a human rejected the proposed infra change. type InfraStatus = "pending" | "proposed" | "queued" | "running" | "completed"; type InfraConclusion = "unknown" | "canceled" | "failure" | "rejected" | "success"; // The current status and conclusion of a deploy. // If the status is not "completed" the conclusion is "unknown". type DeployStatus = "queued" | "running" | "completed"; type DeployConclusion = "unknown" | "canceled" | "failure" | "success"; ``` ### Triggering a rollout **Method**: `POST`
**Path**: `/api/apps/${APP_ID}/envs/${ENV_NAME}/rollouts` #### Path Parameters | Parameter | Description | | ------------ | ---------------------------------------------------------- | | **APP_ID** | The id of the Encore application to trigger a rollout for. | | **ENV_NAME** | The name of the environment to trigger a rollout for. | #### JSON Request Body A rollout can be triggered either by commit SHA or by branch name. **By commit SHA:** ```json { // The commit hash to trigger a deploy for. "sha": "abc123...", // Optional. Set to skip running tests during build. "skip_tests": false, // Optional. Set to force a rebuild instead of reusing a cached build. "force_rebuild": false } ``` **By branch name:** ```json { // The name of the branch to deploy the latest commit from. "branch": "main", // Optional. Set to skip running tests during build. "skip_tests": false, // Optional. Set to force a rebuild instead of reusing a cached build. "force_rebuild": false } ``` Exactly one of `sha` or `branch` must be provided. `skip_tests` and `force_rebuild` are optional and default to `false`. #### Response The API responds with a 2xx status code on successful creation of a new rollout. On success it returns a **Rollout** object as its JSON response payload, representing the current state of the newly created rollout. ### Retrieving a rollout **Method**: `GET`
**Path**: `/api/apps/${APP_ID}/rollouts/${ROLLOUT_ID}` #### Path Parameters | Parameter | Description | | -------------- | ------------------------------------------------------------ | | **APP_ID** | The id of the Encore application to trigger a rollout for. | | **ROLLOUT_ID** | The id of the rollout to retrieve, in the form `"roll_..."`. | #### Response The API responds with a 2xx status code on successful retrieval of the rollout. On success it returns a **Rollout** object as its JSON response payload, representing the current state of the requested rollout. ## Member Management Encore Cloud provides APIs for managing application members, including inviting users, listing members, and updating member roles. ### Member Object ```typescript type Member = { // The user's email address email: string; // The member's role in the application role: "owner" | "reader" | "writer" | "none"; // When the member was invited to the application invited: timestamp; // When the member accepted the invitation accepted: timestamp; // When the membership expires expires: timestamp; // The member's username username: string; // The member's full name full_name: string; // The member's picture URL picture_url: string; } ``` ### Available Roles - **owner**: Full control over the application - **writer**: Can write application resources - **reader**: Can read application resources - **none**: Used to revoke access to an application ### Invite Member Invite a new member to an Encore application. **Method**: `POST`
**Path**: `/api/apps/${APP_ID}/member` #### Path Parameters | Parameter | Description | | ---------- | ---------------------------------------------- | | **APP_ID** | The id of the Encore application. | #### JSON Request Body ```typescript { // The email address of the user to invite "email": string; // The role to assign to the invited member "role": "owner" | "reader" | "writer" | "none"; } ``` #### Response The API responds with a 2xx status code on successful invitation. On success it returns a **Member** object as its JSON response payload, representing the newly invited member. ### List Members Retrieve a list of all members for an Encore application. **Method**: `GET`
**Path**: `/api/apps/${APP_ID}/members` #### Path Parameters | Parameter | Description | | ---------- | ---------------------------------------------- | | **APP_ID** | The id of the Encore application. | #### Response The API responds with a 2xx status code on success. On success it returns an array of **Member** objects as its JSON response payload, representing all current members and pending invites for the application. ```typescript type Response = Member[]; ``` ### Update Member Role Update the role of an existing member. **Method**: `PUT`
**Path**: `/api/apps/${APP_ID}/members` #### Path Parameters | Parameter | Description | | ---------- | ---------------------------------------------- | | **APP_ID** | The id of the Encore application. | #### JSON Request Body ```typescript { // The email address of the member to update "email": string; // The new role to assign to the member "role": "owner" | "reader" | "writer" | "none"; } ``` #### Response The API responds with a 2xx status code on successful update. #### Error Cases - **403 Forbidden**: Insufficient permissions to manage members - **409 Conflict**: Attempting to remove the last owner (error detail: "last_owner") - **404 Not Found**: Member not found ================================================ FILE: docs/platform/integrations/auth-keys.md ================================================ --- seotitle: Auth Keys let you authenticate without a browser seodesc: Learn how to use pre-authentication keys to authenticate without needing to sign in via a web browser. See how to setup reusable and ephemeral auth keys. title: Generating Auth Keys lang: platform --- Pre-authentication keys (“auth keys” for short) let you authenticate the Encore CLI without needing to sign in via a web browser. This is most useful when setting up CI/CD pipelines. ## Types of auth keys - **Reusable Keys** for authenticating more than one machine. - **Ephemeral Keys** - Machines authenticated by this key will be automatically logged out after one hour. **Be very careful with reusable keys!** These can be very dangerous if stolen. They're best kept in a key vault product (eg. 1Password, LastPass) specifically designed for the purpose. ## Authentication **Auth keys** authenticate a machine as the Encore app for which the key was generated. If Ada generates an auth key, and uses it to set up her CI/CD pipeline, then that machine is authenticated as Ada's Encore app. ## Generating a key ### Step 1: Generate an auth key As an Encore user, visit the auth key page by going to **[Your apps](https://app.encore.cloud/) > (Select your app) > App Settings > Auth Keys**. A key can be both **reusable** and **ephemeral** at the same time (you can decide the combination based on your particular use case). **Don't forget to store your key!** Once generated, you will need to copy and store your key in a vault product (eg. 1Password, LastPass). We do not display the full contents of a key in our dashboard for security reasons. This page also gives you the ability to revoke existing keys. ### Step 2: Authenticate with the auth key Using the Encore CLI, you can authenticate with your newly generated key: ```shell $ encore auth login --auth-key=ena_nEQIkfeM43t7oxpleMsIULbhbtLAbYnnLf1D ``` ## Revoking a key You can revoke a key simply by pressing the **Delete** button next to it. This will prevent any machines currently using it to authenticate to Encore Cloud (regardless of the key type). ================================================ FILE: docs/platform/integrations/custom-domains.md ================================================ --- seotitle: Custom Domains for all your environments seodesc: Learn how to setup a custom domain for your cloud environments, to use your own domain to access your backend application built with Encore. title: Custom Domains subtitle: Expose APIs from your own domain lang: platform --- By default, all application [environments](/docs/platform/deploy/environments) are accessible as subdomains of the shared Encore domain `encr.app`. When exposing APIs publicly, you often want to provide a URL endpoint branded with your own domain. Follow these instructions to serve your backend using your own custom domain name. This also has the benefit of providing a built-in Web Application Firewall (WAF) using [Cloudflare WAF](https://www.cloudflare.com/en-gb/application-services/products/waf/). ## Adding a domain Modify the DNS records for your domain, adding a CNAME record pointing at: `custom-domain.encr.app` It's recommended to set a TTL (Time-To-Live) of 30 minutes for the CNAME record. Encore requires that you add a CNAME record for each domain you wish to serve traffic from. CNAME record using wildcards, e.g. `*.example.com`, are not currently supported. Once you've added the CNAME record, go to the Custom Domains settings page by opening **[Your apps](https://app.encore.cloud/) > (Select your app) > Settings > Custom Domains**. Click on `Add Domain` on the top right of the page. Enter the domain name you configured the CNAME on and select which [environment](/docs/platform/deploy/environments) you wish to serve on that domain, then click `Add`. Encore will now set up your domain and issue SSL certificates to serve traffic through. If you configure multiple domains against a single environment, Encore will serve traffic through all configured domains. The `encr.app` subdomain which was created when you originally created an environment will always be configured to serve traffic to that environment. This allows you to migrate to a custom domain safely without risking cutting traffic off to older clients which may be hard coded to access your applications via the default subdomain. ## Domain statuses On the Custom Domains settings page, you can see the various statuses throughout the lifecycle of a custom domain. | Status | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Pending` | The domain is currently queued to be provisioned by Encore. | | `Waiting for CNAME` | Encore is waiting for the CNAME to become active and for the SSL certificate to be issued for the domain. | | `Configuring Edge Routers` | The SSL certificate has been issued and the Encore edge routers are being configured to route traffic on the domain. | | `Active` | The domain is serving traffic to your Encore application. | | `Not Working` | A non-recoverable problem has occurred. This could be a result of the CNAME record being removed or pointed elsewhere. If you see this error, please [contact support](/contact). | ================================================ FILE: docs/platform/integrations/github.md ================================================ --- seotitle: Integrate your Encore application with GitHub seodesc: Learn how to integrate your backend application with GitHub to get automatic Preview Environments for each Pull Request using Encore. title: Integrate with GitHub lang: platform --- Encore applications are easy to integrate with GitHub for source code hosting. To link your application to GitHub, open your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation. Then select **GitHub** in the settings menu. Next, connect your account to GitHub by clicking the **Connect Account to GitHub** button. This will open GitHub where you can grant access either to all repositories or only the specific one(s) you want to link with Encore. When you come back to Encore, click the **Link App to GitHub** button: In the popup, select the repository you would like to link your app with: Click **Link** and you're done! Encore will now automatically start building and running tests against your Pull Requests, and provision Preview Environments for each Pull Request. ## Placing your Encore app in a monorepo sub-folder If you already have a monorepo and want to place your Encore application in a sub-folder, you need to tell Encore which folder the `encore.app` file is in. Do this by opening your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to **Settings** > **General**. Then in the **Root Directory** section, you specify the directory within your Git repository in which your `encore.app` file is located. ## Configure deploy trigger When using GitHub, you can configure Encore to automatically trigger deploys when you push to a specific branch name. To configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name` and hit save. ## Preview Environments for each Pull Request Once you've linked your app with GitHub, Encore will automatically start building and running tests against your Pull Requests. Encore will also provision a dedicated Preview Environment for each pull request. This environment works just like a regular development environment, and lets you test your changes before merging. Learn more in the [Preview Environments documentation](/docs/platform/deploy/preview-environments). ![Preview environment linked in GitHub](/assets/docs/ghpreviewenv.png "Preview environment linked in GitHub") ================================================ FILE: docs/platform/integrations/oauth-clients.md ================================================ --- seotitle: Encore Cloud OAuth Clients seodesc: Learn how to use OAuth Clients for access to the Encore Cloud API title: OAuth Clients lang: platform --- OAuth clients provide a framework for delegated and scoped access to the Encore Cloud API. An OAuth client creates short-lived access tokens on demand, and supports the principle of least privilege by allowing fine-grained control on the access granted to the client using scopes. ## How it works You create an OAuth client that defines the scopes to allow when your client application uses the Encore Cloud API. Scopes are currently grouped into "roles", which include a set of permissions. For example, the `deployer` role grants access to the triggering deployments. An OAuth client consists of a client ID and a client secret. When you create an OAuth client, Encore Cloud creates these for you. Within your client application, use the client ID and client secret to request an API access token from the Encore Cloud's OAuth token endpoint. You use the access token to make calls to the Encore Cloud API. The access token grants permission only for the scopes that were defined when you created the OAuth client. An API access token expires after one hour. For continuous access, shortly before an API access token expires, request a new API access token from Encore Cloud's OAuth token endpoint. OAuth client libraries in popular programming languages can handle the API access token generation and renewal. Encore Cloud's OAuth implementation is based on the [OAuth 2.0 protocol](https://www.rfc-editor.org/rfc/rfc6749). ## Prerequisites You need to be an Owner of the Encore application in order to create or revoke OAuth clients. ### Setting up an OAuth client Open the OAuth clients page in the application settings page. In the Generate OAuth client dialog, select the set of operations that can be performed with tokens created by the new OAuth client. After generating the client, you can see the new OAuth client's ID and secret. Copy both the client ID and secret, as you need them for your client code. Note that after you close the Generated new OAuth client dialog, you won't be able to copy the secret again. **Store the client secret securely.** Your OAuth client is now configured. Use the client ID and secret when you configure your OAuth client application. Note that the provided client secrets are case-sensitive. If an OAuth client is created by a user who is later removed from your application, the OAuth client will continue to function and generate API access tokens. Application owners can see all configured OAuth clients in the OAuth clients page of the application settings. ### Roles Roles define which operations are permitted in API access tokens that are created by your client application. Currently there is a single supported role: **Deployer**. The deployer role allows for programatically triggering deployments. When new Encore Cloud functionality is provided, we will add it to existing roles where applicable. That means a role is not restricted to only access of APIs that existed at the time the client was initially authorized — a role will contain additional access where it makes sense for new or updated functionality. ### Revoking an OAuth client Open the OAuth clients page of the application settings page. Find the OAuth client that you want to delete and select Revoke. Select Revoke OAuth client to confirm you want to revoke the OAuth client. When you revoke an OAuth client, any active API access tokens that were created by the client are also revoked. ### Encore Cloud OAuth token endpoint Encore Cloud's OAuth token endpoint is https://api.encore.cloud/api/oauth/token. See the [Encore Cloud API Reference](/docs/platform/integrations/api-reference) documentation for more information. Make requests to the OAuth token endpoint when you need an API access token. The OAuth token endpoint accepts requests that conform to the OAuth 2.0 client credentials grant request format, and returns responses that conform to the OAuth 2.0 client credentials grant response format. ## OAuth client libraries Popular programming languages provide OAuth client libraries to simplify your use of OAuth clients. For example, the following Go code shows how to create an OAuth client object that uses your client ID and client secret to generate an API access token for calls to the Encore Cloud API. Similar libraries exist for other popular programming languages. ```go package main import ( "context" "os" "golang.org/x/oauth2/clientcredentials" ) func main() { oauthConfig := &clientcredentials.Config{ ClientID: os.Getenv("OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), TokenURL: "https://api.encore.cloud/api/oauth/token", } client := oauthConfig.Client(context.Background()) // Make API calls using `client.Get` etc. resp, err := client.Get("https://api.encore.cloud.com/api/...") // ... } ``` The example requires that you define environment variables `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET`, with their values set to the client ID and client secret that are created when you set up an OAuth client. ### Verifying you can generate API access tokens After you set up an OAuth client, an easy way to confirm that you can generate API access tokens is to make a curl request to the Encore Cloud OAuth token endpoint. ```bash curl -d "client_id=${OAUTH_CLIENT_ID}" -d "client_secret=${OAUTH_CLIENT_SECRET}" \ -d "grant_type=client_credentials" "https://api.encore.cloud/api/oauth/token" ``` The example requires that you define environment variables OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, with their values set to your client ID and client secret. Here's an example response showing the API access token: ```json {"access_token":"MTcyODQ3NTg3NXww...GDxfmxnuq9zDEAmHmP5D44=","token_type":"Bearer","expires_in":3600, "actor": "o2c_my_key_id"} ``` ## Limitations An OAuth access token expires after 1 hour — this duration cannot be modified. ================================================ FILE: docs/platform/integrations/terraform.md ================================================ --- seotitle: Integrate Encore with existing infrastructure seodesc: The Encore Terraform Provider lets you integrate your Encore deployment with existing infrastructure title: Terraform Provider subtitle: Integrate Encore with existing infrastructure infobox: { title: "Terraform Provider", import: "https://registry.terraform.io/providers/encoredev/encore", } lang: platform --- Encore makes it simple to deploy and manage cloud applications. When you're dealing with a large and complex system, you may want to integrate Encore-provisioned resources with an existing infrastructure landscape. For this purpose, Encore maintains a Terraform Provider with data sources for all Encore-provisioned resources. ## Understanding Encore Terraform Data Sources Encore Terraform data sources act as read-only references to resources Encore has already provisioned on your behalf. Unlike Terraform resources (which create or modify infrastructure), data sources only retrieve information. The Encore data sources let's you retrieve cloud identifiers for resources managed by Encore, such as databases, caches, and more. To do this, you only need to provide the name of the resource and the environment it's in. ## Configuring the Encore Terraform Provider To use Encore data sources, you need to declare the Encore Terraform provider in the `required_providers` of your Terraform configuration file. Here's an example of how to declare the provider: ``` terraform { required_providers { encore = { source = "registry.terraform.io/encoredev/encore" } } } ``` Once you've declared the provider, Terraform will automatically download the provider plugin when initializing the working directory using `terraform init`. To authenticate with the Encore API, the provider need an Encore Auth Key. You can generate an auth key from Encore's [Cloud Dashboard](https://encore.dev/docs/platform/integrations/auth-keys). Once you have the auth key, you can configure the provider in your Terraform configuration file like this: ``` provider "encore" { env = "your-env" auth_key = "your-auth-key" } ``` You can also set the `ENCORE_AUTH_KEY` environment variable to avoid hardcoding the auth key in your configuration file. ## Using Encore Terraform Data Sources Once you have the provider configured, you can use the Encore data sources to retrieve information about resources. There are several data sources available, such as `encore_database`, `encore_cache`, and `encore_pubsub_topic`. Each data source has its own set of attributes that you can use to retrieve information about the resource. The full documentation for each data source is available in the [Terraform Registry](https://registry.terraform.io/providers/encoredev/encore). Here's an example of how to use the `encore_pubsub_topic` data source to connect AWS IOT Core to an Encore PubSub topic: ``` data "encore_pubsub_topic" "topic" { name = "my-topic" env = "my-env" } resource "aws_iot_topic_rule" "rule" { name = "my-rule" sql = "SELECT * FROM 'my-topic'" sns { message_format = "RAW" role_arn = aws_iam_role.role.arn target_arn = data.encore_pubsub_topic.topic.aws_sns.arn } } ``` ================================================ FILE: docs/platform/integrations/webhooks.md ================================================ --- seotitle: Subscribe to Encore Cloud webhooks and events seodesc: Encore Cloud lets you define webhooks to react to events in your application, enabling you to build powerful integrations. title: Webhooks & Events subtitle: Set up webhooks to react to Encore events infobox: { title: "Webhooks", import: "go.encore.dev/webhooks", } lang: platform --- Webhooks provide a way for notifications to be delivered to an HTTP endpoint of your choice whenever certain events happen within Encore. For example, you can set up a webhook to be notified whenever a deployment starts or finishes. Webhooks are defined on a per-application basis, and are configured under Settings -> Webhooks in the [Encore Cloud dashboard](https://app.encore.cloud). To simplify using webhooks, Encore.go provides a Go module, [go.encore.dev/webhooks](https://pkg.go.dev/go.encore.dev/webhooks), that provides type definitions and documentation of all supported webhook events. This module is kept up to date as new events are added. ## Webhook Deliveries Each time an event occurs that matches one of your defined webhooks, Encore will send a HTTP POST request to the webhook's configured URL with information about the event. If the HTTP request fails, the delivery is marked as failed and won't be retried. Each event is given a unique event id, which is shared across all webhooks. Within each webhook, each event is given a sequence number, which is incremented for each event that matches that webhook. The sequence number allows for a linear ordering of events within a webhook, making it easy to determine if an event was missed. These are provided in the `X-Encore-Event-Id` and `X-Encore-Sequence-Id` headers respectively, and are also part of the event payload itself. ## Parsing webhook events To parse a webhook event, use the [`webhooks.ParseEvent`](https://pkg.go.dev/go.encore.dev/webhooks#ParseEvent) function. As you'll see in the example below, to parse the webhook event you'll need access to the webhook secret. This is a secret value that is generated by Encore and is used to sign each webhook request. More about this in the next section. For example, to process rollout started and completed webhook events, you could do something like this: ```go package service import ( "net/http" "go.encore.dev/webhooks" ) var secrets struct { EncoreWebhookSecret string } //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { payload, err := io.ReadAll(req.Body) if err != nil { // ... handle error } event, err := webhooks.ParseEvent(payload, req.Header.Get("X-Encore-Signature"), secrets.EncoreWebhookSecret) if err != nil { // ... handle error } switch data := event.Data.(type) { case *webhooks.RolloutCreatedEvent: // ... handle rollout created event case *webhooks.RolloutCompletedEvent: // ... handle rollout completed event } } ``` Note that the example above is written as an Encore API endpoint, but that's not required. The same code works in any Go HTTP server, and the `go.encore.dev/webhooks` library does not depend on any Encore-specific functionality. ## Checking webhook signatures Since the webhook endpoint is publicly accessible, it is important to validate that the request is coming from Encore. To do so, Encore generates a secret for each webhook, which is used to sign each request. The webhook secret can be found on the webhook details page by admins. If you use the [go.encore.dev/webhooks](https://pkg.go.dev/go.encore.dev/webhooks) library then signature validation is handled automatically, but it's also possible to verify the signature manually (see below). ### Preventing replay attacks A [replay attack](https://en.wikipedia.org/wiki/Replay_attack) occurs when an attacker intercepts a valid request, including the payload and signature, and re-transmits it one or more times, causing unintended side effects. To mitigate such attacks, Encore includes a timestamp in the `X-Encore-Signature` header. This timestamp is part of the signed payload, which means that it can't be changed by the attacker without invalidating the signature. This makes it possible to mitigate replay attacks by ensuring the timestamp isn't older than a certain threshold (the `go.encore.dev/webhooks` library defaults to 5 minutes). ### Verifying signatures manually The `X-Encore-Signature` header included in each webhook event contains a timestamp and one or more *schemes*. The timestamp is prefixed with `t=`, and each *scheme* is prefixed by a `v` and a version number. Currently only the `v1` *scheme* is supported. For example, a valid signature header might look like this: ``` X-Encore-Signature: t=1623345600,v1=0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b ``` The `v1` scheme is using a hash-based message authentication code ([HMAC](https://en.wikipedia.org/wiki/HMAC)) with [SHA-256](https://en.wikipedia.org/wiki/SHA-2). To prevent downgrade attacks, ignore all schemes that are not `v1`. It's possible the signature contains multiple signatures, for example if the webhook secret has been rotated recently. When rotating the webhook secret, Encore lets you define for how long the old secret should continue to be valid for. During that window, each webhook event will be signed with the new and the old secret. To validate the webhook signature, follow the algorithm below: **Step 1: Extract the timestamp and signatures from the header** Split the header on `,` to get a list of fields, then split each field on `=` to get the key and value. The value of the `t` key is the timestamp, and represents the UNIX timestamp (in seconds) when the signature was created. The fields with the `v1` key (possibly several, in case of secret rotation) are the signatures. Discard any other fields. **Step 2: Prepare the payload to sign** Create the payload to sign by concatenating the timestamp (as a string) and the request body, separated by `.` like so: ```go payloadToSign := timestamp + "." + string(payload) ``` **Step 3: Compute the expected signature** Compute the HMAC with the SHA256 hash function, using the webhook secret as the key and the `payloadToSign` as the message. Then, encode the resulting HMAC using the base64 URL encoding, and trim any trailing `=` characters. In Go, this can be done like so: ```go h := hmac.New(sha256.New, []byte(webhookSecret)) h.Write([]byte(payloadToSign)) digest := h.Sum(nil) expectedSignature := base64.RawURLEncoding.EncodeToString(digest) ``` **Step 4: Compare the signatures** Compare each signature with the `v1` field in the header with the expected signature. To protect against timing attacks, use a constant-time comparison function (like `crypto/hmac.Equal` in Go). If none of the signatures match, reject the request. If a match is found, compare the timestamp with the current time. If the difference is greater than the allowed threshold (5 minutes is a reasonable default), reject the request. Otherwise, accept the request. ================================================ FILE: docs/platform/introduction.md ================================================ --- seotitle: Introduction to Encore Cloud seodesc: Learn how Encore Cloud works and how it helps backend developers build cloud-based backend applications without manually dealing with infrastructure. title: Encore Cloud subtitle: End-to-end development platform for building robust distributed systems lang: platform --- While cloud services enable us to build powerful applications, they come with significant complexity. Developers spend countless hours managing infrastructure, writing boilerplate code, and dealing with complex deployment processes instead of building features that matter to users. Launching a new app, migrating to the cloud, or breaking apart a monolith into microservices, can therefore be a daunting task. Encore Cloud is purpose-built to solve this problem and removes the need to build your own developer platform. It provides a complete platform for building, testing, and deploying your application. In the Encore Developer Survey 2025, the average impact reported by our users was: - 137% improvement in delivery speed - 93% reduction in time spent on DevOps tasks - 81% reduction in developer onboarding time ## A simplified cloud backend development workflow Encore Cloud provides a complete toolset for backend development: from local development, testing, and observability, to cloud infrastructure and DevOps automation. Encore Cloud's core functionality is enabled by Encore's open source backend frameworks, [Encore.ts](/docs/ts) and [Encore.go](/docs/go). These frameworks let you define essential backend resources – like APIs, microservices, databases, cron jobs, and Pub/Sub – as type-safe objects in your code using simple, declarative syntax. The frameworks have a minimal footprint, where one line of code is enough to define a backend resource, and are designed to be unobtrusive, so that you can focus on building your product without being distracted by the underlying infrastructure. With the backend frameworks you only define **infrastructure semantics** — _the things that matter to your application's behavior_ — not configuration for _specific_ cloud services. Encore Cloud then automatically generates boilerplate and orchestrates the relevant infrastructure for each environment using your cloud provider's API or directly through Encore Cloud's built-in cloud hosting for development. This means that, with Encore Cloud, your application code can be used to run locally, test in preview environments, and provision and deploy to cloud environments on AWS and GCP. When your application is deployed to your cloud, there are **no runtime dependencies on Encore Cloud** and there is **no proprietary code running in your cloud**. ## Local Development The local development tooling is fully open source. When you run your app locally using the [Encore CLI](/docs/ts/install), it parses your code and automatically sets up the necessary local infrastructure on the fly. _No more messing around with Docker Compose!_ Aside from managing infrastructure, Encore's local development workflow comes with a lot of tools to make building distributed systems easier: - **Local environment matches cloud:** Encore automatically handles the semantics of service communication and interfacing with different types of infrastructure services, so that the local environment is a 1:1 representation of your cloud environment. - **Cross-service type-safety:** When building microservices applications with Encore, you get type-safety and auto-complete in your IDE when making cross-service API calls. - **Type-aware infrastructure:** With Encore, infrastructure like Pub/Sub queues are type-aware objects in your program. This enables full end-to-end type-safety when building event-driven applications. - **Tracing:** The [local development dashboard](/docs/ts/observability/dev-dash) provides local tracing to help understand application behavior and find bugs. - **Automatic API docs & clients:** Encore generates [API docs](/docs/ts/observability/service-catalog) and [API clients](/docs/ts/cli/client-generation) in Go, TypeScript, JavaScript, and OpenAPI specification. ## Testing Encore's open source framework comes with several built-in tools to help with testing: - **Built-in service/API mocking:** Encore provides built-in support for [mocking API calls](/docs/go/develop/testing/mocking), and interfaces for automatically generating mock objects for your services. - **Local test infra:** When running tests locally, Encore automatically provides dedicated [test infrastructure](/docs/go/develop/testing#test-only-infrastructure) to isolate individual tests. - **Local test tracing:** The [Local Development Dashboard](/docs/go/observability/dev-dash) provides distributed tracing for tests, providing great visibility into what's happening and making it easier to understand why a test failed. Encore Cloud adds to this tool-set with: - **Preview Environments:** Encore automatically provisions a [Preview Environment](/docs/platform/deploy/preview-environments) for each Pull Request, an effective tool when doing end-to-end testing. ## Infrastructure & DevOps automation ### Infrastructure Management - **Zero-config deployment:** Connect your repo and cloud account, then deploy and Encore Cloud orchestrates your cloud resources by integrating with your cloud provider's API. - **Automatic infrastructure:** Use battle-tested AWS/GCP services (Cloud Run/Fargate, GKE/EKS, CloudSQL/RDS, Pub/Sub / SQS/SNS, etc.) without any manual setup or configuration. - **No IaC required:** Say goodbye to Terraform and YAML - your code is the single source of truth. ### Security & Governance - **Automated IAM:** Least-privilege security permissions generated from parsing your code - **Infrastructure tracking:** Complete visibility of all provisioned resources - **Change management:** Built-in approval workflow for infrastructure changes - **Configuration management:** Simple UI for config changes that automatically 2-way syncs to your cloud ### Monitoring & Observability - **Cost monitoring:** Track infrastructure costs across your cloud resources (currently for GCP, AWS coming soon) - **Integrated observability:** Built-in logging, metrics, and tracing - **Third-party integration:** Works with Datadog, Grafana, and other tools - **Auto-generated documentation:** - Service catalog with complete API docs - Live architecture diagrams showing infrastructure dependencies ## Why choose Encore Cloud? Encore Cloud's end-to-end workflow is an unfair advantage for teams that need to move quickly without sacrificing quality and scalability. By automating over 90% of the normal day-to-day DevOps work, you can focus on building your product instead of building your own developer platform. The benefits of Encore Cloud are: - **Faster Development**: Encore Cloud enables 2-3x faster iterations thanks to the streamlined the development process with its clear abstractions and built-in development tools. - **Reduced Costs**: Encore Cloud's infrastructure management minimizes wasteful cloud expenses and reduces DevOps workload by 90%. - **Scalability & Performance**: Encore Cloud simplifies building microservices applications that can handle growing user bases and demands, without the normal boilerplate and complexity. - **Control & Standardization**: Encore Cloud enforces standardization and provisions infrastructure consistently according to best practices for each cloud provider. - **Quality through understandability:** Built-in tools like automated architecture diagrams, generated API docs, and distributed tracing make it simple for teams to get an overview of their system and ensure the correct behavior. - **Security**: Encore Cloud makes your application secure by automatically implementing security best practices for each cloud provider. ## Common use cases Encore Cloud is designed to give teams a productive and less complex experience when solving most common backend use cases. Many teams use Encore Cloud to build things like: - Consumer apps - High-performance B2B Platforms - Fintech & Crypto applications - Global E-commerce marketplaces - Microservices backends and event-driven systems for scalable SaaS applications - And much more... Check out the [showcase](https://encore.cloud/showcase) section for some examples of real-world products being built with Encore. ## Getting started - [Follow the Quick Start Guide](/docs/ts/quick-start) - [Join Discord](https://encore.dev/discord) to ask questions and meet other Encore developers - Follow and star the project on [GitHub](https://github.com/encoredev/encore) to stay up to date - [Book a demo](https://encore.dev/book) to speak to one of our founders about if Encore Cloud is a good fit for your team ================================================ FILE: docs/platform/management/billing.md ================================================ --- seotitle: Plans & Billing seodesc: Encore is free to use for many projects, and comes with paid plans for teams who want to move quickly. Learn more! title: Plans & Billing subtitle: lang: platform --- Encore offers a **Free** plan for teams that want a simple development workflow and collaboration features. If you want access to all features and want to use Encore's DevOps automation tools for AWS & GCP, there's a paid **Pro** plan available. See the [pricing page](https://encore.dev/pricing) for a feature comparison between each plan and complete pricing information. ## When do I need to upgrade to a paid plan? The Free plan comes with certain limitations, and should your needs exceed one or more of them, you may wish to upgrade to a paid plan: - When you need more than 2 cloud environments - When you want to [automate DevOps in your cloud on AWS/GCP](/docs/platform/infrastructure/infra) - When you want [Preview Environments](/docs/platform/deploy/preview-environments) for each Pull Request - When you want to use a Custom Domain - When you need multiple concurrent builds - When you need guaranteed logs & tracing retention - When you want access to private support & onboarding assistance - When you want custom configuration for environments hosted on Encore Cloud There is a free 14-day trial of the Pro plan, available to all new Encore users. It's a great way to try out all the features and learn if the Pro plan suits your needs. You can activate your trial from the [pricing page](https://encore.dev/pricing). ## Do I need to pay for hosting? All plans come with free use of Encore Cloud, subject to [Fair Use limits](/docs/platform/management/usage). Encore Cloud is intended for development environments and limited scale professional use that does not require specific SLAs. For production use, you can [connect your own cloud account on AWS/GCP](/docs/platform/deploy/own-cloud) and use Encore to deploy there, including provisioning infrastructure and managing IAM. When you connect your own cloud account, you pay for usage directly to your cloud provider. If you prefer to manage deployment yourself, you can export your application as a standalone Docker image and deploy in any way you prefer. ([Learn more](/docs/ts/self-host/build)) ## Payments & Billing FAQ ### What is the price of the paid plan? See the [pricing page](https://encore.dev/pricing) for complete pricing information. If you are a large organization with specific needs, please [email us](mailto:hello@encore.dev) or [book a 1:1](/book) to discuss your needs and get a custom price quote. ### Can I pay with a credit card? Yes, we offer payments via Stripe using all major credit cards. You can upgrade your account via the [pricing page](/pricing). ### What happens if my payment fails? If your payment fails, everything will keep working as normal! We will reach out to you with instructions on how to update your payment information so that we can try to process your payment again. We will not downgrade your account without prior notice. ================================================ FILE: docs/platform/management/compliance.md ================================================ --- seotitle: Security & Compliance seodesc: Learn about Encore's security practices, infrastructure protections, and compliance posture — built on industry-standard controls and trusted cloud providers. title: Security & Compliance subtitle: How Encore protects your applications, code, and data lang: platform --- _Last updated: March 3, 2026_ Your applications, code, and data are among your most important assets. Security is foundational to everything we build at Encore — it is embedded in our architecture, our processes, and our culture. This document provides a comprehensive overview of the security controls and practices we have in place today, structured around the SOC 2 trust service criteria. ### Security at a glance | Area | What we do | | --- | --- | | **Infrastructure** | Hosted on GCP (ISO 27001 / SOC 2 certified). All servers are private, accessible only via VPN. | | **Encryption** | AES-256 at rest, TLS 1.2+ and WireGuard in transit. Customer secrets additionally encrypted via GCP KMS. | | **Zero-trust networking** | All server-to-server communication is authenticated and end-to-end encrypted via Tailscale / WireGuard. | | **Access control** | Principle of least privilege, MFA enforced, regular access reviews, VPN-only infrastructure access. | | **Authentication** | Managed by Clerk (SOC 2 certified). Passwordless by default — Encore never stores or handles user passwords. | | **Monitoring & alerting** | 24/7 monitoring via Grafana, Sentry, Cronitor, and GCP Cloud Monitoring (all SOC 2 certified). | | **Vendor security** | All critical vendors are SOC 2 and/or ISO 27001 certified (GCP, Tailscale, Clerk, GitHub, Sentry, Grafana). | | **Code quality** | Mandatory code review, CI/CD with automated testing, automated vulnerability scanning. | | **Data privacy** | GDPR compliant. Data minimization by design. | | **Responsible disclosure** | Active bug bounty program for security researchers. | ## SOC 2 SOC is short for "System and Organization Controls" — it is the de facto industry standard for software security and privacy. We have implemented controls aligned with the SOC 2 framework and are currently preparing for a formal SOC 2 Type 1 audit, during which an external auditor will verify that our controls meet the standard. After the Type 1 audit, we plan to proceed to Type 2, which involves continuous monitoring over an extended period. ### Trust Service Criteria The five SOC 2 trust service criteria are: security, availability, confidentiality, processing integrity, and privacy. **1\. Security** Protecting systems against unauthorized access. **2\. Availability** Ensuring that the system remains functional and usable. **3\. Confidentiality** Restricting the access of data to a specified set of persons or organizations. Ensuring that network communication is encrypted and cannot be intercepted by unauthorized personnel. **4\. Processing integrity** Ensuring that a system fulfills its purpose and delivers correct data. **5\. Privacy** Minimal processing and use of personal data in accordance with the law. The following sections describe in detail how Encore implements each trust service principle. ## Security We believe security is achieved through proven best practices and industry standards — not through obscurity or homegrown cryptography. Our approach is defense in depth: multiple overlapping layers of protection so that the compromise of any single layer does not result in a breach. We have a designated Security Officer responsible for all aspects of security across infrastructure, software, and data. ### Infrastructure security Encore's core production infrastructure is hosted on GCP (Google Cloud Platform), an ISO27001/SOC 2 compliant vendor. Auxiliary services are provided by Hetzner, an ISO27001 compliant vendor. Tailscale, a SOC 2 compliant vendor, provides VPN (Virtual Private Network) services used to secure communication between all servers. All core data processing is carried out in the US East region (us-east-1), and backups are kept in multiple separate regions in the US. Each region is composed of at least three "availability zones" (AZs) which are isolated locations, designed to take over in case of a catastrophic failure at one location. AZs are separated by a significant distance such that it is unlikely that they are affected by the same issues such as power outages, earthquakes, etc. Physical access to GCP is restricted by GCP's security controls. Furthermore, GCP monitors and immediately responds to power, temperature, fire, water leaks, etc. Access to Encore's production infrastructure is restricted to Encore employees. All systems have access controls and only a limited number of employees have privileged access. Access is only possible through a VPN over Tailscale. The production environment is separated from testing environments, using separate accounts and VPCs (Virtual Private Cloud) in GCP. This ensures that any defect in a test environment cannot impact the production system. The connection to the internet is controlled by dedicated gateways. ### Organizational security An organization is only as strong as its people. All employees undergo a rigorous selection process, and many of Encore's team members bring extensive experience from regulated environments such as online banking and large-scale payment systems. Employees are required to complete annual security awareness training covering physical security, digital hygiene (strong passwords, two-factor authentication), social engineering ("phishing"), and related topics. Individual performance is reviewed on a bi-weekly cadence, and organizational performance is tracked via KPIs reviewed monthly by management. Encore employment policy mandates full-disk encryption on all employee devices. ### Product security Multiple layers of protection ensure that customer data is not accessible to unauthorized persons. Encore's service-based architecture provides natural isolation between components, and we have adopted a zero-trust security model with Tailscale. All server-to-server communication is authenticated and end-to-end encrypted with WireGuard. GCP's VPC (Virtual Private Cloud) provides another layer of isolation from the internet on the network level. None of Encore's servers are publicly accessible on the internet. As a general principle, all of Encore's data is encrypted while being transported across networks and when stored ("in transit and at rest"). In case of unauthorized access to the data, an attacker would only see undecipherable garbage which cannot be decrypted without the corresponding keys. The encryption methods employed by Encore are industry standard and deemed unbreakable by contemporary standards. Data at rest (virtual filesystems, relational databases, and object storage) is encrypted using GCP's industry-standard AES-256, while data in transit is encrypted with TLS ≥ 1.2 (for Encore's REST API) or WireGuard (for internal communication). All customer secret information is further encrypted using GCP's Key Management Service (KMS). Any access to encrypted data by Encore employees requires elevated access and approval by multiple parties, and all such activity is audited. User account authentication is provided by _Clerk_, a SOC 2 compliant vendor. There are two ways for a user to log in to Encore: Single sign-on (SSO) and username plus password. Single sign-on can be used by organizations to fully manage access to Encore and, for example, ensure that former employees no longer have access after the offboarding period. Encore supports Google and GitHub SSO using OAuth. If no SSO is used, the default login method is passwordless login using email and "magic link", also handled by _Clerk_. Encore does not store or in any way handle passwords, neither in plaintext nor cryptographic hash form. This means that Encore does not know the passwords of any users, and no passwords can be reconstructed from our databases. Encore uses automated vulnerability scanning across its codebase and dependencies. All teams continuously monitor their services for vulnerabilities and proactively remediate them, supervised by the Security Officer. All security issues undergo a triaging process by the Security Officer and are escalated based on criticality. ### Responsible disclosure We maintain an active bug bounty program to encourage security researchers to report vulnerabilities before they can be exploited. If you discover a security issue, please report it to [security@encore.dev](mailto:security@encore.dev). We are committed to investigating all reports promptly and working with researchers to resolve issues responsibly. ### Access control We regularly keep track of and review the list of employees who have access to which systems and remove access where applicable to ensure least access principles apply. Offboarding processes ensure that former employees cannot access internal systems anymore after the termination of their contract. Thanks to the VPN, Encore can centrally restrict access to internal networks. #### MFA Multi-factor authentication (MFA) adds another layer of security on top of classic password authentication. In addition to username and password, the user requires another individual token of access. Stealing or guessing the password is not enough for an attacker to gain access to a system, because the second factor would also need to be stolen. Usually, the second factor is a physical device, such as a mobile phone which has been paired with the authentication system. Encore employs MFA to protect access to the infrastructure provider (GCP) and the version control systems (GitHub), among other systems. ## Availability Hosted on a cloud infrastructure, Encore implements a service-based architecture where many dedicated software components operate isolated from one another, but in a coordinated way, much like a complex machine where individual parts can be replaced independently from one another. During the release of a new version of Encore services, Encore's engineers take great care during the preparation of the update so that in case of an unexpected problem, the system can be restored to the previous state in a manner that minimizes user impact. ### Performance monitoring Encore uses a number of performance monitoring systems, such as Sentry, Cronitor, Grafana, and Google Cloud Monitoring (all being SOC 2 compliant vendors). Grafana is used to monitor application performance, such as server response times and user interface speed. Grafana also collects server-side metrics like CPU and RAM usage. Additionally, Encore monitors the performance of databases with GCP tooling. Slack, a SOC 2 compliant vendor, is used as the alerting channel to notify the developers in case the performance of the system has regressed, for example, due to increased response times, or increased error rates. To enable root cause analysis of bugs, Encore collects system logs from all parts of the system. These logs can only be accessed by authorized users. Encore offers a public "Status page" where users and customers can find the current status of Encore systems. It is available at: [https://status.encore.dev/](https://status.encore.dev/). ### Backups and disaster recovery To reduce the risk of simultaneous failure, Encore backs up data to multiple US regions in GCP, with very limited access. Relational databases are backed up on a daily schedule. Encore is currently planning a rehearsal of disaster recovery in Q4 of 2026. In this exercise, a clone of the production environment will be recovered from scratch using backups and tested for soundness. ### Incident handling Whenever an incident occurs, Encore's designated on-call engineer initiates an investigation and escalates to the broader engineering team as necessary based on severity. For issues deemed critical, they follow an iterative response process to identify and contain errors, recover systems and services, and remediate vulnerabilities. Customers and users can report outages via regular support channels (for example via email, or using the [Discord](https://encore.dev/discord) chat group). Encore's internal communication systems have dedicated channels for incident escalation. ## Confidentiality When you use Encore, other users won't be able to see your content, unless you grant access explicitly by inviting them to your application. Encore engineers may use your data to provide support and when necessary to fix bugs. ### Access controls All employees and contractors are contractually bound to confidentiality, which persists after the termination of the work contract. As part of a "clean screen" policy, all computers used by Encore staff must be set to automatically lock the screen after 1 minute of inactivity. All systems access is subject to the "principle of least privilege", meaning that every employee only has access to the systems necessary to perform their official duties. ### Deletion User data will be stored by Encore after the termination of a subscription term, according to [Encore's Terms of Service](https://encore.cloud/legal/terms). When a user requests the deletion of data, the data is made inaccessible or physically deleted, depending on the data type and storage location. For technical reasons, data may remain in backups after this point. ## Processing integrity ### Quality assurance Product quality is very important to Encore. There are many different facets, including: - Accuracy and usability of services provided - High performance of the user interface and Encore services - Almost zero downtime Several measures are put into place in order to keep product quality high: - Code review: Code changes are reviewed by a peer of the developer before it is accepted into the main code branch (for critical systems) or in a weekly post-hoc review process (for auxiliary systems). For critical systems code can only be merged if the reviewer agrees. For auxiliary systems any requested changes by reviewers are made as part of the review process. It is often necessary to add a test alongside, and the code review process ensures that this has been done as well. - Continuous integration: Before code is accepted, it is built by our continuous integration environment and tests are executed. If the build fails, the developer is notified immediately and a fix is required before the code can be merged. - Manual testing: Once the code has been merged, the change is deployed and in most cases tested manually post-release to verify quality in the production environment. - Automated integration testing: A large battery of automated tests is executed against the local and production environments and checks many common workflows for regressions of any kind. In case the tests fail, the engineer will address the issues before proceeding with attempting to merge again. - Testing of Open-Source libraries: Encore uses Open-Source libraries to provide certain functionality. Overnight tests run daily to discover potential issues, and manual testing is performed when any Open-Source libraries are version updated. Any code change is released only if all these steps succeed. Furthermore, access to the code base is protected via multi-factor authentication (MFA), which poses another layer of defense against the malicious injection of code. Since Encore depends on third-party software, we regularly contribute to the quality assurance of our suppliers. Whenever Encore becomes aware of regressions or bugs, they are reported upstream. In this way, Encore is contributing to the quality, stability, and accuracy of other software in the space. ### Process monitoring Where possible, Encore uses software to enforce processes. For example, code review and having tests passed are enforced by the source control management tool GitHub. Regular reviews on different levels (individual, team, company) foster alignment between all individuals and the company objectives. ### Privacy Encore takes data privacy very seriously and complies with the rules of the European Union's GDPR (General Data Protection Regulation). GDPR grants a wide range of rights to Encore's users, such as the right to be informed, the right to access, the right to rectification, the right to erasure, and others. One fundamental rule of the GDPR is the principle of "data minimization", which ensures that we are not processing more personal data than necessary. As a result, Encore Cloud uses only minimal personal data for user authentication and essential communication (a name and contact email). As described above, Encore does not store or handle passwords in any form. ### Privacy policy We are aware that confidential handling of your data is essential to establishing trust. Therefore, [Encore's Privacy Policy](https://encore.cloud/legal/privacy) ensures that the data of our users is protected according to the high standards of GDPR. ## Questions and further information We are committed to transparency in our security practices. If you have questions about our security or compliance posture, or would like to request additional documentation for your vendor review process, please contact us at [hello@encore.dev](mailto:hello@encore.dev). ================================================ FILE: docs/platform/management/permissions.md ================================================ --- seotitle: Roles & Permissions seodesc: Encore helps your whole team build applications and collaborate across backend and frontend teams. title: Roles & Permissions subtitle: For teams building applications together lang: platform --- Encore applications have three membership roles with different permissions: **Admins**, **Members**, and **Viewers**. Here is a breakdown of the key differences between each role: | | Admins | Members | Viewers | | ----------------------------------- | ------ | ------- | ------- | | Manage team members | Y | N | N | | Connect/Disconnect cloud accounts | Y | N | N | | Integrate with GitHub | Y | N | N | | Configure custom domains | Y | N | N | | Manage environments | Y | N | N | | Create auth keys | Y | N | N | | Approve infrastructure provisioning | Y | N | N | | Delete applications | Y | N | N | | Push code changes | Y | Y | N | | Create builds & deployments | Y | Y | N | | Configure secrets | Y | Y | N | | Pull secrets | Y | Y | Y | | Run locally | Y | Y | Y | | View API documentation | Y | Y | Y | | View Encore Flow | Y | Y | Y | ### Admins Admins have full privileges and can administer your entire application. ### Members Members are active contributors to your applications. They are able to do everything that is not limited to Admins. ### Viewers Viewers are read-only members. They can view the Cloud Dashboard and run your application locally. This role is intended for any team members not contributing directly to your Encore application, but who still get value from certain access. In a bigger team, this role is often appropriate for e.g. Frontend developers and Product Managers. ### Custom roles & permissions Custom roles & permissions is an optional add-on to the [Pro plan](/pricing), please [contact us](mailto:hello@encore.dev) to discuss your requirements. ================================================ FILE: docs/platform/management/telemetry.md ================================================ --- seotitle: Encore Telemetry seodesc: Encore collects telemetry data about app usage title: Telemetry lang: platform --- Telemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently. Encore only collects telemetry data in the local development tools and in the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations. ## Why We Collect Data We collect telemetry data for several important reasons: 1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development. 2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience. 3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur. 4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly. ## How Data is Collected Encore collects data in a way that prioritizes user privacy and security. Here's how we do it: 1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts. 2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports. 3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols. 4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance. ### Example of Data Being Sent Here is an example of the type of data that is sent: ```json { "event": "app.create", "anonymousId": "a-uuid-unique-for-the-installation", "properties": { "error": false, "lang": "go", "template": "graphql" } } ``` ## Data We Don't Collect At Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect: 1. **Environment Variables**: We do not collect any environment variables set in your development or production environments. 2. **File Paths**: The specific paths of your files and directories are not collected. 3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects. 4. **Logs**: No log files from your application or development environment are collected. 5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information. Our goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure. ## Disabling Telemetry While telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways: 1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal. ```sh encore telemetry disable ``` 2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable. ```sh export DISABLE_ENCORE_TELEMETRY=1 ``` 3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled. 4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command: ```sh encore telemetry enable ``` ## Debugging Telemetry For users who want more visibility into what telemetry data is being sent, you can enable debug mode: 1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable. ```sh export ENCORE_TELEMETRY_DEBUG=1 ``` 2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent. ## Conclusion Telemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team. Thank you for helping us make Encore better! ================================================ FILE: docs/platform/management/usage.md ================================================ --- seotitle: Usage limits and Fair Use guidelines seodesc: Encore comes with a built-in development cloud with generous Fair Use limits. This makes it easy to get started building your next backend application without requiring a cloud account. title: Usage limits subtitle: Encore Cloud limits and Fair Use guidelines lang: platform --- Encore comes with a built-in development cloud, Encore Cloud, that is free to use for development and limited scale commercial projects without any specific SLA requirements. Encore Cloud is subject to Fair Use guidelines and comes without warranty, as it's not intended for large-scale business-critical use cases. For production use cases, Encore is designed to be used together with your cloud on the major cloud providers (AWS/GCP), and provides full DevOps automation for deployments to your own cloud account. This means Encore has no incentive to increase your usage – rather we can focus on building tools to help you minimize your cloud spending! (Should you wish to use Encore Cloud instead of your own cloud account, please [contact us](/book).) When you use Encore together with AWS/GCP, you can still use Encore Cloud to host Preview Environments and development environments. ### Examples of Fair Use - Prototyping & development - Hobby projects - Commercial use cases that have limited load and do not require any SLAs ### Never Fair Use - Proxies and VPNs - Media hosting for hot-linking - Scrapers - Crypto Mining - CPU-intensive APIs (e.g.: Machine Learning) - Load Testing ## Usage guidelines We expect most users to fall within the usage limits below. We want to be as flexible and permissive as possible, and will wherever possible reach out and work with you to find a good solution should we notice that you are exceeding these limits. For users on a [paid plan](/pricing), we can change these limits to support your needs. If you have significantly higher requirements, this may come with an additional charge (at cost) to cover the extra capacity. Please [contact us](/book) for more information. **We will never charge you for usage of Encore Cloud unless expressly agreed in advance.** ### Usage limits | | Per application | | ---------------- | --------------- | | Requests | 100,000 / day | | Database Storage | 1 GB | | PubSub Messages | 100,000 / day | | Cron Jobs | Once every hour | | Object Storage | 1 GB | ### What happens if I reach a usage limit? If your application reaches a usage limit, we will **not** automatically stop it. Our team will reach out to you, and work with you to find a solution that does not cause any undue disruption to your application. ================================================ FILE: docs/platform/migration/migrate-away.md ================================================ --- title: Migrate away from Encore subtitle: If you love someone, set them free. lang: platform --- _We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._ Picking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity. When designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. In practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure. ## Accommodating for your unique requirements Many software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example: - You can drop down in abstraction level in the API framework using [raw endpoints](/docs/ts/primitives/defining-apis#raw-endpoints) - You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore ## Mitigating risk through Open Source and efficiency We believe that adopting Encore is a low-risk decision for several reasons: - There's no upfront investment needed to get the benefits - Encore apps are normal programs where less than 1% of the code is Encore-specific - All infrastructure and data is in your own cloud - It's simple to integrate with cloud services and systems not natively supported by Encore - Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes) - Everything you need to self-host your application is [Open Source and documented](/docs/ts/self-host/build) ## What to expect when migrating away If you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away. ### Code changes Building with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework. This means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost. ### Deployment If you are self-hosting your application, then you're already done. If you are using Encore Cloud to manage deployments and want to migrate to your own solution, you can use the `encore build docker` command to produce a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility. Learn more in the [self-hosting docs](/docs/ts/self-host/build). ### Tell us what you need We're engineers ourselves and we understand the importance of not being constrained by a single technology. We're working every single day on making it even easier to start, and stop, using Encore. If you have specific concerns, questions, or requirements, we'd love to hear from you! Please reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts. ================================================ FILE: docs/platform/migration/migrate-to-encore.md ================================================ --- title: Migrating an existing system to Encore subtitle: Approaches for adopting Encore seotitle: How to migrate your existing system to Encore seodesc: Learn how to migrate your application to Encore incrementally, and unlock Encore's powerful set of development tools for your team. lang: platform --- By building your application with the Encore open-source framework, you unlock powerful features such as the [local development tools](/docs/ts/observability/dev-dash), [automatic infrastructure provisioning](/docs/platform/infrastructure/infra), [distributed tracing](/docs/ts/observability/tracing), and [service catalog](/docs/ts/observability/service-catalog). **The good news: you don't need a complete rewrite.** This guide shows you how to adopt Encore incrementally, so you can start benefiting immediately while gradually migrating your existing system. ## Why incremental migration? Incremental migration is more reliable than a complete rewrite. Here's why: - **Immediate value** - Start benefiting from Encore's features when developing your next new service. - **Lower risk** - Small, controlled changes instead of a single high-stakes big-bang launch. - **Ship faster** - Deliver improvements incrementally rather than waiting for a complete rewrite. ## Choose your migration strategy We recommend two approaches: 1. **Service by service** (Recommended) - Migrate services one at a time. Run Encore alongside your legacy system, integrated via APIs. 2. **Forklift migration** - Move your entire application in one shot using a catch-all handler, then refactor incrementally. ### Need help? We've helped 100+ teams adopt Encore and we're happy to answer your questions and provide advice to help you with your migration. [Email us](mailto:hello@encore.dev) to ask questions, or [book a 1:1 call](https://encore.dev/book) to discuss your specific situation. **Enterprise customers**: Encore Cloud can adapt to your unique infrastructure—Kubernetes clusters, VPCs, security policies, and compliance needs—typically within days. [Contact us](https://encore.dev/book) to discuss your requirements. ## Service by service migration (Recommended) Migrate services one at a time while your Encore application runs alongside your legacy system, integrated through APIs. ### Key benefits - **Full Encore features immediately** - Get automatic infrastructure provisioning, distributed tracing, and architecture diagrams for each migrated service. - **Independent services** - Each service is self-contained with no cross-application dependencies. - **Simple integration** - Services communicate via APIs. - **Flexible deployment** - Deploy to your existing Kubernetes cluster, or let Encore Cloud set up a new project in your cloud (AWS/GCP). - **Better developer experience** - Start building with modern tooling right away. ### Deployment options Choose how to deploy your Encore application: - **Your Kubernetes cluster** - Deploy directly to your existing Kubernetes infrastructure. Run Encore alongside legacy systems securely in the same environment. - **Encore-managed in your cloud account** - Let Encore handle all infrastructure provisioning and management in your AWS or GCP account, and deploy within your existing VPC and security setup. **Enterprise**: We can adapt to your specific network topology, security policies, and compliance requirements — typically within days. [Contact us](https://encore.dev/book) to discuss your requirements. _Google Cloud example architecture:_ ### Which services to migrate first? Start small and build confidence: - **Low-risk, high-value** - Validate the approach before tackling complex systems. - **Frequently changed** - Get immediate developer experience benefits where it matters most. - **Clear boundaries** - Services with well-defined APIs are easier to migrate. - **Fewer dependencies** - Less connected to legacy infrastructure means simpler migration. ### Practical steps #### 1. Create an Encore app and integrate with GitHub The first step in any project is to create an Encore app. If you've not tried Encore before, we recommend starting by following the [Quick Start Guide](/docs/ts/quick-start). Once you've created you app, [integrate it with your GitHub repository](/docs/platform/integrations/github) and you'll get automatic [Preview Environments](/docs/platform/deploy/preview-environments) for every Pull Request. #### 2. Build your services and APIs Since Encore is designed to build distributed systems, it should be straightforward to build a new system that integrates with your existing backend through APIs. See the [defining APIs documentation](/docs/ts/primitives/defining-apis) for more details. Should you want to accept webhooks, that's simple to do using Encore's [Raw endpoints](/docs/ts/primitives/raw-endpoints). You can also generate API clients in several languages, which makes it simple to integrate with frontends or other systems. See the [Client Generation documentation](/docs/ts/cli/client-generation) for more details. #### 3. Deploy alongside your existing backend **Deploy to Kubernetes** Encore Cloud can deploy directly to your existing Kubernetes cluster: - Run in the same secure environment as your legacy systems - Services communicate within your existing VPC - Gradually shift traffic using your load balancer or service mesh - Use your current cost management and billing - Maintain compliance with your governance policies [Contact us](https://encore.dev/book) to discuss Kubernetes deployment. **Deploy to new Encore-managed infrastructure in your cloud** [Connect your AWS or GCP account](/docs/platform/deploy/own-cloud) to deploy in your existing environment: - Same VPC as your legacy backend (or a new one) - Your current cost management and billing - Maintain compliance with your governance policies See [infrastructure docs](/docs/platform/infrastructure/infra#production-infrastructure) for details. #### Integration patterns Your Encore and legacy systems communicate through APIs: - **Legacy → Encore**: Use Encore-generated API clients - **Encore → Legacy**: Use your existing API communication protocol (Encore is not opinionated) - **Authentication**: Choose to deploy an authentication gateway in front of Encore or implement authentication directly in your Encore app - **Events**: Use Encore's built-in Pub/Sub support for loose coupling #### 4. Expand your migration Continue migrating services incrementally. Strategies to consider: - **Related services**: Migrate services that interact frequently to maximize tracing benefits - **High-churn areas**: Move frequently changed services first - **New features**: Build new functionality in Encore from the start - **Critical paths**: Once confident, migrate business-critical services ## Forklift migration using a catch-all handler Should you prefer, you can use a forklift migration strategy to move your entire application to Encore in one step by wrapping your existing HTTP router in a catch-all handler. ### When to consider this approach This strategy works well when: - Your existing system is a monolith or smaller distributed system - The codebase relies primarily on infrastructure primitives supported by Encore (microservices, databases, pub/sub, caching, object storage, cron jobs, and secrets) - You want to quickly consolidate everything in one place - You're prepared to incrementally refactor to unlock full Encore features like tracing and automatic API documentation ### Trade-offs **Benefits:** - **Quick consolidation**: Get everything in one place from the start. - **Immediate access to core features**: Quickly use Encore's CI/CD system, secrets manager, and deployment capabilities. - **Single codebase**: Simplified development and deployment workflow. **Limitations:** - **Limited initial visibility**: Advanced features like [distributed tracing](/docs/ts/observability/tracing) and [architecture diagrams](/docs/ts/observability/encore-flow) require the [Encore application model](/docs/ts/concepts/application-model) and won't work immediately. - **Requires refactoring**: You'll need to incrementally break out endpoints to unlock full Encore capabilities. - **All-at-once risk**: Unlike service-by-service migration, this is a bigger initial change. ### Practical steps Here follows a quick summary of the high-level steps of a forklift migration. Find more in-depth instructions in the full [forklift migration guide](/docs/ts/migration/express-migration#forklift-migration-quick-start). #### 1. Create an app and structure your code To start, create an Encore application and copy over the code from your existing repository. In order to run your application with Encore, it needs to follow the expected [application structure](/docs/ts/primitives/app-structure), which involves placing the `encore.app` and `package.json` files in the repository root. This should be straightforward to do with minor modifications. As an example, a single service application might look like this on disk: ``` /my-app ├── package.json ├── encore.app ├── // ... other project files │ ├── encore.service.ts // defines your service root ├── api.ts // API endpoints ├── db.ts // Database definition ``` You can also have services nested inside a `backend` folder if you prefer. #### 2. Create a catch-all handler for your HTTP router Now let's mount your existing HTTP router under a [Raw endpoint](/docs/ts/primitives/raw-endpoints), which is an Encore API endpoint type that gives you access to the underlying HTTP request. Here's a basic code example: ```ts import { api } from "encore.dev/api"; export const migrationHandler = api.raw( { expose: true, method: "*", path: "/api/*path" }, async (req, resp) => { // pass request to existing router } ); ``` By mounting your existing HTTP router in this way, it will work as a catch-all handler for all HTTP requests and responses. This should make your application deployable through Encore with little refactoring. #### 3. Iteratively fix remaining compilation errors Exactly what remains to make your application deployable with Encore will depend on your specific app. As you run your app locally, using `encore run`, Encore will parse and compile it, and give you compilation errors to inform what needs to be adjusted. By iteratively making adjustments, you should relatively quickly be able to get your application up and running with Encore. #### 4. Refactor incrementally to unlock Encore features Once your application is deployed, gradually break out specific endpoints using Encore's [API declarations](/docs/ts/primitives/defining-apis) and introduce infrastructure declarations using the Encore backend frameworks. This incremental refactoring will: - Enable Encore to understand your application structure - Unlock powerful features like distributed tracing and architecture diagrams - Improve observability and debugging capabilities - Make your codebase more maintainable and easier to evolve Start with the most frequently modified endpoints or the most critical user flows to maximize the value of refactoring efforts. ## Conclusion Incremental migration lets you adopt Encore without the risk of a complete rewrite. **Service by service migration** is the recommended approach—it gives you Encore's full feature set immediately while running safely alongside your existing systems. **Enterprise customers** benefit from flexible deployment options, including Kubernetes integration and customization that typically takes just days to set up. ### Have questions? We've helped 100+ teams adopt Encore and we're happy to answer your questions and provide advice to help you with your migration. - [Book a call](/book) to get 1:1 assistance - [Email us](mail:hello@encore.dev) to ask questions - [Join Discord](https://encore.dev/discord) to discuss with other developers using Encore ================================================ FILE: docs/platform/migration/try-encore.md ================================================ --- title: Trying Encore for an existing project subtitle: Extending, Refactoring, and Rebuilding seotitle: Trying Encore for an existing project seodesc: Learn how to try Encore for your existing backend application using Extending, Refactoring, or Rebuilding, depending on your situation and priorities. lang: platform --- Making changes to your backend requires a thoughtful approach and how you best evaluate a new tool, like Encore, depends on your situation and priorities. Here we’ll explore three approaches and introduce the common scenarios and procedures for each: - **Extend:** Using Encore to speed up building an independent new system or creating a proof of concept. - **Refactor:** Using Encore when refactoring an existing backend to unlock productivity benefits and remove complexity. - **Rebuild:** Using Encore when rebuilding an existing application from the ground up, ensuring modern best practices and cloud-portability. ## Extend Extending your existing backend best suits teams who are mostly satisfied with their current setup, but are on the lookout for more efficient workflows to cut down delivery times for new projects, or wish to improve the developer experience for ongoing development. ### Use cases - Extending an existing application with a new service or system, integrated using APIs. - Reducing effort when building a new system in an isolated domain, such as a new product experiment. - Tackling an independent project that demands fast delivery times. ### When to consider Encore If your existing setup feels right but you’re curious about Encore, evaluating it in an independent project is the right move. For example when: - You want to create a new service or system and deploy it to your cloud in **AWS** or **GCP**, without manual infrastructure setup. - You want to try out development tools like [preview environments](/docs/platform/deploy/preview-environments), and [local tracing](/docs/ts/observability/dev-dash), without any manual instrumentation. - You want to validate Encore’s workflow and reliability without making changes to existing systems. ### How to adopt Encore when Extending - **1. Identify Extension Points:** Decide on an upcoming project or proof of concept, that is relatively independent of your existing application and is appropriate for building as a new service or system. - **2. Create New Services:** Develop new services or systems using [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to get off the ground quickly. This lets you try out all Encore features and enables you to design your new system with Encore’s [automatic architecture diagrams](/docs/ts/observability/encore-flow). - **3. Integrate via APIs:** Where relevant, integrate your new system with your existing backend application using APIs. This can be made simpler by using Encore’s [generated API clients](/docs/ts/cli/client-generation). - **4. Validate & Iterate:** Deploy the new services to a [cloud environment](/docs/platform/infrastructure/infra), automatically provisioned by Encore, and validate their performance and interoperability. Use Encore’s [distributed tracing](/docs/ts/observability/tracing) to find bugs or performance issues. - **5. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster). ## Refactor Refactoring can serve as a breath of fresh air for your existing code, revitalizing it by optimizing existing structures. In this approach, your goal is to improve on your existing backend application, often focusing on shedding unnecessary complexity and enabling new opportunities. ### Use cases - Transforming a **monolith** into **microservices**. - Changing system architecture, e.g. moving to an [event-driven architecture](/blog/event-driven-architecture). - Cloud migration, e.g. from **AWS** to **GCP**. - Changing foundational infrastructure, e.g. migrating to **Kubernetes**. - Removing unwanted complexity that’s become engrained as you’ve scaled up quickly. ### When to consider Encore Your application is already built using a supported programming language like **Go** or **TypeScript**. and you want to unlock modern development tools like [infrastructure automation](/docs/platform/infrastructure/infra), [preview environments](/docs/platform/deploy/preview-environments), and [distributed tracing](/docs/ts/observability/tracing), with minimal adjustments to your existing backend and no manual setup. ### How to adopt Encore in a Refactor - **1. Assess Your Goal:** Start by evaluating what changes you want to make to your existing application, and look for unnecessary complexities and bottlenecks that can be eliminated. Depending on your goal, you can decide if you want to fully implement Encore’s [API declarations](/docs/ts/primitives/defining-apis) or if you prefer to minimize changes by using a catch-all handler on your current router. Keep in mind that in order to use features like the [Service Catalog](/docs/ts/observability/service-catalog), you need to use the API declarations defined in [Encore.ts](/docs/ts) or [Encore.go](/docs/go). - **2. Implement Backend Framework:** Start using [Encore.ts](/docs/ts) or [Encore.go](/docs/go) in your application by replacing existing infrastructure configuration and boilerplate. This enables you to use Encore's infrastructure automation and removes the hassle of manual infrastructure setup. **Tip:** [Existing databases can be integrated](/docs/go/primitives/connect-existing-db) so you don’t need to migrate existing data. - **3. Resolve compile-time errors:** Encore comes with a parser and compiler that ensures your application correctly implements the Backend Framework. This lets you discover problems at compile time and provides insightful error messages to help you quickly resolve any errors. - **4. Test & Iterate:** Test the refactored application to ensure stability and reliability using Encore’s automatically provisioned cloud [environments](/docs/platform/deploy/environments) and [distributed tracing](/docs/ts/observability/tracing) for fast debugging and iteration. If relevant, you can use a [generated client](/docs/ts/cli/client-generation) to integrate with your existing application frontend. - **5. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster). ## Rebuild The Rebuild strategy is for those who want a fresh start by recreating an application from the ground up. It’s particularly relevant for companies looking to make a bigger change like changing programming language or migrating from legacy self-hosted infrastructure. A full rebuild, although potentially labor-intensive, opens up opportunities to harness the latest cloud services and developer tools like Encore. ### Use cases - Changing programming languages to adopt more performant or modern ones for your project. - Migrating from legacy self-hosted solutions to scalable cloud providers like **AWS** or **GCP**. - Starting fresh by recreating an app from the ground up. ### When to consider Encore - You’re intending to use a supported programming language like **Go** or **TypeScript**. - You want to leverage the scalability and services of cloud providers like **AWS** or **GCP**, but don’t want to become locked-in to one specific provider. (Encore applications are cloud-portable by default.) - You want modern development tools like [infrastructure automation](/docs/platform/infrastructure/infra), [preview environments](/docs/platform/deploy/preview-environments), and [distributed tracing](/docs/ts/observability/tracing), without manual setup or instrumentation. ### How to adopt Encore in a Rebuild - **1. Plan & Design:** Start by creating a design, considering the application's core requirements and architecture. Decide on the programming language, keeping in mind Encore's supported languages. - **2. Develop from Scratch:** Develop your new application using Encore [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to get up and running quickly in a shared environment using Encore’s built-in development cloud. - **3. Test & Iterate:** Test your new application to ensure reliability using Encore’s [distributed tracing](/docs/ts/observability/tracing) for fast debugging and iteration. Use the [generated API clients](/docs/ts/cli/client-generation) to integrate with your application frontend. - **4. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster). ## Get support adopting Encore Each approach has different benefits and is relevant in different scenarios. Which one is right for your team depends on your priorities and existing setup. Whether it’s expanding your horizons with **Extend**, revitalizing existing structures through **Refactor**, or starting afresh with **Rebuild**, we’re available to support as you explore Encore to unlock improved productivity and developer experience. If you'd like to ask questions or get advice about how to get started, we're happy to talk through your project. You can [join Discord](https://encore.dev/discord) to ask questions and meet other Encore developers, or you can also [book a 1:1](/book) with a member of our core team. ================================================ FILE: docs/platform/observability/encore-flow.md ================================================ --- seotitle: Encore Flow automatic microservices architecture diagrams seodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application. title: Flow Architecture Diagram subtitle: Visualize your cloud microservices architecture lang: platform --- Flow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your microservices architecture and identify which services depend on each other and how they work together. ## Birds-eye view Having access to a zoomed out representation of your system can be invaluable in pretty much all parts of the development cycle. Flow helps you: * Track down bottlenecks before they grow into big problems. * Get new team members onboarded much faster. * Pinpoint hot paths in your system, services that might need extra attention. Services and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below the `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or subscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it: ## Highlight dependencies Hover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies. Here the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the database and requests to two of the endpoints from the `user` service as well as requests to one endpoint from the `authentication` service: ## Real-time updates Flow is accessible in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments. When developing locally, Flow will auto update in real-time to reflect your architecture as you make code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones. For cloud environments, Flow auto-updates with each deploy. In the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service: ================================================ FILE: docs/platform/observability/metrics.md ================================================ --- seotitle: Monitoring your backend application with custom metrics seodesc: See how you can monitor your backend application using Encore. title: Metrics subtitle: Built-in support for keeping track of key metrics infobox: { title: "Metrics", import: "encore.dev/metrics", } lang: platform --- Having easy access to key metrics is a critical part of application observability. Encore solves this by providing automatic dashboards of common application-level metrics for each service. Encore also makes it easy to define custom metrics for your application. Once defined, custom metrics are automatically displayed on metrics page in the Cloud Dashboard. By default, Encore also exports metrics data to your cloud provider's built-in monitoring service. ## Defining custom metrics Encore makes it easy to define custom metrics for your application. Once defined, custom metrics are automatically displayed on the metrics page in the Cloud Dashboard. For implementation guides on how to define metrics in your code, see: - [Go metrics documentation](/docs/go/observability/metrics) - [TypeScript metrics documentation](/docs/ts/observability/metrics) ## Integrations with third party observability services To make it easy to use a third party service for monitoring, we're adding direct integrations between Encore and popular observability services. This means you can send your metrics directly to these third party services instead of your cloud provider's monitoring service. ### Grafana Cloud To send metrics data to Grafana Cloud, you first need to Add a Grafana Cloud Stack to your application. Open your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation. Then select **Grafana Cloud** in the settings menu and click on **Add Stack**. Next, open the environment **Overview** for the environment you wish to sent metrics from and click on **Settings**. Then in the **Sending metrics data** section, select your Grafana Cloud Stack from the drop-down and save. That's it! After your next deploy, Encore will start sending metrics data to your Grafana Cloud Stack. To configure Encore to export metrics to Grafana Cloud, create a token with the following steps: 1. In Grafana, navigate to **Administration > Users and access > Cloud access policies** 2. Click **Create access policy**, select **metrics:read** and **metrics:write** scopes, then click **Create** 3. On the newly created access policy, click **Add token**, then **Create** to generate the token ### Datadog To send metrics data to Datadog, you first need to add a Datadog Account to your application. Open your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation. Then select **Datadog** in the settings menu and click on **Add Account**. Next, open the environment **Overview** for the environment you wish to sent metrics from and click on **Settings**. Then in the **Sending metrics data** section, select your Datadog Account from the drop-down and save. That's it! After your next deploy, Encore will start sending metrics data to your Datadog Account. ================================================ FILE: docs/platform/observability/service-catalog.md ================================================ --- seotitle: Service Catalog & Generated API Docs seodesc: See how Encore automatically generates API documentation that always stays up to date and in sync. title: Service Catalog subtitle: Automatically get a Service Catalog and complete API docs lang: platform --- All developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date. To solve this, Encore uses the [Encore Application Model](/docs/ts/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve. The API docs are available both in your [Local Development Dashboard](/docs/ts/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud). ================================================ FILE: docs/platform/observability/tracing.md ================================================ --- seotitle: Distributed Tracing helps you understand your app seodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore. title: Distributed Tracing subtitle: Track requests across your application and infrastructure lang: platform --- Distributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life. Tracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end. As opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations. You view traces in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments. ## Encore's tracing is more comprehensive and more performant than all other tools Unlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before: * Stack traces * Structured logging * HTTP requests * Network connection information * API calls * Database queries * etc. ## Redacting sensitive data Encore's tracing automatically captures request and response payloads to simplify debugging. For cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports redacting fields marked as containing sensitive data. See the documentation on [API Schemas](/docs/ts/primitives/defining-apis#sensitive-data) for more information. ## Trace Sampling Trace sampling lets you control what percentage of traces are recorded and stored. You can configure sampling rates per environment, service, and endpoint, giving you fine-grained control over your tracing volume. ### How sampling works Sampling is determined at the root of the trace. This means if you set an endpoint to sample at 10%, it controls whether a trace is created when that endpoint is called as the initial entry point. If that same endpoint is called as part of an already-ongoing trace (e.g. as an internal service-to-service call), it will always be included in the existing trace regardless of its own sampling rate. This design ensures that all traces are complete — you'll never see partial traces with missing spans. Either a trace is sampled in its entirety, or not at all. ### Configuring sampling rates You can configure sampling rates in the Encore Cloud dashboard. Sampling can be set at three levels of granularity: - **Environment level**: Set a default sampling rate for all traces in an environment. - **Service level**: Override the environment default for a specific service. - **Endpoint level**: Override the service default for a specific endpoint. More specific settings take precedence. For example, if your environment is set to sample 100% of traces but a high-traffic endpoint is set to 10%, that endpoint will only generate new traces 10% of the time it's called as the root of a request. ## Trace Budgets Trace budgets give you full predictability over your tracing costs by letting you set spending limits on a daily and monthly basis. When a budget limit is reached, tracing is paused until the next period begins, ensuring you never receive unexpected charges. ### Included events Encore Cloud includes a generous amount of tracing events in each plan: - **Free tier**: 1M trace events per month included. - **Pro tier**: 20M trace events per month included. Beyond the included events, Pro tier usage is billed at **$1.20 per million events**. ### Setting budgets You can configure your trace budgets in the Encore Cloud dashboard. By setting daily and monthly limits, you define exactly how much you're willing to spend on tracing. This makes tracing costs fully predictable and prevents any surprises on your bill. ================================================ FILE: docs/platform/other/vs-heroku.md ================================================ --- seotitle: Encore compared to Heroku seodesc: See how the Encore Backend Development Platform lets you avoid the lock-in problems of using Heroku. title: Encore compared to Heroku subtitle: Get the convenience you want — without limitations and lock-in lang: platform --- In the early days of the cloud, Heroku was seen as an innovative platform that made deployments and infrastructure management very simple using a Platform as a Service (PaaS) approach. Ultimately, Heroku lost momentum and, as cloud services rapidly evolved in the past decade, the platform didn't manage to provide enough flexibility to support users' needs. Fans of Heroku will recognize much of the same simplicity in Encore's **push to deploy** workflow — the big difference is that **Encore deploys to your own cloud on AWS/GCP**. This means you keep full flexibility to scale your application using battle-tested services from the major cloud providers, and can leverage their full arsenal of thousands of different services. Let's take a look at how Encore compares to PaaS tools like Heroku: | | Encore | Heroku | | ---------------------------------------------------- | ------------------------ | --------------------- | | **Infrastructure approach?** | Infrastructure from Code | Platform as a Service | | **Built-in CI/CD?** | ✅︎ Yes | ✅︎ Yes | | **Built-in Preview Environments?** | ✅︎ Yes | ✅︎ Yes | | **Built-in local dev environment?** | ✅︎ Yes | ❌ No | | **Built-in Distributed Tracing?** | ✅︎ Yes | ❌ No | | **Deploys to major cloud providers like AWS & GCP?** | ✅︎ Yes | ❌ No | | **Avoids cloud lock-in?** | ✅︎ Yes | ❌ No | | **Supports Kubernetes and custom infra?** | ✅︎ Yes | ❌ No | | **Infrastructure is Type-Safe?** | ✅︎ Yes | ❌ No | | **Charges for hosting?** | No | Yes | ## Encore is the simplest way of accessing the full power and flexibility of the major cloud providers With Encore you don't need to be a cloud expert to make full use of the services offered by major cloud providers like AWS and GCP. You simply use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to **declare the infrastructure semantics directly in your application code**, and Encore then [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in your cloud, and provides a local development environment that matches your cloud environment. You get the same, easy to use, "push to deploy" workflow that many developers appreciate with Heroku, while still being able to build large-scale distributed systems and event-driven applications deployed to AWS and GCP. ## Encore's local development workflow lets application developers focus When using a PaaS service like Heroku to deploy your application, you're not at all solving for an efficient local development workflow. This means, with Heroku, developers need to manually set up and maintain their local environment and observability tools, in order to facilitate local development and testing. This can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose. This work is a continuous effort as the system evolves, and becomes more and more complex as the service and infrastructure footprint grows. All this effort takes time away from product development and slows down onboarding time for new developers. **When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools. This greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient. ## Encore provides an end-to-end purpose-built workflow for cloud backend development Encore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps. ### Encore's built-in developer tools - Cross-service type-safety with IDE auto-complete - Distributed Tracing - Test Tracing - Automatic API Documentation - Automatic Architecture Diagrams - API Client Generation - Secrets Management - Service/API mocking ### Encore's built-in DevOps tools - Automatic Infrastructure provisioning on AWS/GCP - Infrastructure Tracking & Approvals workflow - Cloud Configuration 2-way sync between Encore and AWS/GCP - Automatic least privilege IAM - Preview Environments per Pull Request - Cost Analytics Dashboard - Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework ================================================ FILE: docs/platform/other/vs-supabase.md ================================================ --- seotitle: Encore compared to Supabase / Firebase seodesc: See how Encore's Backend Development Platform lets you unlock the simplicity of tools like Supabase and Firebase, while maintaining the control and flexibility of building a real backend application. title: Encore compared to Supabase + Firebase subtitle: Get the simplicity you want — with flexibility and scalability lang: platform --- Supabase and Firebase are two popular _Backend as a Service_ providers, that provide developers with an easy way to get a database up and running for their applications. They also bundle some built-in services for common use cases like authentication. This can be a great way of getting off the ground quickly. But as many developers have come to learn, you risk finding yourself boxed into a corner if you're not in full control of your own backend when new use cases arise. **Encore is not a _Backend as a Service_, it's a platform _for_ backend development**. It gives you many of the same benefits that Supabase and Firebase offer, like not needing to manually provision your [databases](/docs/ts/primitives/databases) (or any other infrastructure for that matter). The key difference is, **Encore provisions your infrastructure in your own cloud account on AWS/GCP.** This also lets you easily use any cloud service offered by the major cloud providers, and you don't risk being limited by the platform and having to start over from scratch. Let's take a look at how Encore compares to BaaS platforms like Supabase and Firebase: | | Encore | Supabase | Firebase | | --------------------------------------------------- | ---------------------------- | -------------------- | -------------------- | | **Approach?** | Backend Development Platform | Backend as a Service | Backend as a Service | | **Native PostgreSQL support?** | ✅︎ Yes | ✅︎ Yes | ❌ No | | **Support pgvector for AI use cases?** | ✅︎ Yes | ✅︎ Yes | ❌ No | | **Supports major cloud providers like AWS/GCP?** | ✅︎ Yes | ❌ No | ✅︎ Yes (GCP only) | | **Supports Microservices?** | ✅︎ Yes | ❌ No | ❌ No | | **Supports Event-Driven systems?** | ✅︎ Yes | ❌ No | ❌ No | | **Supports Kubernetes and custom infra?** | ✅︎ Yes | ❌ No | ❌ No | | **Infrastructure is Type-Safe?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in local dev environment?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in Preview Environments per Pull Request?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in Distributed Tracing?** | ✅︎ Yes | ❌ No | ❌ No | | **Charges for hosting?** | No | Yes | Yes | ## Encore is the simplest way of accessing the full power and flexibility of the major cloud providers With Encore you don't need to be a cloud expert to make full use of the services offered by major cloud providers like AWS and GCP. You simply use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to **declare the infrastructure semantics directly in your application code**, and Encore then [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in your cloud, and provides a local development environment that matches your cloud environment. ### Example: Using PostgreSQL with Encore Here's an example of how to use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to define a PostgreSQL database (Go is used in the example, TypeScript support is also available): To create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable. Databases must be created from within an [Encore service](/docs/go/primitives/services). For example: ``` -- todo/db.go -- package todo // Create the todo database and assign it to the "tododb" variable var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Then, query the database using db.QueryRow, db.Exec, etc. -- todo/migrations/1_create_table.up.sql -- CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false -- etc... ); ``` As seen above, the `sqldb.DatabaseConfig` specifies the directory containing the database migration files, which is how you define the database schema. With this code in place Encore will automatically create the database when starting `encore run` (locally) or on the next deployment (in the cloud). Encore automatically injects the appropriate configuration to authenticate and connect to the database, so once the application starts up the database is ready to be used. [Learn more about using databases with Encore](/docs/go/primitives/databases) ## Encore makes it simple to build type-safe event-driven systems Unlike BaaS platforms like Supabase and Firebase, Encore has extensive support for building microservices backends and event-driven systems. For example, Encore lets you [define APIs](/docs/ts/primitives/apis) using regular functions and enables cross-service type-safety with IDE auto-complete when making API calls between services. With [Encore.ts](/docs/ts) and [Encore.go](/docs/go), you can build event-driven systems by defining Pub/Sub topics and subscriptions as type-safe objects in your application. This gives you type-safety for Pub/Sub with compilation errors for any type-errors. ## Encore's local development workflow lets application developers focus When using a BaaS service like Supabase to handle your infrastructure, you're not at all solving for local development. This means, with Supabase, developers need to manually set up and maintain their local environment in order to facilitate local development and testing. This can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose. This work is a continuous effort as the system evolves, and becomes more and more complex as the service and infrastructure footprint grows. All this effort takes time away from product development and slows down onboarding time for new developers. **When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools. This greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient. ## Encore provides an end-to-end purpose-built workflow for cloud backend development Encore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps. ### Encore's built-in developer tools - Cross-service type-safety with IDE auto-complete - Distributed Tracing - Test Tracing - Automatic API Documentation - Automatic Architecture Diagrams - API Client Generation - Secrets Management - Service/API mocking ### Encore's built-in DevOps tools - Automatic Infrastructure provisioning on AWS/GCP - Infrastructure Tracking & Approvals workflow - Cloud Configuration 2-way sync between Encore and AWS/GCP - Automatic least privilege IAM - Preview Environments per Pull Request - Cost Analytics Dashboard - Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework ================================================ FILE: docs/platform/other/vs-terraform.md ================================================ --- seotitle: Encore compared to Terraform and Pulumi seodesc: See how Encore's infrastructure from code approach lets you avoid the common pitfalls of infrastructure as code solutions like Terraform and Pulumi. title: Encore compared to Terraform & Pulumi subtitle: How Encore is different from Infrastructure as Code tools lang: platform --- There are many tools designed to overcome the challenges of cloud infrastructure complexity. Terraform and Pulumi are _Infrastructure as Code_ tools that help you provision infrastructure by writing infrastructure configuration files. **Encore uses a fundamentally different approach that lets you declare infrastructure as type-safe objects in your application**. Let's take a look at how Encore compares to IaC tools like Terraform and Pulumi: | | Encore | Terraform | Pulumi | | ------------------------------------------------------------------- | ------------------------ | ---------------------- | ---------------------- | | **Approach?** | Infrastructure from Code | Infrastructure as Code | Infrastructure as Code | | **Supports major cloud providers like AWS/GCP?** | ✅︎ Yes | ✅︎ Yes | ✅︎ Yes | | **Supports Kubernetes and custom infra configuration?** | ✅︎ Yes | ✅︎ Yes | ✅︎ Yes | | **Avoid learning a DSL?** | ✅︎ Yes | ❌ No | ✅︎ Yes | | **Infrastructure is Type-Safe?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in local dev environment?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in Preview Environments per Pull Request?** | ✅︎ Yes | ❌ No | ❌ No | | **Built-in Distributed Tracing?** | ✅︎ Yes | ❌ No | ❌ No | | **Avoid manually writing infra config files?** | ✅︎ Yes | ❌ No | ❌ No | | **Avoid manual maintenance of separate codebase for infra config?** | ✅︎ Yes | ❌ No | ❌ No | | **Avoid manual effort to keep environments in sync?** | ✅︎ Yes | ❌ No | ❌ No | ## Encore removes manual effort and maintenance required with IaC A common challenge with Infrastructure as Code (IaC) is that it takes a lot of manual effort to write. What's worse is, you need to repeat the effort for each new environment, or take a short cut by duplicating your prod environment and creating costly over-provisioned test or staging environments. When you use IaC you also end up with a separate codebase to maintain and keep in sync with your application's actual requirements. The complexity and scope of this problem grows as you introduce more infrastructure and more environments. That means as your system grows, with IaC, you will need to spend more and more time to maintain your infrastructure configuration. **Encore's _infrastructure from code_ approach means there are no configuration files to maintain**, nor any refactoring to do when changing the underlying infrastructure. Your application code is the source of truth for the semantic infrastructure requirements. In practise, you use [Encore.ts](/docs/ts) and [Encore.go](/docs/go) to declare infrastructure as type-safe objects in your application code, and **Encore [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in all environments.** Including in your own cloud, with support for major cloud providers like AWS/GCP. (This also means your application is cloud-agnostic by default and **you avoid cloud lock-in**.) ## Encore's local development workflow lets application developers focus When using IaC to provision cloud environments, you're not at all solving for local development. This means, with Terraform, developers need to manually set up and maintain their local environment to mimic what's running in the cloud, in order to facilitate local development and testing. This can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose and NSQ. This work is a continuous effort as the system evolves, and becomes more and more complex as the footprint grows. All this effort takes time away from product development and slows down onboarding time for new developers. **When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools. This greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient. ## Encore ensures your cloud environments are secure by automating IAM When using IaC tools like Terraform, you must always assign explicit permissions using IAM identities and IAM policies. This can be very time consuming when developing a large-scale distributed systems, and when you get it wrong it can lead to glaring security holes or unexpected system behavior. When using Encore, IAM identities and policies are automatically defined according to best practices for least privilege security. This is possible because Encore parses your source code and builds a graph of the logical architecture, it then uses this to define the infrastructure needs. This means Encore knows exactly which services needs access to which infrastructure for your application to function as expected. ## Encore provides an end-to-end purpose-built workflow for cloud backend development Encore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps. ### Encore's built-in developer tools - Cross-service type-safety with IDE auto-complete - Distributed Tracing - Test Tracing - Automatic API Documentation - Automatic Architecture Diagrams - API Client Generation - Secrets Management - Service/API mocking ### Encore's built-in DevOps tools - Automatic Infrastructure provisioning on AWS/GCP - Infrastructure Tracking & Approvals workflow - Cloud Configuration 2-way sync between Encore and AWS/GCP - Automatic least privilege IAM - Preview Environments per Pull Request - Cost Analytics Dashboard - Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework ================================================ FILE: docs/platform/overview.md ================================================ --- seotitle: Encore Cloud Docs seodesc: How Encore Cloud Platform helps you reduce DevOps work by 93% by automating infra in your cloud on AWS/GCP. title: Encore Cloud subtitle: The easiest way to develop and deploy your application to AWS/GCP toc: false lang: platform --- [Encore Cloud](https://encore.cloud) is a development platform for running production applications in your own AWS or GCP environment. It automates infrastructure provisioning, deployments, and operations, while providing built-in observability including distributed tracing, metrics, and logs. Teams using Encore Cloud report **2-3x** faster development speed and **93%** less time spent on DevOps. See more details in [customer stories](https://encore.cloud/customers). Learn more about how it works in the [introduction](/docs/platform/introduction).

Get Started

Sign up and deploy a test application in minutes.

Introduction

Learn about the problems Encore Cloud solves and the philosophy behind it.

Book a call

Speak to one of our experts to figure out if Encore Cloud will work for your project.

Join Discord

Ask questions and get help on Discord.
================================================ FILE: docs/ts/ai-integration.md ================================================ --- seotitle: Using Encore with AI Tools seodesc: Learn how to set up Encore with AI-powered development tools like Cursor and Claude Code to supercharge your backend development workflow. title: AI Tools Integration subtitle: Supercharge your development with AI-powered coding assistants lang: ts --- Encore is built for AI-assisted development. Encore-specific rules and [MCP](/docs/ts/ai-integration#mcp-server) integration let AI understand your architecture and generate type-safe code that follows your patterns. Run `encore run` to start your app; Encore provisions local infrastructure automatically. For production, [self-host](/docs/ts/self-host/build) or use [Encore Cloud](https://encore.cloud) to provision infrastructure in your own AWS or GCP account. ## What AI Enables Encore's declarative APIs and infrastructure primitives give AI a clear model to work with. AI can add databases, pub/sub topics, and other resources with built-in guardrails, and use MCP to introspect your app—services, APIs, databases, and traces—so it can suggest accurate, pattern-consistent code. ## Enabling AI for Your Project There are two ways to set up AI support: - [Method 1: Using the CLI](#method-1-using-the-cli) (recommended) - [Method 2: Using Encore Skills](#method-2-using-encore-skills) ### Method 1: Using the CLI **New projects:** When you run `encore app create`, you'll be prompted to select an AI tool. Encore generates the appropriate configuration files for your chosen tool. **Existing projects:** Run `encore llm-rules init` to add AI support: ```bash encore llm-rules init ``` This prompts you to select a tool and generates the appropriate configuration file (`.cursorrules`, `CLAUDE.md`, etc.). Both commands also set up MCP server configuration for tools that support it (Cursor, Claude Code). If you want to set up MCP manually, see [MCP Server](#mcp-server) below. Supported tools: Cursor, Claude Code, VS Code, AGENTS.md, and Zed. ### Method 2: Using Encore Skills Use the [Encore skills package](https://github.com/encoredev/skills) which works with Cursor, Claude Code, GitHub Copilot, and 10+ other AI agents: ```bash npx add-skill encoredev/skills ``` You can also install specific skills or target specific agents: ```bash # List available skills npx add-skill encoredev/skills --list # Install to specific agents npx add-skill encoredev/skills -a cursor -a claude-code ``` The skills package includes a migration skill that can automatically migrate your existing backend to Encore.ts. See the [Migrate using AI agent](/docs/ts/migration/ai-migration) guide to learn more. ## MCP Server Encore's [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server gives AI agents deep introspection into your application: querying databases, calling APIs, inspecting services, and analyzing traces. ### Start the Server From your Encore app directory: ```bash encore mcp start ``` This displays connection information. Keep it running while using your AI tools. ### Connect Cursor **Quick setup:** Use this button (update `your-app-id` to your actual app ID): Add encore-mcp MCP server to Cursor **Manual setup:** Create `.cursor/mcp.json`: ```json { "mcpServers": { "encore-mcp": { "command": "encore", "args": ["mcp", "run", "--app=your-app-id"] } } } ``` Find your app ID in the `encore.app` file or in the [Encore dashboard](https://app.encore.dev). ### Connect Claude Code From your Encore app directory: ```bash claude mcp add --transport stdio encore-mcp -- encore mcp run --app=your-app-id ``` Verify with `claude mcp list`. You should see `encore-mcp` in the list. ## What AI Can Do With Encore skills and MCP connected, AI can: - **Define infrastructure in code** - AI declares databases, pub/sub, cron jobs, buckets, and other [primitives](/docs/ts/primitives) - **Generate type-safe APIs** - code that follows your patterns and passes validation - **Understand architecture** - inspect services and how they connect via MCP - **Query databases** - introspect schema and data to generate accurate queries - **Debug with tracing** - view request traces, timing, and span details to pinpoint issues - **Test instantly** - run `encore run` to test with real infrastructure, not mocks ### In Practice #### Smarter Debugging with Tracing AI can access Encore's distributed tracing via MCP to debug issues intelligently. Instead of guessing, AI can view actual request traces, analyze timing across services, and inspect span details to pinpoint exactly where things went wrong. This creates a powerful feedback loop: generate code, test it, analyze the traces, and iterate. #### Database Introspection AI can query your actual database schema and data via MCP. This means AI understands your real data model and can generate accurate queries, suggest schema changes, and debug data issues by inspecting actual records. #### Instant Validation with Real Infrastructure When you run `encore run`, Encore provisions real local infrastructure (databases, pub/sub, etc.). AI can generate code and immediately test it against real services, catching issues early and ensuring the code works before you deploy. Example prompts: - "Add an endpoint that publishes to a pub/sub topic, call it and verify in traces" - "Query the users database and show accounts created in the last week" - "Create a new service with CRUD endpoints connected to PostgreSQL" ## Learn More - [MCP Server Documentation](/docs/ts/cli/mcp) - Complete MCP reference - [Encore Skills Repository](https://github.com/encoredev/skills) - Available skills and installation - [Quick Start Guide](/docs/ts/quick-start) - Build your first Encore app ================================================ FILE: docs/ts/cli/cli-reference.md ================================================ --- seotitle: Encore CLI Reference seodesc: The Encore CLI lets you run your local development environment, create apps, and much more. See all CLI commands in this reference guide. title: CLI Reference subtitle: The Encore CLI lets you run your local environment and much more. lang: ts --- ## Running #### Run Runs your application. ```shell $ encore run [--debug] [--watch=true] [--port=4000] [--listen=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-w, --watch` | Watch for changes and live-reload | `true` | | `--listen` | Address to listen on (e.g. `0.0.0.0:4000`) | | | `-p, --port` | Port to listen on | `4000` | | `--json` | Display logs in JSON format | `false` | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `--color` | Whether to display colorized output | auto-detected | | `--redact` | Redact sensitive data in traces when running locally | `false` | | `-l, --level` | Minimum log level to display (`trace\|debug\|info\|warn\|error`) | | | `--debug` | Compile for debugging (`enabled\|break`) | | | `--browser` | Open local dev dashboard in browser on startup (`auto\|never\|always`) | `auto` | #### Test Tests your application. Runs the test script defined in your `package.json`. ```shell $ encore test [flags] ``` Additional flags recognized by `encore test`: | Flag | Description | | --- | --- | | `--codegen-debug` | Dump generated code (for debugging Encore's code generation) | | `--prepare` | Prepare for running tests without running them | | `--trace` | Write trace information about the parse and compilation process to a file | | `--no-color` | Disable colorized output | #### Check Checks your application for compile-time errors using Encore's compiler. ```shell $ encore check [flags] ``` **Flags** | Flag | Description | | --- | --- | | `--codegen-debug` | Dump generated code (for debugging Encore's code generation) | | `--tests` | Parse tests as well | #### Exec Runs executable scripts against the local Encore app. Takes a command that it will execute with the local Encore app environment setup. ```shell $ encore exec -- ``` **Flags** | Flag | Description | | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | ##### Example Run a database seed script ```shell $ encore exec -- npx tsx ./seed.ts ``` ## App Commands to create and link Encore apps #### Clone Clone an Encore app to your computer ```shell $ encore app clone [app-id] [directory] ``` #### Create Create a new Encore app ```shell $ encore app create [name] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--example` | URL to example code to use | | | `-l, --lang` | Programming language to use for the app | | | `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool | | | `--platform` | Whether to create the app with the Encore Platform | `true` | #### Init Create a new Encore app from an existing repository ```shell $ encore app init [name] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-l, --lang` | Programming language to use for the app | #### Link Link an Encore app with the server ```shell $ encore app link [app-id] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-f, --force` | Force link even if the app is already linked | ## Auth Commands to authenticate with Encore #### Login Log in to Encore ```shell $ encore auth login [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-k, --auth-key` | Auth Key to use for login | #### Logout Logs out the currently logged in user ```shell $ encore auth logout ``` #### Signup Create a new Encore account ```shell $ encore auth signup ``` #### Whoami Show the current logged in user ```shell $ encore auth whoami ``` ## Daemon Encore CLI daemon commands #### Restart If you experience unexpected behavior, try restarting the daemon using: ```shell $ encore daemon ``` #### Env Outputs Encore environment information ```shell $ encore daemon env ``` ## Database Management Database management commands #### Connect to database via shell Connects to the database via psql shell Defaults to connecting to your local environment. Specify --env to connect to another environment. Use `--test` to connect to databases used for integration testing. Use `--shadow` to connect to the shadow database, used for database drift detection when using tools like Prisma. `--test` and `--shadow` imply `--env=local`. ```shell $ encore db shell [DATABASE_NAME] [--env=] [flags] ``` `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with. **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Connection URI Outputs a database connection string. Defaults to connecting to your local environment. Specify --env to connect to another environment. ```shell $ encore db conn-uri [] [--env=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Proxy Sets up local proxy that forwards any incoming connection to the databases in the specified environment. ```shell $ encore db proxy [--env=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `-e, --env` | Environment name to connect to | `local` | | `-p, --port` | Port to listen on (defaults to a random port) | `0` | | `-t, --test` | Connect to the integration test database (implies --env=local) | `false` | | `--shadow` | Connect to the shadow database (implies --env=local) | `false` | | `--write` | Connect with write privileges | `false` | | `--admin` | Connect with admin privileges | `false` | | `--superuser` | Connect as a superuser | `false` | #### Reset Resets the databases for the given services. Use --all to reset all databases. ```shell $ encore db reset [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-n, --namespace` | Namespace to use (defaults to active namespace) | | | `--all` | Reset all services in the application | `false` | | `-t, --test` | Reset databases in the test cluster instead | `false` | | `--shadow` | Reset databases in the shadow cluster instead | `false` | ## Code Generation Code generation commands #### Generate client Generates an API client for your app. For more information about the generated clients, see [this page](/docs/ts/cli/client-generation). By default, `encore gen client` generates the client based on the version of your application currently running in your local environment. You can change this using the `--env` flag and specifying the environment name. Use `--lang=` to specify the language. Supported language codes are: - `go`: A Go client using the net/http package - `typescript`: A TypeScript client using the in-browser Fetch API - `javascript`: A JavaScript client using the in-browser Fetch API - `openapi`: An OpenAPI spec ```shell $ encore gen client [] [--env=] [--lang=] [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-l, --lang` | Language to generate code for | | | `-o, --output` | Filename to write the generated client code to | | | `-e, --env` | Environment to fetch the API for | `local` | | `-s, --services` | Names of the services to include in the output | | | `-x, --excluded-services` | Names of the services to exclude in the output | | | `-t, --tags` | Names of endpoint tags to include in the output | | | `--excluded-tags` | Names of endpoint tags to exclude in the output | | | `--openapi-exclude-private-endpoints` | Exclude private endpoints from the OpenAPI spec | `false` | | `--ts:shared-types` | Import types from ~backend instead of re-generating them | `false` | | `--target` | An optional target for the client (`leap`) | | ## Logs Streams logs from your application ```shell $ encore logs [--env=prod] [--json] [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-e, --env` | Environment name to stream logs from (defaults to the primary environment) | | `--json` | Whether to print logs in raw JSON format | | `-q, --quiet` | Whether to print initial message when the command is waiting for logs | ## Kubernetes Kubernetes management commands #### Configure Updates your kubectl config to point to the Kubernetes cluster(s) for the specified environment ```shell $ encore k8s configure --env=ENV_NAME ``` ## Secrets Management Secret management commands #### Set Set a secret value for a specific environment: ```shell $ encore secret set --env ``` Set a secret value for an environment type: ```shell $ encore secret set --type ``` Where `` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`. **Examples** Entering a secret directly in terminal: ```shell $ encore secret set --type dev MySecret Enter secret value: ... Successfully created secret value for MySecret. ``` Piping a secret from a file: ```shell $ encore secret set --type dev,local MySecret < my-secret.txt Successfully created secret value for MySecret. ``` Note that this strips trailing newlines from the secret value. #### List Lists secrets, optionally for a specific key ```shell $ encore secret list [keys...] ``` #### Delete Deletes a secret value ```shell $ encore secret delete ``` ## Namespaces Manage infrastructure namespaces for isolating local infrastructure. See [Infrastructure Namespaces](/docs/ts/cli/infra-namespaces) for more details. #### List List infrastructure namespaces ```shell $ encore namespace list [--output=columns|json] ``` #### Create Create a new infrastructure namespace ```shell $ encore namespace create NAME ``` #### Delete Delete an infrastructure namespace ```shell $ encore namespace delete NAME ``` #### Switch Switch to a different infrastructure namespace. Subsequent commands will use the given namespace by default. Use `-` as the namespace name to switch back to the previously active namespace. ```shell $ encore namespace switch [--create] NAME ``` **Flags** | Flag | Description | | --- | --- | | `-c, --create` | Create the namespace before switching | ## Config Gets or sets configuration values for customizing the behavior of the Encore CLI. Configuration options can be set both for individual Encore applications, as well as globally for the local user. ```shell $ encore config [] [flags] ``` When running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag. **Flags** | Flag | Description | | --- | --- | | `--all` | View all settings | | `--app` | Set the value for the current app | | `--global` | Set the value at the global level | ## Telemetry Reports the current telemetry status ```shell $ encore telemetry ``` #### Enable Enables telemetry reporting ```shell $ encore telemetry enable ``` #### Disable Disables telemetry reporting ```shell $ encore telemetry disable ``` ## MCP MCP (Model Context Protocol) commands for integrating with AI assistants. See [MCP](/docs/ts/cli/mcp) for more details. #### Start Starts an SSE-based MCP session and prints the SSE URL ```shell $ encore mcp start [--app=] ``` #### Run Runs a stdio-based MCP session ```shell $ encore mcp run [--app=] ``` ## Random Utilities for generating cryptographically secure random data. #### UUID Generates a random UUID (defaults to version 4) ```shell $ encore rand uuid [-1|-4|-6|-7] ``` **Flags** | Flag | Description | | --- | --- | | `-1, --v1` | Generate a version 1 UUID | | `-4, --v4` | Generate a version 4 UUID (default) | | `-6, --v6` | Generate a version 6 UUID | | `-7, --v7` | Generate a version 7 UUID | #### Bytes Generates random bytes and outputs them in the specified format ```shell $ encore rand bytes BYTES [-f ] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-f, --format` | Output format (`hex\|base32\|base32hex\|base32crockford\|base64\|base64url\|raw`) | `hex` | | `--no-padding` | Omit padding characters from base32/base64 output | `false` | #### Words Generates random 4-5 letter words for memorable passphrases ```shell $ encore rand words [--sep=SEPARATOR] NUM ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `-s, --sep` | Separator between words | ` ` (space) | ## Deploy Deploy an Encore app to a cloud environment. Requires either `--commit` or `--branch` to be specified. ```shell $ encore deploy --env= (--commit= | --branch=) [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--app` | App slug to deploy to (defaults to current app) | | | `-e, --env` | Environment to deploy to (required) | | | `--commit` | Commit SHA to deploy | | | `--branch` | Branch to deploy | | | `-f, --format` | Output format (`text\|json`) | `text` | ## Version Reports the current version of the encore application ```shell $ encore version ``` #### Update Checks for an update of encore and, if one is available, runs the appropriate command to update it. ```shell $ encore version update ``` ## Build Generates an image for your app, which can be used to [self-host](/docs/ts/self-host/build) your app. #### Docker Builds a portable Docker image of your Encore application. ```shell $ encore build docker IMAGE_TAG [flags] ``` **Flags** | Flag | Description | Default | | --- | --- | --- | | `--base` | Base image to build from | `scratch` | | `-p, --push` | Push image to remote repository | `false` | | `--cgo` | Enable cgo | `false` | | `--config` | Infra configuration file path | | | `--skip-config` | Do not read or generate an infra configuration file | `false` | | `--services` | Services to include in the image | | | `--gateways` | Gateways to include in the image | | | `--os` | Target operating system | `linux` | | `--arch` | Target architecture (`amd64\|arm64`) | `amd64` | ## LLM Rules Generate LLM rules in an existing app #### Init Initialize the LLM rules files ```shell $ encore llm-rules init [flags] ``` **Flags** | Flag | Description | | --- | --- | | `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool (`cursor\|claudecode\|vscode\|agentsmd\|zed`) | ================================================ FILE: docs/ts/cli/client-generation.md ================================================ --- seotitle: Automatic API Client Generation seodesc: Learn how you can use automatic API client generation to get clients for your backend. See how to integrate with your frontend using a type-safe generated client. title: Client Library Generation subtitle: Stop writing the same types everywhere lang: ts --- Encore makes it simple to write scalable distributed backends by allowing you to make function calls that Encore translates into RPC calls. Encore also generates API clients with interfaces that look like the original Go functions, with the same parameters and response signature as the server. The generated clients are single files that use only the standard functionality of the target language, with full type safety. This allow anyone to look at the generated client and understand exactly how it works. The structure of the generated code varies by language, to ensure it's idiomatic and easy to use, but always includes all publicly accessible endpoints, data structures, and documentation strings. Encore currently supports generating the following clients: - **Go** - Using `net/http` for the underlying HTTP transport. - **TypeScript** - Using the browser `fetch` API for the underlying HTTP client. - **JavaScript** - Using the browser `fetch` API for the underlying HTTP client. - **OpenAPI** - Using the OpenAPI Specification's language-agnostic interface to HTTP APIs. (Experimental) If there's a language you think should be added, please submit a pull request or create a feature request on [GitHub](https://github.com/encoredev/encore/issues/new), or [reach out on Discord](/discord). If you ship the generated client to end customers, keep in mind that old clients will continue to be used after you make changes. To prevent issues with the generated clients, avoid making breaking changes in APIs that your clients access.
## Generating a Client To generate a client, use the `encore gen client` command. It generates a type-safe client using the most recent API metadata running in a particular environment for the given Encore application. For example: ```shell # Generate a TypeScript client for calling the hello-a8bc application based on the primary environment encore gen client hello-a8bc --output=./client.ts # Generate a Go client for the hello-a8bc application based on the locally running code encore gen client hello-a8bc --output=./client.go --env=local # Generate an OpenAPI client for the hello-a8bc application based on the primary environment encore gen client hello-a8bc --lang=openapi --output=./openapi.json ``` ### Environment Selection By default, `encore gen client` generates the client based on the version of your application currently running in your local environment. You can change this using the `--env` flag and specifying the environment name. The generated client can be used with any environment, not just the one it was generated for. However, the APIs, data structures and marshalling logic will be based on whatever is present and running in that environment at the point in time the client is generated. ### Service filtering By default `encore gen client` outputs code for all services with at least one publicly accessible (or authenticated) API. You can narrow down this set of services by specifying the `--services` (or `-s`) flag. It takes a comma-separated list of service names. For example, to generate a typescript client for the `email` and `users` services, run: ```shell encore gen client --services=email,users -o client.ts ``` ### Output Mode By default the client's code will be output to stdout, allowing you to pipe it into your clipboard, or another tool. However, using `--output` you can specify a file location to write the client to. If output is specified, you do not need to specify the language as Encore will detect the language based on the file extension. ### Example Script You could combine this into a `package.json` file for your Typescript frontend, to allow you to run `npm run gen` in that project to update the client to match the code running in your staging environment. ```json { "scripts": { // ... "gen": "encore gen client hello-a8bc --output=./client.ts --env=staging" // ... } } ``` ## Using the Client The generated client has all the data structures required as parameters or returned as response values as needed by any of the public or authenticated API's of your Encore application. Each service is exposed as object on the client, with each public or authenticated API exposed as a function on those objects. For instance, if you had a service called `email` with a function `Send`, on the generated client you would call this using; `client.email.Send(...)`. ### Creating an instance When constructing a client, you need to pass a `BaseURL` as the first parameter; this is the URL at which the API can be accessed. The client provides two helpers: - `Local` - This is a constant provided, which will always point at your locally running instance environment. - `Environment("name")` - This is a function which allows you to specify an environment by name However, BaseURL is a string, so if the two helpers do not provide enough flexibility you can pass any valid URL to be used as the BaseURL. ### Authentication If your application has any API's which require [authentication](/docs/ts/develop/auth), then additional options will generated into the client, which can be used when constructing the client. Just like with API's schemas, the data type required by your application's `auth handler` will be part of the client library, allowing you to set it in two ways: If your credentials won't change during the lifetime of the client, simply passing the authentication data to the client through the `WithAuth` (Go) or `auth` (TypeScript) options. However, if the authentication credentials can change, you can also pass a function which will be called before each request and can return a new instance of the authentication data structure or return the existing instance. ### HTTP Client Override If required, you can override the underlying HTTP implementation with your own implementation. This is useful if you want to perform logging of the requests being made, or route the traffic over a secured tunnel such as a VPN. In Go this can be configured using the `WithHTTPClient` option. You are required to provide an implementation of the `HTTPDoer` interface, which the [http.Client](https://pkg.go.dev/net/http#Client) implements. For TypeScript clients, this can be configured using the `fetcher` option and must conform to the same prototype as the browsers inbuilt [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch). ### Structured Errors Errors created or wrapped using Encore's [`errs package`](/docs/ts/primitives/errors) will be returned to the client and deserialized as an `APIError`, allowing the client to perform adaptive error handling based on the type of error returned. You can perform a type check on errors caused by calling an API to see if it is an `APIError`, and once cast as an `APIError` you can access the `Code`, `Message` and `Details` fields. For TypeScript Encore generates a `isAPIError` type guard which can be used. The `Code` field is an enum with all the possible values generated in the library, alone with description of when we would expect them to be returned by your API. See the [errors documentation](/docs/ts/primitives/errors#error-codes) for an online reference of this list. ================================================ FILE: docs/ts/cli/config-reference.md ================================================ --- seotitle: Encore CLI Configuration Options seodesc: Configuration options to customize the behavior of the Encore CLI. title: Configuration Reference subtitle: Configuration options to customize the behavior of the Encore CLI. lang: ts --- The Encore CLI has a number of configuration options to customize its behavior. Configuration options can be set both for individual Encore applications, as well as globally for the local user. Configuration options can be set using `encore config `, and options can similarly be read using `encore config `. When running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag. ## Configuration files The configuration is stored in one ore more TOML files on the filesystem. The configuration is read from the following files, in order: ### Global configuration * `$XDG_CONFIG_HOME/encore/config` * `$HOME/.config/encore/config` * `$HOME/.encoreconfig` ### Application-specific configuration * `$APP_ROOT/.encore/config` Where `$APP_ROOT` is the directory containing the `encore.app` file. The files are read and merged, in the order defined above, with latter files taking precedence over earlier files. ## Configuration options #### run.browser Type: string
Default: auto
Must be one of: always, never, or auto Whether to open the Local Development Dashboard in the browser on `encore run`. If set to "auto", the browser will be opened if the dashboard is not already open. ================================================ FILE: docs/ts/cli/infra-namespaces.md ================================================ --- seotitle: Infrastructure Namespaces seodesc: Learn how Encore's infrastructure namespaces makes it easy to task switch. Stash your infrastructure state and switch to a different task with a single command. title: Infrastructure Namespaces subtitle: Task switching made easy lang: ts --- Encore's CLI allows you to create and switch between multiple, independent *infrastructure namespaces*. Infrastructure namespaces are isolated from each other, and each namespace contains its own independent data. This makes it trivial to switch tasks, confident your old state and data will be waiting for you when you return. If you've ever worked on a new feature that involves making changes to the database schema, only to context switch to reviewing a Pull Request and had to reset your database, you know the feeling. With Encore's infrastructure namespaces, this is a problem of the past. Run `encore namespace switch --create pr:123` (or `encore ns switch -c pr:123` for short) to create and switch to a new namespace. The next `encore run` will run in the new namespace, with a completely fresh database. When you're done, run `encore namespace switch -` to switch back to your previous namespace. ## Usage Below are the commands for working with namespaces. Note that you can use `encore ns` as a short form for `encore namespace`. ```shell # List your namespaces (* indicates the current namespace) $ encore namespace list # Create a new namespace $ encore namespace create my-ns # Switch to a namespace $ encore namespace switch my-ns # Switch to a namespace, creating it if it doesn't exist $ encore namespace switch --create my-ns # Switch to the previous namespace $ encore namespace switch - # Delete a namespace (and all associated data) $ encore namespace delete my-ns ``` Most other Encore commands that interact or use infrastructure take an optional `--namespace` (`-n` for short) that overrides the current namespace. If left unspecified, the current namespace is used. For example: ```shell # Run the app using the "my-ns" namespace $ encore run --namespace my-ns # Open a database shell to the "my-ns" namespace $ encore db shell DATABASE_NAME --namespace my-ns # Reset all databases within the "my-ns" namespace $ encore db reset --all --namespace my-ns ``` ================================================ FILE: docs/ts/cli/mcp.md ================================================ --- seotitle: Encore MCP Server seodesc: Encore's Model Context Protocol (MCP) server provides deep introspection of your application to AI development tools. title: MCP Server subtitle: The Model Context Protocol (MCP) exposes tools that provide application context to LLMs. lang: ts --- Encore provides an MCP server that implements the [Model Context Protocol](https://modelcontextprotocol.io/introduction), an open standard that enables large language models (LLMs) to access contextual information about your application. Think of MCP as a standardized interface—like a "USB-C port for AI applications"—that connects your Encore app's data and functionality to any LLM that supports the protocol. You can connect to Encore's MCP server from any MCP host (such as Claude Desktop, IDEs, or other AI tools) using either Server-Sent Events (SSE) or stdio transport. To set up this connection, simply run: ```bash cd my-encore-app encore mcp start MCP Service is running! MCP SSE URL: http://localhost:9900/sse?app=your-app-id MCP stdio Command: encore mcp run --app=your-app-id ``` Copy the appropriate URL or command to your MCP host's configuration, and you're ready to give your AI assistants rich context about your application. ## Example: Integrating with Cursor [Cursor](https://cursor.com) is one of the most popular AI powered IDE's, and it's simple to use Encore's MCP server together with Cursor. In order to add the Encore MCP server to Cursor, the fastest way is via the button below (make sure to update `your-app-id` in the configuration to your actual Encore app ID). Add encore-mcp MCP server to Cursor If you prefer to configure it manually, create the file `.cursor/mcp.json` with the following settings: ```json { "mcpServers": { "encore-mcp": { "command": "encore", "args": ["mcp", "run", "--app=your-app-id"] } } } ``` Learn more in [Cursor's MCP docs](https://docs.cursor.com/context/model-context-protocol) Now when using Cursor's Agent mode, you can ask it to do advanced actions, such as: "Add an endpoint that publishes to a pub/sub topic, call it and verify that the publish is in the traces" ## Command Reference #### Start Starts an SSE-based MCP server and displays connection information. ```shell $ encore mcp start [--app=] ``` #### Run Establishes an stdio-based MCP session. This command is typically used by MCP hosts to communicate with the server through standard input/output streams. ```shell $ encore mcp run [--app=] ``` ## Exposed Tools Encore's MCP server exposes the following tools that provide AI models with detailed context about your application. These tools enable LLMs to understand your application's structure, retrieve relevant information, and take actions within your system. #### Database Tools - **get_databases**: Retrieve metadata about all SQL databases defined in the application, including their schema, tables, and relationships. - **query_database**: Execute SQL queries against one or more databases in the application. #### API Tools - **call_endpoint**: Make HTTP requests to any API endpoint in the application. - **get_services**: Retrieve comprehensive information about all services and their endpoints in the application. - **get_middleware**: Retrieve detailed information about all middleware components in the application. - **get_auth_handlers**: Retrieve information about all authentication handlers in the application. #### Trace Tools - **get_traces**: Retrieve a list of request traces from the application, including their timing, status, and associated metadata. - **get_trace_spans**: Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata. #### Source Code Tools - **get_metadata**: Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components. - **get_src_files**: Retrieve the contents of one or more source files from the application. #### PubSub Tools - **get_pubsub**: Retrieve detailed information about all PubSub topics and their subscriptions in the application. #### Storage Tools - **get_storage_buckets**: Retrieve comprehensive information about all storage buckets in the application. - **get_objects**: List and retrieve metadata about objects stored in one or more storage buckets. #### Cache Tools - **get_cache_keyspaces**: Retrieve comprehensive information about all cache keyspaces in the application. #### Metrics Tools - **get_metrics**: Retrieve comprehensive information about all metrics defined in the application. #### Cron Tools - **get_cronjobs**: Retrieve detailed information about all scheduled cron jobs in the application. #### Secret Tools - **get_secrets**: Retrieve metadata about all secrets used in the application. #### Documentation Tools - **search_docs**: Search the Encore documentation using Algolia's search engine. - **get_docs**: Retrieve the full content of specific documentation pages. ================================================ FILE: docs/ts/cli/telemetry.md ================================================ --- seotitle: Encore Telemetry seodesc: Encore collects telemetry data about app usage title: Telemetry lang: ts --- Telemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently. Encore only collects telemetry data in the local development tools and the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations. ## Why We Collect Data We collect telemetry data for several important reasons: 1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development. 2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience. 3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur. 4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly. ## How Data is Collected Encore collects data in a way that prioritizes user privacy and security. Here's how we do it: 1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts. 2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports. 3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols. 4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance. ### Example of Data Being Sent Here is an example of the type of data that is sent: ```json { "event": "app.create", "anonymousId": "a-uuid-unique-for-the-installation", "properties": { "error": false, "lang": "go", "template": "graphql" } } ``` ## Data We Don't Collect At Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect: 1. **Environment Variables**: We do not collect any environment variables set in your development or production environments. 2. **File Paths**: The specific paths of your files and directories are not collected. 3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects. 4. **Logs**: No log files from your application or development environment are collected. 5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information. Our goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure. ## Disabling Telemetry While telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways: 1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal. ```sh encore telemetry disable ``` 2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable. ```sh export DISABLE_ENCORE_TELEMETRY=1 ``` 3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled. 4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command: ```sh encore telemetry enable ``` ## Debugging Telemetry For users who want more visibility into what telemetry data is being sent, you can enable debug mode: 1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable. ```sh export ENCORE_TELEMETRY_DEBUG=1 ``` 2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent. ## Conclusion Telemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team. Thank you for helping us make Encore better! ================================================ FILE: docs/ts/community/contribute.md ================================================ --- seotitle: How to contribute to Encore Open Source Project seodesc: Learn how to contribute to the Encore Open Source project by submitting pull requests, reporting bugs, or contributing documentation or example projects. title: Ways to contribute subtitle: Guidelines for contributing to Encore lang: ts --- We’re so excited that you are interested in contributing to Encore! All contributions are welcome, and there are several valuable ways to contribute. ### Open Source Project If you want to contribute to the Encore Open Source project, you can submit a pull request on [GitHub](https://github.com/encoredev/encore/pulls). ### Report issues If you have run into an issue or think you’ve found a bug, please report it via the [issue tracker](https://github.com/encoredev/encore/issues). ### Add or update docs If there’s something you think would be helpful to add to the docs or if there’s something that seems out of date, we appreciate your input. You can view the docs and contribute fixes or improvements directly in [GitHub](https://github.com/encoredev/encore/tree/main/docs). You can also email your feedback to us at [hello@encore.dev](mailto:hello@encore.dev). ### Blog posts If you’ve built something cool using Encore, we’d really like you to talk about it! We love it when developers share their projects on blogs and on Twitter. Use the hashtag **#builtwithencore** and we’ll have an easier time finding your work. – We might also showcase it on the [Encore Twitter account](https://twitter.com/encoredotdev)! ### Meetups & Workshops Organizing a meetup or workshop is a great way to connect with other developers using Encore. It can also be a great first step in trying out Encore for development in your company or other professional organization. If you want help with organizing or planning an event, please don’t hesitate to reach out to us via email at [hello@encore.dev](mailto:hello@encore.dev). ================================================ FILE: docs/ts/community/get-involved.md ================================================ --- seotitle: Encore's Open Source Developer Community seodesc: Learn how to engage in the Open Source Developer Community supporting Encore. title: Community subtitle: Join the most pioneering developer community! lang: ts --- Developers building with Encore are forward-thinkers, who are working on exciting and innovative applications. We rely on this group's feedback, and contributions to the Open Source project, to improve Encore for developers everywhere. Getting involved is a fantastic way of finding support and inspiration among peers. Everyone is welcome in the Encore community, and we hope you to get involved too! ## Get involved There are many ways to get involved. Here's where you can start straight away.

Contribute on GitHub

Use GitHub to report bugs, feedback on proposals, or contribute your ideas.

Join Discord

Connect with fellow Encore developers, ask questions, or just hang out!

Follow on Twitter

Follow Encore on Twitter to keep up with the latest. Share what you've built to help spread the word about the project. ### Contribute to the project Want to make a contribution to Encore? Great, start by reading about the different [ways to contribute](/docs/ts/community/contribute). ### Feedback on the Roadmap [The Encore Roadmap](https://encore.dev/roadmap) is public. It's open to your comments, feature requests, and you can vote on existing entries. ## Community Governance We recommend everyone read the [Community Principles](/docs/ts/community/principles). If you need assistance, have concerns, or have questions for the Community team, please email us at [support@encore.dev](mailto:support@encore.dev). ================================================ FILE: docs/ts/community/open-source.md ================================================ --- seotitle: Encore is Open Source seodesc: We believe Open Source is key to a sustainable and prosperous technology community. Encore builds on Open Source software, and is itself Open Source. title: Open Source subtitle: Encore is Open Source Software lang: ts --- We believe Open Source is key to a long-term sustainable and prosperous technology community. Encore builds on Open Source software, and is largely Open Source itself. ## License Encore's Backend Framework, parser, and compiler are Open Source under Mozilla Public License 2.0. > The MPL is a simple copyleft license. The MPL's "file-level" copyleft is designed to encourage contributors to share modifications they make to your code, while still allowing them to combine your code with code under other licenses (open or proprietary) with minimal restrictions. You can learn more about MPL 2.0 on [the official website](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). ## Contribute Contributions to improve Encore are very welcome. Contribute to Encore on [GitHub](https://github.com/encoredev/encore). ================================================ FILE: docs/ts/community/principles.md ================================================ --- seotitle: Encore Community Principles seodesc: Everyone is welcome in the Encore community, and we want everyone to feel at home and free to contribute. title: Community principles subtitle: Everyone belongs in the Encore community lang: ts --- Everyone is welcome in the Encore community, and it is of utmost importance to us that everyone is able to feel at home and contribute. Therefore we as maintainers, and you as a contributor, must pledge to make participation in our community a harassment-free experience for everyone, regardless of: age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity. ### Code of Conduct To this end, the Encore community is guided by the [Contributor Covenant 2.0 Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/) to ensure everyone is welcome and able to participate. ================================================ FILE: docs/ts/community/submit-template.md ================================================ --- seotitle: Submit a Template to Encore's Templates repo seodesc: Learn how to contribute to Encore's Templates repository and get features in the Encore Templates marketplace. title: Submit a Template subtitle: Your contributions help other developers build lang: ts --- [Templates](/templates) help and inspire developers to build applications using Encore. You are welcome to contribute your own templates! Two types of templates that are especially useful: - **Starters:** Runnable Encore applications for others to use as is, or take inspiration from. - **Bits:** Re-usable code samples to solve common development patterns or integrate Encore applications with third-party APIs and services. ## Submit your contribution Contribute a template by submitting a Pull Request to the [Open Source Examples Repo](https://github.com/encoredev/examples): `https://github.com/encoredev/examples` ### Submitting Starters Follow these steps to submit a **Starter**: 1. Fork the repo. 2. Create a new folder in the root directory of the repo, this is where you will place your template. — Use a short folder name as your template will be installable via the CLI, like so: `encore app create APP-NAME --example=` 3. Include a `README.md` with instructions for how to use the template. We recommend following [this format](https://github.com/encoredev/examples/blob/8c7e33243f6bfb1b2654839e996e9a924dcd309e/uptime/README.md). Once your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website. ### Submitting Bits Follow these steps to submit your **Bits**: 1. Fork the repo. 2. Create a new folder inside the `bits` folder in the repo and place your template inside it. Use a short folder name as your template will soon be installable via the CLI. 3. Include a `README.md` with instructions for how to use the template. Once your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website. ## Contribute from your own repo If you don't want to contribute code to the examples repo, but still want to be featured on the [Templates page](/templates), please contact us at [hello@encore.dev](mailto:hello@encore.dev). ## Dynamic Encore AppID In most cases, you should avoid hardcoding an `AppID` in your template's source code. Instead, use the notation `{{ENCORE_APP_ID}}`. When a developer creates an app using the template, `{{ENCORE_APP_ID}}` will be dymically replaced with their new and unique `AppID`, meaning they will not need to make any manual code adjustments. ================================================ FILE: docs/ts/concepts/application-model.md ================================================ --- seotitle: Encore Application Model seodesc: How Encore understands your application using static analysis title: Encore Application Model subtitle: How Encore understands your application lang: ts --- Encore works by using static analysis to understand your application. This is a fancy term for parsing and analyzing the code you write and creating a graph of how your application works. This graph closely represents your own mental model of the system: boxes and arrows that represent systems and services that communicate with other systems, pass data and connect to infrastructure. We call it the Encore Application Model. Because the Open Source framework, parser, and compiler, are all designed together, Encore can ensure 100% accuracy when creating the application model. Any deviation is caught as a compilation error. Using this model, Encore can provide tools to solve problems that normally would be up to the developer to do manually. From creating architecture diagrams and API documentation to provisioning cloud infrastructure. We're continuously expanding on Encore's capabilities and are building a new generation of developer tools that are enabled by Encore's understanding of your application. The framework, parser, and compiler that enable this are all [Open Source](https://github.com/encoredev/encore). ## Standardization brings clarity Developers make dozens of decisions when creating a backend application. Deciding how to structure the codebase, defining API schemas, picking underlying infrastructure, etc. The decisions often come down to personal preferences, not technical rationale. This creates a huge problem in the form of fragmentation! When every stack looks different, all tools have to be general purpose. When you adopt Encore, many of these stylistic decisions are already made for you. The Encore framework ensures your application follows modern best practices. And when you run your application, Encore's Open Source parser and compiler check that you're sticking to the standard. This means you're free to focus your energy on what matters: writing your application's business logic. ================================================ FILE: docs/ts/concepts/benefits.md ================================================ --- seotitle: Benefits of using Encore.ts seodesc: Get to know the benefits of using Encore's Backend Framework for TypeScript to build cloud-native backend applications. title: Benefits of using Encore.ts lang: ts --- ## Integrated developer experience for enhanced productivity - **Local development with instant infrastructure**: Encore automatically sets up necessary infrastructure as you develop. - **Rapid feedback**: Catch issues early with type-safe infrastructure, avoiding slow deployment cycles. - **No manual configuration required**: No need for Infrastructure-as-Code. Your code is the single source of truth. - **Unified codebase**: One codebase for all environments; local, preview, and cloud. - **Cloud-agnostic by default**: Encore.ts provides an abstraction layer on top of the cloud provider's APIs, so you avoid becoming locked in to a single cloud. - **Evolve infrastructure without code changes**: As requirements evolve, you can change the provisioned infrastructure without needing application code changes. Either using the Open Source [self-hosting tools](/docs/ts/self-host/build) or with the optional [Cloud Platform](https://encore.dev/use-cases/devops-automation), which fully-automates infrastructure management in your own AWS/GCP account. - **AI-assisted development**: Encore is built for AI coding assistants. With [Encore-specific rules and MCP integration](/docs/ts/ai-integration), AI understands your architecture and can generate type-safe, pattern-consistent code and introspect your app—services, APIs, databases, and traces. ## High-performance Rust runtime To enable Encore's functionality in TypeScript, we’ve created a high-performance distributed systems runtime in Rust. It integrates with the standard Node.js runtime for executing JavaScript code, ensuring **100% compatibility with the Node.js ecosystem**. It provides a number of benefits over standard Node.js: - **Handles requests validation, provides API type-safety, has built-in observability, and integrates with databases, Pub/Sub, and more** - **9x increased throughput and 85% reduced latency** compared to standard Node.js/Express.js [See benchmarks](https://encore.dev/blog/event-loops) - **Zero NPM dependencies** for improved security and faster builds ### How it works Encore.ts is designed to let the Node.js event loop — which is single-threaded — focus on executing your business logic, while everything else happens in Encore’s multi-threaded Rust runtime. Here's a high-level overview of how this works: **1. Node.js starts up and initializes the Encore Rust runtime. The Rust runtime then:** - Begins accepting incoming requests - Parses and validates these requests against the API schema **2. For each request, the Encore Runtime:** - Passes the request to your application code - Waits for your code to process the request - Sends the response back to the client **3. When your application needs to interact with infrastructure (like databases or PubSub):** - It delegates these tasks to the Rust runtime - The Rust runtime handles these operations more efficiently than Node.js would, providing faster execution and lower latency ## Enhanced type-safety for distributed systems Encore leverages static code analysis to parse the API schema and TypeScript types you define. This enables a number of features: - Built-in [local development dashboard](/docs/ts/observability/dev-dash) - API Explorer, automatic documentation, and local tracing - Runtime type-safety, automatically validating incoming requests against the API schema - Eliminating runtime errors due to missing required fields ## No DevOps experience required Encore provides open source tools to help you integrate with your cloud infrastructure, enabling you to self-host your application anywhere that supports Docker containers. Learn more in the [self-host documentation](/docs/ts/self-host/build). You can also use [Encore Cloud](https://encore.dev/use-cases/devops-automation), which fully automates provisioning and managing infrastructure in your own cloud on AWS and GCP. This approach dramatically reduces the level of DevOps expertise required to use scalable, production-ready, cloud services like Kubernetes and Pub/Sub. And because your application code is the source of truth for infrastructure requirements, it ensures the infrastructure in all environments is always in sync with the application's current requirements. ## Simplicity without giving up flexibility Encore.ts provides integrations for common infrastructure primitives, but also allows for flexibility. For example, you can always use any cloud infrastructure, even if it's not built into the Encore.ts framework. You can use any database, message broker, or other service that your application needs, just set up the infrastructure and then reference it in your code as you would do traditionally. If you use [Encore Cloud](https://encore.dev/use-cases/devops-automation), it will [automate infrastructure](/docs/platform/infrastructure/infra) using your own cloud account, so you always have full access to your services from the cloud provider's console. ================================================ FILE: docs/ts/concepts/hello-world.md ================================================ --- seotitle: Hello World in Encore.ts seodesc: Get to know Encore.ts with this simple Hello World example. title: Hello World subtitle: Get to know the basics toc: false lang: ts --- Encore lets you easily define type-safe, idiomatic TypeScript API endpoints. It's done in a fully declarative way, enabling Encore to automatically parse and validate the incoming request and ensure it matches the schema, with zero boilerplate. To define an API, use the `api` function from the `encore.dev/api` module to wrap a regular TypeScript async function that receives the request data as input and returns response data. This tells Encore that the function is an API endpoint. Encore will then automatically generate the necessary boilerplate at compile-time. This means you need less than 10 lines of code to define a production-ready deployable service and API endpoint: ```TypeScript import { api } from "encore.dev/api"; export const get = api( { expose: true, method: "GET", path: "/hello/:name" }, async ({ name }: { name: string }): Promise => { const msg = `Hello ${name}!`; return { message: msg }; } ); interface Response { message: string; } ``` ## Getting started video Get to know the basics of Encore.ts in this getting started video. ## Using databases, Pub/Sub, and other primitives Encore's Backend Framework makes it simple to add more primitives, such as additional microservices, databases, Pub/Sub, etc. See how to use each primitive: - [Services](/docs/ts/primitives/services) - [APIs](/docs/ts/primitives/defining-apis) - [Databases](/docs/ts/primitives/databases) - [Cron Jobs](/docs/ts/primitives/cron-jobs) - [Pub/Sub & Queues](/docs/ts/primitives/pubsub) - [Secrets](/docs/ts/primitives/secrets) ================================================ FILE: docs/ts/develop/auth.md ================================================ --- seotitle: Adding authentication to APIs to auth users seodesc: Learn how to add authentication to your APIs and make sure you know who's calling your backend APIs. title: Authenticating users subtitle: Knowing what's what and who's who infobox: { title: "Authentication", import: "encore.dev/auth", } lang: ts --- Almost every application needs to know who's calling it, whether the user represents a person in a consumer-facing app or an organization in a B2B app. Encore supports both use cases in a simple yet powerful way. As described in the docs for [defining APIs](/docs/ts/primitives/defining-apis), each API endpoint can be marked as requiring authentication, using the option `auth: true` when defining the endpoint. ## Authentication Handlers When an API is defined with `auth: true`, you must define an authentication handler in your application. The authentication handler is responsible for inspecting incoming requests to determine what user is authenticated (if any), and computing any other associated authentication information. The authentication handler is defined similarly to API endpoints, using the `authHandler` function imported from `encore.dev/auth`. Like API endpoints, the authentication handler defines what request information it's interested in, in the form of HTTP headers, query strings, or cookies. A simple authentication handler that inspects the `Authorization` header might look like this: ```ts import { Header, Gateway } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; // AuthParams specifies the incoming request information // the auth handler is interested in. In this case it only // cares about requests that contain the `Authorization` header. interface AuthParams { authorization: Header<"Authorization">; } // The AuthData specifies the information about the authenticated user // that the auth handler makes available. interface AuthData { userID: string; } // The auth handler itself. export const auth = authHandler( async (params) => { // TODO: Look up information about the user based on the authorization header. return {userID: "my-user-id"}; } ) // Define the API Gateway that will execute the auth handler: export const gateway = new Gateway({ authHandler: auth, }) ``` With this in place, Encore will provision an API Gateway that will process incoming requests to your application, and whenever a request contains an `Authorization` header it will first call the authentication handler to resolve information about the user. ### Rejecting authentication If the auth handler returns an `AuthData` object, Encore will consider the request authenticated. To instead _reject_ the request, throw an exception. To signal that the credentials are not valid, throw an `APIError` with code `Unauthenticated`. For example: ```ts import { APIError } from "encore.dev/api"; export const auth = authHandler( async (params) => { throw APIError.unauthenticated("bad credentials"); } ) ``` ## Understanding the Authentication Process Encore's authentication process proceeds in two steps: 1. Determine if the request is authenticated 2. Call the endpoint, if permissible #### Step 1: Determining if the request is authenticated Whenever an incoming request contains any of the authentication parameters (defined by the auth handler), Encore's API Gateway calls the auth handler to resolve the authentication data. This happens regardless of the endpoint the request is for. Importantly, it happens even when calling an endpoint that does not require authentication. There are three possible outcomes from calling the auth handler: 1. If the auth handler succeeds, by returning `AuthData`, the request is considered authenticated. 2. If the auth handler throws an `APIError` with code `Unauthenticated`, the request is considered unauthenticated, exactly as if there was no authentication parameters in the request to begin with. 3. If the auth handler throws any other exception, the API Gateway aborts the request and returns the error to the caller. Finally, if the request does not contain authentication data, the request is considered unauthenticated. #### Step 2: Calling the endpoint, if permissible Once the API Gateway has determined whether the request is authenticated, it checks whether the API Endpoint being called requires authentication data. If it does require authentication, and the request is not authenticated, the API Gateway aborts the request and returns an "unauthenticated" error to the caller. In all other situations, the API Gateway proceeds by calling the target endpoint. If the request was successfully authenticated, the authentication data is passed along to the endpoint, regardless of whether the endpoint requires authentication or not. ## Using auth data If a request has been successfully authenticated, the API Gateway forwards the authentication data to the target endpoint. The endpoint can query the available auth data from the `getAuthData` function, available from the `~encore/auth` module. This module is dynamically generated by Encore to enable type-safe resolution of the auth data. ### Propagating auth data Encore automatically propagates the auth data when you make API calls to other Encore API endpoints using the generated `~encore/clients` package. If an endpoint calls another endpoint during its processing, and the target endpoint requires authentication while the original request does not have any authentication data, the API call will fail with error code `Unauthenticated`. This behavior preserves the guarantee that endpoints that require authentication always have valid authentication data present. ## Overriding auth information You can override the auth data for a specific endpoint when calling it via `~encore/clients` by passing `CallOpts`. Example: ```ts import { svc } from "~encore/clients"; const resp = await svc.endpoint(params, { authData: { userID: "...", userEmail: "..." } }); ``` Overriding auth data is useful for testing endpoints that require authentication without having to authenticate the request manually. ## Mocking auth You can mock `getAuthData` with vitest. Example: ```ts import { describe, expect, test, vi } from "vitest"; import * as auth from "~encore/auth"; import { get } from "./hello"; describe("get", () => { test("should combine string with parameter value", async () => { const spy = vi.spyOn(auth, 'getAuthData'); spy.mockImplementation(() => ({ userEmail: "user@email.com" })) const resp = await get({ name: "world" }); expect(resp.message).toBe("Hello world! You are authenticated with user@email.com"); }); }); ``` ================================================ FILE: docs/ts/develop/debug.md ================================================ --- seotitle: How to debug your TS backend application seodesc: Learn how to debug your TS backend application using Encore. title: Debug with your IDE lang: ts --- Encore makes it easy to debug your application using your favorite IDE. ## Enable debugging mode Next, run your Encore application with `encore run --debug=break`. This will cause Encore to run your app with the `--inspect-brk` flag, which will pause your application until a debugger is attached. Encore will print the URL to the terminal, which you will use to attach your debugger: ```shell $ encore run --debug=break Your API is running at: http://127.0.0.1:4000 Development Dashboard URL: http://localhost:9400/ai-chat-ts-qhwi Process ID: 38965 Debugger listening on ws://127.0.0.1:9229/473dd95f-e71e-4bf2-9eda-6132dd0d6ae3 ``` (Your process id and url will differ). If you don't want the application to pause on startup, you can use `encore run --debug` instead. This will start the application and wait for a debugger to attach, but it won't pause the application until the debugger is attached. ## Attach your debugger When your Encore application is running, it’s time to attach the debugger. The instructions differ depending on how you would like to debug. If instructions for your editor aren’t listed below, consult your editor for information on how to attach a debugger to a running process. ### Visual Studio Code To debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Node.js -> Attach`. The generated config should look something like this: ```json { "version": "0.2.0", "configurations": [ { "name": "Attach", "port": 9229, "request": "attach", "skipFiles": [ "/**" ], "type": "node" } ] } ``` Next, open the **Run and Debug** menu in the toolbar on the left, select Attach (the configuration you just created), and then press the green arrow. That’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ## WebStorm To debug with WebStorm (or any other JetBrains IDE), you must first configure a Node.js Attach configuration. Press `Run -> Edit Configurations`, click the `+` button, and choose `Attach to Node.js/Chrome`. Give it a name and hit `OK`. Now select the configuration you just created and press the green bug. That's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect. ================================================ FILE: docs/ts/develop/env-vars.md ================================================ --- seotitle: Environment Variables Reference seodesc: Learn how to configure Encore's development environment using environment variables. title: Environment Variables subtitle: Configure your development environment lang: ts --- Encore works out of the box without configuration, but provides several environment variables for advanced use cases such as debugging, testing, or adapting Encore to specific workflow requirements. ## Daemon & Development Dashboard These variables control how the Encore daemon operates and where it exposes its services. ### ENCORE_DAEMON_LOG_PATH Controls the location of the Encore daemon log file. **Default:** `/encore/daemon.log` **Example:** ```bash export ENCORE_DAEMON_LOG_PATH=/var/log/encore/daemon.log ``` ### ENCORE_DEVDASH_LISTEN_ADDR Overrides the listen address for the local development dashboard. **Default:** Automatically assigned by the daemon **Format:** Network address (e.g., `localhost:9400`) **Example:** ```bash export ENCORE_DEVDASH_LISTEN_ADDR=localhost:8080 encore run ``` ### ENCORE_MCPSSE_LISTEN_ADDR Overrides the listen address for the MCP SSE (Model Context Protocol Server-Sent Events) endpoint. **Default:** Automatically assigned by the daemon **Format:** Network address **Example:** ```bash export ENCORE_MCPSSE_LISTEN_ADDR=localhost:9401 ``` ### ENCORE_OBJECTSTORAGE_LISTEN_ADDR Overrides the listen address for the object storage service endpoint. **Default:** Automatically assigned by the daemon **Format:** Network address **Example:** ```bash export ENCORE_OBJECTSTORAGE_LISTEN_ADDR=localhost:9402 ``` ## Logging Configuration These variables control the logging behavior for TypeScript applications. ### ENCORE_RUNTIME_LOG Sets the log level for Encore's internal runtime operations (written in Rust). **Default:** `debug` (automatically set to `error` during `encore run`) **Valid values:** `trace`, `debug`, `info`, `warn`, `error` **Example:** ```bash # See detailed runtime logs export ENCORE_RUNTIME_LOG=trace encore run ``` If `RUST_LOG` is set, it takes precedence over `ENCORE_RUNTIME_LOG`. The runtime log controls logging for internal Encore modules. ### ENCORE_LOG Sets the log level for your application code. **Default:** `Trace` (log everything) **Valid values:** `Off`, `Error`, `Warn`, `Info`, `Debug`, `Trace` **Example:** ```typescript import log from "encore.dev/log"; log.info("This message respects ENCORE_LOG level"); ``` ```bash # Only show errors and warnings export ENCORE_LOG=Warn encore run ``` ### ENCORE_NOLOG Disables all logging when set to any non-empty value. **Default:** Not set **Example:** ```bash # Disable all logs export ENCORE_NOLOG=1 encore run ``` ## Advanced Development These variables are primarily useful for advanced development scenarios, such as contributing to Encore itself or using custom builds. ### ENCORE_RUNTIMES_PATH Specifies the path to the Encore runtimes directory. **Default:** Auto-detected relative to the Encore installation (`/runtimes`) **Example:** ```bash export ENCORE_RUNTIMES_PATH=/path/to/custom/runtimes ``` ### ENCORE_RUNTIME_LIB Specifies the path to the native Node.js runtime library used by TypeScript applications. **Default:** `/js/encore-runtime.node` **Example:** ```bash export ENCORE_RUNTIME_LIB=/path/to/custom/encore-runtime.node ``` ### ENCORE_TSPARSER_PATH Specifies the path to the TypeScript parser binary. **Default:** Auto-detected from `encore` binary location or system `PATH` **Example:** ```bash export ENCORE_TSPARSER_PATH=/path/to/custom/tsparser-encore ``` For most users, these paths are automatically detected and don't need to be set. They are primarily useful when contributing to Encore or testing custom builds. ## Debugging ### ENCORE_API_INCLUDE_INTERNAL_MESSAGE Controls whether internal error messages are included in API error responses. **Default:** automatically set to `1` during local development with `encore run` **Format:** Any non-empty, non-"0" value is considered `true` **Example:** ```bash # Manually enable for debugging export ENCORE_API_INCLUDE_INTERNAL_MESSAGE=1 ``` ### RUST_LOG Controls Rust-level logging for the Encore runtime. This provides more granular control than `ENCORE_RUNTIME_LOG`. **Default:** Not set **Format:** Standard Rust `env_logger` format (see [env_logger documentation](https://docs.rs/env_logger)) **Example:** ```bash # Enable info logs for all modules in the runtime export RUST_LOG=info encore run ``` `RUST_LOG` takes precedence over `ENCORE_RUNTIME_LOG`. Use `RUST_LOG` for fine-grained control over specific runtime modules. ================================================ FILE: docs/ts/develop/integrations/better-auth.md ================================================ --- seotitle: Using Better Auth with Encore.ts for Authentication seodesc: Learn how to add production-ready authentication to your Encore.ts application using Better Auth, with automatic database provisioning and secrets management. title: Better Auth lang: ts --- [Better Auth](https://www.better-auth.com) is a TypeScript authentication library that supports email/password, OAuth, two-factor, magic links, and sessions. This guide shows how to use it with Encore's [database provisioning](https://encore.dev/docs/ts/primitives/databases) and [secrets management](https://encore.dev/docs/ts/primitives/secrets). To get started quickly, create a new app from the example: ```shell $ encore app create --example=ts/betterauth ``` Or follow the steps below to add Better Auth to an existing Encore app. If you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first. ## Install ```shell $ npm install better-auth pg ``` ## Set up the database Better Auth needs a database for users and sessions. Encore [provisions and manages databases](https://encore.dev/docs/ts/primitives/databases) for you automatically, just define it in code: ```ts -- db.ts -- import { SQLDatabase } from "encore.dev/storage/sqldb"; export const db = new SQLDatabase("auth", { migrations: "./migrations", }); ``` Locally, Encore starts a PostgreSQL instance automatically when you run `encore run`. You'll need [Docker](https://docker.com/get-started/) running for the local database. ## Configure Better Auth Create the Better Auth instance using Encore's database and secrets: ```ts -- auth.ts -- import { betterAuth } from "better-auth"; import { Pool } from "pg"; import { secret } from "encore.dev/config"; import { db } from "./db"; const authSecret = secret("AuthSecret"); const pool = new Pool({ connectionString: db.connectionString, }); export const auth = betterAuth({ secret: authSecret(), basePath: "/auth", database: pool, trustedOrigins: ["http://localhost:4000"], emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: secret("GithubClientId")(), clientSecret: secret("GithubClientSecret")(), }, }, }); ``` Set the secrets using the Encore CLI: ```shell $ encore secret set --type dev,local,pr,production AuthSecret $ encore secret set --type dev,local,pr,production GithubClientId $ encore secret set --type dev,local,pr,production GithubClientSecret ``` **Tip:** Generate a strong auth secret with `openssl rand -base64 32` and paste it when prompted for `AuthSecret`. Locally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed. ## Connect to Encore's auth handler Wire Better Auth into Encore's [authentication system](https://encore.dev/docs/ts/develop/auth) so you can use `auth: true` on any API endpoint: ```ts -- handler.ts -- import { APIError, Gateway } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import { Header } from "encore.dev/api"; import { auth } from "./auth"; interface AuthParams { authorization: Header<"Authorization">; } interface AuthData { userID: string; } const handler = authHandler( async (params) => { const session = await auth.api.getSession({ headers: new Headers({ authorization: params.authorization, }), }); if (!session) { throw APIError.unauthenticated("invalid session"); } return { userID: session.user.id }; } ); export const gateway = new Gateway({ authHandler: handler }); ``` ## Expose Better Auth routes Better Auth needs HTTP routes for sign-in, sign-up, and OAuth callbacks. Expose these using a [raw endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints): ```ts -- routes.ts -- import { api } from "encore.dev/api"; import { auth } from "./auth"; // Better Auth expects a Web Request, but Encore raw endpoints receive // a Node.js IncomingMessage. We convert between the two formats. export const authRoutes = api.raw( { expose: true, path: "/auth/*path", method: "*" }, async (req, res) => { // Read the request body const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } const body = Buffer.concat(chunks); // Build a Web Request from the Node.js request const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value) headers.append(key, Array.isArray(value) ? value.join(", ") : value); } const url = `http://${req.headers.host}${req.url}`; const webReq = new Request(url, { method: req.method, headers, body: ["GET", "HEAD"].includes(req.method || "") ? undefined : body, }); // Pass to Better Auth and forward the response const response = await auth.handler(webReq); response.headers.forEach((value, key) => { res.setHeader(key, value); }); res.writeHead(response.status); res.end(await response.text()); } ); ``` ## Use in your endpoints Any endpoint with `auth: true` will now require a valid Better Auth session: ```ts import { api } from "encore.dev/api"; import { getAuthData } from "~encore/auth"; export const getProfile = api( { auth: true, expose: true, method: "GET", path: "/profile" }, async (): Promise<{ userID: string }> => { const data = getAuthData()!; return { userID: data.userID }; } ); ``` ## Deploy When you deploy, Encore automatically provisions and manages the infrastructure your app needs: - **Database** provisioned as Cloud SQL on GCP or RDS on AWS. Migrations run automatically on deploy. - **Secrets** encrypted per environment (preview, staging, production), never shared between them. - **Networking** including TLS, load balancing, and DNS. ### Self-hosting Build a Docker image and deploy anywhere: ```shell $ encore build docker my-app:latest ``` See the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details. ### Encore Cloud Deploy your application to a free staging environment in Encore's development cloud: ```shell $ git push encore main ``` You can also connect your own AWS or GCP account and Encore will automatically provision databases, run migrations, and manage secrets in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details. ## Related resources - [Encore authentication docs](https://encore.dev/docs/ts/develop/auth) - [Better Auth documentation](https://www.better-auth.com/docs) - [Encore databases](https://encore.dev/docs/ts/primitives/databases) - [Encore secrets](https://encore.dev/docs/ts/primitives/secrets) ================================================ FILE: docs/ts/develop/integrations/polar.md ================================================ --- seotitle: Using Polar with Encore.ts for Payments & Subscriptions seodesc: Learn how to add payments, subscriptions, and license keys to your Encore.ts application using Polar as your Merchant of Record. title: Polar lang: ts --- [Polar](https://polar.sh) handles payments, subscriptions, and license keys as your Merchant of Record. This guide shows how to integrate Polar with an Encore application. To get started quickly, create a new app from the example: ```shell $ encore app create --example=ts/polar ``` Or follow the steps below to add Polar to an existing Encore app. If you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first. ## Install the SDK ```shell $ npm install @polar-sh/sdk ``` ## Polar setup Before writing code, you'll need to set up a few things in the [Polar dashboard](https://sandbox.polar.sh) (use the sandbox for development): 1. **Create an access token.** Go to Settings > [Developers > Personal Access Tokens](https://sandbox.polar.sh/settings/developers/pat) and create a new token. 2. **Create a product.** Go to [Products](https://sandbox.polar.sh/products) and create at least one product. Copy its **product ID**, you'll need it to create checkout sessions. 3. **Set up a webhook** (optional for local dev). Go to Settings > [Webhooks](https://sandbox.polar.sh/settings/webhooks) and point it to your API URL followed by `/webhooks/polar`. For local development, use a tunnel like [ngrok](https://ngrok.com) to expose your local server. See the [Polar documentation](https://docs.polar.sh) for more details on products, pricing, and webhooks. ## Set your secrets Store your Polar credentials as [Encore secrets](https://encore.dev/docs/ts/primitives/secrets): ```shell $ encore secret set --type dev,local,pr,production PolarAccessToken ``` Locally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed. ## Initialize the client Create a file to configure the Polar SDK. Use Encore's [`secret()`](https://encore.dev/docs/ts/primitives/secrets) function to access the token. Use `sandbox` for development and `production` when deployed: ```ts -- polar.ts -- import { Polar } from "@polar-sh/sdk"; import { secret } from "encore.dev/config"; const polarAccessToken = secret("PolarAccessToken"); const server = process.env.ENCORE_ENVIRONMENT === "production" ? "production" : "sandbox"; export const polar = new Polar({ accessToken: polarAccessToken(), server, }); ``` ## Create a checkout Use the Polar SDK to create checkout sessions for your products: ```ts -- checkout.ts -- import { api } from "encore.dev/api"; import { polar } from "./polar"; import { getAuthData } from "~encore/auth"; interface CreateCheckoutRequest { productId: string; } interface CreateCheckoutResponse { checkoutUrl: string; } export const createCheckout = api( { auth: true, expose: true, method: "POST", path: "/checkout" }, async (req: CreateCheckoutRequest): Promise => { const authData = getAuthData()!; const baseUrl = process.env.ENCORE_API_URL || "http://localhost:4000"; const session = await polar.checkouts.create({ products: [req.productId], customerEmail: authData.email, successUrl: `${baseUrl}/?success=true`, }); return { checkoutUrl: session.url || "" }; } ); ``` ## Handle webhooks Create a [raw endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints) to receive webhook events from Polar: ```ts -- webhooks.ts -- import { api } from "encore.dev/api"; import log from "encore.dev/log"; export const handleWebhook = api.raw( { expose: true, path: "/webhooks/polar", method: "POST" }, async (req, res) => { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } const event = JSON.parse(Buffer.concat(chunks).toString()); log.info("Received Polar webhook", { type: event.type }); switch (event.type) { case "subscription.active": // Grant access to your product break; case "subscription.canceled": // Revoke access break; case "order.paid": // Fulfill the order break; } res.writeHead(200); res.end(); } ); ``` Register your webhook URL in the [Polar dashboard](https://sandbox.polar.sh/settings/webhooks) under Settings > Webhooks. Use your Encore API URL followed by `/webhooks/polar`. Enable the events you want to handle (e.g. `subscription.active`, `subscription.canceled`, `order.paid`). ## Deploy When you deploy, Encore automatically provisions and manages the infrastructure your app needs: - **Secrets** encrypted per environment (preview, staging, production), never shared between them. - **Databases** provisioned as Cloud SQL on GCP or RDS on AWS. - **Networking** including TLS, load balancing, and DNS. ### Self-hosting Build a Docker image and deploy anywhere: ```shell $ encore build docker my-app:latest ``` See the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details. ### Encore Cloud Deploy your application to a free staging environment in Encore's development cloud: ```shell $ git push encore main ``` You can also connect your own AWS or GCP account and Encore will automatically provision the infrastructure and manage secrets in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details. ## Related resources - [Polar + Encore example app](https://github.com/encoredev/examples/tree/main/ts/polar) - [Polar documentation](https://docs.polar.sh) - [Polar sandbox dashboard](https://sandbox.polar.sh) - [Encore secrets](https://encore.dev/docs/ts/primitives/secrets) - [Raw endpoints](https://encore.dev/docs/ts/primitives/raw-endpoints) ================================================ FILE: docs/ts/develop/integrations/resend.md ================================================ --- seotitle: Using Resend with Encore.ts for Transactional Email seodesc: Learn how to send transactional emails from your Encore.ts application using Resend, with async delivery via Pub/Sub and built-in observability. title: Resend lang: ts --- [Resend](https://resend.com) provides transactional email with high deliverability and React Email templates. This guide shows how to use it with Encore's [Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub) and [secrets management](https://encore.dev/docs/ts/primitives/secrets). To get started quickly, create a new app from the example: ```shell $ encore app create --example=ts/resend ``` Or follow the steps below to add Resend to an existing Encore app. If you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first. ## Install the SDK ```shell $ npm install resend ``` If you want to use React Email templates: ```shell $ npm install resend @react-email/components ``` ## Resend setup Before writing code, you'll need to configure a few things in the [Resend dashboard](https://resend.com): 1. **Create an API key.** Go to [API Keys](https://resend.com/api-keys) and create a new key. 2. **Verify a domain** (optional for testing). Go to [Domains](https://resend.com/domains) and add your sending domain. Until you verify a domain, you can use `onboarding@resend.dev` as the `from` address for testing. See the [Resend documentation](https://resend.com/docs) for more details on domain verification and sending limits. ## Set your API key Store your Resend API key as an [Encore secret](https://encore.dev/docs/ts/primitives/secrets): ```shell $ encore secret set --type dev,local,pr,production ResendAPIKey ``` Locally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed. ## Initialize the client ```ts -- resend.ts -- import { Resend } from "resend"; import { secret } from "encore.dev/config"; const resendApiKey = secret("ResendAPIKey"); export const resend = new Resend(resendApiKey()); ``` ## Send an email Use the Resend SDK in an Encore API endpoint: ```ts -- send.ts -- import { api } from "encore.dev/api"; import { resend } from "./resend"; interface SendEmailRequest { to: string; subject: string; html: string; } interface SendEmailResponse { id: string; } export const sendEmail = api( { expose: true, method: "POST", path: "/email/send" }, async (req: SendEmailRequest): Promise => { const { data, error } = await resend.emails.send({ from: "Your App ", to: req.to, subject: req.subject, html: req.html, }); if (error) { throw new Error(`Failed to send email: ${error.message}`); } return { id: data!.id }; } ); ``` The `from` address must use a domain you've verified in [Resend](https://resend.com/domains). For testing, you can use `onboarding@resend.dev` which works with any API key. ## Async delivery with Pub/Sub For better performance, send emails asynchronously using Encore's [Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub). This keeps your API endpoints fast and handles retries automatically: ```ts -- topic.ts -- import { Topic, Subscription } from "encore.dev/pubsub"; import { resend } from "./resend"; interface EmailEvent { to: string; subject: string; html: string; } export const emailTopic = new Topic("email-send", { deliveryGuarantee: "at-least-once", }); const _ = new Subscription(emailTopic, "send-via-resend", { handler: async (event) => { const { error } = await resend.emails.send({ from: "Your App ", to: event.to, subject: event.subject, html: event.html, }); if (error) { throw new Error(error.message); } }, }); ``` Then publish from any endpoint: ```ts import { emailTopic } from "./topic"; // Inside any API endpoint await emailTopic.publish({ to: "user@example.com", subject: "Welcome!", html: "

Thanks for signing up.

", }); ``` Locally, Pub/Sub runs in-process so messages are delivered immediately, making it easy to test and debug. ## Deploy When you deploy, Encore automatically provisions and manages the infrastructure your app needs: - **Secrets** encrypted per environment (preview, staging, production), never shared between them. - **Pub/Sub** provisioned as GCP Pub/Sub or SQS/SNS on AWS, with automatic retries and dead-letter queues. - **Networking** including TLS, load balancing, and DNS. ### Self-hosting Build a Docker image and deploy anywhere: ```shell $ encore build docker my-app:latest ``` See the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details. ### Encore Cloud Deploy your application to a free staging environment in Encore's development cloud: ```shell $ git push encore main ``` You can also connect your own AWS or GCP account and Encore will automatically provision Pub/Sub topics, manage secrets, and handle networking in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details. ## Related resources - [Resend + Encore example app](https://github.com/encoredev/examples/tree/main/ts/resend) - [Resend documentation](https://resend.com/docs) - [Encore Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub) - [Encore secrets](https://encore.dev/docs/ts/primitives/secrets) ================================================ FILE: docs/ts/develop/metadata.md ================================================ --- seotitle: Metadata API – Get data about the app and environment seodesc: See how to use Encore's Metadata API to get information about the app and the environment it's running in. title: Metadata subtitle: Use the metadata API to get information about the app and the environment it's running in infobox: { title: "Metadata API", import: "encore.dev" } lang: ts --- While Encore tries to provide a cloud-agnostic environment, sometimes it's helpful to know more about the environment your application is running in. For this reason Encore provides an API for accessing metadata about the [application](#application-metadata) and the environment it's running in as part of the `encore.dev` package. ## Application Metadata Calling `appMeta()` from the `encore.dev` package returns an object that contains information about the application, including: - `appId` - the application name. - `apiBaseUrl` - the URL the application API can be publicly accessed on. - `environment` - the [environment](/docs/platform/deploy/environments) the application is currently running in. - `build` - the revision information of the build from the version control system. - `deploy` - the deployment ID and when this version of the app was deployed. ## Current Request The `currentRequest()` function, also provided by the `encore.dev` module, can be called from anywhere within your application and returns a `Request` object that contains information the current request being processed. The object contains different fields depending on whether the current request is an API call or a Pub/Sub message being processed. ```typescript -- API Call -- /** Describes an API call being processed. */ export interface APICallMeta { /** Specifies that the request is an API call. */ type: "api-call"; /** Describes the API Endpoint being called. */ api: APIDesc; /** The HTTP method used in the API call. */ method: Method; /** * The request URL path used in the API call, * excluding any query string parameters. * For example "/path/to/endpoint". */ path: string; /** * The request URL path used in the API call, * including any query string parameters. * For example "/path/to/endpoint?with=querystring". */ pathAndQuery: string; /** * The parsed path parameters for the API endpoint. * The keys are the names of the path parameters, * from the API definition. * * For example {id: 5}. */ pathParams: Record; /** * The request headers from the HTTP request. * The values are arrays if the header contains multiple values, * either separated by ";" or when the header key appears more than once. */ headers: Record; /** * The parsed request payload, as expected by the application code. * Not provided for raw endpoints or when the API endpoint expects no * request data. */ parsedPayload?: Record; } -- Pub/Sub Message -- /** Describes a Pub/Sub message being processed. */ export interface PubSubMessageMeta { /** Specifies that the request is a Pub/Sub message. */ type: "pubsub-message"; /** The service processing the message. */ service: string; /** The name of the Pub/Sub topic. */ topic: string; /** The name of the Pub/Sub subscription. */ subscription: string; /** * The unique id of the Pub/Sub message. * It is the same id returned by `topic.publish()`. * The message id stays the same across delivery attempts. */ messageId: string; /** * The delivery attempt. The first attempt starts at 1, * and increases by 1 for each retry. */ deliveryAttempt: number; /** * The parsed request payload, as expected by the application code. */ parsedPayload?: Record; } ``` This works automatically as a result of Encore's request tracking. If no request is processed by the caller, which can happen if you call it during service initialization, `currentRequest()` returns `undefined`. ## Example Use Cases ### Using Cloud Specific Services All the [clouds](/docs/platform/deploy/own-cloud) contain a large number of services, not all of which Encore natively supports. By using information about the [environment](/docs/platform/deploy/environments), you can define the implementation of these and use different services for each environment's provider. For instance if you are pushing audit logs into a data warehouse, when running on GCP you could use BigQuery, but when running on AWS you could use Redshift, when running locally you could simply write them to a file. ```ts import { appMeta } from "encore.dev"; // Emit an audit event. async function audit(userID: string, event: Record) { const cloud = appMeta().environment.cloud; switch (cloud) { case "aws": return writeIntoRedshift(userID, event); case "gcp": return writeIntoBigQuery(userID, event); case "local": return writeIntoFile(userID, event); default: throw new Error(`unknown cloud: ${cloud}`); } } ``` ### Checking Environment type When implementing a signup system, you may want to skip email verification on user signups when developing the application. Using the `appMeta` API, we can check the environment and decide whether to send an email or simply mark the user as verified upon signup. ```ts import { appMeta } from "encore.dev"; export const signup = api( { expose: true }, async (params: SignupParams): Promise => { // more code... // If this is a testing environment, skip sending the verification email. switch (appMeta().environment.type) { case "test": case "development": await markEmailVerified(userID); break; default: await sendVerificationEmail(userID); break; } // more code... }, ); ``` ================================================ FILE: docs/ts/develop/middleware.md ================================================ --- seotitle: Using Middleware in your Encore.ts application seodesc: See how you can use middleware in your Encore.ts application to handle cross-cutting generic functionality, like request logging, auth, or tracing. title: Middleware subtitle: Handling cross-cutting, generic functionality lang: ts --- Middleware is a way to write reusable code that runs before, after, or both before and after the handling of API requests, often across several (or all) API endpoints. Middleware is commonly used to implement cross-cutting concerns like [request logging](/docs/ts/observability/logging), [authentication](/docs/ts/develop/auth), [tracing](/docs/ts/observability/tracing), and so on. One of the benefits of Encore.ts is that it handles these common use cases out-of-the-box, so there's no need to write your own middleware. However, when developing applications there's often some use cases where it can be useful to write reusable functionality that applies to multiple API endpoints, and middleware is a good solution for this. Encore provides built-in support for middleware by adding functions to the [Service definitions](/docs/ts/primitives/services) configuration. Each middleware can be configured with a `target` option to specify what API endpoints it applies to. ## Middleware functions The simplest way to create a middleware is to use the `middleware` helper in `encore.dev/api`, here is an example of a middleware that will run for endpoints that require auth: ```ts import { middleware } from "encore.dev/api"; export default new Service("myService", { middlewares: [ middleware({ target: { auth: true } }, async (req, next) => { // do something before the api handler const resp = await next(req); // do something after the api handler return resp }) ] }); ``` Middleware forms a chain, allowing each middleware to introspect and process the incoming request before handing it off to the next middleware by calling the `next` function that's passed in as an argument. For the last middleware in the chain, calling `next` results in the actual API handler being called. The `req` parameter provides information about the incoming request, it has different fields depending on what kind of handler it is. You can get information about the current request via `req.requestMeta` if the endpoint is a [typed API endpoint](/docs/ts/primitives/defining-apis) or a [Streaming API endpoint](/docs/ts/primitives/streaming-apis). For [Streaming API endpoints](/docs/ts/primitives/streaming-apis) you can also access the stream via `req.stream` method. For [Raw Endpoints](/docs/ts/primitives/raw-endpoints) you can access the raw request and the raw response via `req.rawRequest` and `req.rawResponse`. The `next` function returns a `HandlerResponse` object which contains the response from the API. Extra response headers can be added using `resp.header.set(key, value)` or `resp.header.add(key, value)`, if the endpoint is a [typed API endpoint](/docs/ts/primitives/defining-apis). To pass data from middleware to an API handler, you can assign values to `req.data` within the middleware. These values can then be accessed in the handler using `currentRequest()`. Here’s an example: ```ts const mw = middleware(async (req, next) => { // Assign a value to the request req.data.myMiddlewareData = { some: "data" }; return await next(req); }); export const ep = api( { expose: true, method: "GET", path: "/endpoint" }, async () => { const callMeta = currentRequest() as APICallMeta; // Access the value in the API handler const myData = callMeta.middlewareData?.myMiddlewareData; // Use the data as needed }, ); ``` ## Middleware ordering Middleware runs in the order they are defined in the [Service definitions](/docs/ts/primitives/services) configuration, i.e: ```ts export default new Service("myService", { middlewares: [ first, second, third ], }); ``` ## Targeting APIs The `target` option specifies which endpoints within the service the middleware should run on. If not set, the middleware will run for all endpoints by default. For better performance, use the `target` option instead of filtering within the middleware function. This allows the applicable middleware to be determined per endpoint during startup, reducing runtime overhead. The following options are available for targeting endpoints: - `tags`: A list of tags evaluated with `OR`, meaning the middleware applies to an endpoint if the endpoint has at least one of these tags. - `expose`: A boolean indicating whether the middleware should be applied to endpoints that are exposed or not exposed. - `auth`: A boolean indicating whether the middleware should be applied to endpoints that require authentication or not. - `isRaw`: A boolean indicating whether the middleware should be applied to raw endpoints. - `isStream`: A boolean indicating whether the middleware should be applied to stream endpoints. ================================================ FILE: docs/ts/develop/monorepo/nx.md ================================================ --- seotitle: Using Encore with Nx in a monorepo seodesc: Learn how to set up Encore.ts in an Nx monorepo with shared packages that require building before use. title: Nx subtitle: Using Encore in an Nx monorepo lang: ts --- [Nx](https://nx.dev) is a build system for JavaScript and TypeScript monorepos. This guide shows how to set up an Encore application within an Nx monorepo that depends on shared packages requiring compilation. ## Overview When using Encore in an Nx monorepo, you may have shared packages (like utility libraries or shared types) that need to be built before the Encore app can use them. Since Encore parses your application on startup, these dependencies must be compiled first. This guide covers two scenarios: - **Local development**: Use Nx to build dependencies before running `encore run` - **Deployment**: Use Encore's `prebuild` hook to automatically build dependencies when deploying via Encore Cloud or exporting a Docker image ## Project structure A typical Nx setup with Encore looks like this: ``` my-nx-workspace/ ├── apps/ │ └── backend/ # Encore application │ ├── encore.app │ ├── package.json │ ├── project.json │ ├── tsconfig.json │ └── article/ │ └── article.ts ├── packages/ │ └── shared/ # Shared library requiring build │ ├── package.json │ ├── project.json │ ├── tsconfig.json │ ├── src/ │ │ └── index.ts │ └── dist/ # Built output │ └── index.js ├── nx.json ├── package.json └── package-lock.json ``` ## Configuration ### Root package.json Configure npm workspaces to include your apps and packages: ```json { "name": "my-nx-workspace", "private": true, "scripts": { "build": "nx run-many -t build", "dev": "nx run-many -t dev" }, "devDependencies": { "nx": "^21.0.0", "typescript": "^5.0.0" }, "workspaces": [ "apps/*", "packages/*" ] } ``` ### nx.json Configure Nx's build pipeline in the root `nx.json`: ```json { "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "build": { "dependsOn": ["^build"], "outputs": ["{projectRoot}/dist/**"], "cache": true }, "dev": { "cache": false } } } ``` The `"dependsOn": ["^build"]` configuration ensures that a project's dependencies are built before the project itself. ### Shared package Your shared package needs to compile TypeScript to JavaScript and expose the built output. **packages/shared/package.json:** ```json { "name": "@repo/shared", "version": "1.0.0", "private": true, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc" }, "devDependencies": { "typescript": "^5.0.0" } } ``` **packages/shared/project.json:** ```json { "name": "@repo/shared", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "packages/shared/src", "projectType": "library", "targets": { "build": { "executor": "nx:run-commands", "options": { "command": "tsc", "cwd": "packages/shared" }, "outputs": ["{projectRoot}/dist"] } } } ``` **packages/shared/tsconfig.json:** ```json { "compilerOptions": { "outDir": "dist", "rootDir": "src", "moduleResolution": "bundler", "module": "ES2022", "target": "ES2022", "declaration": true }, "include": ["src"], "exclude": ["node_modules", "dist"] } ``` **packages/shared/src/index.ts:** ```ts // Types shared between frontend and backend export interface Article { slug: string; title: string; preview: string; } export interface CreateArticleRequest { title: string; content: string; } // Utility functions export function slugify(text: string): string { return text .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-"); } export function truncate(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return text.slice(0, maxLength - 3) + "..."; } ``` ### Encore application The Encore app needs three key configurations: 1. **encore.app** - Use the `prebuild` hook to build dependencies during deployment 2. **package.json** - Declare the dependency on the shared package 3. **project.json** - Configure Nx targets and task dependencies To create the Encore app, run `encore app init --lang ts` from the `apps/backend` directory. Then add the `prebuild` hook to the generated `encore.app` file: **apps/backend/encore.app:** ```json { "id": "generated-id", "lang": "typescript", "build": { "hooks": { "prebuild": "npx nx build-deps @repo/backend" } } } ``` The `prebuild` hook runs when deploying via Encore Cloud or when exporting a Docker image with the Encore CLI. The `build-deps` target builds all dependencies of the backend. **apps/backend/package.json:** ```json { "name": "@repo/backend", "version": "1.0.0", "type": "module", "scripts": { "dev": "encore run" }, "dependencies": { "@repo/shared": "*", "encore.dev": "latest" } } ``` **apps/backend/project.json:** ```json { "name": "@repo/backend", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/backend", "projectType": "application", "targets": { "dev": { "executor": "nx:run-commands", "options": { "command": "encore run", "cwd": "apps/backend" }, "dependsOn": ["^build"], "cache": false }, "build-deps": { "dependsOn": ["^build"], "cache": true } } } ``` The `"dependsOn": ["^build"]` configuration uses the `^` prefix to indicate "run the build target on all dependencies first". This automatically builds all shared packages before running `encore run`, without needing to list each dependency explicitly. ### Using the shared package With this setup, you can import from your shared package in your Encore services: **apps/backend/article/article.ts:** ```ts import { api } from "encore.dev/api"; import type { Article, CreateArticleRequest } from "@repo/shared"; import { slugify, truncate } from "@repo/shared"; export const create = api( { expose: true, method: "POST", path: "/article" }, async ({ title, content }: CreateArticleRequest): Promise
=> { return { slug: slugify(title), title: title, preview: truncate(content, 100), }; }, ); ``` ## Running the application ### Installation First, install all dependencies from the monorepo root: ```shell $ npm install ``` This installs dependencies for all workspaces, including Nx. ### Local development For local development, you need to build the shared packages before running `encore run`. From the monorepo root: ```shell $ npx nx build-deps @repo/backend $ cd apps/backend && encore run ``` Or use Nx's `dev` target which handles the dependency ordering: ```shell $ npx nx dev @repo/backend ``` The `"dependsOn": ["^build"]` configuration ensures all dependencies are built before the backend's dev target runs. ### Deployment When deploying via Encore Cloud or exporting a Docker image, the `prebuild` hook in `encore.app` automatically runs the Nx build. When deploying a monorepo to Encore Cloud, configure the root path to your Encore app in the app settings: **Settings > General > Root Directory** (e.g., `apps/backend`). ## Key points - **Local development**: Run `npx nx build-deps @repo/backend` before `encore run`, or use `npx nx dev @repo/backend` to handle dependency ordering automatically - **Prebuild hook**: The `prebuild` hook in `encore.app` runs during deployment (Encore Cloud) or Docker export, not during local development - **Task dependencies**: Use `"dependsOn": ["^build"]` in `project.json` to automatically build all dependencies before running a target ================================================ FILE: docs/ts/develop/monorepo/turborepo.md ================================================ --- seotitle: Using Encore with Turborepo in a monorepo seodesc: Learn how to set up Encore.ts in a Turborepo monorepo with shared packages that require building before use. title: Turborepo subtitle: Using Encore in a Turborepo monorepo lang: ts --- [Turborepo](https://turbo.build/repo) is a build system for JavaScript and TypeScript monorepos. This guide shows how to set up an Encore application within a Turborepo monorepo that depends on shared packages requiring compilation. ## Overview When using Encore in a Turborepo monorepo, you may have shared packages (like utility libraries or shared types) that need to be built before the Encore app can use them. Since Encore parses your application on startup, these dependencies must be compiled first. This guide covers two scenarios: - **Local development**: Use Turborepo to build dependencies before running `encore run` - **Deployment**: Use Encore's `prebuild` hook to automatically build dependencies when deploying via Encore Cloud or exporting a Docker image ## Project structure A typical Turborepo setup with Encore looks like this: ``` my-turborepo/ ├── apps/ │ └── backend/ # Encore application │ ├── encore.app │ ├── package.json │ ├── tsconfig.json │ └── article/ │ └── article.ts ├── packages/ │ └── shared/ # Shared library requiring build │ ├── package.json │ ├── tsconfig.json │ ├── src/ │ │ └── index.ts │ └── dist/ # Built output │ └── index.js ├── turbo.json ├── package.json └── package-lock.json ``` ## Configuration ### Root package.json Configure npm workspaces to include your apps and packages: ```json { "name": "my-turborepo", "private": true, "packageManager": "npm@10.0.0", "scripts": { "build": "turbo run build", "dev": "turbo run dev" }, "devDependencies": { "turbo": "^2.0.0", "typescript": "^5.0.0" }, "workspaces": [ "apps/*", "packages/*" ] } ``` The `packageManager` field is required by Turborepo. Adjust the version to match your installed npm version (run `npm --version` to check). ### turbo.json Configure Turborepo's build pipeline in the root `turbo.json`. The `@repo/backend#dev` task depends on the shared package being built first: ```json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "@repo/backend#dev": { "dependsOn": ["@repo/shared#build"], "cache": false, "persistent": true }, "dev": { "cache": false, "persistent": true } } } ``` The `@repo/backend#dev` task configuration ensures the shared package is built before running `encore run` in local development. ### Shared package Your shared package needs to compile TypeScript to JavaScript and expose the built output: **packages/shared/package.json:** ```json { "name": "@repo/shared", "version": "1.0.0", "private": true, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc" }, "devDependencies": { "typescript": "^5.0.0" } } ``` **packages/shared/tsconfig.json:** ```json { "compilerOptions": { "outDir": "dist", "rootDir": "src", "moduleResolution": "bundler", "module": "ES2022", "target": "ES2022", "declaration": true }, "include": ["src"], "exclude": ["node_modules", "dist"] } ``` **packages/shared/src/index.ts:** ```ts // Types shared between frontend and backend export interface Article { slug: string; title: string; preview: string; } export interface CreateArticleRequest { title: string; content: string; } // Utility functions export function slugify(text: string): string { return text .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-"); } export function truncate(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return text.slice(0, maxLength - 3) + "..."; } ``` ### Encore application The Encore app needs two key configurations: 1. **encore.app** - Use the `prebuild` hook to build dependencies during deployment 2. **package.json** - Declare the dependency on the shared package To create the Encore app, run `encore app init --lang ts` from the `apps/backend` directory. Then add the `prebuild` hook to the generated `encore.app` file: **apps/backend/encore.app:** ```json { "id": "generated-id", "lang": "typescript", "build": { "hooks": { "prebuild": "npx turbo build --filter=@repo/backend^..." } } } ``` The `prebuild` hook runs when deploying via Encore Cloud or when exporting a Docker image with the Encore CLI. The filter `@repo/backend^...` tells Turborepo to build all dependencies of `@repo/backend`. The `^` excludes the backend itself, building only its dependencies. **apps/backend/package.json:** ```json { "name": "@repo/backend", "version": "1.0.0", "type": "module", "scripts": { "dev": "encore run" }, "dependencies": { "@repo/shared": "*", "encore.dev": "latest" } } ``` ### Using the shared package With this setup, you can import from your shared package in your Encore services: **apps/backend/article/article.ts:** ```ts import { api } from "encore.dev/api"; import type { Article, CreateArticleRequest } from "@repo/shared"; import { slugify, truncate } from "@repo/shared"; export const create = api( { expose: true, method: "POST", path: "/article" }, async ({ title, content }: CreateArticleRequest): Promise
=> { return { slug: slugify(title), title: title, preview: truncate(content, 100), }; }, ); ``` ## Running the application ### Installation First, install all dependencies from the monorepo root: ```shell $ npm install ``` This installs dependencies for all workspaces, including Turborepo. ### Local development For local development, you need to build the shared packages before running `encore run`. From the monorepo root: ```shell $ npx turbo run build $ cd apps/backend && encore run ``` Or use Turborepo's `dev` task which handles the dependency ordering: ```shell $ npx turbo run dev --filter=@repo/backend ``` The `turbo.json` configuration ensures `@repo/shared` is built before the backend's dev task runs. ### Deployment When deploying via Encore Cloud or exporting a Docker image, the `prebuild` hook in `encore.app` automatically runs the Turborepo build pipeline. When deploying a monorepo to Encore Cloud, configure the root path to your Encore app in the app settings: **Settings > General > Root Directory** (e.g., `apps/backend`). ## Key points - **Local development**: Run `npx turbo run build` before `encore run`, or use `npx turbo run dev --filter=@repo/backend` to handle dependency ordering automatically - **Prebuild hook**: The `prebuild` hook in `encore.app` runs during deployment (Encore Cloud) or Docker export, not during local development - **Turborepo filter**: Using `--filter=@repo/backend^...` builds only the dependencies of the backend (the `^` excludes the package itself) ================================================ FILE: docs/ts/develop/multithreading.md ================================================ --- seotitle: Multithreading in Encore.ts seodesc: See how Encore.ts provides true multithreading for JavaScript applications, and how to enable Worker Pooling for CPU-intensive workloads. title: Multithreading subtitle: True multithreading for JavaScript applications lang: ts --- Encore.ts runs using a high-performance Rust runtime that uses multiple threads to handle incoming requests. The Encore.ts Rust runtime handles virtually everything outside of your core business logic: - Parsing and validating incoming requests - Making API calls to other services - Serializing and writing API responses - Observability integrations like distributed tracing - Infrastructure integrations, like executing database queries, reading and writing from object storage, publishing and consuming messages from Pub/Sub, and more This architecture allows for much higher performance and scalability compared to traditional JavaScript frameworks. By offloading most of this to multithreaded Rust, the single-threaded JavaScript event loop becomes free to focus on executing your core business logic. But for more CPU-intensive workloads, the single-threaded JavaScript event loop can still become a performance bottleneck. For these use cases Encore.ts offers Worker Pooling. With Worker Pooling enabled, Encore.ts starts up multiple NodeJS event loops and load-balances incoming requests across them. This can provide a significant performance boost for CPU-intensive workloads. ## Enabling Worker Pooling To enable Worker Pooling, add `"build": {"worker_pooling": true}` to your `encore.app` file. ## Designing your application to work with Worker Pooling Most application code will work with Worker Pooling without any changes. However, it's important to understand the implications of running in a multi-threaded environment. When utilizing Worker Pooling, Encore.ts will automatically spin up multiple NodeJS isolates (one per CPU) to handle incoming requests. Each NodeJS isolate is a separate JavaScript runtime, with its own event loop and memory space. This means that you cannot rely on global shared state that is shared across all incoming requests, since each request may be handled by a different NodeJS isolate. ================================================ FILE: docs/ts/develop/orms/drizzle.md ================================================ --- seotitle: Using Drizzle with Encore seodesc: Learn how to use Drizzle with Encore to interact with SQL databases. title: Using Drizzle ORM with Encore lang: ts --- Encore.ts supports integrating [Drizzle](https://orm.drizzle.team/), a TypeScript ORM for Node.js and the browser. To use Drizzle with Encore, start by creating a `SQLDatabase` instance and providing the connection string to Drizzle. ## 1. Setting Up the Database Connection In `database.ts`, initialize the `SQLDatabase` and configure Drizzle: ```typescript // database.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { drizzle } from "drizzle-orm/node-postgres"; import { users } from "./schema"; // Create SQLDatabase instance with migrations configuration const db = new SQLDatabase("test", { migrations: { path: "migrations", source: "drizzle", }, }); // Initialize Drizzle ORM with the connection string const orm = drizzle(db.connectionString); // Query all users await orm.select().from(users); ``` ## 2. Configuring Drizzle Create a Drizzle configuration file `drizzle.config.ts` to specify settings like migration output, schema, and database dialect: ```typescript // drizzle.config.ts import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; export default defineConfig({ out: 'migrations', schema: 'schema.ts', dialect: 'postgresql', }); ``` ## 3. Defining the Database Schema Define your database tables in `schema.ts` using Drizzle's `pg-core` module: ```typescript // schema.ts import * as p from "drizzle-orm/pg-core"; export const users = p.pgTable("users", { id: p.serial().primaryKey(), name: p.text(), email: p.text().unique(), }); ``` ## 4. Generating Migrations Run the following command in the directory containing `drizzle.config.ts` to generate migrations: ```bash drizzle-kit generate ``` ## 5. Applying Migrations Migrations are automatically applied when you run your Encore application, so you don’t need to run `drizzle-kit migrate` or any similar commands manually. ================================================ FILE: docs/ts/develop/orms/knex.md ================================================ --- seotitle: Using Knex.js with Encore seodesc: Learn how to use Knex.js with Encore to interact with SQL databases. title: Using Knex.js with Encore lang: ts --- Encore.ts supports integrating [Knex.js](http://knexjs.org/), a SQL query builder for Node.js. To use Knex.js with Encore.ts, start by creating an `SQLDatabase` instance and provide its connection string to Knex.js. ## 1. Setting Up the Database Connection In `site.ts`, initialize the `SQLDatabase` and configure Knex.js: ```typescript // site.ts import { SQLDatabase } from "encore.dev/storage/sqldb"; import knex from "knex"; // Create SQLDatabase instance with migrations configuration const SiteDB = new SQLDatabase("siteDB", { migrations: "./migrations", }); // Initialize Knex with the database connection string const orm = knex({ client: "pg", connection: SiteDB.connectionString, }); // Define the Site interface export interface Site { id: number; url: string; } // Query builder for the "site" table const Sites = () => orm("site"); // Example queries // Query all sites await Sites().select(); // Query a site by id await Sites().where("id", id).first(); // Insert a new site await Sites().insert({ url: params.url }); ``` ## 2. Creating Migrations Currently, Encore does not support JavaScript migration files generated by `knex migrate:make`. Instead, you can create and maintain [migration files](/docs/ts/primitives/databases#database-migrations) in SQL format. Example migration file to create the `site` table: ```sql -- migrations/1_create_table.up.sql -- CREATE TABLE site ( id SERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE ); ``` ## 3. Applying Migrations Encore automatically applies migrations when you run your application. You do not need to run `knex migrate:latest` or similar commands manually. ================================================ FILE: docs/ts/develop/orms/overview.md ================================================ --- seotitle: Using ORMs with Encore.ts seodesc: Learn how to use ORMs with Encore.ts to seamlessly interact with SQL databases from your TypeScript / Node.js backend. title: Using ORMs and Migration Frameworks with Encore.ts lang: ts --- Encore provides built-in support for ORMs and migration frameworks by offering named databases and SQL-based migration files. For developers who prefer not to write raw SQL, Encore allows seamless integration with popular ORMs and migration tools. ## Overview Encore’s approach to database management is flexible. It uses standard SQL migration files, allowing integration with ORMs like [Sequelize](https://sequelize.org/) and migration tools like [Atlas](https://atlasgo.io/). - **ORM Compatibility:** If your ORM can connect to a database via a standard SQL driver, it will work with Encore. - **Migration Tool Compatibility:** If your migration tool generates SQL migration files without additional customization, it can be used with Encore. ## Connecting to a Database Encore provides the `SQLDatabase` class, which allows you to create a named database and retrieve its connection string. This connection string can be used by your chosen ORM or migration framework to establish a database connection. Example setup: ```typescript import { SQLDatabase } from "encore.dev/storage/sqldb"; // Initialize a named database with migration directory const SiteDB = new SQLDatabase("siteDB", { migrations: "./migrations", }); // Retrieve the connection string for ORM use const connStr = SiteDB.connectionString; ``` ## Example ORM implementations Here are some guides to using different ORMs with Encore: - [Using Knex.js with Encore](/docs/ts/develop/orms/knex) - [Using Sequelize with Encore](/docs/ts/develop/orms/sequelize) - [Using Drizzle with Encore](/docs/ts/develop/orms/drizzle) - [Using Prisma with Encore](/docs/ts/develop/orms/prisma) This setup enables Encore to support a wide variety of ORMs and migration frameworks, making database management both flexible and straightforward. ================================================ FILE: docs/ts/develop/orms/prisma.md ================================================ --- seotitle: Using Prisma with Encore.ts seodesc: Learn how to use Prisma with Encore to interact with SQL databases. title: Using Prisma with Encore.ts lang: ts --- [Prisma](https://prisma.io/) is a modern TypeScript ORM that provides type-safe database access and migrations. With Prisma, you define your database schema in a `schema.prisma` file and use Prisma's CLI to generate SQL migrations and a TypeScript client. This guide explains how to integrate Prisma with Encore.ts, leveraging Encore's built-in database management while using Prisma's powerful ORM features. ## How Prisma works with Encore Encore and Prisma work together seamlessly: - **Prisma** generates the migration files and TypeScript client - **Encore** manages database creation, connections, and applies migrations - You use Encore's `SQLDatabase` to provide connection strings to Prisma The key to this integration is configuring Prisma to use Encore's shadow database for its operations, preventing any conflicts between the two systems. ## Quick Example Here's a complete example of using Prisma with Encore.ts: ```ts -- users/database.ts -- import { SQLDatabase } from "encore.dev/storage/sqldb"; // Define a database named 'users', using the database migrations // in the "./prisma/migrations" folder (generated by Prisma). export const DB = new SQLDatabase("users", { migrations: { path: "./prisma/migrations", source: "prisma", }, }); -- users/prisma/schema.prisma -- generator client { provider = "prisma-client" output = "./generated" previewFeatures = ["queryCompiler", "driverAdapters"] } datasource db { provider = "postgresql" // Connect Prisma CLI to Encore's shadow database // This prevents interference with Encore's migration system url = env("SHADOW_DB_URL") } model User { id Int @id @default(autoincrement()) email String @unique name String createdAt DateTime @default(now()) } -- users/prisma/client.ts -- import { PrismaClient } from "./generated/client"; import { PrismaPg } from "@prisma/adapter-pg"; import { DB } from "../database"; // Create and export the Prisma client instance export const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: DB.connectionString }), }); // Re-export types from the generated client export * from "./generated/client"; -- users/api.ts -- import { api } from "encore.dev/api"; import { prisma } from "./prisma/client"; interface CreateUserRequest { email: string; name: string; } // Example API endpoint using Prisma export const createUser = api( { method: "POST", path: "/users", expose: true }, async (req: CreateUserRequest) => { const user = await prisma.user.create({ data: req, }); return user; } ); ``` ## Step-by-Step Setup ### 1. Install Dependencies First, install Prisma and its required dependencies: ```bash npm install prisma --save-dev npm install @prisma/client @prisma/adapter-pg dotenv --save ``` ### 2. Create Project Structure Create the following directory structure for your service: ``` my-service/ ├── database.ts ├── prisma/ │ ├── schema.prisma │ ├── migrations/ │ ├── generated/ (will be created by Prisma) │ └── client.ts └── api.ts ``` ### 3. Set Up the Database Create `database.ts` to define your Encore database: ```ts import { SQLDatabase } from "encore.dev/storage/sqldb"; // Export the database so it can be used in the Prisma client export const DB = new SQLDatabase("myapp", { migrations: { path: "./prisma/migrations", source: "prisma", }, }); ``` Run `encore run` to create the database (make sure the migrations folder has been created first). ### 4. Configure Database Connections Prisma needs to connect to Encore's shadow database for migration operations. The shadow database is a temporary database that Prisma uses to detect schema drift and generate migrations without affecting your main database. Get the connection strings: ```bash # Main database connection (for Prisma Studio) encore db conn-uri myapp # Shadow database connection (for Prisma CLI operations) encore db conn-uri myapp --shadow ``` Create a `.env` file in your project root: ``` # Connection strings for local development DB_URL= SHADOW_DB_URL= ``` ### 5. Create Prisma Configuration Create `prisma.config.ts` in your project root: ```ts import "dotenv/config"; import type { PrismaConfig } from "prisma"; import { PrismaPg } from "@prisma/adapter-pg"; type Env = { DB_URL: string; }; export default { earlyAccess: true, schema: "./my-service/prisma/schema.prisma", studio: { adapter: async (env: Env) => { // Connect Prisma Studio to the main Encore database return new PrismaPg({ connectionString: env.DB_URL }); }, }, } satisfies PrismaConfig; ``` ### 6. Create Your Prisma Schema Create `my-service/prisma/schema.prisma`: ``` generator client { provider = "prisma-client" output = "./generated" previewFeatures = ["queryCompiler", "driverAdapters"] } datasource db { provider = "postgresql" // IMPORTANT: Use shadow database URL for Prisma CLI operations url = env("SHADOW_DB_URL") } // Define your models here model User { id Int @id @default(autoincrement()) email String @unique name String } ``` ### 7. Create the Prisma Client Wrapper Create `my-service/prisma/client.ts`: ```ts import { PrismaClient } from "./generated/client"; import { PrismaPg } from "@prisma/adapter-pg"; import { DB } from "../database"; // Create and export the Prisma client instance export const prismaClient = new PrismaClient({ adapter: new PrismaPg({ connectionString: DB.connectionString }), }); // Re-export types from the generated client export * from "./generated/client"; ``` ### 8. Generate Initial Migration Generate and apply your first migration: ```bash npx prisma migrate dev --name init encore run ``` This will: 1. Create the `prisma/migrations` directory 2. Generate SQL migration files 3. Generate the Prisma client in `prisma/generated` 4. Apply the migration to your development database ### 9. Use Prisma in Your Code Now you can use Prisma in your API endpoints: ```ts import { api, APIError } from "encore.dev/api"; import { prismaClient, Prisma } from "./prisma/client"; interface CreateUserRequest { email: string; name: string; } export const createUser = api( { method: "POST", path: "/users", expose: true }, async (req: CreateUserRequest) => { try { const user = await prismaClient.user.create({ data: req, }); return user; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2002") { throw APIError.alreadyExists("User with this email already exists"); } } throw error; } }, ); export const getUsers = api( { method: "GET", path: "/users", expose: true }, async (): Promise<{ users: { name: string; email: string; id: number }[]; }> => { return { users: await prismaClient.user.findMany() }; }, ); ``` ## Working with Migrations ### Generate New Migrations When you make changes to your `schema.prisma` file: ```bash npx prisma migrate dev --name describe-your-change ``` Example: ```bash npx prisma migrate dev --name add-user-role ``` ### How Migrations Work 1. **Prisma generates** SQL migration files based on your schema changes 2. **Encore applies** these migrations automatically: - Locally when you run `encore run` - In cloud environments during deployment - During tests when using `encore test` ## Deployment ### Generate Client During Build For Encore Cloud deployments, add to your `package.json`: ```json { "scripts": { "postinstall": "npx prisma generate" } } ``` This ensures the Prisma client is generated during deployment. ### Production Considerations - Encore automatically provides database connection strings in production - No environment variables need to be configured for production - Migrations are applied automatically during deployment ## Using Prisma Studio Prisma Studio provides a GUI for your database: ```bash npx prisma studio ``` Opens at `http://localhost:5555` where you can: - Browse and filter data - Create, update, and delete records - Explore relationships ## Troubleshooting ### Connection Issues If Prisma or Prisma Studio can't connect: 1. Verify connection strings in `.env` 2. Restart Encore (`encore run`) ================================================ FILE: docs/ts/develop/orms/sequelize.md ================================================ --- seotitle: Using Sequelize with Encore.ts seodesc: Learn how to use Sequelize with Encore to interact with SQL databases. title: Using Sequelize with Encore.ts lang: ts --- Encore.ts supports integrating [Sequelize](https://sequelize.org/), a promise-based Node.js ORM. To set up Sequelize with Encore, start by creating a `SQLDatabase` instance and providing the connection string to Sequelize. ## 1. Setting Up the Database Connection In `database.ts`, initialize the `SQLDatabase` and configure Sequelize: ```typescript // database.ts import { Model, InferAttributes, InferCreationAttributes, CreationOptional, DataTypes, Sequelize, } from "sequelize"; import { SQLDatabase } from "encore.dev/storage/sqldb"; // Create SQLDatabase instance with migrations configuration const DB = new SQLDatabase("encore_sequelize_test", { migrations: "./migrations", }); // Initialize Sequelize with the connection string const sequelize = new Sequelize(DB.connectionString); // Define the User model class User extends Model, InferCreationAttributes> { declare id: CreationOptional; declare name: string; declare surname: string; } // Example usage: Count all users const count = await User.count(); ``` ## 2. Creating Migrations Encore does not currently support JavaScript migration files generated by tools like `sequelize-cli model:generate`. Instead, create and manage your own [migration files](/docs/ts/primitives/databases#database-migrations) in SQL format. Example migration file for creating the `user` table: ```sql -- migrations/1_create_user.up.sql -- CREATE TABLE "user" ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, surname TEXT NOT NULL ); ``` ## 3. Applying Migrations Migrations are automatically applied when you run your Encore application, so you don’t need to run `sequelize db:migrate` or similar commands manually. --- For more information, see the example on GitHub: ================================================ FILE: docs/ts/develop/running-scripts.md ================================================ --- seotitle: How to use `encore exec` for running scripts seodesc: Learn how to use the `encore exec` command to run scripts like database seeding in your Encore app. title: Running Scripts subtitle: Run scripts with your application's infrastructure and runtime configured and initialized lang: ts --- In local development, you may need to run scripts or commands, such as seeding a database with initial data. For that to work the database needs to be started, and the Encore runtime needs to be configured and initialized. ## Using `encore exec` The `encore exec` command allows you to execute custom commands while leveraging Encore's infrastructure setup. This is particularly useful for tasks like database seeding, running scripts, or other one-off commands that require the app's environment to be initialized. ### How it works The `encore exec` command initializes the required infrastructure for your Encore app and executes the specified command. This ensures that your commands run in the correct context with all dependencies properly configured. ### Example: Database Seeding In this example, `npx tsx ./seed.ts` runs a TypeScript script (`seed.ts`) to populate the database with initial data ```bash encore exec -- npx tsx ./seed.ts ``` Here’s what happens: 1. Encore initializes the app infrastructure. 2. The `npx tsx ./seed.ts` command is executed in the context of the initialized app. ### General Syntax ```bash encore exec -- ``` Substitute `` with the specific command you wish to run. ### Use Cases - **Database Seeding**: Populate your database with initial data using a script. - **Client Generation**: Generate a client for interacting with an external dependency. - **Custom Scripts**: Run any script that depends on the app's initialized environment. ### Notes - Ensure that the command you provide is executable in your environment. - Use `--` to separate `encore exec` options from the command you want to run. ================================================ FILE: docs/ts/develop/testing.md ================================================ --- seotitle: Automated testing for your backend application seodesc: Learn how create automated tests for your microservices backend application, and run them automatically on deploy using Go and Encore. title: Automated testing subtitle: Confidence at speed lang: ts --- Encore provides built-in testing tools that make it simple to test your application using a variety of test runners. To run tests with Encore: 1. Configure the `test` command in your `package.json` to use the test runner of your choice. 2. Configure your test runner. 3. Run `encore test` from the CLI. The `encore test` command automatically sets up all necessary infrastructure in test mode before running your tests. ## Recommended Setup: Vitest We recommend [Vitest](https://vitest.dev) as your test runner because it offers: - Fast execution - Native ESM and TypeScript support - Jest API compatibility ### Setting up Vitest 1. Create `vite.config.ts` in your application's root directory: ```ts /// import { defineConfig } from "vite"; import path from "path"; export default defineConfig({ resolve: { alias: { "~encore": path.resolve(__dirname, "./encore.gen"), }, }, }); ``` 2. Update your `package.json` to include: ```json { "scripts": { "test": "vitest" } } ``` You're done! Now you can run your tests with `encore test`. ### Optional: IDE Integration #### VS Code Setup If using Vitest, follow these steps: 1. Install the official Vitest VS Code extension 2. Add to `.vscode/settings.json`: ```json { "vitest.commandLine": "encore test" } ``` As of Vitest plugin version 0.5 ([issue](https://github.com/vitest-dev/vscode/issues/306)), environment configuration requires an updated approach. The following configuration is required to ensure proper functionality: Update `settings.json` to include: ```json "vitest.nodeEnv": { // generated with `encore daemon env | grep ENCORE_RUNTIME_LIB | cut -d'=' -f2` "ENCORE_RUNTIME_LIB": "/opt/homebrew/Cellar/encore/1.44.5/libexec/runtimes/js/encore-runtime.node" } ``` When running tests within VSCode, file-level parallel execution must be disabled. Update your `vite.config.ts` as follows: ```typescript // File vite.config.ts export default defineConfig({ resolve: { alias: { "~encore": path.resolve(__dirname, "./encore.gen"), }, }, test: { fileParallelism: false, }, }); ``` To improve the performance in CI, you can re-enable the parallel execution by overwriting the config in cli `encore test --fileParallelism=true`. ## Integration Testing Best Practices Encore applications typically focus on integration tests rather than unit tests because: - Encore eliminates most boilerplate code - Your code primarily consists of business logic involving databases and inter-service API calls - Integration tests better verify this type of functionality ### Test Environment Benefits When running tests, Encore automatically: - Sets up separate test databases - Configures databases for optimal test performance by: - Skipping `fsync` - Using in-memory filesystems - Removing durability overhead These optimizations make integration tests nearly as fast as unit tests. ================================================ FILE: docs/ts/faq.md ================================================ --- seotitle: Frequently Asked Questions seodesc: See quick answers to common questions about Encore title: FAQ subtitle: Quick answers to common questions lang: ts --- ## About the project **Is Encore Open Source?** Yes, check out the project on [GitHub](https://github.com/encoredev/encore). **Is there a community?** Yes, you're welcome to join the developer community on [Discord](https://encore.dev/discord). ## Can I use X with Encore? **Can I use Python with Encore?** Encore currently supports Go and TypeScript. Python support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026. **Can mix TypeScript and Go in one application?** Support for mixing languages in coming. Currently, if you want to use both TypeScript and Go, you need to create a separate application per language and integrate using APIs. **Can I use Azure / Digital Ocean?** Encore Cloud currently supports automating deployments to AWS and GCP. Azure support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026. If you want to use other cloud providers like Azure or Digital Ocean, you can follow the [self-hosting instructions](/docs/how-to/self-host). **Can I use MongoDB / MySQL with Encore?** Encore currently has built-in support for PostgreSQL. To use another type of database, like MongoDB and MySQL, you will need to set it up and integrate as you normally would when not using Encore. **Can I use AWS lambda with Encore?** Not right now. Encore currently supports AWS Fargate and EKS (along with CloudRun and GKE on Google Cloud Platform). **Can I use Bun / Deno with Encore.ts?** Right now Encore.ts officially supports Node and has experimental support for Bun. Deno support is on the way. Note that Encore.ts already provides performance improvements thanks to its Rust-based runtime. [Learn more](https://encore.dev/blog/event-loops). To enable the Bun experiment, add `"experiments": ["bun-runtime"]` to your `encore.app` file, and add `"packageManager": "bun"` to your `package.json` file. ## IDE Integrations **Is there an Encore plugin for Goland / IntelliJ?** Yes, Encore's official Goland plugin is available in the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore). **Is there an Encore plugin for VS Code?** Not yet, it's coming soon. ## Troubleshooting **symlink creation error on Windows** Encore currently relies on symbolic links, which may be disabled by default. A common fix for this issue is to enable "developer mode" in the Windows settings (Settings > System > For developers > Developer mode). **`node` errors** You might need to restart the Encore daemon, e.g. if your PATH has changed since installing nvm. Restart the daemon by running `encore daemon`. ================================================ FILE: docs/ts/frontend/cors.md ================================================ --- seotitle: Handling CORS (Cross-Origin Resource Sharing) seodesc: See how you can configure CORS for your Encore application. title: CORS subtitle: Configure CORS (Cross-Origin Resource Sharing) for your Encore application lang: ts --- CORS is a web security concept that defines which website origins are allowed to access your API. A deep-dive into CORS is out of scope for this documentation, but [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) provides a good overview. In short, CORS affects requests made by browsers to resources hosted on other origins (a combination of the scheme, domain, and port). ## Configuring CORS Encore provides a default CORS configuration that is suitable for many APIs. You can override these settings by specifying the `global_cors` key in the `encore.app` file, which has the following structure: ```cue { // debug enables CORS debug logging. "debug": true | false, // allow_headers allows an app to specify additional headers that should be // accepted by the app. // // If the list contains "*", then all headers are allowed. "allow_headers": [...string], // expose_headers allows an app to specify additional headers that should be // exposed from the app, beyond the default set always recognized by Encore. // // If the list contains "*", then all headers are exposed. "expose_headers": [...string], // allow_origins_without_credentials specifies the allowed origins for requests // that don't include credentials. If nil it defaults to allowing all domains // (equivalent to ["*"]). "allow_origins_without_credentials": [...string], // allow_origins_with_credentials specifies the allowed origins for requests // that include credentials. If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). "allow_origins_with_credentials": [...string], } ``` ## Allowed origins The main CORS configuration is the list of allowed origins, meaning which websites are allowed to access your API (via browsers). For this purpose, CORS makes a distinction between requests that contain authentication information (cookies, HTTP authentication, or client certificates) and those that do not. CORS applies stricter rules to authenticated requests. By default, Encore allows unauthenticated requests from all origins but disallows requests that do include authorization information from other origins. This is a good default for many APIs. This can be changed by setting the `allow_origins_without_credentials` key (see above). For convenience Encore also allows all origins when developing locally. For security reasons it's necessary to explicitly specify which origins are allowed to make authenticated requests. This is done by setting the `allow_origins_with_credentials` key (see above). ## Allowed headers and exposed headers CORS also lets you specify which headers are allowed to be sent by the client ("allowed headers"), and which headers are exposed to scripts running in the browser ("exposed headers"). Encore automatically configures headers by parsing your program using static analysis. If your API defines a request or response type that contains a header field, Encore automatically adds the header to the list of exposed and allowed headers in request types respectively. To add additional headers to these lists, you can set the `allow_headers` and `expose_headers` keys (see above). This can be useful when your application relies on custom headers in e.g. raw endpoints that aren't seen by Encore's static analysis. ================================================ FILE: docs/ts/frontend/hosting.mdx ================================================ --- seotitle: Integrate your backend application with a frontend seodesc: Learn how to host your frontend when having a Encore.ts backend application. title: Hosting a frontend subtitle: Keep using your favorite frontend hosting provider lang: ts --- Encore is not opinionated about where you host your frontend, pick the platform that suits your situation best. ## Hosting a frontend using Encore Encore is primarily designed for backend development. It is possible to serve a frontend using Encore but for production we recommend that you deploy your frontend using Vercel, Netlify, or a similar service. ### Template engines You can make use of template engines like Handlebars, Pug, or EJS to render HTML files on the server. Learn more about in the [Template Engine](/docs/ts/frontend/template-engine) docs. ### Serving static assets You can create a `api.static` endpoint that serves static frontend assets, including HTML files. ```ts import { api } from "encore.dev/api"; // Using fallback route to serve all files in the ./assets directory under the root path. export const rootAssets = api.static({ expose: true, path: "/!path", dir: "./assets", // When a file matching the request isn't found, Encore automatically serves a 404 Not Found response. // You can customize the response by setting the notFound option to specify a file that should be served instead: notFound: "./assets/not_found.html", }); ``` Keep in mind that this approach will not work if you have a Single-Page Application (SPA) that uses client-side routing. ================================================ FILE: docs/ts/frontend/mono-vs-multi-repo.mdx ================================================ --- seotitle: Integrate your backend application with a frontend seodesc: Learn how to structure your application, using a Monorepo or a Multi-repo approach. title: Mono vs Multi Repo subtitle: How to structure your frontend and backend lang: ts --- Encore is not opinionated about if you have your backend and frontend code in the same repo or not. Pick the approach that fits your application best. ## Monorepo If you use a monorepo then it is often a good idea to place your backend and frontend in separate folders in the root of your repo, like so: ``` /my-app ├── backend │ ├── encore.app │ ├── package.json // Backend dependencies │ └── ... └── frontend ├── package.json // Frontend dependencies └── ... ``` This way, you can keep your frontend and backend dependencies separate, while still having the codebases in the same repository. If you are using Encore Cloud for deployment, remember to configure the "Root Directory" in app settings in the [Encore Cloud dashboard](https://app.encore.cloud) to point to where you have your `encore.app` file. You can also have a monorepo where the `encore.app` file is in the root of the repo, the frontend code will then be inside your Encore app. If you go this route you will most likely need two different `tsconfig.json` files, one for the frontend and one for the backend. ================================================ FILE: docs/ts/frontend/request-client.mdx ================================================ --- seotitle: Get type-safe requests between your backend and frontend seodesc: Learn how to use Encore's built-in client generation to get type-safety between your backend and frontend. title: Request client for the frontend subtitle: Get type-safety between your backend and frontend lang: ts --- Encore is able to generate frontend request clients (TypeScript or JavaScript). This lets you to keep the request/response types in sync without manual work and assists you in calling the APIs. Generate a client by running: ```bash $ encore gen client --output=./src/client.ts --env= ``` Adding this as a script to your `package.json` is often a good idea to be able to run it whenever a change is made to your Encore API: ```json { ... "scripts": { ... "generate-client:staging": "encore gen client --output=./src/client.ts --env=staging", "generate-client:local": "encore gen client --output=./src/client.ts --env=local" } } ``` After that you are ready to use the request client in your code. In this example, the frontend is calling the `GetNote` endpoint on the `note` service in order to retrieve a specific meeting note (which has the properties `id`, `cover_url` & `text`): ```ts import Client, { Environment, Local } from "src/client.ts"; // Making request to locally running backend... const client = new Client(Local); // or to a specific deployed environment // const client = new Client(Environment("staging")); // Calling APIs as typesafe functions 🌟 const response = await client.note.GetNote("note-uuid"); console.log(response.id); console.log(response.cover_url); console.log(response.text); ``` See more in the [client generation docs](/docs/ts/cli/client-generation). ### Asynchronous state management When building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale. [TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works well with the Encore request client. Here is a simple example of using an Encore request client together with TanStack Query: ```ts import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import Client, { todo } from '../encore-client' // Create a Encore client const encoreClient = new Client(window.location.origin); // Create a react-query client const queryClient = new QueryClient() function App() { return ( // Provide the client to your App ) } function Todos() { // Access the client const queryClient = useQueryClient() // Queries const query = useQuery({ queryKey: ['todos'], queryFn: () => encoreClient.todo.List() }) // Mutations const mutation = useMutation({ mutationFn: (params: todo.AddParams) => encoreClient.todo.Add(params), onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) return (
    {query.data?.map((todo) => (
  • {todo.title}
  • ))}
) } render(, document.getElementById('root')) ``` This example assumes that we have a `todo` service with a `List` and `Add` endpoint. When adding the new todo, TanStack Query will automatically invalidate the `todos` query and refetch it. For a real-world example, take a look at the [Uptime Monitoring](https://github.com/encoredev/examples/tree/main/uptime) app which also makes use of TanStack Query's `refetchInterval` option for polling the backend. ### Testing When unit testing a component that interacts with your Encore API you can mock methods on the request client to return a value suitable for the test. This makes your test URL agnostic because you are not intercepting specific requests on the fetch layer. You also get type errors in your tests if the request client gets updated. Here is an example from the [Uptime Monitoring Starter](https://github.com/encoredev/examples/tree/main/uptime) where we are mocking a GET request method and spying on a POST request method: ```ts import { render, waitForElementToBeRemoved } from "@testing-library/react"; import App from "./App"; import { site } from "./client"; import { userEvent } from "@testing-library/user-event"; describe("App", () => { beforeEach(() => { // Return mocked data from the List (GET) endpoint jest .spyOn(site.ServiceClient.prototype, "List") .mockReturnValue(Promise.resolve({ sites: [{ id: 1, url: "test.dev" }] })); // Spy on the Add (POST) endpoint jest.spyOn(site.ServiceClient.prototype, "Add"); }); it("render sites", async () => { render(); await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); // Verify that the List endpoint has been called expect(site.ServiceClient.prototype.List).toBeCalledTimes(1); // Verify that the sites are rendered with our mocked data screen.getAllByText("test.dev"); }); it("add site", async () => { render(); await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); // Interact with the page and add 'another.com' await userEvent.click(screen.getByText("Add website")); await userEvent.type( screen.getByPlaceholderText("google.com"), "another.com", ); await userEvent.click(screen.getByText("Save")); // Verify that the Add endpoint has been called with the correct parameters expect(site.ServiceClient.prototype.Add).toHaveBeenCalledWith({ url: "another.com", }); }); }) ``` In the example above we need to mock the `List` method on `site.ServiceClient.prototype` because the request client has not yet been initialized when we're creating the mock. If you have access to the instance of the request client in your test (which could be the case if you are passing the client around in your components) you can instead do `jest.spyOn(client.site, "List")` and `expect(client.site.List).toHaveBeenCalled()` which would give you the same result. ## REST vs. GraphQL Encore allows for building backends using both REST and GraphQL, you should pick the approach that suits your use case best. Encore's request client only works for REST APIs so if you choose to build a GraphQL backend you will need to use another request library for your frontend. Take a look at the [GraphQL tutorial](/docs/ts/tutorials/graphql) for an example of building a GraphQL backend with Encore. ================================================ FILE: docs/ts/frontend/template-engine.md ================================================ --- seotitle: How to use a template engine in you Encore.ts application seodesc: Learn how to use a template engine to create server-rendered HTML with dynamic data. title: Use a template engine lang: ts --- In this guide you will learn how to use a template engine, like [EJS](https://ejs.co) and [Handlebars](https://handlebarsjs.com), to create server-rendered HTML views. ## Serving a specific template file Breakdown of the example: * We import a NPM package for rendering templates, in this case [EJS](https://ejs.co/). * We have a [Raw Endpoint](/docs/ts/primitives/raw-endpoints) to handle template rendering, in this case we are serving a specific template file (`person.html`) under `/person`. * We make use of the EJS to render the template with the given data. * We set the `content-type` header to `text-html` and then respond with the generated HTML. ```ts -- template/template.ts -- import { api } from "encore.dev/api"; import ejs, { Options } from "ejs"; const BASE_PATH = "./template/views"; const ejsOptions: Options = { views: [BASE_PATH] }; export const serveSpecificTemplate = api.raw( { expose: true, path: "/person", method: "GET" }, async (req, resp) => { const viewPath = `${BASE_PATH}/person.html`; const html = await ejs.renderFile( viewPath, // Supplying data to the view { name: "Simon" }, ejsOptions, ); resp.setHeader("content-type", "text/html"); resp.end(html); }, ); -- template/views/person.html --

Person Page

Name: <%= name %>

``` ## Serving from a dynamic path This example is similar to the one above, but in this case we use a fallback path to serve a template file based on the path. We use the `currentRequest` function to get the `path` and then render the template file based on the `path`. If no path is provided, we default to `index.html`. ```ts import { api } from "encore.dev/api"; import { APICallMeta, currentRequest } from "encore.dev"; import ejs, { Options } from "ejs"; const BASE_PATH = "./template/views"; const ejsOptions: Options = { views: [BASE_PATH] }; export const servePathTemplate = api.raw( { expose: true, path: "/!path", method: "GET" }, async (req, resp) => { const { path } = (currentRequest() as APICallMeta).pathParams; const viewPath = `${BASE_PATH}/${path ?? "index"}.html`; const html = await ejs.renderFile(viewPath, ejsOptions); resp.setHeader("content-type", "text/html"); resp.end(html); }, ); ``` ## Serving inline HTML In this example we are serving inline HTML with EJS. We use the `ejs.render` function to render the inline HTML with the given data. ```ts import { api } from "encore.dev/api"; import ejs, { Options } from "ejs"; const inlineHTML = `

Static Inline HTML Example

Name: <%= name %>!

`; export const serveInlineHTML = api.raw( { expose: true, path: "/html", method: "GET" }, async (req, resp) => { const html = ejs.render(inlineHTML, { name: "Simon" }); resp.setHeader("Content-Type", "text/html"); resp.end(html); }, ); ``` ## Static files In the above example we are fetching a stylesheet from the `/public` path. We can use the `api.static` function to serve all files in the `./assets` directory under the `/public` path prefix: ```ts // Serve all files in the ./assets directory under the /public path prefix. export const assets = api.static({ expose: true, path: "/public/*path", dir: "./assets", }); ``` Learn more about serving static files in the [Static Files](/docs/ts/primitives/static-assets) guide. ================================================ FILE: docs/ts/how-to/file-uploads.md ================================================ --- seotitle: How to handle file uploads in you Encore.ts application seodesc: Learn how to store file uploads as bytes in a database and serving them back to the client. title: Handling file uploads lang: ts --- In this guide you will learn how to handle file uploads from a client in your Encore.ts backend. ## Storing a single file in a database Breakdown of the example: * We have a [PostgreSQL database](/docs/ts/primitives/databases) table named `files` with columns `name` and `data` to store the file name and the file data. * We have a [Raw Endpoint](/docs/ts/primitives/raw-endpoints) to handle file uploads. The endpoint has a `bodyLimit` set to `null` to allow for unlimited file size. * We make use of the [busboy](https://www.npmjs.com/package/busboy) library to help with the file handling. * We convert the file data to a `Buffer` and store the file as a `BYTEA` in the database. ```ts -- upload.ts -- import { api } from "encore.dev/api"; import log from "encore.dev/log"; import busboy from "busboy"; import { SQLDatabase } from "encore.dev/storage/sqldb"; // Define a database named 'files', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. export const DB = new SQLDatabase("files", { migrations: "./migrations", }); type FileEntry = { data: any[]; filename: string }; /** * Raw endpoint for storing a single file to the database. * Setting bodyLimit to null allows for unlimited file size. */ export const save = api.raw( { expose: true, method: "POST", path: "/upload", bodyLimit: null }, async (req, res) => { const bb = busboy({ headers: req.headers, limits: { files: 1 }, }); const entry: FileEntry = { filename: "", data: [] }; bb.on("file", (_, file, info) => { entry.filename = info.filename; file .on("data", (data) => { entry.data.push(data); }) .on("close", () => { log.info(`File ${entry.filename} uploaded`); }) .on("error", (err) => { bb.emit("error", err); }); }); bb.on("close", async () => { try { const buf = Buffer.concat(entry.data); await DB.exec` INSERT INTO files (name, data) VALUES (${entry.filename}, ${buf}) ON CONFLICT (name) DO UPDATE SET data = ${buf} `; log.info(`File ${entry.filename} saved`); // Redirect to the root page res.writeHead(303, { Connection: "close", Location: "/" }); res.end(); } catch (err) { bb.emit("error", err); } }); bb.on("error", async (err) => { res.writeHead(500, { Connection: "close" }); res.end(`Error: ${(err as Error).message}`); }); req.pipe(bb); return; }, ); -- migrations/1_create_tables.up.sql -- CREATE TABLE files ( name TEXT PRIMARY KEY, data BYTEA NOT NULL ); ``` ### Frontend ```html

``` ## Handling multiple file uploads When handling multiple file uploads, we can use the same approach as above, but we need to handle multiple files in the busboy event listeners. When storing the files in the database, we loop through the files and save them one by one. ```ts export const saveMultiple = api.raw( { expose: true, method: "POST", path: "/upload-multiple", bodyLimit: null }, async (req, res) => { const bb = busboy({ headers: req.headers }); const entries: FileEntry[] = []; bb.on("file", (_, file, info) => { const entry: FileEntry = { filename: info.filename, data: [] }; file .on("data", (data) => { entry.data.push(data); }) .on("close", () => { entries.push(entry); }) .on("error", (err) => { bb.emit("error", err); }); }); bb.on("close", async () => { try { for (const entry of entries) { const buf = Buffer.concat(entry.data); await DB.exec` INSERT INTO files (name, data) VALUES (${entry.filename}, ${buf}) ON CONFLICT (name) DO UPDATE SET data = ${buf} `; log.info(`File ${entry.filename} saved`); } // Redirect to the root page res.writeHead(303, { Connection: "close", Location: "/" }); res.end(); } catch (err) { bb.emit("error", err); } }); bb.on("error", async (err) => { res.writeHead(500, { Connection: "close" }); res.end(`Error: ${(err as Error).message}`); }); req.pipe(bb); return; }, ); ``` ### Frontend ```html

``` ## Handling large files In order to not run into a **Maximum request length exceeded**-error when uploading large files you might need to adjust the endpoints `bodyLimit`. You can also set the `bodyLimit` to `null` to allow for unlimited file size uploads. If unset it defaults to 2MiB. ## Retrieving files from the database When retrieving files from the database, we can use a GET endpoint to fetch the file data by its name. We can then serve the file back to the client by creating a `Buffer` from the file data and sending it in the response. ```ts import { api } from "encore.dev/api"; import { APICallMeta, currentRequest } from "encore.dev"; export const DB = new SQLDatabase("files", { migrations: "./migrations", }); export const get = api.raw( { expose: true, method: "GET", path: "/files/:name" }, async (req, resp) => { try { const { name } = (currentRequest() as APICallMeta).pathParams; const row = await DB.queryRow` SELECT data FROM files WHERE name = ${name}`; if (!row) { resp.writeHead(404); resp.end("File not found"); return; } const chunk = Buffer.from(row.data); resp.writeHead(200, { Connection: "close" }); resp.end(chunk); } catch (err) { resp.writeHead(500); resp.end((err as Error).message); } }, ); ``` You should now be able to retrieve a file from the database by making a GET request to `http://localhost:4000/files/name-of-file.ext`. ================================================ FILE: docs/ts/how-to/nestjs.md ================================================ --- seotitle: Use Encore together with NestJS seodesc: Learn how to use NestJS to structure your business logic and Encore for creating infrastructure resources. title: Use NestJS with Encore lang: ts --- [Nest](https://docs.nestjs.com/) (NestJS) is a framework for building efficient, scalable TypeScript server-side applications. Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. Encore is not opinionated when it comes to application architecture, so you can use it together with NestJS to structure your business logic and Encore for creating backend primitives like APIs, Databases, and Cron Jobs. ## Adding Encore to a NestJS project If you already have a NestJS project, you can add Encore to it by following these steps: 1. Run `encore app init` in the root of your project to create a new Encore application. 2. Add `encore.dev` as a dependency by running `npm install encore.dev`. 3. Add the following `paths` to your `tsconfig.json`: ```json -- tsconfig.json -- { "compilerOptions": { "paths": { "~encore/*": [ "./encore.gen/*" ] } } } ``` ## Standalone Nest application In order for Encore to be able to provision infrastructure resources, generate API documentation etc. we need to run our application using Encore. This means that we need to replace the ordinary Nest bootstrapping and instead run our Nest app as a [standalone application](https://docs.nestjs.com/standalone-applications). We do this by calling `NestFactory.createApplicationContext(AppModule)` and then selecting the modules/services we need: ```ts -- applicationContext.ts -- const applicationContext: Promise<{ catsService: CatsService }> = NestFactory.createApplicationContext(AppModule).then((app) => { return { catsService: app.select(CatsModule).get(CatsService, {strict: true}), // other services... }; }); export default applicationContext; ``` The `applicationContext` variable can then be used to access your Nest modules/services from your Encore your APIs. ## Defining an Encore service When running an app using Encore you need at least one [Encore service](/docs/ts/primitives/services#defining-a-service). You can define a service in two ways: 1. Create a folder and inside that folder defining one or more APIs. Encore recognizes this as a service, and uses the folder name as the service name. 2. Add a file named `encore.service.ts` in a directory. The file must export a service instance, by calling `new Service`, imported from `encore.dev/service`: ```ts import {Service} from "encore.dev/service"; export default new Service("my-service"); ``` Encore will consider this directory and all its subdirectories as part of the service. If you already have a Nest app then the easiest way to get going is to go with the second approach and add a `encore.service.ts` in the root of your app, then you do not need to change your existing folder structure. ## Replacing Nest controllers with Encore APIs If you already have a Nest app then you can keep most of your business logic (modules, services and providers) as is but in order for Encore to be able to manage your APIs, you need to replace your Nest controllers with Encore APIs. Let's assume you have a `cats/cats.controller.ts` in your Nest app that looks like this: ```ts -- cats/cats.controller.ts -- @Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) { } @Post() @Roles(['admin']) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } @Get() async findAll(): Promise { return this.catsService.findAll(); } @Get(':id') findOne( @Param('id', new ParseIntPipe()) id: number, ) { return this.catsService.get(id); } } ``` When converting this to using Encore it would look like this: ```ts -- cats/cats.controller.ts -- export const findAll = api( {expose: true, method: 'GET', path: '/cats'}, async (): Promise<{ cats: Cat[] }> => { const {catsService} = await applicationContext; return {cats: await catsService.findAll()}; }, ); export const get = api( {expose: true, method: 'GET', path: '/cats/:id'}, async ({id}: { id: number }): Promise<{ cat: Cat }> => { const {catsService} = await applicationContext; return {cat: await catsService.get(id)}; }, ); export const create = api( {expose: true, auth: true, method: 'POST', path: '/cats'}, async (dto: CreateCatDto): Promise => { const {catsService} = await applicationContext; catsService.create(dto); }, ); ``` We use the `applicationContext` (that we defined above) to access our `catsService` and pass in the necessary parameters. Both Encore and Nest use the concept of a `service`. With Encore you define a service by creating a folder and inside that folder defining one or more APIs. Encore recognizes this as a service, and uses the folder name as the service name. When deploying, Encore will automatically provision the required infrastructure for each service. So in the example above we have a `cats` service with three APIs because `cats.controller.ts` is placed inside a folder named `cats`. ## Making use of other Encore features Encore also allows you to easily make use of other backend primitives in your Nest app, like [Databases](/docs/ts/primitives/databases), [Cron Jobs](/docs/ts/primitives/cron-jobs), [Pub/Sub & Queues](/docs/ts/primitives/pubsub) and [Secrets](/docs/ts/primitives/secrets). Take a look at our [Encore + NestJS example](https://github.com/encoredev/examples/tree/main/ts/nestjs) which uses both a PostgreSQL Database and an [Auth Handler](/docs/ts/develop/auth) to authenticate incoming requests. ## Running your Encore app After those steps we are ready to run our app locally: ```shell $ encore run ``` You should see log messages about both Encore and Nest staring up. That means your local development environment is up and running and ready to take some requests! ### Open the Local Development Dashboard You can now start using your [Local Development Dashboard](/docs/ts/observability/dev-dash). Open [http://localhost:9400](http://localhost:9400) in your browser to access it. The Local Development Dashboard is a powerful tool to help you move faster when you're developing new features. It comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful observability features like [distributed tracing](/docs/ts/observability/tracing). ================================================ FILE: docs/ts/install.md ================================================ --- seotitle: Install Encore to start building seodesc: See how you can install Encore on all platforms, and get started building your next backend application in minutes. title: Installation subtitle: Install the Encore CLI to get started with local development lang: ts --- If you are new to Encore, we recommend following the [quick start guide](/docs/ts/quick-start). ## Install the Encore CLI To develop locally with Encore, you first need to install the Encore CLI. This is what provisions your local development environment, and runs your Local Development Dashboard complete with logs, tracing, and API documentation. ### Prerequisites - [Node.js](https://nodejs.org/en/download/) is required to run Encore.ts apps. - [Docker](https://www.docker.com) is required for Encore to set up local databases. ### Optional: Add AI/LLM instructions To help AI coding assistants (Cursor, Claude Code, GitHub Copilot, etc.) understand how to use Encore, run this from your app directory: ```bash encore llm-rules init ``` This prompts you to select your tool and generates the appropriate config (e.g. `.cursorrules`, `CLAUDE.md`) and MCP setup where supported. For full details and other options, see [AI Tools Integration](/docs/ts/ai-integration). ### Build from source If you prefer to build from source, [follow these instructions](https://github.com/encoredev/encore/blob/main/CONTRIBUTING.md). ## Update to the latest version Check which version of Encore you have installed by running `encore version` in your terminal. It should print something like: ```shell encore version v1.28.0 ``` If you think you're on an older version of Encore, you can easily update to the latest version by running `encore version update` from your terminal. ================================================ FILE: docs/ts/migration/ai-migration.mdx ================================================ --- seotitle: Migrate to Encore.ts Using an AI Agent seodesc: Learn how to use Encore's AI migration skill to automatically migrate your existing backend to Encore.ts, with validation at every step. title: Migrate using AI agent lang: ts --- Encore's AI migration skill analyzes your existing backend, builds a dependency-aware migration plan, and converts your code to Encore.ts — one unit at a time, with validation at every step. It works with any source framework: Express, Fastify, Hono, Koa, NestJS, and more. The skill has been tested with Claude Code but should work with other agents as well. ## Prerequisites Install the Encore skills package in your AI coding tool: ```bash npx add-skill encoredev/skills ``` You'll also need: - The source codebase accessible on your local machine - An Encore project to migrate into (the skill can help create one) - Your source application running locally (optional — enables HTTP comparison validation) ## Starting a migration Create a new Encore app from the "Empty app" template by running: ```bash encore app create ``` From inside your Encore app, open your AI coding tool and ask it to migrate your existing app: ``` Migrate ../path/to/existing/project to Encore.ts by using the encore-migrate skill ``` The skill walks you through four phases: **Discover**, **Plan**, **Migrate**, and **Complete**. ## How it works ### Phase 1 — Discover The AI reads your source codebase and inventories everything: API endpoints, databases, Pub/Sub topics, cron jobs, auth middleware, secrets, and tests. It groups related entities into **migration units** — typically aligned with your existing service boundaries or URL path prefixes — and presents a summary for you to review. You can adjust the groupings before moving on. Split units that are too large, merge ones that are too small, or rename them to match your domain. ### Phase 2 — Plan The AI creates a `migration-plan.md` file and a `migration-plan/` directory in your Encore project. The summary file tracks overall progress and dependency order. Each migration unit gets its own detail file listing every endpoint, database table, and test to migrate. Dependencies determine the order. Secrets and config go first, then databases, auth, leaf services, dependent services, Pub/Sub, and finally cron jobs. ### Phase 3 — Migrate The AI works through one migration unit at a time. For each entity it: 1. **Implements** the Encore equivalent — [API endpoints](/docs/ts/primitives/defining-apis), [database schemas](/docs/ts/primitives/databases), [infrastructure declarations](/docs/ts/primitives/services) 2. **Migrates tests** from the source framework to Encore's [testing patterns](/docs/ts/develop/testing) 3. **Validates** the result using up to three layers (see [Validation](#validation)) 4. **Updates the plan** files to track progress After completing a unit, it suggests the next one based on the dependency order. You can also pick a different unit or tell it to keep going through multiple units. ### Phase 4 — Complete When all units are done, the AI presents a final summary: what was migrated, what was skipped, and what needs manual attention. It suggests a final test suite run and, if your source system has a frontend, recommends reconnecting it to the new Encore backend using the [Request Client](/docs/ts/frontend/request-client). ## Full-stack and monorepo support When the source codebase contains frontend code (React, Vue, Angular, Next.js, etc.), the AI identifies it and marks it as out of scope — only backend code is migrated. For full-stack frameworks like **Next.js**, **Remix**, **Nuxt**, **SvelteKit**, and **Astro**, the AI detects server-side routes (e.g., Next.js `pages/api/` or Remix `loader` functions) and asks what you want to do with them: 1. **Migrate all** server-side routes to Encore 2. **Migrate some** — you pick which ones move 3. **Keep all in the frontend framework** — only migrate standalone backend code This is useful when you want an Encore backend but prefer to keep a thin BFF or SSR data-fetching layer in your frontend framework. ## Validation Every entity is validated before it's marked as migrated. The AI uses three layers: **Test migration** — Source tests are converted to Encore's [testing patterns](/docs/ts/develop/testing) and run. They must pass before the entity is marked as done. **HTTP comparison** — When both systems are running locally, the AI calls the same endpoint on both and compares the HTTP status code and response body structure. This layer is skipped for endpoints with side effects or that require auth credentials the AI can't obtain. **Verification gate** — No entity is marked as `migrated` without concrete evidence from the current session: test output, HTTP comparison results, or your explicit approval to skip. ## Resuming across sessions The migration plan is persisted to files in your Encore project, so you can close your editor and come back later. When you resume, the AI reads `migration-plan.md`, reports the current status, and suggests the next unit to work on. ``` Resume the migration ``` ``` What's left to migrate? ``` ================================================ FILE: docs/ts/migration/express-migration.md ================================================ --- seotitle: Migrate from Express to Encore.ts seodesc: Learn how migrate your Express.js app over to use Encore.ts for better performance and improved development tools. title: Migrating from Express.js lang: ts --- If you have an existing app using [Express.js](https://expressjs.com/) and want to migrate it to Encore.ts, this guide is for you. This guide can also serve as a comparison between the two frameworks. ## Why migrate to Encore.ts? Express.js is a great choice for building simple APIs, but as your application grows you will likely run into limitations. There is a large community around Express.js, providing many plugins and middleware to work around these limitations. However relying heavily on plugins can make it hard to find the right tools for your use case. It also means that you will need to maintain a lot of dependencies. Encore.ts is a framework that aims to make it easier to build robust and type-safe backends with TypeScript. Encore.ts has 0 npm dependencies, is built with performance in mind, and has a lot of built-in features for building production ready backends. You can deploy an Encore.ts app to any hosting service that accepts Docker containers, or use [Encore Cloud](/use-cases/devops-automation) to fully automate your DevOps and infrastructure. ### Performance Unlike a lot of other Node.js frameworks, Encore.ts is not built on top of Express.js. Instead, Encore.ts has its own high-performance runtime, with a multi-threaded, asynchronous event loop written in Rust. The Encore Runtime handles all I/O like accepting and processing incoming HTTP requests. This runs as a completely independent event loop that utilizes as many threads as the underlying hardware supports. The result of this is that Encore.ts performs **9x faster** than Express.js. Learn more about the [Encore.ts Runtime](/blog/event-loops). ### Built-in benefits When using Encore.ts you get a lot of built-in features without having to install any additional dependencies: | Built-in benefits | | | | :----------------------------------------------------- | :--------------------------------------------------: | -------------------------------------------------------------: | | [Pub/Sub integrations](/docs/ts/primitives/pubsub) | [Type-safe API schemas](/docs/ts/primitives/apis) | [API Client generation](/docs/ts/cli/client-generation) | | [Secrets management](/docs/ts/primitives/secrets) | [CORS handling](/docs/ts/develop/cors) | [Local Development Dashboard](/docs/ts/observability/dev-dash) | | [Database integrations](/docs/ts/primitives/databases) | [Architecture Diagrams](/docs/ts/observability/flow) | [Service Catalog](/docs/ts/observability/service-catalog) | | [Request validation](/blog/event-loops) | [Cron Jobs](/docs/ts/primitives/cron-jobs) | [Local tracing](/docs/ts/observability/tracing) | ## Migration guide Below we've outlined two main strategies you can use to migrate your existing Express.js application to Encore.ts. Pick the strategy that best suits your situation and application. ### Forklift migration (quick start) When you quickly want to migrate to Encore.ts and don't need all the functionality to begin with, you can use a forklift migration strategy. This approach moves the entire application over to Encore.ts in one shot, by wrapping your existing HTTP router in a catch-all handler. **Approach benefits** - You can get your application up and running with Encore.ts quickly and start moving features over to Encore.ts while the rest of the application is still untouched. - You will see a partial performance boost right away because the HTTP layer is now running on the Encore Rust runtime. But to get the full performance benefits, you will need to start using Encore's [API declarations](/docs/ts/primitives/defining-apis) and [infrastructure declarations](/docs/ts#explore-how-to-use-each-backend-primitive). **Approach drawbacks** - Because all requests will be proxied through the catch-all handler, you will not be able to get all the benefits from the [distributed tracing](/docs/ts/observability/tracing), which rely on the [Encore application model](/docs/ts/concepts/application-model). - [Encore Flow](/docs/ts/observability/flow) and the [Service Catalog](/docs/ts/observability/service-catalog) will not be able to show you the full picture of your application until you start moving services and APIs over to Encore.ts. - You will not be able to use the [API Client generation](/docs/ts/cli/client-generation) feature until you start defining APIs in Encore.ts. #### 1. Install Encore If this is the first time you're using Encore, you first need to install the CLI that runs the local development environment. Use the appropriate command for your system: - **macOS:** `brew install encoredev/tap/encore` - **Linux:** `curl -L https://encore.dev/install.sh | bash` - **Windows:** `iwr https://encore.dev/install.ps1 | iex` [Installation docs](https://encore.dev/docs/install) #### 2. Add Encore.ts to your project ```bash npm i encore.dev ``` #### 3. Initialize an Encore app Inside your project directory, run the following command to create an Encore app: ```bash encore app init ``` This will create an `encore.app` file in the root of your project. #### 4. Configure your tsconfig.json To the `tsconfig.json` file in the root of your project, add the following: ```json -- tsconfig.json -- { "compilerOptions": { "paths": { "~encore/*": [ "./encore.gen/*" ] } } } ``` When Encore.ts is parsing your code it will specifically look for `~encore/*` imports. #### 5. Define an Encore.ts service When running an app using Encore.ts you need at least one [Encore service](/docs/ts/primitives/services). Apart from that, Encore.ts in not opinionated in how you structure your code, you are free to go with a monolith or microservice approach. Learn more in our [App Structure docs](/docs/ts/primitives/app-structure). In the root of your App, add a file named `encore.service.ts`. The file must export a service instance, by calling `new Service`, imported from `encore.dev/service`: ```ts import {Service} from "encore.dev/service"; export default new Service("my-service"); ``` Encore will consider this directory and all its subdirectories as part of the service. #### 6. Create a catch-all handler for your HTTP router Now let's mount your existing app router under a [Raw endpoint](/docs/ts/primitives/raw-endpoints), which is an Encore API endpoint type that gives you access to the underlying HTTP request. Here's a basic code example: ```typescript import { api, RawRequest, RawResponse } from "encore.dev/api"; import express, { request, response } from "express"; Object.setPrototypeOf(request, RawRequest.prototype); Object.setPrototypeOf(response, RawResponse.prototype); const app = express(); app.get('/foo', (req: any, res) => { res.send('Hello World!') }) export const expressApp = api.raw( { expose: true, method: "*", path: "/!rest" }, app, ); ``` By mounting your existing app router in this way, it will work as a catch-all handler for all HTTP requests and responses. #### 7. Run you app locally You will now be able to run your Express.js app locally using the `encore run` command. #### Next steps: Incrementally move over Encore.ts to get all the benefits You can now gradually break out specific endpoints using the Encore's [API declarations](#apis) and introduce infrastructure declarations for databases and cron jobs etc. This will let Encore.ts understand your application and unlock all Encore.ts features. See the [Feature-by-feature migration](#feature-by-feature-migration) section for more details. You will eventually be able to remove Express.js as a dependency and run your app entirely on Encore.ts. You can also [join Discord](https://encore.dev/discord) to ask questions and meet fellow Encore developers. #### Forklift example
### Full migration This approach aims to fully replace your applications dependency on Express.js with Encore.ts, unlocking all the features and performance of Encore.ts. Below are two examples that you can use to identify the refactoring you will need to do. In the next section you will find a [Feature-by-feature migration](#feature-by-feature-migration) guide to help you with the refactoring details. **Approach benefits** - Get all the advantages of Encore.ts, like [distributed tracing](/docs/ts/observability/tracing) and [architecture diagrams](/docs/ts/observability/flow), which rely on the [Encore application model](/docs/ts/concepts/application-model). - Get the [full performance benefit](https://encore.dev/blog/event-loops) of Encore.ts - **9x faster** than Express.js. **Approach drawbacks** - This approach may require more time and effort up front compared to the [Incremental migration strategy](#incremental-migration-strategy). #### App comparison Here is a side-by-side comparison of an Express.js app and an Encore.ts app. The examples show how to create APIs, handle request validation, error handling, serving static files, and rendering templates. **Express.js**
**Encore.ts**
## Feature-by-feature migration Check out our [Express.js compared to Encore.ts example](https://github.com/encoredev/examples/tree/main/ts/expressjs-migration) on GitHub for all of the code snippets in this feature comparison. ### APIs With Express.js, you create APIs using the `app.get`, `app.post`, `app.put`, `app.delete` functions. These functions take a path and a callback function. You then use the `req` and `res` objects to handle the request and response. With Encore.ts, you create APIs using the `api` function. This function takes an options object and a callback function. The main difference compared to Express.js is that Encore.ts is type-safe, meaning you define the request and response schemas in the callback function. You then return an object matching the response schema. In case you need to operate at a lower abstraction level, Encore supports defining raw endpoints that let you access the underlying HTTP request. Learn more in the [API Schemas docs](/docs/ts/primitives/defining-apis#api-schemas). **Express.js** ```typescript import express, {Request, Response} from "express"; const app: Express = express(); // GET request with dynamic path parameter app.get("/hello/:name", (req: Request, res: Response) => { const msg = `Hello ${req.params.name}!`; res.json({message: msg}); }) // GET request with query string parameter app.get("/hello", (req: Request, res: Response) => { const msg = `Hello ${req.query.name}!`; res.json({message: msg}); }); // POST request example with JSON body app.post("/order", (req: Request, res: Response) => { const price = req.body.price; const orderId = req.body.orderId; // Handle order logic res.json({message: "Order has been placed"}); }); ``` **Encore.ts** ```typescript import {api, Query} from "encore.dev/api"; // Dynamic path parameter :name export const dynamicPathParamExample = api( {expose: true, method: "GET", path: "/hello/:name"}, async ({name}: { name: string }): Promise<{ message: string }> => { const msg = `Hello ${name}!`; return {message: msg}; }, ); interface RequestParams { // Encore will now automatically parse the query string parameter name?: Query; } // Query string parameter ?name export const queryStringExample = api( {expose: true, method: "GET", path: "/hello"}, async ({name}: RequestParams): Promise<{ message: string }> => { const msg = `Hello ${name}!`; return {message: msg}; }, ); interface OrderRequest { price: string; orderId: number; } // POST request example with JSON body export const order = api( {expose: true, method: "POST", path: "/order"}, async ({price, orderId}: OrderRequest): Promise<{ message: string }> => { // Handle order logic console.log(price, orderId) return {message: "Order has been placed"}; }, ); // Raw endpoint export const myRawEndpoint = api.raw( {expose: true, path: "/raw", method: "GET"}, async (req, resp) => { resp.writeHead(200, {"Content-Type": "text/plain"}); resp.end("Hello, raw world!"); }, ); ``` ### Microservices communication Express.js does not have built-in support for creating microservices or for service-to-service communication. You will most likely use `fetch` or something equivalent to call another service. With Encore.ts, calling another service is just like calling a local function, with complete type-safety. Under the hood, Encore.ts will translate this function call into an actual service-to-service HTTP call, resulting in trace data being generated for each call. Learn more in our [Service-to-Service Communication docs](/docs/ts/primitives/app-structure#multi-service-application-distributed-system). **Express.js** ```typescript import express, {Request, Response} from "express"; const app: Express = express(); app.get("/save-post", async (req: Request, res: Response) => { try { // Calling another service using fetch const resp = await fetch("https://another-service/posts", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: req.query.title, content: req.query.content, }), }); res.json(await resp.json()); } catch (e) { res.status(500).json({error: "Could not save post"}); } }); ``` **Encore.ts** ```typescript import {api} from "encore.dev/api"; import {anotherService} from "~encore/clients"; export const microserviceCommunication = api( {expose: true, method: "GET", path: "/call"}, async (): Promise<{ message: string }> => { // Calling the foo endpoint in anotherService const fooResponse = await anotherService.foo(); const msg = `Data from another service ${fooResponse.data}!`; return {message: msg}; }, ); ``` ### Authentication In Express.js you can create a middleware function that checks if the user is authenticated. You can then use this middleware function in your routes to protect them. You will have to specify the middleware function for each route that requires authentication. With Encore.ts, when an API is defined with `auth: true`, you must define an authentication handler in your application. The authentication handler is responsible for inspecting incoming requests to determine what user is authenticated. The authentication handler is defined similarly to API endpoints, using the `authHandler` function imported from `encore.dev/auth`. Like API endpoints, the authentication handler defines what request information it's interested in, in the form of HTTP headers, query strings, or cookies. If a request has been successfully authenticated, the API Gateway forwards the authentication data to the target endpoint. The endpoint can query the available auth data from the `getAuthData` function, available from the `~encore/auth` module. Learn more in our [Auth Handler docs](/docs/ts/develop/auth) **Express.js** ```typescript import express, {NextFunction, Request, Response} from "express"; const app: Express = express(); // Auth middleware function authMiddleware(req: Request, res: Response, next: NextFunction) { // TODO: Validate up auth token and verify that this is an authenticated user const isInvalidUser = req.headers["authorization"] === undefined; if (isInvalidUser) { res.status(401).json({error: "invalid request"}); } else { next(); } } // Endpoint that requires auth app.get("/dashboard", authMiddleware, (_, res: Response) => { res.json({message: "Secret dashboard message"}); }); ``` **Encore.ts** ```typescript import { api, APIError, Gateway, Header } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import { getAuthData } from "~encore/auth"; interface AuthParams { authorization: Header<"Authorization">; } // The function passed to authHandler will be called for all incoming API call that requires authentication. export const myAuthHandler = authHandler( async (params: AuthParams): Promise<{ userID: string }> => { // TODO: Validate up auth token and verify that this is an authenticated user const isInvalidUser = params.authorization === undefined; if (isInvalidUser) { throw APIError.unauthenticated("Invalid user ID"); } return { userID: "user123" }; }, ); export const gateway = new Gateway({ authHandler: myAuthHandler }); // Auth endpoint example export const dashboardEndpoint = api( // Setting auth to true will require the user to be authenticated { auth: true, method: "GET", path: "/dashboard" }, async (): Promise<{ message: string; userID: string }> => { return { message: "Secret dashboard message", userID: getAuthData()!.userID, }; }, ); ``` ### Request validation Express.js does not have built-in request validation. You have to use a library like [Zod](https://github.com/colinhacks/zod). With Encore.ts, request validation for headers, query params and body is. You supply a schema for the request object and in the request payload does not match the schema the API will return a 400 error. Learn more in the [API Schemas docs](/docs/ts/primitives/defining-apis#api-schemas). **Express.js** ```typescript import express, {NextFunction, Request, Response} from "express"; import {z, ZodError} from "zod"; const app: Express = express(); // Request validation middleware function validateData(schemas: { body: z.ZodObject; query: z.ZodObject; headers: z.ZodObject; }) { return (req: Request, res: Response, next: NextFunction) => { try { // Validate headers schemas.headers.parse(req.headers); // Validate request body schemas.body.parse(req.body); // Validate query params schemas.query.parse(req.query); next(); } catch (error) { if (error instanceof ZodError) { const errorMessages = error.errors.map((issue: any) => ({ message: `${issue.path.join(".")} is ${issue.message}`, })); res.status(400).json({error: "Invalid data", details: errorMessages}); } else { res.status(500).json({error: "Internal Server Error"}); } } }; } // Request body validation schemas const bodySchema = z.object({ someKey: z.string().optional(), someOtherKey: z.number().optional(), requiredKey: z.array(z.number()), nullableKey: z.number().nullable().optional(), multipleTypesKey: z.union([z.boolean(), z.number()]).optional(), enumKey: z.enum(["John", "Foo"]).optional(), }); // Query string validation schemas const queryStringSchema = z.object({ name: z.string().optional(), }); // Headers validation schemas const headersSchema = z.object({ "x-foo": z.string().optional(), }); // Request validation example using Zod app.post( "/validate", validateData({ headers: headersSchema, body: bodySchema, query: queryStringSchema, }), (_: Request, res: Response) => { res.json({message: "Validation succeeded"}); }, ); ``` **Encore.ts** ```typescript import {api, Header, Query} from "encore.dev/api"; enum EnumType { FOO = "foo", BAR = "bar", } // Encore.ts automatically validates the request schema and returns and error // if the request does not match the schema. interface RequestSchema { foo: Header<"x-foo">; name?: Query; someKey?: string; someOtherKey?: number; requiredKey: number[]; nullableKey?: number | null; multipleTypesKey?: boolean | number; enumKey?: EnumType; } // Validate a request export const schema = api( {expose: true, method: "POST", path: "/validate"}, (data: RequestSchema): { message: string } => { console.log(data); return {message: "Validation succeeded"}; }, ); ``` ### Error handling In Express.js you either throw an error (which results in a 500 response) or use the `status` function to set the status code of the response. In Encore.ts throwing an error will result in a 500 response. You can also use the `APIError` class to return specific error codes. Learn more in our [API Errors docs](/docs/ts/primitives/errors). **Express.js** ```typescript import express, {Request, Response} from "express"; const app: Express = express(); // Default error handler app.get("/broken", (req, res) => { throw new Error("BROKEN"); // This will result in a 500 error }); // Returning specific error code app.get("/get-user", (req: Request, res: Response) => { const id = req.query.id || ""; if (id.length !== 3) { res.status(400).json({error: "invalid id format"}); } // TODO: Fetch something from the DB res.json({user: "Simon"}); }); ``` **Encore.ts** ```typescript import {api, APIError} from "encore.dev/api"; // Default error handler // Default error handler export const broken = api( {expose: true, method: "GET", path: "/broken"}, async (): Promise => { throw new Error("This is a broken endpoint"); // This will result in a 500 error }, ); // Returning specific error code export const brokenWithErrorCode = api( {expose: true, method: "GET", path: "/broken/:id"}, async ({id}: { id: string }): Promise<{ user: string }> => { if (id.length !== 3) { throw APIError.invalidArgument("invalid id format"); } // TODO: Fetch something from the DB return {user: "Simon"}; }, ); ``` ### Serving static files Express.js has a built-in middleware function to serve static files. You can use the `express.static` function to serve files from a specific directory. Encore.ts also has built-in support for static file serving with the `api.static` method. The files are served directly from the Encore.ts Rust Runtime. This means that zero JavaScript code is executed to serve the files, freeing up the Node.js runtime to focus on executing business logic. This dramatically speeds up both the static file serving, as well as improving the latency of your API endpoints. Learn more in our [Static Assets docs](/docs/ts/primitives/static-assets). **Express.js** ```typescript import express from "express"; const app: Express = express(); app.use("/assets", express.static("assets")); ``` **Encore.ts** ```typescript import { api } from "encore.dev/api"; export const assets = api.static( { expose: true, path: "/assets/*path", dir: "./assets" }, ); ``` ### Template rendering Express.js has a built-in support for rendering templates. With Encore.ts you can use the `api.raw` function to serve HTML templates, in this example we are using Handlebars.js but you can use whichever templating engine you prefer. Learn more in our [Raw Endpoints docs](/docs/ts/primitives/raw-endpoints) **Express.js** ```typescript import express, {Request, Response} from "express"; const app: Express = express(); app.set("view engine", "pug"); // Set view engine to Pug // Template engine example. This will render the index.pug file in the views directory app.get("/html", (_, res) => { res.render("index", {title: "Hey", message: "Hello there!"}); }); ``` **Encore.ts** ```typescript import {api} from "encore.dev/api"; import Handlebars from "handlebars"; const html = `

Hello {{name}}!

`; // Making use of raw endpoints to serve dynamic templates. // https://encore.dev/docs/ts/primitives/raw-endpoints export const serveHTML = api.raw( {expose: true, path: "/html", method: "GET"}, async (req, resp) => { const template = Handlebars.compile(html); resp.setHeader("Content-Type", "text/html"); resp.end(template({name: "Simon"})); }, ); ```
### Testing Express.js does not have built-in testing support. You can use libraries like [Vitest](https://vitest.dev/) and [Supertest](https://www.npmjs.com/package/supertest). With Encore.ts you are able to call the API endpoints directly in your tests, just like any other function. You then run the tests using the `encore test` command. Learn more in our [Testing docs](/docs/ts/develop/testing). **Express.js** ```typescript import {describe, expect, test} from "vitest"; import request from "supertest"; import express from "express"; import getRequestExample from "../get-request-example"; /** * We need to add the supertest library to make fake HTTP requests to the Express.js app without having to * start the server. We also use the vitest library to write tests. */ describe("Express App", () => { const app = express(); app.use("/", getRequestExample); test("should respond with a greeting message", async () => { const response = await request(app).get("/hello/John"); expect(response.status).to.equal(200); expect(response.body).to.have.property("message"); expect(response.body.message).to.equal("Hello John!"); }); }); ``` **Encore.ts** ```typescript import {describe, expect, test} from "vitest"; import {dynamicPathParamExample} from "../get-request-example"; // This test suite demonstrates how to test an Encore route. // Run tests using the `encore test` command. describe("Encore app", () => { test("should respond with a greeting message", async () => { // You can call the Encore.ts endpoint directly in your tests, // just like any other function. const resp = await dynamicPathParamExample({name: "world"}); expect(resp.message).toBe("Hello world!"); }); }); ``` ### Database Express.js does not have built-in database support. You can use libraries like [pg-promise](https://www.npmjs.com/package/pg-promise) to connect to a PostgreSQL database but you also have to manage Docker Compose files for different environments. With Encore.ts, you create a database by importing `encore.dev/storage/sqldb` and calling `new SQLDatabase`, assigning the result to a top-level variable. Database schemas are defined by creating migration files. Each migration runs in order and expresses the change in the database schema from the previous migration. Encore.ts automatically provisions databases to match what your application requires. Encore.ts provisions databases in an appropriate way depending on the environment. When running locally, Encore creates a database cluster using Docker. In the cloud, it depends on the environment type: To query data, use the `.query` or `.queryRow` methods. To insert data, or to make database queryies that don't return any rows, use `.exec`. Learn more in our [Database docs](/docs/ts/primitives/databases). **Express.js** ```typescript -- db.ts -- import express, {Request, Response} from "express"; import pgPromise from "pg-promise"; const app: Express = express(); // Connect to the DB with the credentials from docker-compose.yml const db = pgPromise()({ host: "localhost", port: 5432, database: "database", user: "user1", password: "user1@123", }); interface User { name: string; id: number; } // Get one User from DB app.get("/user/:id", async (req: Request, res: Response) => { const user = await db.oneOrNone( ` SELECT * FROM users WHERE id = $1 `, req.params.id, ); res.json({user}); }); -- docker-compose.yml -- version: '3.8' services: db: build: context: . dockerfile: Dockerfile.postgis # Use custom Dockerfile restart: always environment: POSTGRES_USER: user1 POSTGRES_PASSWORD: user1@123 POSTGRES_DB: database healthcheck: # This command checks if the database is ready, right on the source db server test: [ "CMD-SHELL", "pg_isready" ] interval: 5s timeout: 5s retries: 5 ports: - "5432:5432" volumes: - postgres_data_v:/var/lib/postgresql/data volumes: postgres_data_v: -- Dockerfile.postgis -- FROM postgres:latest # Install PostGIS extension RUN apt-get update \ && apt-get install -y postgis postgresql-12-postgis-3 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # To execute some initial queries, we can write queries in init.sql COPY init.sql /docker-entrypoint-initdb.d/ # Enable PostGIS extension RUN echo "CREATE EXTENSION IF NOT EXISTS postgis;" >> /docker-entrypoint-initdb.d/init.sqld ``` **Encore.ts** ```typescript -- db.ts -- import {api} from "encore.dev/api"; import {SQLDatabase} from "encore.dev/storage/sqldb"; // Define a database named 'users', using the database migrations in the "./migrations" folder. // Encore automatically provisions, migrates, and connects to the database. export const DB = new SQLDatabase("users", { migrations: "./migrations", }); interface User { name: string; id: number; } // Get one User from DB export const getUser = api( {expose: true, method: "GET", path: "/user/:id"}, async ({id}: { id: number }): Promise<{ user: User | null }> => { const user = await DB.queryRow` SELECT name FROM users WHERE id = ${id} `; return {user}; }, ); // Add User from DB export const addUser = api( { expose: true, method: "POST", path: "/user" }, async ({ name }: { name: string }): Promise => { await DB.exec` INSERT INTO users (name) VALUES (${name}) `; return; }, ); -- migrations/1_create_tables.up.sql -- CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE ); ``` ### Logging Express.js does not have built-in support for logging. You can use libraries like [Winston](https://www.npmjs.com/package/winston) to log messages. Encore.ts offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. Logging is integrated with the built-in [Distributed Tracing](/docs/ts/observability/tracing) functionality, and all logs are automatically included in the active trace. **Encore.ts** ```typescript import log from "encore.dev/log"; log.error(err, "something went terribly wrong!"); log.info("log message", {is_subscriber: true}); ``` ================================================ FILE: docs/ts/migration/migrate-away.md ================================================ --- title: Migrate away from Encore subtitle: If you love someone, set them free. lang: ts --- _We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._ Picking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity. When designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. In practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure. ## Accommodating for your unique requirements Many software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example: - You can drop down in abstraction level in the API framework using [raw endpoints](/docs/ts/primitives/defining-apis#raw-endpoints) - You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore ## Mitigating risk through Open Source and efficiency We believe that adopting Encore is a low-risk decision for several reasons: - There's no upfront investment needed to get the benefits - Encore apps are normal programs where less than 1% of the code is Encore-specific - All infrastructure and data is in your own cloud - It's simple to integrate with cloud services and systems not natively supported by Encore - Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes) - Everything you need to self-host your application is [Open Source and documented](/docs/ts/self-host/build) ## What to expect when migrating away If you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away. ### Code changes Building with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework. This means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost. ### Deployment If you are self-hosting your application, then you're already done. If you are using Encore Cloud to manage deployments and want to migrate to your own solution, you can use the [self-hosting instructions](/docs/ts/self-host/build) and Open Source CLI tooling. The `encore build docker` command produces a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility. Learn more in the [self-hosting docs](/docs/ts/self-host/build). ### Tell us what you need We're engineers ourselves and we understand the importance of not being constrained by a single technology. We're working every single day on making it even easier to start, and stop, using Encore. If you have specific concerns, questions, or requirements, we'd love to hear from you! Please reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts. ================================================ FILE: docs/ts/observability/dev-dash.md ================================================ --- seotitle: Development dashboard for local development seodesc: Encore's Local Development Dashboard comes with build-in distributed tracing, API docs, and real-time architecture diagrams. title: Local Development Dashboard subtitle: Built-in tools for simplicity and productivity lang: ts --- Encore provides an efficient local development workflow that automatically provisions [local infrastructure](/docs/platform/infrastructure/infra#local-development) and supports automated testing with dedicated test infrastructure. The local environment also comes with a built-in Local Development Dashboard to simplify development and improve productivity. It has several features to help you design, develop, and debug your application: * [Service Catalog](/docs/ts/observability/service-catalog) with Automatic API Documentation * API Explorer to call your APIs * [Distributed Tracing](/docs/ts/observability/tracing) for simple and powerful debugging * [Encore Flow](/docs/develop/encore-flow) for visualizing your microservices architecture All these features update in real-time as you make changes to your application. To access the dashboard, start your Encore application with `encore run` and it will open automatically. You can also follow the link in your terminal: ```bash $ encore run API Base URL: http://localhost:4000 Dev Dashboard URL: http://localhost:9400/hello-world-cgu2 ``` ================================================ FILE: docs/ts/observability/flow.md ================================================ --- seotitle: Encore Flow automatic microservices architecture diagrams seodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application. title: Flow Architecture Diagram subtitle: Visualize your cloud microservices architecture lang: ts --- Flow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your microservices architecture and identify which services depend on each other and how they work together. ## Birds-eye view Having access to a zoomed out representation of your system can be invaluable in pretty much all parts of the development cycle. Flow helps you: * Track down bottlenecks before they grow into big problems. * Get new team members onboarded much faster. * Pinpoint hot paths in your system, services that might need extra attention. Services and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below the `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or subscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it: ## Highlight dependencies Hover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies. Here the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the database and requests to two of the endpoints from the `user` service as well as requests to one endpoint from the `authentication` service: ## Real-time updates Flow is accessible in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments. When developing locally, Flow will auto update in real-time to reflect your architecture as you make code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones. For cloud environments, Flow auto-updates with each deploy. In the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service: ================================================ FILE: docs/ts/observability/logging.md ================================================ --- seotitle: Use structured logging to understand your application seodesc: Learn how to use structured logging, a combination of free-form log messages and type-safe key-value pairs, to understand your backend application's behavior. title: Logging subtitle: Structured logging helps you understand your application lang: ts infobox: { title: "Structured Logging", import: "encore.dev/log", } --- Encore offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. This enables straightforward analysis of what your application is doing, in a way that is easy for a computer to parse, analyze, and index. This makes it simple to quickly filter and search through logs. Encore’s logging is integrated with the built-in [Distributed Tracing](/docs/ts/observability/tracing) functionality, and all logs are automatically included in the active trace. This dramatically simplifies debugging of your application. ## Usage First, add `import log from "encore.dev/log";` to your module. Then call one of the logging functions `error`, `warn`, `info`, `debug`, or `trace` to emit a log message. For example: ```ts log.info("log message", {is_subscriber: true}) log.error(err, "something went terribly wrong!") ``` The first parameter is the log message (or optionally an error for the error function) . After that follows a single object with key-value pairs for structured logging. If you’re logging many log messages with the same key-value pairs each time it can be a bit cumbersome. To help with that, use `log.with()` to group them into a Logger object, which then copies the key-value pairs into each log event: ```ts const logger = log.with({is_subscriber: true}) logger.info("user logged in", {login_method: "oauth"}) // includes is_subscriber=true ``` ## Live-streaming logs Encore also makes it simple to live-stream logs directly to your terminal, from any environment, by running: ``` $ encore logs --env=prod ``` ================================================ FILE: docs/ts/observability/metrics.md ================================================ --- seotitle: Custom metrics in TypeScript seodesc: Learn how to define and use custom metrics in your TypeScript backend application with Encore. title: Metrics subtitle: Track custom metrics in your TypeScript application infobox: { title: "Metrics", import: "encore.dev/metrics", } lang: ts --- Encore provides built-in support for defining custom metrics in your TypeScript applications. Once defined, metrics are automatically collected and displayed in the Encore Cloud Dashboard, and can be exported to third-party observability services. See the [Platform metrics documentation](/docs/platform/observability/metrics) for information about integrations with third-party services like Grafana Cloud and Datadog. ## Defining custom metrics Define custom metrics by importing from [`encore.dev/metrics`](https://encore.dev/docs/ts/primitives/metrics) and creating a new metric using the `Counter`, `CounterGroup`, `Gauge`, or `GaugeGroup` classes. For example, to count the number of orders processed: ```typescript import { Counter } from "encore.dev/metrics"; export const ordersProcessed = new Counter("orders_processed"); function process(order: Order) { // ... ordersProcessed.increment(); } ``` ## Metric types Encore currently supports two metric types: counters and gauges. **Counters** measure the count of something. A counter's value must always increase, never decrease. (Note that the value gets reset to 0 when the application restarts.) Typical use cases include counting the number of requests, the amount of data processed, and so on. **Gauges** measure the current value of something. Unlike counters, a gauge's value can fluctuate up and down. Typical use cases include measuring CPU usage, the number of active instances running of a process, and so on. ### Counter example ```typescript import { Counter } from "encore.dev/metrics"; export const ordersProcessed = new Counter("orders_processed"); function processOrder() { ordersProcessed.increment(); // ... process order } ``` You can also increment by a specific value instead of 1: ```typescript import { Counter } from "encore.dev/metrics"; export const bytesProcessed = new Counter("bytes_processed"); function processData(data: Buffer) { bytesProcessed.increment(data.length); // ... process data } ``` ### Gauge example ```typescript import { Gauge } from "encore.dev/metrics"; export const cpuUsage = new Gauge("cpu_usage"); function updateMetrics() { const usage = getCpuUsage(); // returns a number between 0-100 cpuUsage.set(usage); } ``` Another example tracking active connections: ```typescript import { Gauge } from "encore.dev/metrics"; let activeCount = 0; export const activeConnections = new Gauge("active_connections"); function onConnect() { activeCount++; activeConnections.set(activeCount); } function onDisconnect() { activeCount--; activeConnections.set(activeCount); } ``` ## Defining labels Encore's metrics package provides a type-safe way of attaching labels to metrics. To define labels, create an interface type representing the labels and then use `CounterGroup` or `GaugeGroup`. The labels interface defines the structure of labels, where each property corresponds to a single label. Each property must be of type `string`, `number`, or `boolean`. When using `number` type for labels, the value will be converted to an integer using `Math.floor()`. ### Counter with labels ```typescript import { CounterGroup } from "encore.dev/metrics"; interface Labels { success: boolean; } export const ordersProcessed = new CounterGroup("orders_processed"); function process(order: Order) { let success = false; try { // ... process order success = true; } catch (err) { success = false; } ordersProcessed.with({ success }).increment(); } ``` ### Gauge with labels ```typescript import { GaugeGroup } from "encore.dev/metrics"; interface ConnectionLabels { region: string; } export const activeConnectionsByRegion = new GaugeGroup("active_connections"); const connectionCounts = new Map(); function onConnect(region: string) { const count = (connectionCounts.get(region) || 0) + 1; connectionCounts.set(region, count); activeConnectionsByRegion.with({ region }).set(count); } function onDisconnect(region: string) { const count = Math.max(0, (connectionCounts.get(region) || 0) - 1); connectionCounts.set(region, count); activeConnectionsByRegion.with({ region }).set(count); } ``` ### Multiple labels You can define multiple labels for a metric: ```typescript import { CounterGroup } from "encore.dev/metrics"; interface JobLabels { jobType: string; priority: number; success: boolean; } export const jobsProcessed = new CounterGroup("jobs_processed"); function processJob(jobType: string, priority: number) { try { // ... process job jobsProcessed.with({ jobType, priority, success: true }).increment(); } catch (err) { jobsProcessed.with({ jobType, priority, success: false }).increment(); } } ``` ## Metric references Encore uses static analysis to determine which services are using each metric, and what operations each service is performing. This means metric objects can't be passed around however you like, as it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions, Encore supports defining a "reference" to a metric that can be passed around any way you want. To create a reference, call the `.ref()` method on any metric: ```typescript import { Counter } from "encore.dev/metrics"; export const ordersProcessed = new Counter("orders_processed"); // Create a reference that can be passed around const metricRef = ordersProcessed.ref(); // Pass the reference to other functions function logMetric(metric: Counter) { metric.increment(); } logMetric(metricRef); ``` This works for all metric types (`Counter`, `CounterGroup`, `Gauge`, and `GaugeGroup`). Each combination of label values creates a unique time series tracked in memory and stored by the monitoring system. Using numerous labels can lead to a combinatorial explosion, causing high cloud expenses and degraded performance. As a general rule, limit the unique time series to tens or hundreds at most, rather than thousands. ================================================ FILE: docs/ts/observability/service-catalog.md ================================================ --- seotitle: Service Catalog & Generated API Docs seodesc: See how Encore automatically generates API documentation that always stays up to date and in sync. title: Service Catalog subtitle: Automatically get a Service Catalog and complete API docs lang: ts --- All developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date. To solve this, Encore uses the [Encore Application Model](/docs/ts/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve. The API docs are available both in your [Local Development Dashboard](/docs/ts/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud). ================================================ FILE: docs/ts/observability/tracing.md ================================================ --- seotitle: Distributed Tracing helps you understand your app seodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore. title: Distributed Tracing subtitle: Track requests across your application and infrastructure lang: ts --- Distributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life. Tracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end. As opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations. You view traces in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments. ## Encore's tracing is more comprehensive and more performant than all other tools Unlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before: * Stack traces * Structured logging * HTTP requests * Network connection information * API calls * Database queries * etc. ## Redacting sensitive data Encore's tracing automatically captures request and response payloads to simplify debugging. For cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports marking endpoints as sensitive. When an endpoint is marked as sensitive, the request and response details from that endpoint will be automatically redacted from the traces it produces. See the documentation on [API Schemas](/docs/ts/primitives/defining-apis#sensitive-data) for more information. ================================================ FILE: docs/ts/overview.md ================================================ --- seotitle: Start building backends using Encore.ts seodesc: Learn how Encore.ts works, and get to know the powerful features that help you build cloud backend applications easier than ever before. title: Encore.ts subtitle: Use Encore.ts to build production-ready backend applications and distributed systems toc: false lang: ts ---

Quick Start Guide

Build your first Encore.ts application in minutes
Encore.ts is an open source backend framework for building type-safe distributed system. It provides a declarative approach to working with essential backend primitives like APIs, microservices, databases, queues, caches, cron jobs, and storage buckets. The framework comes with a lot of built-in tooling for a productive end-to-end developer experience: - **Local Environment Management**: Encore automatically sets up and runs your local development environment and all local infrastructure. - **Enhanced Observability**: Encore comes with tools like a [Local Development Dashboard](/docs/ts/observability/dev-dash), [tracing](/docs/ts/observability/tracing), and a database explorer for monitoring application behavior. - **Automatic Documentation**: Generates and maintains [up-to-date documentation](/docs/ts/observability/service-catalog) for APIs and services, and created [architecture diagrams](/docs/ts/observability/flow) for your system. - **AI Integration:** Encore comes with built-in tools for effective AI assisted development, like [AI instructions](/docs/ts/ai-integration) and an [MCP server](/docs/ts/cli/mcp). - **DevOps Automation Platform (Optional)**: [Encore Cloud](https://encore.cloud) is an optional platform for automating infrastructure provisioning and DevOps processes in your cloud on AWS and GCP.

Watch an intro video

Get to know the core concepts of Encore in this short video.

Example apps

Ready-made starter apps to inspire your development.

Join Discord

Find answers, ask questions, and chat with other Encore developers.
================================================ FILE: docs/ts/primitives/api-calls.mdx ================================================ --- seotitle: API Calls with Encore.ts seodesc: Learn how to make type-safe API calls in TypeScript with Encore.ts title: API Calls subtitle: Making API calls is as simple as making function calls lang: ts --- Calling API endpoints between services, i.e. service-to-service calls, looks like regular function calls with Encore.ts. This gives you a simple monolith-like developer experience, even when you use multiple services. The only thing you need to do is import the service you want to call from `~encore/clients` and then call its API endpoints like functions. This works because, when compiling your application, Encore uses [static analysis](/docs/ts/concepts/application-model) to parse all APIs and make them available through the `~encore/clients` module for internal calls. You get all the benefits of function calls, like compile-time checking of all the parameters and auto-completion in your editor, while still allowing the division of code into logical components, services, and systems. ### Example In the example below, we import the service `hello` and call the `ping` endpoint using a function call to `hello.ping`. ```typescript import { hello } from "~encore/clients"; // import 'hello' service export const myOtherAPI = api({}, async (): Promise => { const resp = await hello.ping({ name: "World" }); console.log(resp.message); // "Hello World!" }); ``` ### Service client references Encore uses static analysis to determine which services are calling each other. This means service client objects can't be passed around however you like, as it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions, Encore supports defining a "reference" to a service client that can be passed around any way you want. To create a reference, call the `.ref()` method on any service client: ```typescript import { hello } from "~encore/clients"; // Create a reference that can be passed around const helloRef = hello.ref(); // Pass the reference to other functions function doSomething(client: typeof helloRef) { return client.ping({ name: "World" }); } doSomething(helloRef); ``` ================================================ FILE: docs/ts/primitives/app-structure.md ================================================ --- seotitle: Structuring your microservices backend application seodesc: Learn how to structure your microservices backend application. See recommended app structures for monoliths, small microservices backends, and large scale microservices applications. title: App Structure subtitle: Structuring your Encore application lang: ts --- Encore uses a monorepo design and it's best to use one Encore app for your entire backend application. This lets Encore build an application model that spans your entire app, necessary to get the most value out of many features like [distributed tracing](/docs/ts/observability/tracing) and [Encore Flow](/docs/ts/observability/flow). If you have a large application, see advice on how to [structure an app with several systems](#large-applications-with-several-systems). It's simple to integrate Encore applications with pre-existing systems you might have, using APIs and built-in tools like [client generation](/docs/ts/cli/client-generation). See more on how to approach building new functionality incrementally with Encore in the [migrating to Encore](/docs/platform/migration/migrate-to-encore) documentation. ## Monolith or Microservices Encore is not opinionated about monoliths vs. microservices. It does however let you build microservices applications with a monolith-style developer experience. For example, you automatically get IDE auto-complete when making [API calls between services](/docs/ts/primitives/api-calls), along with cross-service type-safety. When creating a cloud environment on AWS/GCP, Encore enables you to configure if you want to combine multiple services into one process or keep them separate. This can be useful for improved efficiency at smaller scales, and for co-locating services for increased performance. Learn more in the [environments documentation](/docs/platform/deploy/environments). ## Defining services To create an Encore service, add a file named `encore.service.ts` in a directory. The file must export a service instance, by calling `new Service`, imported from `encore.dev/service`. For example: ```ts import { Service } from "encore.dev/service"; export default new Service("my-service"); ``` That's it! Encore will consider this directory and all its subdirectories as part of the service. Within the service, you can then [define APIs](/docs/ts/primitives/defining-apis) and use infrastructure resources like querying databases. ## Examples Let's take a look at a few different approaches to structuring your Encore application, depending on the size and complexity of your application. ### Single-service application The best place to start, especially if you're new to Encore, is by having a single service in your application. Once you've familiarized yourself with the Encore development model, it's easy to break it up into multiple services. The best way to do this is by defining the `encore.service.ts` in the root of your project, next to the `package.json` file. On disk it might look like this (but feel free to change as you see fit): ``` /my-app ├── package.json ├── encore.app ├── // ... other project files │ ├── encore.service.ts // defines your service root ├── api.ts // API endpoints ├── db.ts // Database definition ``` Services can have subdirectories, so as the complexity of your service grows you can add subdirectories as you see fit, to better organize the code base. ### Multi-service application (Distributed System) For larger applications it's often useful to break it apart into multiple services. This helps improve reliability, scalability, and lead to clearer code organization. Encore makes it easy to structure your application as multiple services. Just like before, you add an `encore.service.ts` file to mark a directory (and its subdirectories) as a service. Note that services cannot be nested: each must be defined in its own directory, and cannot live in a subdirectory of another service. If you have a single-service project with a `encore.service.ts` file at the top-level directory of your project, and you want to break it apart, start by moving that service code into a subdirectory. On disk it might look like this: ``` /my-app ├── encore.app // ... and other top-level project files │ ├── hello // hello service (directory) │   ├── migrations // hello service db migrations (directory) │   │ └── 1_create_table.up.sql // hello service db migration │   ├── encore.service.ts // hello service definition │   ├── hello.ts // hello service APIs │   └── hello_test.ts // tests for hello service │ └── world // world service (directory) │   ├── encore.service.ts // world service definition └── world.ts // world service APIs ``` ### Large applications with several systems If you have a large application with several logical domains, each consisting of multiple services, it can be practical to separate these into distinct systems. Systems are not a special construct in Encore, they only help you divide your application logically around common concerns and purposes. Encore only handles services, the compiler will read your systems and extract the services of your application. As applications grow, systems help you decompose your application without requiring any complex refactoring. To create systems, simply create a sub-directory for each system and put the relevant service packages within it. As an example, a company building a Trello app might divide their application into three systems: the **Trello** system (for the end-user facing app with boards and cards), the **User** system (for user and organization management), and the **Premium** system (for handling payments and subscriptions). On disk it might look like this: ``` /my-trello-clone ├── encore.app ├── package.json // ... and other top-level project files │ ├── trello // trello system (a directory) │   ├── board // board service (a directory) │   │ ├── encore.service.ts // service definition │   │ └── board.ts // service code │   │ │   └── card // card service (a directory) │   ├── encore.service.ts // service definition │ └── card.ts // service code │ ├── premium // premium system (a directory) │   ├── payment // payment service (a directory) │   │ ├── encore.service.ts // service definition │   │ └── payment.ts // service code │   │ │   └── subscription // subscription service (a directory) │   ├── encore.service.ts // service definition │ └── subscription.ts // service code │ └── usr // usr system (a directory) ├── org // org service (a directory) │   ├── encore.service.ts // service definition │   └── org.ts // service code │ └── user // user service (a directory)    ├── encore.service.ts // service definition    └── user.ts // service code ``` The only refactoring needed to divide an existing Encore application into systems is to move services into their respective subfolders. This is a simple way to separate the specific concerns of each system. What matters for Encore are the packages containing services, and the division in systems or subsystems will not change the endpoints or architecture of your application. ## Package Management For Encore.ts projects, using a single root-level `package.json` file (monorepo approach) is the recommended practice. It has several benefits: - Ensures consistent dependency versions across your services - Simplifies TypeScript configuration management - Makes it easier to share common types and utilities - Reduces npm install overhead - Works seamlessly with TypeScript's project references Encore.ts also supports separate `package.json` files in sub-packages, with the following limitations: - The Encore.ts application must use one package with a single `package.json` file - Other separate packages must be pre-transpiled to JavaScript Further package management options are planned for the future, particularly for supporting automatically transpiling and bundling workspace packages. ================================================ FILE: docs/ts/primitives/caching.md ================================================ --- seotitle: Using caches in your TypeScript backend application seodesc: Learn how to implement caches to optimize response times and reduce cost in your TypeScript microservices cloud backend. title: Caching subtitle: Optimize response times and reduce costs by avoiding re-work infobox: { title: "Caching", import: "encore.dev/storage/cache", } lang: ts --- A cache is a high-speed storage layer, commonly used in distributed systems to improve user experiences by reducing latency, improving system performance, and avoiding expensive computation. For scalable systems you typically want to deploy the cache as a separate infrastructure resource, allowing you to run multiple instances of your application concurrently. Encore's built-in Caching API lets you use high-performance caches (using [Redis](https://redis.io/)) in a cloud-agnostic declarative fashion. At deployment, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra). ## Cache clusters To use caching in Encore, you must first define a *cache cluster*. Each cache cluster defined in your application will be provisioned as a separate Redis instance by Encore. This gives you fine-grained control over which service(s) should use the same cache cluster and which should have a separate one. It looks like this: ```typescript import { CacheCluster } from "encore.dev/storage/cache"; const cluster = new CacheCluster("my-cache", { // EvictionPolicy tells Redis how to evict keys when the cache reaches // its memory limit. For typical cache use cases, "allkeys-lru" is a good default. evictionPolicy: "allkeys-lru", }); ``` When starting out it's recommended to use a single cache cluster that's shared between your different services. ### Referencing clusters across services To use the same cache cluster from multiple services, use `CacheCluster.named()` to reference an existing cluster by name instead of creating a new one: ```typescript import { CacheCluster, StringKeyspace } from "encore.dev/storage/cache"; // Reference a cluster defined in another service const cluster = CacheCluster.named("my-cache"); const sessions = new StringKeyspace<{ sessionId: string }>(cluster, { keyPattern: "session/:sessionId", }); ``` ### Eviction policies The eviction policy determines how Redis handles keys when the cache reaches its memory limit: - `"allkeys-lru"` - Evicts least recently used keys first (default) - `"noeviction"` - Returns errors when memory limit is reached - `"allkeys-lfu"` - Evicts least frequently used keys first - `"allkeys-random"` - Evicts random keys - `"volatile-lru"` - Evicts least recently used keys with an expiry set - `"volatile-lfu"` - Evicts least frequently used keys with an expiry set - `"volatile-ttl"` - Evicts keys with shortest TTL first - `"volatile-random"` - Evicts random keys with an expiry set ## Keyspaces When using a cache, each cached item is stored at a particular key, which is typically an arbitrary string. If you use a cache cluster to cache different sets of data, it's important that distinct data sets have non-overlapping keys. Each value stored in the cache also has a specific type, and certain cache operations can only be performed on certain types. For example, a common cache operation is to increment an integer value that is stored in the cache. If you try to apply this operation on a value that is not an integer, an error is returned. Encore provides a simple, type-safe solution to these problems through Keyspaces. In order to begin storing data in your cache, you must first define a Keyspace. Each keyspace has a Key type and a Value type. The Key type is much like a map key, in that it tells Encore where in the cache the item is stored. The Key type is combined with the Key Pattern to produce a string that is the Redis cache key. The Value type is the type of the values stored in that keyspace. For many keyspaces this is specified in the name of the constructor. For example, `StringKeyspace` stores `string` values, `IntKeyspace` stores `number` values (as 64-bit integers). ### Example: Rate limiting For example, if you want to rate limit the number of requests per user ID it looks like this: ```typescript import { CacheCluster, IntKeyspace, expireIn } from "encore.dev/storage/cache"; import { api, APIError } from "encore.dev/api"; import { getAuthData } from "~encore/auth"; const cluster = new CacheCluster("rate-limit", { evictionPolicy: "allkeys-lru", }); // RequestsPerUser tracks the number of requests per user. // The cache items expire after 10 seconds without activity. const requestsPerUser = new IntKeyspace<{ userId: string }>(cluster, { keyPattern: "requests/:userId", defaultExpiry: expireIn(10 * 1000), // 10 seconds in milliseconds }); export const myEndpoint = api( { expose: true, method: "GET", path: "/my-endpoint", auth: true }, async (): Promise<{ message: string }> => { const auth = getAuthData(); if (!auth) { throw APIError.unauthenticated("not authenticated"); } const count = await requestsPerUser.increment({ userId: auth.userID }, 1); if (count > 10) { throw APIError.resourceExhausted("rate limit exceeded"); } return { message: "Hello!" }; } ); ``` As you can see, the `requestsPerUser` defines a `keyPattern` which is set to `"requests/:userId"`. Here `:userId` refers to the field in the key type object. When you call `requestsPerUser.increment({ userId: "user123" }, 1)`, Encore generates the Redis key `"requests/user123"`. ### Key patterns with multiple fields You can define key types with multiple fields to create more complex key patterns: ```typescript interface ResourceKey { userId: string; resourcePath: string; } // ResourceRequestsPerUser tracks the number of requests per user and resource. const resourceRequestsPerUser = new IntKeyspace(cluster, { keyPattern: "requests/:userId/:resourcePath", defaultExpiry: expireIn(10 * 1000), }); // Usage: await resourceRequestsPerUser.increment( { userId: "user123", resourcePath: "api/users" }, 1 ); ``` ## Keyspace types Encore comes with several keyspace types, each designed for different use cases: ### StringKeyspace Stores string values. ```typescript import { StringKeyspace } from "encore.dev/storage/cache"; const tokens = new StringKeyspace<{ tokenId: string }>(cluster, { keyPattern: "token/:tokenId", defaultExpiry: expireIn(3600 * 1000), // 1 hour }); // Set a value await tokens.set({ tokenId: "abc123" }, "user-token-value"); // Get a value (returns undefined on cache miss) const token = await tokens.get({ tokenId: "abc123" }); // Delete a value await tokens.delete({ tokenId: "abc123" }); ``` Additional string operations: - `append(key, value)` - Appends to the existing value - `getRange(key, start, end)` - Gets a substring - `setRange(key, offset, value)` - Overwrites part of the string - `len(key)` - Gets the string length ### IntKeyspace Stores 64-bit integer values. Values are floored to integers using `Math.floor`. For fractional values, use `FloatKeyspace` instead. ```typescript import { IntKeyspace } from "encore.dev/storage/cache"; const counters = new IntKeyspace<{ counterId: string }>(cluster, { keyPattern: "counter/:counterId", }); // Set a value await counters.set({ counterId: "visits" }, 0); // Increment and get new value const newCount = await counters.increment({ counterId: "visits" }, 1); // Decrement const decremented = await counters.decrement({ counterId: "visits" }, 1); ``` ### FloatKeyspace Stores 64-bit floating-point values. ```typescript import { FloatKeyspace } from "encore.dev/storage/cache"; const scores = new FloatKeyspace<{ oddsId: string }>(cluster, { keyPattern: "odds/:oddsId", }); // Set a value await scores.set({ oddsId: "game1" }, 1.5); // Increment by a float amount const newOdds = await scores.increment({ oddsId: "game1" }, 0.1); ``` ### StructKeyspace Stores structured data (objects) serialized as JSON. ```typescript import { StructKeyspace } from "encore.dev/storage/cache"; interface UserProfile { name: string; email: string; preferences: { theme: "light" | "dark"; notifications: boolean; }; } const profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, { keyPattern: "profile/:userId", defaultExpiry: expireIn(3600 * 1000), }); // Set a structured value await profiles.set( { userId: "user123" }, { name: "Alice", email: "alice@example.com", preferences: { theme: "dark", notifications: true }, } ); // Get the value const profile = await profiles.get({ userId: "user123" }); ``` ### StringListKeyspace Stores ordered lists of string values. ```typescript import { StringListKeyspace } from "encore.dev/storage/cache"; const recentItems = new StringListKeyspace<{ userId: string }>(cluster, { keyPattern: "recent/:userId", }); // Push items to the list await recentItems.pushRight({ userId: "user123" }, "item1", "item2"); // Get items from the list const items = await recentItems.getRange({ userId: "user123" }, 0, -1); // Get all // Pop an item (returns undefined if empty) const lastItem = await recentItems.popRight({ userId: "user123" }); ``` ### NumberListKeyspace Stores ordered lists of numeric values. ```typescript import { NumberListKeyspace } from "encore.dev/storage/cache"; const scoreHistory = new NumberListKeyspace<{ playerId: string }>(cluster, { keyPattern: "scores/:playerId", }); // Push scores await scoreHistory.pushRight({ playerId: "player1" }, 100, 200, 150); // Get all scores const scores = await scoreHistory.items({ playerId: "player1" }); ``` ### StringSetKeyspace Stores unordered sets of unique string values. ```typescript import { StringSetKeyspace } from "encore.dev/storage/cache"; const tags = new StringSetKeyspace<{ articleId: string }>(cluster, { keyPattern: "tags/:articleId", }); // Add members to the set await tags.add({ articleId: "post1" }, "typescript", "encore", "backend"); // Check membership const hasTag = await tags.contains({ articleId: "post1" }, "typescript"); // Get all members const allTags = await tags.items({ articleId: "post1" }); // Remove members await tags.remove({ articleId: "post1" }, "backend"); ``` ### NumberSetKeyspace Stores unordered sets of unique numeric values. ```typescript import { NumberSetKeyspace } from "encore.dev/storage/cache"; const uniqueScores = new NumberSetKeyspace<{ gameId: string }>(cluster, { keyPattern: "unique-scores/:gameId", }); // Add scores await uniqueScores.add({ gameId: "game1" }, 100, 200, 300); // Check if a score exists const has100 = await uniqueScores.contains({ gameId: "game1" }, 100); ``` ## Expiry options Encore provides several ways to set cache entry expiration: ```typescript import { expireIn, expireInSeconds, expireInMinutes, expireInHours, expireDailyAt, neverExpire, keepTTL, } from "encore.dev/storage/cache"; // Expire in milliseconds const expiry1 = expireIn(5000); // 5 seconds // Expire in seconds const expiry2 = expireInSeconds(30); // Expire in minutes const expiry3 = expireInMinutes(5); // Expire in hours const expiry4 = expireInHours(24); // Expire at a specific time each day (UTC) const expiry5 = expireDailyAt(0, 0, 0); // Midnight UTC // Never expire (Redis may still evict based on eviction policy) const expiry6 = neverExpire; // Keep existing TTL when updating (for write operations) const expiry7 = keepTTL; ``` ## Write options When setting values, you can override the default expiry: ```typescript // Set with a specific expiry (overrides default) await keyspace.set(key, value, { expiry: expireInMinutes(30) }); // Keep existing TTL when updating await keyspace.set(key, value, { expiry: keepTTL }); // Only set if key doesn't exist (throws CacheKeyExists otherwise) await keyspace.setIfNotExists(key, value); // Only set if key already exists (throws CacheMiss otherwise) await keyspace.replace(key, value); ``` ## Error handling Cache operations can throw specific error types, all extending the base `CacheError` class: - `CacheMiss` — thrown by `replace()` when the key does not exist. - `CacheKeyExists` — thrown by `setIfNotExists()` when the key already exists. Read operations like `get()` return `undefined` on cache miss instead of throwing. ```typescript import { CacheError, CacheMiss, CacheKeyExists } from "encore.dev/storage/cache"; // get returns undefined on cache miss const value = await keyspace.get(key); if (value === undefined) { // Key doesn't exist in cache } // replace throws CacheMiss if the key doesn't exist try { await keyspace.replace(key, newValue); } catch (err) { if (err instanceof CacheMiss) { console.log("Key doesn't exist, can't replace"); } throw err; } ``` ## Local development For local development, Encore maintains a local, in-memory implementation of Redis. This implementation is designed to store a small amount of keys (currently 100). When the number of keys exceeds this value, keys are randomly purged to get below the limit. This is designed in order to simulate the ephemeral, transient nature of caches while also limiting memory use. The precise behavior for local development may change over time and should not be relied on. ================================================ FILE: docs/ts/primitives/cookies.mdx ================================================ --- seotitle: Using Cookies in Encore.ts API Endpoints seodesc: Learn how to work with cookies in your TypeScript backend applications built with Encore.ts title: Working with Cookies subtitle: Type-safe cookie handling for web applications lang: ts --- Encore.ts provides type-safe cookie handling for your API endpoints. Cookies are commonly used for session management, personalization, and tracking. This guide explains how to use cookies in different contexts within your Encore.ts application. ## Where Cookies Can Be Used Cookies can be utilized in three main contexts within Encore.ts: 1. **Auth handler parameters**: Access cookies during authentication 2. **Response cookies**: Set cookies to be sent back to clients 3. **Request cookies**: Access cookies sent by clients ## Using Cookies in Auth Handlers Cookies can be used in authentication handlers: ```ts import { Cookie, Gateway } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; // Define auth parameters with cookies interface AuthParams { sessionId: Cookie<"sessionId">; } // Auth handler that uses cookies const auth = authHandler(async ({ sessionId }) => { return validateAndGetUser(sessionId.value); }); // Configure the gateway with the auth handler export const gateway = new Gateway({ authHandler: auth, }); ``` ## Setting Cookies in Responses You can set cookies in your API responses by including them in your response type: ```ts import { api, Cookie } from "encore.dev/api"; // Define a response type with a cookie interface LoginResponse { user: UserData; sessionId: Cookie<"sessionId">; } // Create an API endpoint that sets a cookie export const login = api({ method: "POST", path: "/login", expose: true, }, async (params): Promise => { // Authenticate user const user = await authenticateUser(params.username, params.password); const sessionId = generateSessionId(); // Store session await storeSession(sessionId, user.id); // Return response with cookie return { user, sessionId: { value: sessionId, httpOnly: true, secure: true, sameSite: "Strict", maxAge: 60 * 60 * 24 * 7, // 7 days } }; }); ``` ## Using Cookies in Requests When creating API endpoints, you can access cookies sent by the client by defining them in your parameter type: ```ts import { api } from "encore.dev/api"; // Define a request type with a cookie interface Params { language?: Cookie<"language">; } // Create an API endpoint that uses the cookie export const get = api( { method: "GET", path: "/user/profile", expose: true, }, async ({ language }) => { // Access the cookie value const lang = language?.value ?? "en"; return { msg: `your language: ${lang}` }; }, ); ``` ## Typed Cookie Values Encore.ts allows you to specify the type of cookie values for improved type safety. By default, cookie values are strings, but you can define cookies with different data types. If you omit the type parameter, Encore.ts will automatically use `string` as the type. ### Available Cookie Types You can use the following types for cookie values: - `string`: Text data (default when generic parameter is omitted) - `number`: Numeric values - `boolean`: True/false values - `Date`: Date objects ### How to Specify Cookie Types When defining cookies, you can specify the type using the generic parameter: ```ts interface UserPreferences { // Cookies with different types userId: Cookie; darkMode: Cookie; lastVisit: Cookie; language: Cookie; // Using omitted type parameter (implicitly string) theme: Cookie<"theme">; } export const savePreferences = api({ method: "POST", path: "/user/preferences", expose: true, }, async (params) => { // Type-safe access to cookie values const userId = params.userId.value; // number const isDarkMode = params.darkMode.value; // boolean const lastVisit = params.lastVisit.value; // Date const language = params.language.value; // string const theme = params.theme.value; // string (implicitly typed) // Save preferences... }); ``` ## Cookie Options When setting cookies, you can configure various options: - **expires**: Sets an expiration date - **maxAge**: Sets the cookie lifetime in seconds - **domain**: Specifies which domains can receive the cookie - **path**: Limits the cookie to a specific path - **secure**: Only sends the cookie over HTTPS - **httpOnly**: Makes the cookie inaccessible to JavaScript - **sameSite**: Controls when cookies are sent with cross-site requests - `"Strict"`: Only sent in same-site requests - `"Lax"`: Sent with same-site requests and top-level navigations - `"None"`: Sent with all requests (requires `secure: true`) - **partitioned**: Creates a partitioned cookie using the CHIPS model ## Generated client The generated Encore.ts client does not explicitly handle cookies. Instead, it relies on the browser's built-in cookie handling. When using the client in browser environments, cookies will be automatically included in requests and stored from responses. For cross-site requests, you need to configure the client to include credentials: ```ts // Create a client that includes credentials (cookies) for cross-site requests const client = new Client(Local, { requestInit: { credentials: "include" } }); ``` ## Best Practices 1. Use `httpOnly: true` for cookies containing sensitive data 2. Set `secure: true` for production environments 3. Configure appropriate `sameSite` settings based on your requirements 4. Use the `maxAge` or `expires` options to limit cookie lifetime By following these guidelines, you can effectively leverage cookies in your Encore.ts applications while maintaining security and type safety. ================================================ FILE: docs/ts/primitives/cron-jobs.md ================================================ --- seotitle: Create recurring tasks with Encore's Cron Jobs API seodesc: Learn how to create periodic and recurring tasks in your backend application using Encore's Cron Jobs API. title: Cron Jobs subtitle: Run recurring and scheduled tasks infobox: { title: "Cron Jobs", import: "encore.dev/cron", example_link: "/docs/tutorials/uptime" } lang: ts --- When you need to run periodic and recurring tasks, Encore.ts provides a declarative way of using Cron Jobs. When a Cron Job is defined in your application, Encore automatically calls your specified API according to the defined schedule. This eliminates the need for infrastructure maintenance, as Encore manages scheduling, monitoring, and execution of Cron Jobs. Cron Jobs do not run when developing locally or in [Preview Environments](/docs/platform/deploy/preview-environments), but you can always call the API manually to test the behavior. ## Defining a Cron Job To define a Cron Job, import `encore.dev/cron` and call `new CronJob`, assigning the result to a top-level variable. ### Example ```ts import { CronJob } from "encore.dev/cron"; import { api } from "encore.dev/api"; // Send a welcome email to everyone who signed up in the last two hours. const _ = new CronJob("welcome-email", { title: "Send welcome emails", every: "2h", endpoint: sendWelcomeEmail, }) // Emails everyone who signed up recently. // It's idempotent: it only sends a welcome email to each person once. export const sendWelcomeEmail = api({}, async () => { // Send welcome emails... }); ``` The `"welcome-email"` argument to `new CronJob` is a unique ID you give to each Cron Job. If you later refactor the code and move the Cron Job definition to another package, Encore uses this ID to keep track that it's the same Cron Job and not a different one. When this code gets deployed Encore will automatically register the Cron Job in Encore Cloud and begin calling the `sendWelcomeEmail` API every two hours. The Encore Cloud dashboard provides a convenient user interface for monitoring and debugging Cron Job executions across all your environments via the `Cron Jobs` menu item: ![Cron Jobs UI](/assets/docs/cron.png) ## Keep in mind when using Cron Jobs - Cron Jobs do not execute during local development or in [Preview Environments](/docs/platform/deploy/preview-environments). However, you can manually invoke the API to test its behavior. - In Encore Cloud, Cron Job executions are limited to **once every hour**, with the exact minute randomized within that hour for users on the Free Tier. To enable more frequent executions or to specify the exact minute within the hour, consider [deploying to your own cloud](/docs/platform/deploy/own-cloud) or upgrading to the [Pro plan](/pricing). - Both public and private APIs are supported for Cron Jobs. - Ensure that the API endpoints used in Cron Jobs are idempotent, as they may be called multiple times under certain network conditions. - API endpoints utilized in Cron Jobs must not accept any request parameters. ## Cron schedules Above we used the `every` field, which executes the Cron Job on a periodic basis. It runs around the clock each day, starting at midnight (UTC). In order to ensure a consistent delay between each run, the interval used **must divide 24 hours evenly**. For example, `10m` and `6h` are both allowed (since 24 hours is evenly divisible by both), whereas `7h` is not (since 24 is not evenly divisible by 7). The Encore compiler will catch this and give you a helpful error at compile-time if you try to use an invalid interval. ### Cron expressions For more advanced use cases, such as running a Cron Job on a specific day of the month, or a specific week day, or similar, the `every` field is not expressive enough. For these use cases, Encore provides full support for [Cron expressions](https://en.wikipedia.org/wiki/Cron) by using the `schedule` field instead of the `every` field. For example: ```ts // Run the monthly accounting sync job at 4am (UTC) on the 15th day of each month. const _ = new CronJob("accounting-sync", { title: "Cron Job Example", schedule: "0 4 15 * *", endpoint: accountingSync, }) ``` ================================================ FILE: docs/ts/primitives/database-extensions.md ================================================ --- seotitle: Pre-installed PostgreSQL extensions seodesc: See the list of pre-installed PostgreSQL extensions available when using Encore title: PostgreSQL Extensions subtitle: Pre-installed extensions infobox: { title: "SQL Databases", import: "encore.dev/storage/sqldb" } lang: go --- Encore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development, CI/CD, and for databases hosted on Encore Cloud. The docker image ships with the following PostgreSQL extensions pre-installed and available for use (via `CREATE EXTENSION`): | Extension | Version | Description | | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------- | | refint | 1.0 | functions for implementing referential integrity (obsolete) | | pg_buffercache | 1.3 | examine the shared buffer cache | | pg_freespacemap | 1.2 | examine the free space map (FSM) | | plpgsql | 1.0 | PL/pgSQL procedural language | | citext | 1.6 | data type for case-insensitive character strings | | adminpack | 2.1 | administrative functions for PostgreSQL | | moddatetime | 1.0 | functions for tracking last modification time | | amcheck | 1.3 | functions for verifying relation integrity | | seg | 1.4 | data type for representing line segments or floating-point intervals | | pg_stat_statements | 1.10 | track planning and execution statistics of all SQL statements executed | | pg_trgm | 1.6 | text similarity measurement and index searching based on trigrams | | isn | 1.2 | data types for international product numbering standards | | btree_gist | 1.7 | support for indexing common datatypes in GiST | | intarray | 1.5 | functions, operators, and index support for 1-D arrays of integers | | pg_surgery | 1.0 | extension to perform surgery on a damaged relation | | uuid-ossp | 1.1 | generate universally unique identifiers (UUIDs) | | insert_username | 1.0 | functions for tracking who changed a table | | bloom | 1.0 | bloom access method - signature file based index | | pgcrypto | 1.3 | cryptographic functions | | dblink | 1.2 | connect to other PostgreSQL databases from within a database | | tsm_system_rows | 1.0 | TABLESAMPLE method which accepts number of rows as a limit | | pg_prewarm | 1.2 | prewarm relation data | | old_snapshot | 1.0 | utilities in support of old_snapshot_threshold | | pageinspect | 1.11 | inspect the contents of database pages at a low level | | intagg | 1.1 | integer aggregator and enumerator (obsolete) | | pg_visibility | 1.2 | examine the visibility map (VM) and page-level visibility info | | cube | 1.5 | data type for multidimensional cubes | | tablefunc | 1.0 | functions that manipulate whole tables, including crosstab | | xml2 | 1.1 | XPath querying and XSLT | | fuzzystrmatch | 1.1 | determine similarities and distance between strings | | pg_walinspect | 1.0 | functions to inspect contents of PostgreSQL Write-Ahead Log | | btree_gin | 1.3 | support for indexing common datatypes in GIN | | sslinfo | 1.2 | information about SSL certificates | | tcn | 1.0 | Triggered change notifications | | hstore | 1.8 | data type for storing sets of (key, value) pairs | | dict_int | 1.0 | text search dictionary template for integers | | earthdistance | 1.1 | calculate great-circle distances on the surface of the Earth | | file_fdw | 1.0 | foreign-data wrapper for flat file access | | autoinc | 1.0 | functions for autoincrementing fields | | ltree | 1.2 | data type for hierarchical tree-like structures | | unaccent | 1.1 | text search dictionary that removes accents | | pgrowlocks | 1.2 | show row-level locking information | | tsm_system_time | 1.0 | TABLESAMPLE method which accepts time in milliseconds as a limit | | dict_xsyn | 1.0 | text search dictionary template for extended synonym processing | | pgstattuple | 1.5 | show tuple-level statistics | | postgres_fdw | 1.1 | foreign-data wrapper for remote PostgreSQL servers | | lo | 1.1 | Large Object maintenance | | postgis_sfcgal-3 | 3.4.2 | PostGIS SFCGAL functions | | address_standardizer_data_us-3 | 3.4.2 | Address Standardizer US dataset example | | address_standardizer-3 | 3.4.2 | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. | | postgis_topology-3 | 3.4.2 | PostGIS topology spatial types and functions | | postgis-3 | 3.4.2 | PostGIS geometry and geography spatial types and functions | | postgis_raster-3 | 3.4.2 | PostGIS raster types and functions | | postgis_tiger_geocoder-3 | 3.4.2 | PostGIS tiger geocoder and reverse geocoder | | vector | 0.7.0 | vector data type and ivfflat and hnsw access methods | | postgis | 3.4.2 | PostGIS geometry and geography spatial types and functions | | address_standardizer | 3.4.2 | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. | | postgis_topology | 3.4.2 | PostGIS topology spatial types and functions | | postgis_tiger_geocoder | 3.4.2 | PostGIS tiger geocoder and reverse geocoder | | address_standardizer_data_us | 3.4.2 | Address Standardizer US dataset example | | postgis_sfcgal | 3.4.2 | PostGIS SFCGAL functions | | postgis_raster | 3.4.2 | PostGIS raster types and functions | ================================================ FILE: docs/ts/primitives/databases.md ================================================ --- seotitle: Using SQL databases for your backend application seodesc: Learn how to use SQL databases for your backend application. See how to provision, migrate, and query PostgreSQL databases using Go and Encore. title: Using SQL databases subtitle: Provisioning, migrating, querying infobox: { title: "SQL Databases", import: "encore.dev/storage/sqldb", example_link: "/docs/tutorials/rest-api" } lang: ts --- Encore treats SQL databases as logical resources and natively supports **PostgreSQL** databases. ## Creating a database To create a database, import `encore.dev/storage/sqldb` and call `new SQLDatabase`, assigning the result to a top-level variable. Use a migration file in a directory `migrations` to define the database schema. For example: ```typescript -- todo/todo.ts -- import { SQLDatabase } from "encore.dev/storage/sqldb"; // Create the todo database and assign it to the "db" variable const db = new SQLDatabase("todo", { migrations: "./migrations", }); // Then, query the database using db.query, db.exec, etc. -- todo/migrations/1_create_table.up.sql -- CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false -- etc... ); ``` As seen above, the `new SQLDatabase()` call takes two parameters: the name of the database, and a configuration object. The configuration object specifies the directory containing the database migration files, which is how you define the database schema. See the [Defining the database schema](#defining-the-database-schema) section below for more details. With this code in place, Encore will automatically create the database using [Docker](https://docker.com) when you run the command `encore run` in your local environment. Make sure Docker is installed and running on your machine before running `encore run`. If your application is already running when you define a new database, you will need to stop and restart `encore run`. This is necessary for Encore to create the new database using Docker. In cloud environments, Encore automatically injects the appropriate configuration to authenticate and connect to the database, so once the application starts up the database is ready to be used. ## Database Migrations Encore automatically handles `up` migrations, while `down` migrations must be run manually. Each `up` migration runs sequentially, expressing changes in the database schema from the previous migration. ### Naming Conventions **File Name Format:** Migration files must start with a number followed by an underscore (`_`), and must increase sequentially. Each file name must end with `.up.sql`. **Examples:** - `1_first_migration.up.sql` - `2_second_migration.up.sql` - `3_migration_name.up.sql` You can also prefix migration files with leading zeroes for better ordering in the editor (e.g., `0001_migration.up.sql`). ### Defining the Database Schema The first migration typically defines the initial table structure. For instance, a `todo` service might create `todo/migrations/1_create_table.up.sql` with the following content: ```sql CREATE TABLE todo_item ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false ); ``` ### Migration File Structure Migration files are created in a `migrations` directory within an Encore service. Each file is named `_.up.sql`, where `` is a sequence number for ordering and `` describes the migration. **Example Directory Structure:** ``` /my-app ├── encore.app // ... other top-level project files │ └── todo // todo service    ├── migrations // database migrations (directory)    │ ├── 1_create_table.up.sql // first migration file    │ └── 2_add_field.up.sql // second migration file    ├── todo.ts // todo service code    └── todo.test.ts // tests for todo service ``` ## Using databases Once you have created the database using `const db = new SQLDatabase(...)` you can start querying and inserting data into the database by calling methods on the `db` variable. ### Querying data To query data, use the following methods: - `db.query`: Returns an asynchronous iterator, yielding rows one by one. - `db.queryRow`: Returns a single row, or `null` if no rows are found. - `db.queryAll`: Returns an array of all rows. - `db.rawQuery`: Similar to `db.query`, but takes a raw SQL string and parameters. - `db.rawQueryRow`: Similar to `db.queryRow`, but takes a raw SQL string and parameters. - `db.rawQueryAll`: Similar to `db.queryAll`, but takes a raw SQL string and parameters. Typical usage looks like this: ```ts const allTodos = await db.query`SELECT * FROM todo_item`; for await (const todo of allTodos) { // Process each todo } ``` Or to query a single todo item by id: ```ts async function getTodoTitle(id: number): string | undefined { const row = await db.queryRow`SELECT title FROM todo_item WHERE id = ${id}`; return row?.title; } ``` Or to query using raw SQL and parameters: ```ts async function getTodoTitle(id: number): string | undefined { const row = await db.rawQueryRow("SELECT title FROM todo_item WHERE id = $1", id); return row?.title; } ``` ### Inserting data To insert data, or to make database queries that don't return any rows, use `db.exec` or `db.rawExec`. For example: ```ts await db.exec` INSERT INTO todo_item (title, done) VALUES (${title}, false) `; ``` Or using raw SQL and parameters: ```ts await db.rawExec( "INSERT INTO todo_item (title, done) VALUES ($1, $2)", title, false ); ``` ### Transactions Transactions allow you to group multiple database operations into a single unit of work. If any operation within the transaction fails, the entire transaction is rolled back, ensuring data consistency. The transaction type implements `AsyncDisposable`, which automatically rolls back the transaction if it is not explicitly committed or rolled back. This ensures that no open transactions are left accidentally. For example: ```ts await using tx = await db.begin(); await db.exec` INSERT INTO todo_item (title, done) VALUES (${title1}, false) `; await db.exec` INSERT INTO todo_item (title, done) VALUES (${title2}, false) `; await tx.commit(); ``` ## Connecting to databases It's often useful to be able to connect to the database from outside the backend application. For example for scripts, ad-hoc querying, or dumping data for analysis. Currently Encore does not expose user credentials for databases in the local environment or for environments on Encore Cloud. You can use a connection string to connect instead, see below. ### Using the Encore CLI Encore's CLI comes with built-in support for connecting to databases: * `encore db shell [--env=]` opens a [psql](https://www.postgresql.org/docs/current/app-psql.html) shell to the database named `` in the given environment. Leaving out `--env` defaults to the local development environment. `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with. * `encore db conn-uri [--env=]` outputs a connection string for the database named ``. When specifying a cloud environment, the connection string is temporary. Leaving out `--env` defaults to the local development environment. * `encore db proxy [--env=]` sets up a local proxy that forwards any incoming connection to the databases in the specified environment. Leaving out `--env` defaults to the local development environment. See `encore help db` for more information on database management commands. ### Using database user credentials For cloud environments on AWS/GCP you can view database user credentials (created by Encore when provisioning databases) via the Cloud Dashboard: * Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure** page for the appropriate environment, and locate the `USERS` section within the relevant **Database Cluster**. ## Handling migration errors When Encore applies database migrations, there's always a possibility the migrations don't apply cleanly. This can happen for many reasons: - There's a problem with the SQL syntax in the migration - You tried to add a `UNIQUE` constraint but the values in the table aren't actually unique - The existing database schema didn't look like you thought it did, so the database object you tried to change doesn't actually exist - ... and so on If that happens, Encore rolls back the migration. If it happens during a cloud deployment, the deployment is aborted. Once you fix the problem, re-run `encore run` (locally) or push the updated code (in the cloud) to try again. Encore tracks which migrations have been applied in the `schema_migrations` table: ```sql database=# \d schema_migrations Table "public.schema_migrations" Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- version | bigint | | not null | dirty | boolean | | not null | Indexes: "schema_migrations_pkey" PRIMARY KEY, btree (version) ``` The `version` column tracks which migration was last applied. If you wish to skip a migration or re-run a migration, change the value in this column. For example, to re-run the last migration, run `UPDATE schema_migrations SET version = version - 1;`. *Note that Encore does not use the `dirty` flag by default.* ## Using an ORM Encore has all the tools needed to support ORMs and migration frameworks out-of-the-box through named databases and migration files. Writing plain SQL might not work for your use case, or you may not want to use SQL in the first place. ORMs like [Prisma](/docs/ts/develop/orms/prisma) and [Drizzle](/docs/ts/develop/orms/drizzle) can be used with Encore by integrating their logic with a system's database. Encore is not restrictive, it uses plain SQL migration files for its migrations. * If your ORM of choice can connect to any database using a standard SQL driver, then it can be used with Encore. * If your migration framework can generate SQL migration files without any modifications, then it can be used with Encore. For more information on using ORMs with Encore, see the [ORMs](/docs/ts/develop/orms) page. ## Sharing databases between services There are two primary ways of sharing a database between services: - You can define the `SQLDatabase` object in a shared module as an exported variable, and reference this object from every service that needs to access the database. - You can define the `SQLDatabase` object in one service using `new SQLDatabase("name", ...)`, and have other services access it by creating a reference using `SQLDatabase.named("name")`. Both approaches have the same effect, but the latter is more explicit. ## PostgreSQL Extensions Encore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development, CI/CD, and for databases hosted on Encore Cloud. This docker image ships with many popular PostgreSQL extensions pre-installed. In particular, [pgvector](https://github.com/pgvector/pgvector) and [PostGIS](https://postgis.net) are available. See [the full list of available extensions](/docs/ts/primitives/databases-extensions). ## Troubleshooting When you run your application locally with `encore run`, Encore will provision local databases using Docker. If this fails with a database error, it can often be resolved if you restart the Encore daemon using `encore daemon` and then try `encore run` again. If this does not resolve the issue, here are steps to resolve common errors: **Error: sqldb: unknown database** This error is often caused by a problem with the initial migration file, such as incorrect naming or location. - Verify that you've [created the migration file](#defining-the-database-schema) correctly, then try `encore run` again. **Error: could not connect to the database** When you can't connect to the database in your local environment, there's likely an issue with Docker: - Make sure that you have [Docker](https://docker.com) installed and running, then try `encore run` again. - If this fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again. **Error: Creating PostgreSQL database cluster Failed** This means Encore was not able to create the database. Often this is due to a problem with Docker. - Check if you have permission to access Docker by running `docker images`. - Set the correct permissions with `sudo usermod -aG docker $USER` (Learn more in the [Docker documentation](https://docs.docker.com/engine/install/linux-postinstall/)) - Then log out and log back in so that your group membership is refreshed. **Error: unable to save docker image** This error is often caused by a problem with Docker. - Make sure that you have [Docker](https://docker.com) installed and running. - In Docker, open **Settings > Advanced** and make sure that the setting `Allow the default Docker socket to be used` is checked. - If it still fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again. **Error: unable to add CA to cert pool** This error is commonly caused by the presence of the file `$HOME/.postgresql/root.crt` on the filesystem. When this file is present the PostgreSQL client library will assume the database server has that root certificate, which will cause the above error. - Remove or rename the file, then try `encore run` again. **Resetting databases** If your local database is in a bad state (e.g. due to an incomplete migration or corrupt data), you can reset it by running: ```shell $ encore db reset ``` This drops and recreates the database, re-running all migrations from scratch. Use `--all` to reset all databases at once. ================================================ FILE: docs/ts/primitives/defining-apis.mdx ================================================ --- seotitle: Defining type-safe TypeScript APIs with Encore.ts seodesc: Learn how to create APIs for your cloud backend application using TypeScript and Encore.ts title: Defining Type-Safe APIs subtitle: Simplifying type-safe API development lang: ts --- Encore.ts simplifies creating type-safe, idiomatic TypeScript API endpoints and provides built-in validation for incoming requests. At their core, APIs in Encore.ts are normal `async` functions with request and response data types defined as TypeScript interfaces, which Encore.ts uses to encode API requests to HTTP messages. Encore.ts also parses your source code to understand the request and response schema of each endpoint, automatically handling validation of incoming requests against your schema. ## Defining API endpoints To define an API endpoint, use the `api` function from the `encore.dev/api` module. This function wraps a regular TypeScript async function, designating it as an API endpoint. Encore.ts then generates the necessary boilerplate at compile-time. In the example below, we define the API endpoint `ping` which accepts `POST` requests and is exposed as `hello.ping` (because our service name is `hello`). ```typescript // inside the hello.ts file import { api } from "encore.dev/api"; export const ping = api( { method: "POST" }, async (p: PingParams): Promise => { return { message: `Hello ${p.name}!` }; }, ); ``` ### Exposing API endpoints to the outside world When you define an API, by default it is not exposed to the outside world, and it can only be called by other APIs within the same Encore application. To expose an API to the internet, add the `expose: true` field to the options object passed in as the first argument to `api`. - `{ expose: false }` – defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs. This is default value if the `expose` field isn't set. - `{ expose: true }` – defines a public API that anybody on the internet can call ### Requiring authentication data To require authentication for an API endpoint, add `auth: true` to the API options. With this option, Encore will first call the authentication handler you've defined to validate the authentication of incoming requests. Setting `auth: true` can also be useful for internal APIs that aren't exposed to the internet. In that case, it means that the internal caller must have valid authentication data associated with its request. Finally, even if an API endpoint does not specify `auth: true`, it will still receive any authentication data that was provided. For more information on defining APIs that require authentication, see the [authentication guide](/docs/ts/develop/auth). ## API Schemas ### Request and response schemas In the example above we defined an API that uses request and response schemas, where the request data is of type `Params` and the response data of type `Response`. That means we need to define them like so: ```typescript -- hello.ts -- import { api } from "encore.dev/api"; // PingParams is the request data for the Ping endpoint. interface PingParams { name: string; } // PingResponse is the response data for the Ping endpoint. interface PingResponse { message: string; } // hello is an API endpoint that responds with a simple response. export const hello = api( { method: "POST", path: "/hello" }, async (p: PingParams): Promise => { return { message: `Hello ${p.name}!` }; }, ); ``` Request and response schemas are both optional. There are four different ways of defining an API: **Using both request and response data:**
`api({ ... }, async (params: Params): Promise => {});` **Only returning a response:**
`api({ ... }, async (): Promise => {});` **With only request data:**
`api({ ... }, async (params: Params): Promise => {});` **Without any request or response data:**
`api({ ... }, async (): Promise => {});` Alternatively, you can express these using type parameters, since `api` is a generic function: **Using both request and response data:**
`api({ ... }, async (params) => {});` **Only returning a response:**
`api({ ... }, async () => {});` **With only request data:**
`api({ ... }, async (params) => {});` **Without any request or response data:**
`api({ ... }, async () => {});` ### Customizing request and response encoding Encore parses the source code to understand the request and response schema of each endpoint. By default, the data is parsed as a JSON body for incoming requests, and written back as JSON responses. This can be customized on a per-field basis, allowing individual fields to be parsed from query strings and HTTP headers with ease. This is done by using the `Header` and `Query` types defined in the `encore.dev/api` module. ### Headers Headers are defined by setting the field type to `Header<"Name-Of-Header">`. It can be used in both request and response data types. In the example below, the `language` field will be fetched from the `Accept-Language` HTTP header. ```ts import { Header } from "encore.dev/api"; interface Params { language: Header<"Accept-Language">; // parsed from header author: string; // not a header } ``` ### Query parameters For `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default, since those HTTP methods do not support request bodies. For other HTTP methods (that do support request bodies), parameters are by default read from the HTTP request body as JSON. In those cases, the `Query` type can be used to specify that a field should be parsed from the query string instead. Query strings are not supported in HTTP responses, and are treated as being part of the HTTP response body in JSON. In the example below, the `limit` field will be read from the `limit` query parameter for all HTTP methods, whereas the `author` field will be parsed from the query string only if the method of the request is `GET`, `HEAD` or `DELETE` (and otherwise from the HTTP request body as JSON). ```ts interface Params { limit: Query; // always a query parameter author: string; // query if GET, HEAD or DELETE, otherwise body parameter } ``` ### Path parameters Path parameters are specified by the `path` field in the API Options in `api` call. To specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path can be parsed as a wildcard parameter by using `*name` with a matching function parameter. Each path parameter (whether a single segment like `:name` or a wildcard parameter like `*name`) must have a matching field in the request data type. For example: ```ts // Retrieves a blog post by its id. export const getBlogPost = api( { method: "GET", path: "/blog/:id/*path" }, async (params: {id: number; path: string}): Promise { // Use id and path to query database... } ) ``` ### Cookie parameters Cookies are defined by setting the field type to `Cookie<"Name-Of-Cookie">`. It can be used in both request and response data types. In the example below we define an optional field that will be parsed from the cookie header. ```ts import { Cookie } from "encore.dev/api"; interface Params { settings?: Cookie<"settings">; // parsed from cookie header } ``` Read more about cookies [here](/docs/ts/primitives/cookies) ### Fallback routes Encore supports defining fallback routes that will be called if no other endpoint matches the request, using the syntax `path: "/!fallback"`. This is often useful when migrating an existing backend service over to Encore, as it allows you to gradually migrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using a raw endpoint with a fallback route. For example: ```ts // Route all requests to the existing HTTP router if no other endpoint matches. export const fallback = api.raw( { expose: true, method: "*", path: "/!path" }, async (req, resp) { // Call old router } ) ``` ## Custom HTTP status codes By default, Encore automatically sets appropriate HTTP status codes for your API responses. We recommend using these default status codes, but there are situations where you might need to set a custom HTTP status code, such as when porting an existing API that clients depend on for specific status codes. To set a custom HTTP status code, include an `HttpStatus` field in your response interface: ```ts import { api, HttpStatus } from "encore.dev/api"; interface Response { msg: string; status: HttpStatus; } export const endpoint = api( { expose: true, method: "GET", path: "/path" }, async (): Promise => { return { msg: "Hello", status: HttpStatus.Created }; } ); ``` The `HttpStatus` enum includes all standard HTTP status codes like `HttpStatus.OK`, `HttpStatus.Created`, `HttpStatus.BadRequest`, etc. ## Raw endpoints In case you need to operate at a lower abstraction level, Encore supports defining raw endpoints that let you access the underlying HTTP request. This is often useful for things like accepting webhooks. Learn more in the [raw endpoints guide](/docs/ts/primitives/raw-endpoints). ## Sensitive data When handling sensitive information like API keys, passwords, or personally identifiable information (PII), you may want to prevent these details from appearing in traces. Encore provides the `sensitive` option for API endpoints for this purpose. To mark an endpoint as sensitive, add `sensitive: true` to the API options: ```typescript export const processPayment = api( { expose: true, method: "POST", path: "/payments", sensitive: true }, async (params: PaymentParams): Promise => { return { /* ... */ }; } ); ``` When `sensitive: true` is set, Encore automatically redacts all request and response payloads and excludes HTTP headers from traces. ================================================ FILE: docs/ts/primitives/errors.md ================================================ --- seotitle: API Errors – Types, Wrappers, and Codes seodesc: See how to return structured error information from your APIs using Encore's errs package, and how to build precise error messages for complex business logic. title: API Errors subtitle: Returning structured error information from your APIs infobox: { title: "API Errors", import: "encore.dev/api", } lang: ts --- Encore provides a standardized format of returning errors from API endpoints. It looks like this: ```json // HTTP 404 Not Found { "code": "not_found", "message": "sprocket not found", "details": null } ``` To return this, throw the `APIError` exception that Encore provides in the `encore.dev/api` module, with the appropriate error code: ```typescript import { APIError, ErrCode } from "encore.dev/api"; throw new APIError(ErrCode.NotFound, "sprocket not found"); // or as a shorthand you can also write: throw APIError.notFound("sprocket not found"); ``` ## Error Codes The `ErrCode` type in the `encore.dev/api` module defines error codes for common error scenarios. They are identical to the codes defined by `gRPC` for interoperability. The table below summarizes the error codes. | Code | String | HTTP Status | |-----------------------|-------------------------|---------------------------| | `OK` | `"ok"` | 200 OK | | `Canceled` | `"canceled"` | 499 Client Closed Request | | `Unknown` | `"unknown"` | 500 Internal Server Error | | `InvalidArgument` | `"invalid_argument"` | 400 Bad Request | | `DeadlineExceeded` | `"deadline_exceeded"` | 504 Gateway Timeout | | `NotFound` | `"not_found"` | 404 Not Found | | `AlreadyExists` | `"already_exists"` | 409 Conflict | | `PermissionDenied` | `"permission_denied"` | 403 Forbidden | | `ResourceExhausted` | `"resource_exhausted"` | 429 Too Many Requests | | `FailedPrecondition` | `"failed_precondition"` | 400 Bad Request | | `Aborted` | `"aborted"` | 409 Conflict | | `OutOfRange` | `"out_of_range"` | 400 Bad Request | | `Unimplemented` | `"unimplemented"` | 501 Not Implemented | | `Internal` | `"internal"` | 500 Internal Server Error | | `Unavailable` | `"unavailable"` | 503 Unavailable | | `DataLoss` | `"data_loss"` | 500 Internal Server Error | | `Unauthenticated` | `"unauthenticated"` | 401 Unauthorized | ## Additional details To attach additional structured details to errors, use the `withDetails` method on an `APIError`. The details will be returned with the error to external clients. ================================================ FILE: docs/ts/primitives/graphql.mdx ================================================ --- seotitle: GraphQL API seodesc: Learn how to create a GraphQL API for your cloud backend application using TypeScript and Encore.ts title: GraphQL subtitle: Serve a GraphQL API under a Raw endpoint lang: ts --- Encore.ts has great support for GraphQL with its type-safe approach to building APIs. Encore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)). ## Concept To serve a GraphQL API, you can leverage [Raw endpoints](/docs/ts/primitives/raw-endpoints). Raw endpoints provide direct access to the underlying HTTP request and response objects, enabling integration with a GraphQL library. Below is an outline of the high-level steps required for setup: 1. Take client requests with a Raw endpoint. 2. Pass along the request object (body, headers, query params, etc.) to the GraphQL library. 3. Use the GraphQL library to handle the queries and mutations. 4. Return the GraphQL response from the Raw endpoint. Which GraphQL library you choose is up to you. It should work any GraphQL library that allows you to pass along the request object and get a GraphQL response object back without having to start a new HTTP server. ## Example Here's an example using [Apollo](https://www.apollographql.com/docs/apollo-server/) to create a GraphQL API: ```ts import { HeaderMap } from "@apollo/server"; import { api } from "encore.dev/api"; const { ApolloServer, gql } = require("apollo-server"); import { json } from "node:stream/consumers"; // Type definition schema const typeDefs = gql` ... `; // Resolver functions const resolvers = { // ... }; const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); export const graphqlAPI = api.raw( { expose: true, path: "/graphql", method: "*" }, async (req, res) => { // Make sure the Apollo server is started server.assertStarted("/graphql"); // Extract headers in a format that Apollo understands const headers = new HeaderMap(); for (const [key, value] of Object.entries(req.headers)) { if (value !== undefined) { headers.set(key, Array.isArray(value) ? value.join(", ") : value); } } // Get response from Apollo server const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({ httpGraphQLRequest: { headers, method: req.method!.toUpperCase(), body: await json(req), search: new URLSearchParams(req.url ?? "").toString(), }, context: async () => { return { req, res }; }, }); // Set headers for (const [key, value] of httpGraphQLResponse.headers) { res.setHeader(key, value); } // Set status code res.statusCode = httpGraphQLResponse.status || 200; // Write response if it's complete if (httpGraphQLResponse.body.kind === "complete") { res.end(httpGraphQLResponse.body.string); return; } // Write response in chunks if it's async for await (const chunk of httpGraphQLResponse.body.asyncIterator) { res.write(chunk); } res.end(); }, ); ``` ## Call REST APIs in resolvers It's often a good idea to create REST endpoints for your business logic and let your resolvers forward requests to those endpoints. This has a few benefits: 1. **Getting traces** - Calls to Encore endpoints results in traces being created, even for internal API calls. Having traces makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)). 2. **Thin resolvers** - By making your REST request/response objects extend the generated GraphQL types, your resolvers will just be thin wrappers around your REST endpoints. 3. **Testing** - You can easily test your resolvers by mocking the API calls. 4. **REST & GraphQL** - You will have both a REST and GraphQL API. Here's an example of how it might look like if you can call a REST API from a resolver: ```graphql -- schema.graphql -- type Query { books: [Book] } type Book { title: String! author: String! } ``` ```ts -- resolver.ts -- // Import the book service from the generated service clients import { book } from "~encore/clients"; import { QueryResolvers } from "../__generated__/resolvers-types"; const queries: QueryResolvers = { books: async () => { // Call book.list to get the list of books const { books } = await book.list(); return books; }, }; export default queries; ``` ```ts -- book.ts -- import { api } from "encore.dev/api"; // Import Book type the generated schema types import { Book } from "../__generated__/resolvers-types"; const db: Book[] = [ { title: "To Kill a Mockingbird", author: "Harper Lee", }, // ... ]; // REST endpoint to get the list of books export const list = api( { expose: true, method: "GET", path: "/books" }, async (): Promise<{ books: Book[] }> => { return { books: db }; }, ); ``` ================================================ FILE: docs/ts/primitives/object-storage.md ================================================ --- seotitle: Using Object Storage in your backend application seodesc: Learn how you can use Object Storage to store files and unstructured data in your backend application. title: Object Storage subtitle: Simple and scalable storage APIs for files and unstructured data infobox: { title: "Object Storage", import: "encore.dev/storage/objects", } lang: ts --- Object Storage is a simple and scalable solution to store files and unstructured data in your backend application. The most common implementation is Amazon S3 ("Simple Storage Service") and its semantics are universally supported by every major cloud provider. Encore.ts provides a cloud-agnostic API for working with Object Storage, allowing you to store and retrieve files with ease. It has support for Amazon S3, Google Cloud Storage, as well as any other S3-compatible implementation (such as DigitalOcean Spaces, MinIO, etc.). Additionally, when you use Encore's Object Storage API you also automatically get: * Automatic tracing and instrumentation of all Object Storage operations * Built-in local development support, storing objects on the local filesystem * Support for integration testing, using a local, in-memory storage backend ## Creating a Bucket The core of Object Storage is the **Bucket**, which represents a collection of files. In Encore, buckets must be declared as package level variables, and cannot be created inside functions. Regardless of where you create a bucket, it can be accessed from any service by referencing the variable it's assigned to. When creating a bucket you can configure additional properties, like whether the objects in the bucket should be versioned. For example, to create a bucket for storing profile pictures: ```ts import { Bucket } from "encore.dev/storage/objects"; export const profilePictures = new Bucket("profile-pictures", { versioned: false }); ``` ## Uploading files To upload a file to a bucket, use the `upload` method on the bucket variable. ```ts const data = Buffer.from(...); // image data const attributes = await profilePictures.upload("my-image.jpeg", data, { contentType: "image/jpeg", }); ``` The `upload` method additionally takes an optional `UploadOptions` parameter for configuring additinal options, like setting the content type (see above), or to reject the upload if the object already exists. ## Downloading files To download a file from a bucket, use the `download` method on the bucket variable: ```ts const data = await profilePictures.download("my-image.jpeg"); ``` The `download` method additionally takes a set of options to configure the download, like downloading a specific version if the bucket is versioned. ## Listing objects To list objects in a bucket, use the `list` method on the bucket variable. It returns an async iterator of `ListEntry` objects that you can use to easily iterate over the objects in the bucket using a `for await` loop. For example, to list all profile pictures: ```ts for await (const entry of profilePictures.list({})) { // Do something with entry } ``` The `ListOptions` type can be used to limit the number of objects returned, or to filter them to a specific key prefix. ## Deleting objects To delete an object from a bucket, use the `remove` method on the bucket variable. For example, to delete a profile picture: ```ts await profilePictures.remove("my-image.jpeg"); ``` ## Retrieving object attributes You can retrieve information about an object using the `attrs` method on the bucket variable. It returns the attributes of the object, like its size, content type, and ETag. For example, to get the attributes of a profile picture: ```ts const attrs = await profilePictures.attrs("my-image.jpeg"); ``` For convenience there is also `exists` which returns a boolean indicating whether the object exists. ```ts const exists = await profilePictures.exists("my-image.jpeg"); ``` ## Configuring Public Buckets To configure a bucket to be publicly accessible, set the `public` property to `true` when creating the bucket. This allows objects in the bucket to be accessed via a public URL. For example, to create a public bucket for storing profile pictures: ```ts export const publicProfilePictures = new Bucket("public-profile-pictures", { public: true, versioned: false }); ``` When self-hosting, see how to configure public buckets in the [infrastructure configuration docs](/docs/go/self-host/configure-infra). When deploying with Encore Cloud it will automatically configure the bucket to be publicly accessible and [configure CDN](/docs/platform/infrastructure/infra#production-infrastructure) for optimal content delivery. ### Accessing Public Objects Once a bucket is configured as public, you can access its objects using the `publicUrl` method. This method returns the public URL for the specified object. For example, to get the public URL of a profile picture: ```ts const url = publicProfilePictures.publicUrl("my-image.jpeg"); console.log(`Public URL: ${url}`); ``` ## Error handling The methods throw exceptions if something goes wrong, like if the object doesn't exist or the operation fails. If an object does not exist, it throws an `ObjectNotFound` error. If an upload fails due to a precondition not being met (like if the object already exists and the `notExists: true` option is set), it throws a `PreconditionFailed` error. Other errors are returned as `ObjectsError` errors (which the above errors also extend). ## Bucket references Encore uses static analysis to determine which services are accessing each bucket, and what operations each service is performing. That information is used for features such as rendering architecture diagrams, and is used by Encore Cloud to provision infrastructure correctly and configure IAM permissions. This means `Bucket` objects can't be passed around however you like, as it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions, Encore supports defining a "reference" to a bucket that can be passed around any way you want. ### Using bucket references Define a bucket reference by calling `bucket.ref()` from within a service, where `DesiredPermissions` is one of the pre-defined permission types defined in the `encore.dev/storage/objects` module. This means you're effectively pre-declaring the permissions you need, and only the methods that are allowed by those permissions are available on the returned reference object. For example, to get a reference to a bucket that can only download objects: ```typescript import { Uploader } from "encore.dev/storage/objects"; const ref = profilePictures.ref(); // You can now freely pass around `ref`, and you can use // `ref.upload()` just like you would `profilePictures.upload()`. ``` To ensure Encore still is aware of which permissions each service needs, the call to `bucket.ref` must be made from within a service, so that Encore knows which service to associate the permissions with. Encore provides permission interfaces for each operation that can be performed on a bucket: * `Downloader` for downloading objects * `Uploader` for uploading objects * `Lister` for listing objects * `Attrser` for getting object attributes * `Remover` for removing objects * `SignedDownloader` for generating signed download URLs for objects * `SignedUploader` for generating signed upload URLs for objects If you need multiple permissions you can combine them using `&`. For example, `profilePictures.ref` gives you a reference that allows calling both `download` and `upload`. For convenience Encore also provides a `ReadWriter` permission that gives complete read-write access to the bucket, granting all the permissions above. It is equivalent to `Downloader & Uploader & Lister & Attrser & Remover`. ## Signed Upload URLs You can use `signedUploadUrl` to create signed URLs to allow clients to upload content directly into the bucket over the internet. The URL is always restricted to one filename, and has a set expiration date. Anyone in possession of the URL can upload data under this filename without any additional authentication. ```typescript const uploadUrl = await profilePictures.signedUploadUrl("my-user-id", {ttl: 7200}) // Pass url to client ``` The client can now `PUT` to this URL with the content as a binary payload. ```bash curl -X PUT --data-binary @/home/me/dog-wizard.jpeg "https://storage.googleapis.com/profile-pictures/my-user-id/?x-goog-signature=b7a1<...>" ``` ### Why signed upload URLs? Signed URLs are an alternative to accepting the content payload directly in your API. Content upload requests are sometimes inconvenient to handle well: they can be long running and very large. With signed URLs, the content flows directly into the storage bucket, and only object IDs and metadata go through your API service. The trade-off is that the upload flow becomes more complex from a client point of view. ## Signed Download URLs You can use `signedDownloadUrl` to create signed URLs to allow clients to download content directly from the bucket, even if it's private. The URL is always restricted to one filename, and has a set expiration date. Anyone in possession of the URL can download the file without any additional authentication. ```typescript const url = await documents.signedDownloadUrl("letter-1234", {ttl: 7200}) // Pass url to client ``` ### Why signed download URLs? Similar to the upload case, signed download URLs is a way to avoid handing large files or bulk traffic through your API. With signed URLs, the content flows directly from the storage bucket, and only object IDs and metadata go through your API service. Note: unless the content is private, prefer serving urls with `publicUrl()` over signed URLs. Public URLs go over CDN, which is typically significantly more performant and cost effective. ================================================ FILE: docs/ts/primitives/pubsub.md ================================================ --- seotitle: Using PubSub in your backend application seodesc: Learn how you can use PubSub as an asynchronous message queue in your backend application, a great approach for decoupling services for better reliability. title: Pub/Sub subtitle: Decoupling services and building asynchronous systems infobox: { title: "Pub/Sub Messaging", import: "encore.dev/pubsub", example_link: "/docs/ts/tutorials/uptime" } lang: ts --- Publishers & Subscribers (Pub/Sub) let you build systems that communicate by broadcasting events asynchronously. This is a great way to decouple services for better reliability and responsiveness. Encore's Backend Framework lets you use Pub/Sub in a cloud-agnostic declarative fashion. At deployment, Encore automatically [provisions the required infrastructure](/docs/platform/infrastructure/infra). ## Creating a Topic The core of Pub/Sub is the **Topic**, a named channel on which you publish events. Topics must be declared as package level variables, and cannot be created inside functions. Regardless of where you create a topic, it can be published to from any service, and subscribed to from any service. When creating a topic, it must be given an event type, a unique name, and a configuration to define its behaviour. For example, to create a topic with events about user signups: ```ts import { Topic } from "encore.dev/pubsub" export interface SignupEvent { userID: string; } export const signups = new Topic("signups", { deliveryGuarantee: "at-least-once", }); ``` ## Publishing events To publish an **Event**, call `publish` on the topic passing in the event object (which is the type specified in the `new Topic` constructor). For example: ```ts const messageID = await signups.publish({userID: id}); // If we get here the event has been successfully published, // and all registered subscribers will receive the event. // The messageID variable contains the unique id of the message, // which is also provided to the subscribers when processing the event. ``` By defining the `signups` topic variable as an exported variable you can also publish to the topic from other services in the same way. ## Subscribing to Events To **Subscribe** to events, you create a Subscription as a top-level variable, by calling the `new Subscription` constructor. Each subscription needs: - the topic to subscribe to - a name which is unique for the topic - a configuration object with at least a `handler` function to process the events - a configuration object For example, to create a subscription to the `signups` topic from earlier: ```ts import { Subscription } from "encore.dev/pubsub"; const _ = new Subscription(signups, "send-welcome-email", { handler: async (event) => { // Send a welcome email using the event. }, }); ``` Subscriptions can be defined in the same service as the topic is declared, or in any other service of your application. Each subscription to a single topic receives the events independently of any other subscriptions to the same topic. This means that if one subscription is running very slowly, it will grow a backlog of unprocessed events. However, any other subscriptions will still be processing events in real-time as they are published. ### Error Handling If a subscription function returns an error, the event being processed will be retried, based on the retry policy configured on that subscription. After the max number of retries is reached,the event will be placed into a dead-letter queue (DLQ) for that subscriber. This allows the subscription to continue processing events until the bug which caused the event to fail can be fixed. Once fixed, the messages on the dead-letter queue can be manually released to be processed again by the subscriber. ## Customizing message delivery ### At-least-once delivery The above examples configure the topic to ensure that, for each subscription, events will be delivered _at least once_. This means that if the topic believes the event was not processed, it will attempt to deliver the message again. **Therefore, all subscription handlers should be [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning).** This helps ensure that if the handler is called two or more times, from the outside there's no difference compared to calling it once. This can be achieved using a database to track if you have already performed the action that the event is meant to trigger, or ensuring that the action being performed is also idempotent in nature. ### Exactly-once delivery Topics can also be configured to deliver events _exactly once_ by setting the `deliveryGuarantee` field to `"exactly-once"`. This enables stronger guarantees on the infrastructure level to minimize the likelihood of message re-delivery. However, there are still some rare circumstances when a message might be redelivered. For example, if a networking issue causes the acknowledgement of successful processing the message to be lost before the cloud provider receives it (the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)). As such, if correctness is critical under all circumstances, it's still advisable to design your subscription handlers to be idempotent. By enabling exactly-once delivery on a topic the cloud provider enforces certain throughput limitations: - AWS: 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)). - GCP: At least 3,000 messages per second across all topics in the region (can be higher on the region see [GCP PubSub Quotas](https://cloud.google.com/pubsub/quotas#quotas)). Exactly-once delivery does not perform message deduplication on the publishing side. If `publish` is called twice with the same message, the message will be delivered twice. ### Message Attributes By default, each field in the event type is encoded as JSON and sent as part of the Pub/Sub message payload. Pub/Sub topics also support sending data as "attributes", which are key-value pairs that enable other behavior like subscriptions that filter messages or ensuring message ordering. To define that a field should be sent as an attribute, define it with the `Attribute` type. For example, to add an attribute named `source`: ```ts import { Topic, Attribute } from "encore.dev/pubsub"; export interface SignupEvent { userID: string; source: Attribute; } export const signups = new Topic("signups", { deliveryGuarantee: "at-least-once", }); ``` ### Ordered Topics Topics are unordered by default, meaning that messages can be delivered in any order. This allows for better throughput on the topic as messages can be processed in parallel. However, in some cases, messages must be delivered in the order they were published for a given entity. To create an ordered topic, configure the topic's `orderingAttribute` to match the name of a top-level `Attribute` field in the event type. This field ensures that messages delivered to the same subscriber are delivered in the order of publishing for that specific field value. Messages with a different value on the ordering attribute are delivered in an unspecified order. To maintain topic order, messages with the same ordering key aren't delivered until the earliest message is processed or dead-lettered, potentially causing delays due to [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking). Mitigate processing issues by ensuring robust logging and alerts, and appropriate subscription retry policies. The `orderingAttribute` currently has no effect in local environments. #### Throughput limitations Each cloud provider enforces certain throughput limitations for ordered topics: - **AWS:** 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)) - **GCP:** 1 MBps for each ordering key (See [GCP Pub/Sub Resource Limits](https://cloud.google.com/pubsub/quotas#resource_limits)) #### Ordered topic example ```ts import { Topic, Attribute } from "encore.dev/pubsub"; export interface CartEvent { shoppingCartID: Attribute; event: string; } export const cartEvents = new Topic("cart-events", { deliveryGuarantee: "at-least-once", orderingAttribute: "shoppingCartID", }) async function example() { // These are delivered in order as they all have the same shopping cart ID await cartEvents.publish({shoppingCartID: 1, event: "item_added"}); await cartEvents.publish({shoppingCartID: 1, event: "checkout_started"}); await cartEvents.publish({shoppingCartID: 1, event: "checkout_completed"}); // This may be delivered at any point as it has a different shopping cart ID. await cartEvents.publish({shoppingCartID: 2, event: "item_added"}); } ``` ## Topic references Encore uses static analysis to determine which services are accessing each Pub/Sub topic, and what operations each service is performing. That information is used for features such as rendering architecture diagrams, and is used by Encore Cloud to provision infrastructure correctly and configure IAM permissions. This means `Topic` objects can't be passed around however you like, as it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions, Encore supports defining a "reference" to a topic that can be passed around any way you want. ### Using topic references Define a topic reference by calling `topic.ref()` from within a service, where `DesiredPermissions` is one of the pre-defined permission types defined in the `encore.dev/pubsub` module. This means you're effectively pre-declaring the permissions you need, and only the methods that are allowed by those permissions are available on the returned reference object. For example, to get a reference to a topic that can publish messages: ```typescript import { Publisher } from "encore.dev/pubsub"; const ref = cartEvents.ref(); // You can now freely pass around `ref`, and you can use // `ref.publish()` just like you would `cartEvents.publish()`. ``` To ensure Encore still is aware of which permissions each service needs, the call to `topic.ref` must be made from within a service, so that Encore knows which service to associate the permissions with. Currently, the only permission type is `Publisher`, which allows publishing events to the topic. We plan to add more permission types in the future. ================================================ FILE: docs/ts/primitives/raw-endpoints.mdx ================================================ --- seotitle: Raw Endpoints seodesc: Learn how to create raw API endpoints for your cloud backend application using TypeScript and Encore.ts title: Defining Raw Endpoints subtitle: Drop down in abstraction to access the raw HTTP request lang: ts --- Sometimes you need to operate a lower abstraction than Encore.ts normally provides. For example, you might want to access the underlying HTTP request, often useful for things like accepting webhooks. Encore.ts has you covered using "raw endpoints". To define a raw endpoint, use the `api.raw` function. It works similarly to `api`, but does not accept a request and response schema. Instead, it works like the Node.js `http` module and `Express.js`, where the function receives two parameters: a request object and a response writer. It looks like this: ```ts import { api } from "encore.dev/api"; export const myRawEndpoint = api.raw( { expose: true, path: "/raw", method: "GET" }, async (req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("Hello, raw world!"); }, ); ``` It can be called like so: ```shell $ curl http://localhost:4000/raw Hello, raw world! ``` ================================================ FILE: docs/ts/primitives/secrets.md ================================================ --- seotitle: Securely storing API keys and secrets seodesc: Learn how to store API keys, and secrets, securely for your backend application. Encore's built in vault makes it simple to keep your app secure. title: Storing Secrets and API keys subtitle: Simply storing secrets securely lang: ts --- Wouldn't it be nice to store secret values like API keys, database passwords, and private keys directly in the source code? Of course, we can’t do that – it's horrifyingly insecure! (Unfortunately, it's also [very common](https://www.ndss-symposium.org/ndss-paper/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/).) Encore's built-in secrets manager makes it simple to store secrets in a secure way and lets you use them in your program like regular variables. ## Using secrets in your application To use a secret in your application, define a top-level variable directly in your code by calling the `secret` function from `encore.dev/config`. For example: ```ts import { secret } from "encore.dev/config"; // Personal access token for deployments const githubToken = secret("GitHubAPIToken"); // Then, resolve the secret value by calling `githubToken()`. ``` When you've defined a secret in your program, the Encore compiler will check that they are set before running or deploying your application. When running your application locally, if a secret is not set, you will get a warning notifying you that a secret value is missing. When deploying to a cloud environment, all secrets must be defined, otherwise the deploy will fail. Once you've provided values for all secrets, call the secret as a function. For example: ```ts async function callGitHub() { const resp = await fetch("https:///api.github.com/user", { credentials: "include", headers: { Authorization: `token ${githubToken()}`, }, }); // ... handle resp } ``` Secret keys are globally unique for your whole application. If multiple services use the same secret name they both receive the same secret value at runtime. ## Storing secret values ### Using the Encore Cloud dashboard The simplest way to set up secrets is with the Secrets Manager in the Encore Cloud Dashboard. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), go to **Settings** in the main navigation, and then click on **Secrets** in the settings menu. From here you can create secrets, save secret values, and configure different values for different environments. ### Using the CLI If you prefer, you can also set up secrets from the CLI using:
`encore secret set --type ` `` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`. For example `encore secret set --type prod SSHPrivateKey` sets the secret value for production environments,
and `encore secret set --type dev,preview,local GitHubAPIToken` sets the secret value for development, preview, and local environments. In some cases, it can be useful to define a secret for a specific environment instead of an environment type. You can do so with `encore secret set --env `. Secret values for specific environments take precedence over values for environment types. ### Environment settings Each secret can only have one secret value for each environment type. For example: If you have a secret value that's shared between `development`, `preview` and `local`, and you want to override the value for `local`, you must first edit the existing secret and remove `local` using the Secrets Manager in the [Encore Cloud dashboard](https://app.encore.cloud). You can then add a new secret value for `local`. The end result should look something like the picture below. ### Overriding local secrets When setting secrets via the `encore secret set` command, they are automatically synced to all developers working on the same application, courtesy of the Encore Platform. In some cases, however, you want to override a secret only for your local machine. This can be done by creating a file named `.secrets.local.cue` in the root of your Encore application, next to the `encore.app` file. The file contains key-value pairs of secret names to secret values. For example: ```cue GitHubAPIToken: "my-local-override-token" SSHPrivateKey: "custom-ssh-private-key" ``` ## How it works: Where secrets are stored When you store a secret Encore stores it encrypted using Google Cloud Platform's [Key Management Service](https://cloud.google.com/security-key-management) (KMS). - **Production / Your own cloud:** When you deploy to production using your own cloud account on GCP or AWS, Encore provisions a secrets manager in your account (using either KMS or AWS Secrets Manager) and replicates your secrets to it. The secrets are then injected into the container using secret environment variables. - **Local:** For local secrets Encore automatically replicates them to developers' machines when running `encore run`. - **Development / Encore Cloud:** Environments on Encore's development cloud (running on GCP under the hood) work the same as self-hosted GCP environments, using GCP Secrets Manager. ================================================ FILE: docs/ts/primitives/services.mdx ================================================ --- seotitle: Defining Services with Encore.ts seodesc: Learn how to create microservices and define APIs for your cloud backend application using TypeScript and Encore. The easiest way of building cloud backends. title: Defining Services subtitle: Simplifying (micro-)service development lang: ts --- Encore.ts makes it simple to build applications with one or many services, without needing to manually handle the typical complexity of developing microservices. ## Defining services To create an Encore service, add a file named `encore.service.ts` in a directory. The file must export a service instance, by calling `new Service`, imported from `encore.dev/service`. For example: ```ts import { Service } from "encore.dev/service"; export default new Service("my-service"); ``` That's it! Encore will consider this directory and all its subdirectories as part of the service. With multiple services, each service lives in its own directory with its own `encore.service.ts`: ``` /my-app ├── package.json ├── encore.app │ ├── hello // hello service │ ├── encore.service.ts │ └── hello.ts │ └── world // world service ├── encore.service.ts └── world.ts ``` For more on how to structure your application, see the [app structure guide](/docs/ts/primitives/app-structure). ================================================ FILE: docs/ts/primitives/static-assets.mdx ================================================ --- seotitle: Serve static assets seodesc: Learn how to serve static assets with Encore.ts title: Static Assets subtitle: How to serve static assets lang: ts --- Encore.ts has built-in support for serving static assets (such as images, HTML and CSS files, and JavaScript files). This is particularly useful when you want to serve a static website or a single-page application (SPA) that has been pre-compiled into static files. ## API Reference Serving static files in Encore.ts works similarly to regular API endpoints, but using the `api.static` function instead. ```typescript import { api } from "encore.dev/api"; export const assets = api.static( { expose: true, path: "/frontend/*path", dir: "./assets" }, ); ``` This will serve all files in the `./assets` directory under the `/frontend` path prefix. Encore automatically serves `index.html` files at the root of a directory. In the case above, that means that requesting the URL `/frontend` will serve the file `./assets/index.html`, and `/frontend/hello` will serve the file `./assets/hello` or `./assets/hello/index.html` (whichever exists). ### Serving static files at the root By default, Encore requires that API endpoint paths don't conflict with other API endpoints. This can cause problems when you want to serve static files at the root of your domain (such as by setting `path: "/*path"`), since that would conflict with all other paths. To support this use case, Encore allows defining a route as a "fallback route", that gets called only when no other API endpoint matches. Fallback routes use the syntax `!path` instead of `*path`. It looks like this: ```typescript import { api } from "encore.dev/api"; export const assets = api.static( { expose: true, path: "/!path", dir: "./assets" }, ); ``` ### Configuring the 404 response When a file matching the request isn't found, Encore automatically serves a 404 Not Found response. You can customize the response by setting the `notFound` option to specify a file that should be served instead: ```typescript import { api } from "encore.dev/api"; export const assets = api.static( { expose: true, path: "/!path", dir: "./assets", notFound: "./not_found.html" }, ); ``` ## Performance When defining static files, the files are served directly from the Encore.ts Rust Runtime. This means that zero JavaScript code is executed to serve the files, freeing up the Node.js runtime to focus on executing business logic. This dramatically speeds up both the static file serving, as well as improving the latency of your API endpoints. ================================================ FILE: docs/ts/primitives/streaming-apis.mdx ================================================ --- seotitle: Developing Streaming APIs seodesc: Learn how to create services that stream data. title: Streaming APIs subtitle: How to create APIs that stream data lang: ts --- Encore makes it easy to create API endpoints that can stream data to and from your applications. ## Different kinds of stream Encore supports three types of streams, each designed for a specific data flow direction: - [**StreamIn**](#streamin): When you need to stream data into your service. - [**StreamOut**](#streamout): When you need to stream data out from your service. - [**StreamInOut**](#streaminout): When you need to stream data into and out of your service. ## How it works When you connect to a streaming API endpoint, the client and server will do a handshake in the form of a HTTP request. If the server accepts the handshake request, a stream is returned to the client and to the API handler. Under the hood the stream is a WebSocket that can be used to send and receive messages over. Path parameters, query parameters and headers can be passed via the handshake request. The stream returned to the client and to the API handler are typed with the incoming and outgoing message types that you specify in your API. ## Defining streaming APIs Similar to how you can define [RESTful API endpoints](/docs/ts/primitives/defining-apis) with Encore, you can also easily define type-safe streaming API endpoints. They accept a handshake type, an incoming and an outgoing message type (depending on your choice of stream direction). The type parameters are required for Encore to understand your API. If you don't need any data from the handshake, you can ignore that type, and only specify the incoming and outgoing message types. ### StreamIn Use `api.streamIn` when you want to have a stream from client to server, for example if you are uploading something from the client to the server: ```typescript import { api } from "encore.dev/api"; import log from "encore.dev/log"; // Used to pass initial data, optional. interface Handshake { user: string; } // What the clients sends over the stream. interface Message { data: string; done: boolean; } // Returned when the stream is done, optional. interface Response { success: boolean; } export const uploadStream = api.streamIn( { path: "/upload", expose: true }, async (handshake, stream) => { const chunks: string[] = []; try { // The stream object is an AsyncIterator that yields incoming messages. for await (const data of stream) { chunks.push(data.data); // Stop the stream if the client sends a "done" message if (data.done) break; } } catch (err) { log.error(`Upload error by ${handshake.user}:`, err); return { success: false }; } log.info(`Upload complete by ${handshake.user}`); return { success: true }; }, ); ``` For `api.streamIn` you need to specify the incoming message type, the handshake type is optional. You can also specify a optional outgoing type if your API handler responds with some data when it is done with the incoming stream. ```ts api.streamIn( {...}, async (handshake, stream): Promise => {...}) ``` ```ts api.streamIn( {...}, async (handshake, stream) => {...}) ``` ```ts api.streamIn( {...}, async (stream): Promise => {...}) ``` ```ts api.streamIn( {...}, async (stream) => {...}) ``` ### StreamOut Use `api.streamOut` if you want to have a stream of messages from the server to client, for example if you are streaming logs from the server: ```typescript import { api, StreamOut } from "encore.dev/api"; import log from "encore.dev/log"; // Used to pass initial data, optional. interface Handshake { rows: number; } // What the server sends over the stream. interface Message { row: string; } export const logStream = api.streamOut( { path: "/logs", expose: true }, async (handshake, stream) => { try { for await (const row of mockedLogs(handshake.rows, stream)) { // Send the message to the client await stream.send({ row }); } } catch (err) { log.error("Upload error:", err); } }, ); // This function generates an async iterator that yields mocked log rows async function* mockedLogs(rows: number, stream: StreamOut) { for (let i = 0; i < rows; i++) { yield new Promise((resolve) => { setTimeout(() => { resolve(`Log row ${i + 1}`); }, 500); }); } // Close the stream when all logs have been sent await stream.close(); } ``` For `api.streamOut` you need to specify the outgoing message type, the handshake type is optional. ```ts api.streamOut( {...}, async (handshake, stream) => {...}) ``` ```ts api.streamOut( {...}, async (stream) => {...}) ``` ### StreamInOut Use `api.streamInOut` when you want to stream messages in both directions, for example if you are building a chat application: ```typescript import { api } from "encore.dev/api"; interface InMessage { // ... } interface OutMessage { // ... } export const ChatStream = api.streamInOut( { path: "/chat", expose: true }, async (stream) => { for await (const chatMessage of stream) { // Respond to the message by sending something back await stream.send({ /* ... */ }) } } ); ``` For `api.streamInOut` you need to specify both the incoming and outgoing message types, the handshake type is optional. ```ts api.streamInOut( {...}, async (handshake, stream) => {...}) ``` ```ts api.streamInOut( {...}, async (stream) => {...}) ``` ## Handshake When you connect to a streaming API endpoint, the client and server will do a handshake in the form of a HTTP request. For all stream types the handshake type is optional, and only needs to be used whenever you need data from the initial request, such as path parameters, query parameters or headers. Note that if you add a handshake data type you also get two arguments to your handler, one for the handshake data and one for the stream, and if you omit the handshake type you only get the stream. ## Requiring authentication You can use your `authHandler` in the same way as for regular endpoints, just specify `auth: true` in your endpoint options. The auth data will be passed from the client to the server via query parameters or headers in the initial handshake request. After a request has been successfully authenticated, you can access authentication data passed from the `authHandler` by calling `getAuthData()`. See more details in the [auth handler docs](/docs/ts/develop/auth#authentication-handlers). ## Broadcasting messages To broadcast messages to all connected clients, you can store the streams in a map and iterate over them when a new message is received. If a client disconnects, you can remove the stream from the map. ```ts import { api, StreamInOut } from "encore.dev/api"; // Map to hold all connected streams const connectedStreams: Map< string, StreamInOut > = new Map(); // Object sent from the client to the server when establishing a connection interface HandshakeRequest { id: string; } // Object by both server and client interface ChatMessage { username: string; msg: string; } export const chat = api.streamInOut( { expose: true, auth: false, path: "/chat" }, async (handshake, stream) => { connectedStreams.set(handshake.id, stream); try { // The stream object is an AsyncIterator that yields incoming messages. // The loop will continue as long as the client keeps the connection open. for await (const chatMessage of stream) { for (const [key, val] of connectedStreams) { try { // Send the users message to all connected clients. await val.send(chatMessage); } catch (err) { // If there is an error sending the message, remove the client from the map. connectedStreams.delete(key); } } } } catch (err) { // If there is an error reading from the stream, remove the client from the map. connectedStreams.delete(handshake.id); } // When the client disconnects, remove them from the map. connectedStreams.delete(handshake.id); }, ); ``` ## Connecting with the client Using the [generated client](/docs/ts/cli/client-generation), you can connect to a streaming API endpoint that have `expose` set to `true`. The client stream acts as an async iterator, allowing you to retrieve messages by simply iterating over it: ```typescript const stream = client.serviceName.endpointName(); for await (const msg of stream) { // Do something with each message } ``` To send messages to the service, use the async `send` method: ```typescript const stream = client.serviceName.endpointName(); await stream.send({ ... }); ``` To handle network errors or do some cleanup after the connection is closed, you can attach event listeners on the underlying socket: ```typescript const stream = client.serviceName.endpointName(); stream.socket.on("error", (event) => { // An error occurred }); stream.socket.on("close", (event) => { // Connection was closed }); ``` ## Service to service streaming Like with [other endpoint types](/docs/ts/primitives/api-calls) you can easily use streaming between services by importing `~encore/clients`. If you want the stream to only be reachable by other services (and not from the public internet), set the `expose` option to false. Example of using a stream endpoint from a regular api endpoint: ```typescript import { chat } from "~encore/clients"; // import 'chat' service export const myOtherAPI = api({}, async (): Promise => { const stream = await chat.myStreamingEndpoint(); // send a message to the chat service over the stream await stream.send({ msg: "data" }); for await (const msg of stream) { // handle incoming message } }); ``` ================================================ FILE: docs/ts/primitives/types.mdx ================================================ --- seotitle: Types in Encore.ts API schemas seodesc: Learn how to work with types in Encore.ts schemas title: Types subtitle: Types in API schemas lang: ts --- When you define APIs in Encore.ts, the TypeScript types you use for request and response data are analyzed to generate your API schema. This schema is used for automatic validation, API documentation, and generating type-safe clients. To ensure your API schema can be properly represented and serialized, Encore.ts reduces complex TypeScript types into basic types that can be represented in JSON. This means your API schemas should use simple, serializable types like strings, numbers, booleans, objects, and arrays. ## Decimal JavaScript's native `number` type uses floating-point arithmetic, which can lead to precision errors when working with decimal values. For example, `0.1 + 0.2` equals `0.30000000000000004` instead of `0.3`. Additionally, JavaScript numbers are limited to values between `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` (approximately ±9 quadrillion). To handle decimal values with arbitrary precision and arbitrarily large numbers, Encore.ts provides the `Decimal` type from `encore.dev/types`. This type is especially useful for financial calculations, prices, scientific computations, or any scenario where exact decimal precision and large number support are required. ### Using Decimal in APIs The `Decimal` type can be used in your API request and response schemas just like any other type: ```typescript import { api } from "encore.dev/api"; import { Decimal } from "encore.dev/types"; interface PaymentRequest { amount: Decimal; currency: string; } interface PaymentResponse { total: Decimal; tax: Decimal; } export const processPayment = api( { expose: true, method: "POST", path: "/payments" }, async (req: PaymentRequest): Promise => { const taxRate = new Decimal("0.15"); // 15% tax const tax = req.amount.mul(taxRate); const total = req.amount.add(tax); return { total, tax }; } ); ``` ### Creating Decimal values You can create a `Decimal` from strings, numbers, or bigints: ```typescript import { Decimal } from "encore.dev/types"; const price1 = new Decimal("19.99"); const price2 = new Decimal(29.99); const price3 = new Decimal(100n); ``` For maximum precision, it's recommended to use string literals when creating `Decimal` values to avoid any floating-point conversion issues. ### Arithmetic operations The `Decimal` type supports basic arithmetic operations: ```typescript const a = new Decimal("10.50"); const b = new Decimal("2.25"); const sum = a.add(b); // 12.75 const difference = a.sub(b); // 8.25 const product = a.mul(b); // 23.625 const quotient = a.div(b); // 4.666666... ``` ## Type compatibility and limitations Encore.ts analyzes your TypeScript types to generate API schemas, but TypeScript's type system is incredibly complex and supports many advanced features. While we continuously add support for new type patterns, not all TypeScript type combinations are currently supported for API schemas. ### Working with ORM types A common scenario where you might encounter type compatibility issues is when using types from ORMs (Object-Relational Mappers) or database libraries directly in your API schemas. These types often use complex features from the TypeScript type system. In such cases, it's often better to create dedicated API types and convert between them and your ORM types. ### Benefits of separate API types Creating dedicated API types instead of reusing ORM types can have several advantages: - **Better API design**: Your API schema doesn't have to match your database schema 1:1. You can expose only the fields that make sense for your API consumers. - **Security**: Avoid accidentally exposing sensitive internal fields like password hashes or soft-delete timestamps. - **Stability**: Changes to your database schema don't automatically affect your API contract. - **Type compatibility**: Avoid issues with complex ORM-specific types that may not be supported in API schemas. If you encounter a type that doesn't work in your API schema, creating a dedicated API type as shown above can be a good approach. In many cases, reusing your database types directly works fine, so use separate API types when it makes sense for your use case. ================================================ FILE: docs/ts/primitives/validation.mdx ================================================ --- seotitle: Request validation with Encore.ts seodesc: Learn how to validation incoming requests with Encore.ts title: Validation subtitle: Validate incoming requests lang: ts --- When receiving incoming requests it's best practice to validate the payload to ensure it meets your expectations and includes all required fields. Encore.ts has request validation built in, designed to work seamlessly with TypeScript. It uses the natural TypeScript types directly to validate incoming requests, so you get the best of both worlds: the clean, concise TypeScript syntax, and runtime schema validation. This means your APIs are type-safe during both runtime and compile-time. Encore.ts makes it easy to define API endpoints that combine data from different sources: some fields from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this: ```ts import { Header, Query, api } from "encore.dev/api"; interface Request { // Optional query parameter. Parsed from the request URL. limit?: Query; // Custom header that must be set. Parsed from the HTTP headers. myHeader: Header<"X-My-Header">; // Required enum. Parsed from the request body. type: "sprocket" | "widget"; } export const myEndpoint = api( { expose: true, method: "POST", path: "/api" }, async ({ limit, myHeader, type }) => { // ... }, ); ``` In the above example, if a request to the endpoint is missing the `X-My-Header` HTTP header or the `type` field in the request body, Encore will return a `400` Bad Request response. The `limit` query parameter is optional and will be either a number or `undefined` in the endpoint handler function. ## Supported validation types ### String fields ```ts interface Schema { name: string; } ``` ### Number fields The `number` type will accept both int and float values: ```ts interface Schema { age: number; } ``` ### Boolean fields ```ts interface Schema { isHuman: boolean; } ``` ### Array fields You can define fields wit array values of `string`, `number`, `boolean`, `null`: ```ts interface Schema { strings: string[]; numbers: number[]; booleans: boolean[]; nulls: null[]; } ``` You can have an array of objects: ```ts interface Schema { users: { name: string; age: number }[]; } ``` It is also possible to have arrays of multiple types: ```ts interface Schema { values: (string | number)[]; } ``` ### Enum fields String enums can be used in the request schema to validate that a field is one of a set of predefined values. For example: ```ts interface Schema { type: "BLOG_POST" | "COMMENT" } ``` You can also use TypeScript enums: ```ts enum PostType { BlogPost = "BLOG_POST", Comment = "COMMENT" } interface Schema { type: PostType, } ``` The two above examples are equivalent. ### Optional fields You can make a field optional by adding a `?` after the field name: ```ts interface Request { name?: string; } export const myEndpoint = api( { expose: true, method: "POST", path: "/body" }, async (req: Request) => { // req.name is a string or undefined }, ); ``` Request will reach the endpoint handler even if the `name` field is missing. If the field is missing, the value will be `undefined`. ### Nullable fields You can make a field nullable by using the `| null` type: ```ts interface Request { name: string | null; } export const myEndpoint = api( { expose: true, method: "POST", path: "/body" }, async (req: Request) => { // req.name is a string or null }, ); ``` ### Union fields You can define a field that can be one of several types by using a union type: ```ts interface Request { value: string | number | boolean; } export const myEndpoint = api( { expose: true, method: "POST", path: "/body" }, async (req: Request) => { // req.value is a string, number, or boolean }, ); ``` ### Reference schema ```ts interface Schema { str: string; // String int: number; // Number list: number[]; // Array of numbers listOfTypes: (number | string )[]; // Array multiple types nullable: number | null; // Nullable maybe?: string; // Optional multiple: boolean | number | string | { name: string }; // Union enum: "John" | "Foo"; // Enum } ``` ## Value based validation rules Use Encore.ts's composable value based validation for use cases like checking the format of an email address or the length of a string. It uses TypeScript's type system and allows you to define validation rules directly in your type definitions. ### Built-in validation Rules - `Min` / `Max`: Validate minimum/maximum values for numbers - `MinLen` / `MaxLen`: Validate minimum/maximum lengths for strings and arrays - `IsURL` / `IsEmail`: Validate that a string is a URL or email address - `StartsWith` / `EndsWith` / `MatchesRegexp`: Validate that a string matches a specific pattern ### Examples Import validation rules from the `encore.dev/validate` package: ```ts import { Min, Max, MinLen, MaxLen, IsEmail, IsURL } from "encore.dev/validate"; interface Schema { // Number between 3 and 1000 (inclusive) count: number & (Min<3> & Max<1000>); // String between 5 and 20 characters username: string & (MinLen<5> & MaxLen<20>); // Must be either a valid URL or email address contact: string & (IsURL | IsEmail); // Array of up to 10 email addresses recipients: Array & MaxLen<10>; } ``` ### Combining Rules You can combine multiple validation rules using: - `&` (and) to require all rules to pass - `|` (or) to require at least one rule to pass ```ts interface Schema { // Must be both >= 3 and <= 1000 count: number & (Min<3> & Max<1000>); // Must be either a URL or an email contact: string & (IsURL | IsEmail); } ``` ### Performance These validation rules are executed directly in Rust at runtime, before the request reaches your JavaScript code. This provides excellent performance while maintaining type safety at both compile-time and runtime. ## Body By default, the data is parsed as a JSON body for incoming requests: ```ts interface Request { name: string; // Parsed from the JSON body } export const myEndpoint = api( { expose: true, method: "POST", path: "/body" }, async (req) => { // req.name is a string }, ); ``` Here, `name` is a required field in the request body. If the request body is missing the `name` field, Encore will return a `400` Bad Request response. ## Query For HTTP methods that support request bodies, parameters are by default read from the HTTP request body as JSON. In those cases, the `Query` type can be used to specify that a field should be parsed from the query string instead. ```typescript import { api, Query } from "encore.dev/api"; interface Schema { query: Query; // this will be parsed from the '?query=...' parameter in the request url } // A simple API endpoint that echoes the data back. export const echo = api( { method: "POST", path: "/example" }, async (params: Schema) => { // params.query is a string }, ); ``` This API endpoint expects incoming requests to look like this: ```output POST /example?query=hello HTTP/1.1 Content-Type: application/json ``` For `GET`, `HEAD` and `DELETE` requests, parameters are read and validated from the query string by default, since those HTTP methods do not support request bodies. For those methods, the `Query` type is not necessary: ```typescript import { Query } from "encore.dev/api"; interface Schema { limit: Query; // always a query parameter author: string; // query if GET, HEAD or DELETE, otherwise body parameter } ``` ### Nested query fields Using the `Query` type as a nested fields has no effect: ```typescript import { api, Query } from "encore.dev/api"; interface Data { query: Query; // this will be parsed from the '?query=...' parameter in the request url nested: { query2: Query; // Query has no effect inside nested fields }; } export const echo = api( { method: "POST", path: "/nested" }, async (params: Data) => { // ... }, ); ``` Nested query params will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this: ```output POST /nested HTTP/1.1 Content-Type: application/json { "nested": { "query2": "not a query string" } } ``` ## Headers Request headers are defined and validated by setting the field type to `Header<"Name-Of-Header">`. It can be used in both request and response data types. In the example below, the `language` field will be fetched from the `Accept-Language` HTTP header. If the request is missing the `Accept-Language` header, Encore will return a `400` Bad Request response. ```ts import { Header } from "encore.dev/api"; interface Params { language: Header<"Accept-Language">; // parsed from header author: string; // not a header } ``` ### Nested header fields Using the `Header` type as a nested fields has no effect: ```typescript import { api, Header } from "encore.dev/api"; interface Data { header: Header<"X-Header">; // this field will be read from the http header nested: { header2: Header<"X-Other-Header">; // Header has no effect inside nested fields }; } // A simple API endpoint that echoes the data back. export const echo = api( { method: "POST", path: "/nested" }, async (params: Data) => { // ... }, ); ``` Nested headers will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this: ```output POST /nested HTTP/1.1 Content-Type: application/json X-Header: this is a header { "nested": { "header2": "not a header", } } ``` ## Params Dynamic path parameters are also defined in the request schema. The parameter will be parsed from the request URL and made available in the request object: ```ts import { api } from "encore.dev/api"; interface Request { // Required path parameter. Parsed from the request URL. id: string; } export const myEndpoint = api( { expose: true, method: "POST", path: "/user/:id" }, async ({ id }: Request) => { // ... }, ); ``` You can also use the `number` type for path parameters: ```ts interface Request { id: number; } ``` Encore.ts will then try to parse the path parameter as a number. If the path parameter is not a valid number, Encore will return a `400` Bad Request response. ## Combining sources You can combine data from different sources in the same request schema. For example, you can have fields that are parsed from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this: ```ts import { Header, Query, api } from "encore.dev/api"; interface Request { // Required path parameter. Parsed from the request URL. id: number; // Optional query parameter. Parsed from the request URL. limit?: Query; // Custom header that must be set. Parsed from the HTTP headers. myHeader: Header<"X-My-Header">; // Required enum. Parsed from the request body. type: "sprocket" | "widget"; } export const myEndpoint = api( { expose: true, method: "POST", path: "/user/:id" }, async ({ id, limit, myHeader, type }: Request) => { // ... }, ); ``` ## Errors If the validation is not successful, Encore will return a `400` Bad Request response with a JSON body that contains the error message: ```output HTTP/1.1 400 Bad Request { "code": "invalid_argument", "message": "unable to decode request body", "internal_message": "Error(\"missing field name\", line: 1, column: 18)" } ``` ## Response Encore.ts will not perform runtime validation for response data, but you will get compilation errors if you try to return a value that does not match the expected response type. ### Reusing the request type as the response type You often want to return the same data type that you received in a request. In this case, you can reuse the request type as the response type: ```typescript import { api, Header, Query } from "encore.dev/api"; interface Data { header: Header<"X-Header">; // this field will be read from the http header query: Query; // this will be parsed from the '?query=...' parameter in the request url body: string; // this will be sent as part of the JSON body } // A simple API endpoint that echoes the data back. export const echo = api( { method: "POST", path: "/echo" }, async (params: Data): Promise => { return params; // echo the data back }, ); ``` This API endpoint expects incoming requests to look like this: ```output POST /echo?query=hello HTTP/1.1 Content-Type: application/json X-Header: this is a header { "body": "a body", } ``` For HTTP responses the `Query` type is considered to be part of the JSON response body, since query strings only make sense for incoming requests. Responses returned from this endpoint will be serialized as a HTTP response to looks like this: ```output HTTP/1.1 200 OK Content-Type: application/json X-Header: this is a header { "query": "hello", "body": "a body", } ``` ## Under the hood Encore.ts parses your source code to understand the request and response schema that each API endpoint expects, including things like HTTP headers, query parameters, and so on. The schemas are then processed, optimized, and stored as a Protobuf file. When the Encore.ts Rust runtime starts up, it reads the Protobuf file and pre-computes a request decoder and response encoder, optimized for each API endpoint, using the exact type definition each API endpoint expects. In fact, Encore.ts even handles request validation directly in Rust, ensuring invalid requests never have to even touch the JavaScript layer, mitigating many denial of service attacks. Encore’s understanding of the request schema also improves performance. JavaScript runtimes like Deno and Bun use a similar architecture (in fact, Deno also uses Rust+Tokio+Hyper), but lack Encore’s understanding of the request schema. As a result, they need to hand over the un-processed HTTP requests to the single-threaded JavaScript engine for execution. Encore.ts, on the other hand, handles much more of the request processing inside Rust, and only hands over the decoded request objects. By handling much more of the request life-cycle in multi-threaded Rust, the JavaScript event-loop is freed up to focus on executing application business logic instead of parsing HTTP requests, resulting in a significant performance improvement. ================================================ FILE: docs/ts/quick-start.mdx ================================================ --- seotitle: Quick Start Guide – Learn how to build backends with Encore.ts seodesc: See how you to build and ship a cloud based backend application using Go and Encore. Install Encore and build a REST API in just a few minutes. title: Quick Start Guide subtitle: Build your first Encore.ts app in 5 minutes lang: ts --- Follow the steps below or use [Leap](https://leap.new) (our AI builder) to get started. In this short guide, you'll learn key concepts and experience the Encore workflow. It should only take about 5 minutes to complete and by the end you'll have an API running in Encore's free development Cloud (Encore Cloud). To make it easy to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Install the Encore CLI To develop with Encore, you need the Encore CLI. It provisions your local environment, and runs your local development dashboard complete with tracing and API documentation. 🥐 Install Encore by running the appropriate command for your system: ### Prerequisites - [Node.js](https://nodejs.org/en/download/) is required to run Encore.ts apps. - [Docker](https://www.docker.com) is required for Encore to set up local databases. ## 2. Create your app 🥐 Create your app by running: ```shell $ encore app create ``` If this is your first time using Encore, you’ll be prompted to create a free Encore Cloud account. This enables Encore to manage things like secrets and fully automate cloud deployments (which you’ll use later in the tutorial). 🥐 Select `TypeScript` as your app’s language. 🥐 Choose a starter template. Pick `Hello World` and continue. Optional: Install AI instructions to improve how tools like Cursor and Claude Code work with Encore. After selecting your template, choose the AI instructions for the tool you plan to use. 🥐 Pick a name for your app. Encore will now create your app in a folder named after your app. ### Let's take a look at the code Part of what makes Encore different is the simple developer experience when building distributed systems. Let's look at the code to better understand how to build applications with Encore. 🥐 Open the `hello.ts` file in your code editor. It's located in the folder: `your-app-name/hello/`. You should see this: ```ts -- hello/hello.ts -- import { api } from "encore.dev/api"; export const world = api( { method: "GET", path: "/hello/:name", expose: true }, async ({ name }: { name: string }): Promise => { return { message: `Hello ${name}!` }; } ); interface Response { message: string; } ``` As you can see, it's all standard TypeScript. You define an API endpoint by wrapping a regular async function in a call to `api`. Doing this makes Encore identify the `world` function as a public API endpoint. Encore automatically handles authentication, HTTP routing, request validation, error handling, observability, API documentation, and more. The `world` endpoint is part of the `hello` service because in the same folder you will also find a file named `encore.service.ts` which looks like this: ```ts -- hello/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("hello"); ``` This is how you define services with Encore. Encore will now consider files in the `hello` directory and all its subdirectories as part of the `hello` service. If you want to create more services, simply create a new folders, add a `encore.service.ts` file that is exporting a new `Service`. _If you're curious, you can read more about defining [services](/docs/ts/primitives/services) and [APIs](/docs/ts/primitives/apis)._ The Encore.ts [Backend Framework](/docs/ts) provides several declarative ways of using backend primitives like databases, Pub/Sub, and scheduled tasks by simply writing code. ## 3. Start your app & Explore Local Development Dashboard 🥐 Now let's run your app locally: ```shell $ cd your-app-name # replace with the app name you picked $ encore run ``` You should see this: That means your local development environment is up and running! Encore takes care of setting up all the necessary infrastructure for your applications, even including databases and Pub/Sub. ### Open the Local Development Dashboard You can now start using your [Local Development Dashboard](/docs/ts/observability/dev-dash). 🥐 Open [http://localhost:9400](http://localhost:9400) in your browser to access it. The Local Development Dashboard is a powerful tool to help you move faster when you're developing new features. It comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful observability features like [distributed tracing](/docs/ts/observability/tracing). Through the Local Development Dashboard you also have access to [Encore Flow](/docs/ts/observability/encore-flow), a visual representation of your microservice architecture that updates in real-time as you develop your application. ### Call your API 🥐 While you keep the app running, call your API from the API Explorer: You can also open a separate terminal to call your API endpoint: ```shell $ curl http://localhost:4000/hello/world {"Message": "Hello, world!"} ``` If you see this JSON response, you've successfully made an API call to your very first Encore application. Well done, you're on your way! ### Review a trace of the request You can now take a look at the trace for the request you just made by clicking on it in the right column in the local dashboard. With such a simple API, there's not much to it, just a simple request and response. However, just imagine how powerful it is to have tracing when you're developing a more complex system with multiple services, Pub/Sub, and databases. (Learn more about Encore's tracing capabilities in the [tracing docs](/docs/ts/observability/tracing).) ## 4. Make a code change Let's put our mark on this API and make our first code change. 🥐 Head back to your code editor and look at the `hello.ts` file again. If you can't come up a creative change yourself, why not simply change the "Hello" message to a more sassy "Howdy"? 🥐 Once you've made your change, save the file. When you save, the daemon run by the Encore CLI instantly detects the change and automatically recompiles your application and reloads your local development environment. The output where you're running your app will look something like this: ```output Changes detected, recompiling... Reloaded successfully. TRC registered endpoint endpoint=World path=/hello/:name service=hello TRC listening for incoming HTTP requests ``` 🥐 Test your change by calling your API again. ```shell $ curl http://localhost:4000/hello/world {"Message": "Howdy, world!"} ``` Great job, you made a change and your app was reloaded automatically. Now you're ready to head to the cloud! ## 5. Deploy your app ### Generating Docker image You can either deploy by generating a Docker image for your app using: ```shell $ encore build docker MY-IMAGE:TAG ``` This will compile your application using the host machine and then produce a Docker image containing the compiled application. You can now deploy this anywhere you like. Learn more in the [self-host docs](/docs/ts/self-host/build). ### Deploy using Encore Cloud Optionally, you can use [Encore Cloud](https://encore.dev/use-cases/devops-automation) to automatically deploy your application. It comes with built-in free development hosting, and for production offers fully automated deployment to your own cloud on AWS or GCP. 🥐 To deploy, simply push your changes to Encore: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore Cloud will now build and test your app, provision the needed infrastructure, and deploy your application to a staging environment. After triggering the deployment, you will see a URL where you can view its progress in the Encore Cloud dashboard. It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` 🥐 Open the URL to access the Cloud Dashboard and check the progress of your deployment. You can now use the Cloud Dashboard to view production [traces](/docs/ts/observability/tracing), [connect your cloud account](/docs/platform/deploy/own-cloud), [integrate with GitHub](/docs/platform/integrations/github), and much more. ## What's next? - Check out the [REST API tutorial](/docs/ts/tutorials/rest-api) to learn how to create endpoints, use databases, and more. - Join the friendly community on [Discord](/discord) to ask questions and meet other Encore developers. ================================================ FILE: docs/ts/self-host/build.md ================================================ --- seotitle: Build Docker Images seodesc: Learn how to build Docker images for your Encore application, which can be self-hosted on your own infrastructure. title: Build Docker Images lang: ts --- Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. This can be a good choice if [Encore Cloud](/docs/platform) isn't a good fit for your use case, or if you want to [migrate away](/docs/ts/migration/migrate-away). ## Building your own Docker image To build your own Docker image, use `encore build docker MY-IMAGE:TAG` from the CLI. This will compile your application using the host machine and then produce a Docker image containing the compiled application. The base image defaults to `scratch` for GO apps and `node:slim` for TS, but can be customized with `--base`. This is exactly the same code path that Encore's CI system uses to build Docker images, ensuring compatibility. By default, all your services are included and started by the Docker image. If you want to specify specific services and gateways to include, you can use the `--services` and `--gateways` flags. ```bash encore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG ``` You can target a specific architecture with `--arch` (useful when your build machine differs from your deploy target): ```bash encore build docker --arch=arm64 MY-IMAGE:TAG ``` To provide an [infrastructure configuration](/docs/ts/self-host/configure-infra) file at build time, use `--config`: ```bash encore build docker --config=infra-config.json MY-IMAGE:TAG ``` The image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image. ```bash docker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG ``` Congratulations, you've built your own Docker image! 🎉 Continue to learn how to [configure infrastructure](/docs/ts/self-host/configure-infra). ================================================ FILE: docs/ts/self-host/ci-cd.md ================================================ --- seotitle: Integrate with your CI/CD pipeline seodesc: Learn how to integrate Encore.ts with your CI/CD pipeline. title: Integrate with your CI/CD pipeline lang: ts --- Encore seamlessly integrates with any CI/CD pipeline through its CLI tools. You can automate Docker image creation using the `encore build` command as part of your deployment workflow. ## Integrating with CI/CD Platforms While every CI/CD pipeline is unique, integrating Encore follows a straightforward process. Here are the key steps: 1. Install the Encore CLI in your CI environment 2. Use `encore build docker` to create Docker images 3. Push the images to your container registry 4. Deploy to your infrastructure If your app is linked with Encore Cloud, you'll need to authenticate the CLI in your CI environment using an [auth key](/docs/platform/integrations/auth-keys). Generate one from **App Settings > Auth Keys** in the Encore Cloud dashboard, store it as a CI secret, and run `encore auth login --auth-key=` before building. Refer to your CI/CD platform's documentation for more details on how to integrate CLI tools like `encore build`. ### GitHub actions example This example shows how to build, push, and deploy an Encore Docker image to DigitalOcean using GitHub Actions. The DigitalOcean application is set up re-deploy the application every time an image with the tag `latest` is uploaded. ```yaml name: Build, Push and Deploy a Encore Docker Image to DigitalOcean on: push: branches: [ main ] permissions: contents: read packages: write jobs: build-push-deploy-image: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Download Encore CLI script uses: sozo-design/curl@v1.0.2 with: args: --output install.sh -L https://encore.dev/install.sh - name: Install Encore CLI run: bash install.sh - name: Authenticate with Encore run: /home/runner/.encore/bin/encore auth login --auth-key=${{ secrets.ENCORE_AUTH_KEY }} - name: Log in to DigitalOcean container registry run: docker login registry.digitalocean.com -u my-email@gmail.com -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Build Docker image run: /home/runner/.encore/bin/encore build docker myapp - name: Tag Docker image run: docker tag myapp registry.digitalocean.com//:latest - name: Push Docker image run: docker push registry.digitalocean.com//:latest ``` ## Building Docker Images The `encore build docker` command provides several options to customize your builds: ```bash # Build specific services and gateways encore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG # Customize the base image encore build docker --base=node:18-alpine MY-IMAGE:TAG # Build for a specific architecture (useful when CI and deploy targets differ) encore build docker --arch=arm64 MY-IMAGE:TAG ``` The image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image. ```bash docker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG ``` Learn more about the `encore build docker` command in the [build Docker images](/docs/ts/self-host/build) guide. Continue to learn how to [configure infrastructure](/docs/ts/self-host/configure-infra). ================================================ FILE: docs/ts/self-host/configure-infra.md ================================================ --- title: Configure Infrastructure seotitle: Configure Infrastructure seodesc: Learn how to configure infrastructure resources for your Encore app. lang: ts --- If you are using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration. The `build` command lets you provide this by specifying a path to a config file using the `--config` flag. ```bash encore build docker --config path/to/infra-config.json MY-IMAGE:TAG ``` The configuration file should be a JSON file using the [Encore Infra Config](https://encore.dev/schemas/infra.schema.json) schema. This supports configuring things like: - How to access infrastructure resources (what provider to use, what credentials to use, etc.) - How to call other services over the network ("service discovery"), most notably their base URLs. - Observability configuration (where to export metrics, etc.) - Metadata about the environment the application is running in, to power Encore's metadata APIs. - The values for any application-defined secrets. This configuration is necessary for the application to behave correctly. ## Example Here's an example configuration file you can use. ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "metadata": { "app_id": "my-app", "env_name": "my-env", "env_type": "production", "cloud": "gcp", "base_url": "https://my-app.com" }, "sql_servers": [ { "host": "my-db-host:5432", "databases": { "my-db": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ], "service_discovery": { "myservice": { "base_url": "https://myservice:8044" } }, "redis": { "encoreredis": { "database_index": 0, "auth": { "type": "acl", "username": "encoreredis", "password": {"$env": "REDIS_PASSWORD"} }, "host": "my-redis-host", } }, "metrics": { "type": "prometheus", "remote_write_url": "https://my-remote-write-url" }, "graceful_shutdown": { "total": 30 }, "auth": [ { "type": "key", "id": 1, "key": {"$env": "SVC_TO_SVC_KEY"} } ], "secrets": { "AppSecret": {"$env": "APP_SECRET"} }, "pubsub": [ { "type": "gcp_pubsub", "project_id": "my-project", "topics": { "encore-topic": { "name": "gcp-topic-name", "subscriptions": { "encore-subscription": { "name": "gcp-subscription-name" } } } } } ], "object_storage": [ { "type": "gcs", "buckets": { "my-gcs-bucket": { "name": "my-gcs-bucket", } } } ] } ``` ## Configuring Infrastructure To use infrastructure resources, additional configuration must be added so that Encore is aware of how to access each infrastructure resource. See below for examples of each type of infrastructure resource. ### 1. Basic Environment Metadata Configuration ```json { "metadata": { "app_id": "my-encore-app", "env_name": "production", "env_type": "production", "cloud": "aws", "base_url": "https://api.myencoreapp.com" } } ``` - `app_id`: The ID of your Encore application. - `env_name`: The environment name, such as `production`, `staging`, or `development`. - `env_type`: Specifies the type of environment (`production`, `test`, `development`, or `ephemeral`). - `cloud`: The cloud provider hosting the infrastructure (e.g., `aws`, `gcp`, or `azure`). - `base_url`: The base URL for services in the environment. ### 2. Graceful Shutdown Configuration ```json { "graceful_shutdown": { "total": 30, "shutdown_hooks": 10, "handlers": 20 } } ``` - `total`: The total time allowed for the shutdown process in seconds. - `shutdown_hooks`: The time allowed for executing shutdown hooks. - `handlers`: The time allocated for processing request handlers during the shutdown. ### 3. Authentication Methods Configuration Private endpoints will not require authentication if no authentication methods are specified. This is typically fine when services are deployed on a private network such as a VPC. But sometimes you might need to connect to other services over the public internet, in which case you'll want to ensure private endpoints are only accessible to other backend services. To do that you can configure authentication methods. Encore currently supports authentication through a shared key, which you can specify in your infrastructure configuration file. ```json { "auth": [ { "type": "key", "id": 1, "key": { "$env": "SERVICE_API_KEY" } } ] } ``` - `type`: The authentication method type (e.g., `key`). - `id`: The ID associated with the authentication method. - `key`: The authentication key, which can be set using an environment variable reference. ### 4. Service Discovery Configuration Service discovery is used to access other services over the network. You can configure service discovery in the infrastructure configuration file. If you export all services into the same docker image, you don't need to configure service discovery as it will be automatically configured when the services are started. ```json { "service_discovery": { "user-service": { "base_url": "https://user.myencoreapp.com", "auth": [ { "type": "key", "id": 1, "key": { "$env": "USER_SERVICE_API_KEY" } } ] } } } ``` - `user-service`: Configuration for a service named `user-service`. - `base_url`: The base URL for the service. - `auth`: Authentication methods used for accessing the service. If no authentication methods are specified, the service will use the auth methods defined in the `auth` section. ### 5. Metrics Configuration Similarly to cloud infrastructure resources, Encore supports configurable metrics exports: * Prometheus * DataDog * GCP Cloud Monitoring * AWS CloudWatch This is configured by setting the metrics field. Below are examples for each of the supported metrics providers: #### 5.1. Prometheus Configuration ```json { "metrics": { "type": "prometheus", "collection_interval": 15, "remote_write_url": { "$env": "PROMETHEUS_REMOTE_WRITE_URL" } } } ``` #### 5.2. Datadog Configuration ```json { "metrics": { "type": "datadog", "collection_interval": 30, "site": "datadoghq.com", "api_key": { "$env": "DATADOG_API_KEY" } } } ``` #### 5.3. GCP Cloud Monitoring Configuration ```json { "metrics": { "type": "gcp_cloud_monitoring", "collection_interval": 60, "project_id": "my-gcp-project", "monitored_resource_type": "gce_instance", "monitored_resource_labels": { "instance_id": "1234567890", "zone": "us-central1-a" }, "metric_names": { "cpu_usage": "compute.googleapis.com/instance/cpu/usage_time" } } } ``` #### 5.4. AWS CloudWatch Configuration ```json { "metrics": { "type": "aws_cloudwatch", "collection_interval": 60, "namespace": "MyAppMetrics" } } ``` ### 6. SQL Database Configuration The SQL databases you've declared in your Encore app must be configured in the infrastructure configuration file. There must be exactly one database configuration for each declared database. You can configure multiple SQL servers if needed. ```json { "sql_servers": [ { "host": "db.myencoreapp.com:5432", "tls_config": { "disabled": false, "ca": "---BEGIN CERTIFICATE---\n..." }, "databases": { "main_db": { "max_connections": 100, "min_connections": 10, "username": "db_user", "password": { "$env": "DB_PASSWORD" } } } } ] } ``` - `host`: SQL server host, optionally including the port. - `tls_config`: TLS configuration for secure connections. If the server uses TLS with a non-system CA root, or requires a client certificate, specify the appropriate fields as PEM-encoded strings. Otherwise, they can be left empty. - `databases`: List of databases, each with connection settings. ### 7. Secrets Configuration #### 7.1. Using Direct Secrets You can set the secret value directly in the configuration file, or use an environment variable reference to set the secret value. ```json { "secrets": { "API_TOKEN": "embedded-secret-value", "DB_PASSWORD": { "$env": "DB_PASSWORD" } } } ``` #### 7.2. Using Environment Reference As an alternative, you can use an environment variable reference to set the secret value. The env variable should be set in the environment where the application is running. The content of the environment variable should be a JSON string where each key is the secret name and the value is the secret value. ```json { "secrets": { "$env": "SECRET_JSON" } } ``` ### 8. Redis Configuration ```json { "redis": { "cache": { "host": "redis.myencoreapp.com:6379", "database_index": 0, "auth": { "type": "auth", "auth_string": { "$env": "REDIS_AUTH_STRING" } }, "max_connections": 50, "min_connections": 5 } } } ``` - `host`: Redis server host, optionally including the port. - `auth`: Authentication configuration for the Redis server. - `key_prefix`: Prefix applied to all keys. ### 9. Pub/Sub Configuration Encore currently supports the following Pub/Sub providers: - `nsq` for [NSQ](https://nsq.io/) - `gcp` for [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) - `aws` for AWS [SNS](https://aws.amazon.com/sns/) + [SQS](https://aws.amazon.com/sqs/) - `azure` for [Azure Service Bus](https://azure.microsoft.com/en-us/products/service-bus) The configuration for each provider is different. Below are examples for each provider. #### 9.1. GCP Pub/Sub ```json { "pubsub": [ { "type": "gcp_pubsub", "project_id": "my-gcp-project", "topics": { "user-events": { "name": "user-events-topic", "project_id": "my-gcp-project", "subscriptions": { "user-notification": { "name": "user-notification-subscription", "push_config": { "id": "user-push", "service_account": "service-account@my-gcp-project.iam.gserviceaccount.com" } } } } } } ] } ``` #### 9.2. AWS SNS/SQS ```json { "pubsub": [ { "type": "aws_sns_sqs", "topics": { "my-topic": { "arn": "arn:aws:sns:us-east-1:123456789012:my-topic", "subscriptions": { "my-queue": { "url": "https://sqs.eu-east-1.amazonaws.com/123456789012/my-queue" } } } } } ] } ``` - `my-topic`: This is the name of the topic as it is declared in your Encore app. - `my-queue`: This is the name of the queue as it is declared in your Encore app. - `arn`: The ARN of the SNS topic. - `url`: The URL of the SQS queue. #### 9.3. NSQ Configuration ```json { "pubsub": [ { "type": "nsq", "hosts": "nsq.myencoreapp.com:4150", "topics": { "order-events": { "name": "order-events-topic", "subscriptions": { "order-processor": { "name": "order-processor-subscription" } } } } } ] } ``` ### 10. Object Storage Configuration Encore currently supports the following object storage providers: - `gcs` for [Google Cloud Storage](https://cloud.google.com/storage) - `s3` for [AWS S3](https://aws.amazon.com/s3/) or a custom S3-compatible provider #### 10.1. GCS Configuration ```json { "object_storage": [ { "type": "gcs", "buckets": { "my-gcs-bucket": { "name": "my-gcs-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `name`: The full name of the GCS bucket. - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. #### 10.2. S3 Configuration ```json { "object_storage": [ { "type": "s3", "region": "us-east-1", "buckets": { "my-s3-bucket": { "name": "my-s3-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `region`: The AWS region where the bucket is located. - `name`: The full name of the S3 bucket. - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. #### 10.3. Custom S3 Provider Configuration You can also configure a custom S3 provider by specifying the endpoint, access key id, and secret access key. Custom S3 providers are useful if you are using a S3-compatible storage provider such as [Cloudflare R2](https://developers.cloudflare.com/r2/). ```json { "object_storage": [ { "type": "s3", "region": "auto", "endpoint": "https://...", "access_key_id": "...", "secret_access_key": { "$env": "BUCKET_SECRET_ACCESS_KEY" }, "buckets": { "my-s3-bucket": { "name": "my-s3-bucket", "key_prefix": "my-optional-prefix/", "public_base_url": "https://my-gcs-bucket-cdn.example.com/my-optional-prefix" } } } ] } ``` - `region`: The region where the bucket is located. - `name`: The full name of the bucket - `key_prefix`: An optional prefix to apply to all keys in the bucket. - `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended. This guide covers typical infrastructure configurations. Adjust according to your specific requirements to optimize your Encore app's infrastructure setup. ================================================ FILE: docs/ts/self-host/deploy-to-digital-ocean.md ================================================ --- seotitle: How to deploy an Encore app to DigitalOcean seodesc: Learn how to deploy an Encore application to DigitalOcean's App Platform using Docker. title: Deploy to DigitalOcean lang: ts --- If you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to DigitalOcean's App Platform using Docker. ### Video tutorial ### Prerequisites 1. **DigitalOcean Account**: Make sure you have a DigitalOcean account. If not, you can [sign up here](https://www.digitalocean.com/). 2. **Docker Installed**: Ensure Docker is installed on your local machine. You can download it from the [Docker website](https://www.docker.com/get-started). 3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/ts/install). 4. **DigitalOcean CLI (Optional)**: You can install the DigitalOcean CLI for more flexibility and automation, but it’s not necessary for this tutorial. ### Step 1: Create an Encore App 1. **Create a New Encore App**: - If you haven’t already, create a new Encore app using the Encore CLI. - You can use the following command to create a new app: ```bash encore app create myapp ``` - Select the `Hello World` template. - Follow the prompts to create the app. 2. **Build a Docker image**: - Build the Encore app to generate the docker image for deployment: ```bash encore build docker myapp ``` ### Step 2: Push the Docker Image to a Container Registry To deploy your Docker image to DigitalOcean, you need to push it to a container registry. DigitalOcean supports its own container registry, but you can also use DockerHub or other registries. Here’s how to push the image to DigitalOcean’s registry: 1. **Create a DigitalOcean Container Registry**: - Go to the [DigitalOcean Control Panel](https://cloud.digitalocean.com/registries) and create a new container registry. - Follow the instructions to set it up. 2. **Login to DigitalOcean's registry**: Use the login command provided by DigitalOcean, which will look something like this: ```bash doctl registry login ``` You’ll need the DigitalOcean CLI for this, which can be installed from [DigitalOcean CLI documentation](https://docs.digitalocean.com/reference/doctl/how-to/install/). 3. **Tag your Docker image**: Tag your image to match the registry’s URL. ```bash docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` 4. **Push your Docker image to the registry**: ```bash docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` ### Step 3: Deploy the Docker Image to DigitalOcean App Platform 1. **Navigate to the App Platform**: Go to [DigitalOcean's App Platform](https://cloud.digitalocean.com/apps). 2. **Create a New App**: - Click on **"Create App"**. - Choose the **"DigitalOcean Container Registry"** option. 3. **Select the Docker Image Source**: - Select the image you pushed earlier. 4. **Configure the App Settings**: - **Set up scaling options**: Configure the number of containers, CPU, and memory settings. - **Environment variables**: Add any environment variables your application might need. - **Choose the region**: Pick a region close to your users for better performance. 5. **Deploy the App**: - Click **"Next"**, review the settings, and click **"Create Resources"**. - DigitalOcean will take care of provisioning the infrastructure, pulling the Docker image, and starting the application. ### Step 4: Monitor and Manage the App 1. **Access the Application**: - Once deployed, you will get a public URL to access your application. - Test the app to ensure it’s running as expected, e.g. ```bash curl https://myapp.ondigitalocean.app/hello/world ``` 2. **View Logs and Metrics**: - Go to the **"Runtime Logs"** tab in the App Platform to view logs - Go to the **"Insights"** tab to view performance metrics. 3. **Manage Scaling and Deployment Settings**: - You can change the app configuration, such as scaling settings, deployment region, or environment variables. ### Step 5: Add a Database to Your App DigitalOcean’s App Platform provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a managed database for your app: 1. **Navigate to the DigitalOcean Control Panel**: - Go to [DigitalOcean Control Panel](https://cloud.digitalocean.com/). - Click on **"Databases"** in the left-hand sidebar. 2. **Create a New Database Cluster**: - Click **"Create Database Cluster"**. - Choose **PostgreSQL** - Select the **database version**, **data center region**, and **cluster configuration** (e.g., development or production settings based on your needs). - **Name the database** and configure other settings if necessary, then click **"Create Database Cluster"**. 3. **Configure the Database Settings**: - Once the database is created, go to the **"Connection Details"** tab of the database dashboard. - Copy the **connection string** or individual settings (host, port, username, password, database name). You will need these details to connect your app to the database. - Download the **CA certificate** 4. **Create a Database** - Connect to the database using the connection string provided by DigitalOcean. ```bash psql -h mydb.db.ondigitalocean.com -U doadmin -d mydb -p 25060 ``` - Create a database ```sql CREATE DATABASE mydb; ``` - Create a table ```sql CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(50) ); INSERT INTO users (name) VALUES ('Alice'); ``` 5. **Declare a Database in your Encore app**: - Open your Encore app’s codebase. - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases)) ```typescript const mydb = new SQLDatabase("mydb", { migrations: "./migrations", }); export const getUser = api( { expose: true, method: "GET", path: "/names/:id" }, async ({id}: {id:number}): Promise<{ id: number; name: string }> => { return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string }; } ); ``` 6. **Create an Encore Infrastructure config** - Create a file named `infra.config.json` in the root of your Encore app. - Add the **CA certificate** and the connection details to the file: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "mydb.db.ondigitalocean.com:25060", "tls_config": { "ca": "-----BEGIN CERTIFICATE-----\n..." }, "databases": { "mydb": { "name": "mydb", "username": "doadmin", "password": {"$env": "DB_PASSWORD"} } } }] } ``` 7. **Set Up Environment Variables (Optional)**: - Go to the DigitalOcean App Platform dashboard. - Select your app. - In the **"Settings"** section, go to **"App-Level Environment Variables"** - Add the database password as an encrypted environment variable called `DB_PASSWORD`. 8. **Build and push the Docker image**: - Build the Docker image with the updated configuration. ```bash encore build docker --config infra.config.json myapp ``` - Tag and push the Docker image to the DigitalOcean container registry. ```bash docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest ``` 9. **Test the Database Connection**: - Redeploy the app on DigitalOcean to apply the changes. - Test the database connection by calling the API ```bash curl https://myapp.ondigitalocean.app/names/1 ``` ### Troubleshooting Tips - **Deployment Failures**: Check the build logs for any errors. Make sure the Docker image is correctly tagged and pushed to the registry. - **App Not Accessible**: Verify that the correct port is exposed in the Dockerfile and the App Platform configuration. - **Database Connection Issues**: Ensure the database connection details are correct and the database is accessible from the app. ### Conclusion That’s it! You’ve successfully deployed an Encore app to DigitalOcean’s App Platform using Docker. You can now scale your app, monitor its performance, and manage it easily through the DigitalOcean dashboard. If you encounter any issues, refer to the DigitalOcean documentation or the Encore community for help. Happy coding! ================================================ FILE: docs/ts/self-host/deploy-to-railway.md ================================================ --- seotitle: How to deploy an Encore app to Railway seodesc: Learn how to deploy an Encore application to Railway using Docker and GitHub Actions. title: Deploy to Railway lang: ts --- If you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to Railway using Docker through GitHub Actions. ### Prerequisites 1. **Railway Account**: Make sure you have a Railway account. If not, you can [sign up here](https://railway.com/). 2. **Docker Installed**: Ensure Docker is installed on your local machine, Docker is used by Encore to run databases locally. You can download it from the [Docker website](https://www.docker.com/get-started). 3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/ts/install). ### Step 1: Create an Encore App and a GitHub repository 1. **Create a New Encore App**: - Create a new Encore app using the Encore CLI by running the following command: ```bash encore app create ``` - Select the `Hello World` template. - Follow the prompts to create the app. 2. **Push the code to a GitHub repo**: - Create a new repo (public or private) on GitHub and push the code to it. ### Step 2: Push the Docker Image to GitHub's Container Registry To deploy your Docker image to Railway, you first need to push it to a container registry. We will be using GitHub's container registry, but you can also use DockerHub or other registries. Instead of pushing the image manually we will be using GitHub actions to automate the process. 1. **Create a GitHub Actions YAML file**: - In your repo, create a `.github/workflows/deploy-image-yaml` file with the following contents: ```yaml name: Build, Push and Deploy a Docker Image to Railway on: push: branches: [ main ] permissions: contents: read packages: write jobs: build-push-deploy-image: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Download Encore CLI script uses: sozo-design/curl@v1.0.2 with: args: --output install.sh -L https://encore.dev/install.sh - name: Install Encore CLI run: bash install.sh - name: Build Docker image run: /home/runner/.encore/bin/encore build --config railway-infra.config.json docker myapp - name: Tag Docker image run: docker tag myapp ghcr.io/${{ github.repository }}:latest - name: Push Docker image run: docker push ghcr.io/${{ github.repository }}:latest ``` This will install the Encore CLI, build the Docker image, tag it, and push it to GitHub's container registry everytime you push to the `main` branch. The dynamic values like `${{ github.repository }}` will be filled in automatically by GitHub, you should not need to do anything. 2. **Add, commit and push the changes**: - Push the changes to your GitHub repository to trigger the GitHub action. ### Step 3: Deploy the Docker Image to Railway 1. **Create a new Project on Railway**: - Log in to Railway and go to your dashboard. - Click on **"New"**. - Choose the **"Empty project"** option. 2. **Create a new service inside your new project**: - Click on **"Create"**. - Select the "Docker Image" option. - Enter the Docker Image URI, should be something like `ghcr.io/username/repo:latest`. You can should be able to find the Docker Image under **Packages** in your GitHub repo. - Deploy the service. 3. **Expose the service**: - Click on the tne newly created service. - Go to the **"Settings"** tab. - Click on **"Generate Domain"**. - Select `8080` as the port. - Click on **"Generate"**. 4. **Access the application**: - Once deployed, and exposed you will get a public URL to access your application. It should look something like this: `https://repo-name-production.up.railway.app/`. - Test the app to ensure it's running as expected, e.g. ```bash curl https://repo-name-production.up.railway.app/hello/world ``` ### Step 4: Automate the Deployment Process Railway has no way of knowing that you've pushed a new image to the container registry, but we can use Railway's GraphQL API to trigger a new deployment whenever a new image is pushed to the registry. 1. **Generate a Railway API Token**: - Go to your Railway dashboard. - Click on your profile icon in the top right corner. - Go to **"Account Settings"**. - Click on **"Tokens"**. - Give the token a name and click on **"Create"**. - Copy the generated token. 2. **Add the Railway API Token to GitHub Secrets**: - Go to your GitHub repository. - Go to **"Settings"**. - Click on **"Secrets and variables" → "Actions"**. - Click on **"New repository secret"**. - Add a new secret called `RAILWAY_API_TOKEN` and paste the token you copied earlier. 3. **Add a JavaScript script to your repo**: - Create a new file in your repo named `script.js` with the following contents: ```javascript const TOKEN = process.argv.slice(2)[0]; const ENVIRONMENT_ID = "" const SERVICE_ID = "" const resp = await fetch('https://backboard.railway.com/graphql/v2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'authorization': `Bearer ${TOKEN}`, }, body: JSON.stringify({ query: ` mutation ServiceInstanceRedeploy { serviceInstanceRedeploy( environmentId: "${ENVIRONMENT_ID}" serviceId: "${SERVICE_ID}" ) }` }), }) const data = await resp.json() if (data.errors) { console.error(data.errors) throw new Error('Failed to redeploy service') } console.log(data) ``` - Replace `` and `` with the actual values. You can find these values in the Railway dashboard URL when you're on the service page. 4. **Add new steps to the GitHub Actions YAML file**: - At the bottom of the existing file, add the following steps to call the script: ```yaml - name: Set up Node uses: actions/setup-node@v4 with: node-version: 22 - name: Trigger Railway deployment run: node script.js ${{ secrets.RAILWAY_API_TOKEN }} ``` Whenever you push a new Docker Image to the container registry, the GitHub action will trigger a new deployment on Railway. ### Step 5: Add a Database to Your App Railway provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a database for your app: 1. **Create a database for your app on Railway**: - Navigate to your Railway app. - Click on **"Create"** → **"Database""** → **"Add PostgreSQL""** 2. **Copy the connection details**: - Click on the database you just created. - Click the **"Data"** → **"Connect"** → **"Public Network"**. - Copy the raw `psql` command connection details. 3. **Create a database table**: - Connect to the database using the `psql` command: ```bash PGPASSWORD= psql -h .rlwy.net -U postgres -p 39684 -d railway ``` - Create a table ```sql CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT ); INSERT INTO users (name) VALUES ('Alice'); ``` 4. **Declare a Database in your Encore app**: - Open your Encore app’s codebase. - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases)) ```typescript const mydb = new SQLDatabase("mydb", { migrations: "./migrations", }); export const getUser = api( { expose: true, method: "GET", path: "/names/:id" }, async ({id}: {id:number}): Promise<{ id: number; name: string }> => { return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string }; } ); ``` 5. **Create an Encore Infrastructure config** - Create a file named `infra.config.json` in the root of your Encore app. - Add the connection details to the file: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": ".rlwy.net:39684", "tls_config": { "disable_ca_validation": true }, "databases": { "mydb": { "name": "railway", "username": "postgres", "password": {"$env": "DB_PASSWORD"} } } }] } ``` Railway does not allow for downloading the CA certificate for the database, so we disable the CA validation. 7. **Set Up Environment Variables (Optional)**: - Click on the deployed image in your app view on Railway. - Click **"Variables"**. - Add the database password as an environment variable called `DB_PASSWORD`. 8. **Make a new deployment**: - Add commit and push the changes to your GitHub repository, this will trigger a new deploy on Railway. 9. **Test the Database Connection**: - Test the database connection by calling the API ```bash curl https://myapp.railway.app/names/1 ``` ### Conclusion That’s it! You’ve successfully deployed an Encore app to Railway using Docker. You can now scale your app, monitor its performance, and manage it easily through the Railway dashboard. If you encounter any issues, refer to the Railway documentation or the Encore community for help. Happy coding! ================================================ FILE: docs/ts/tutorials/graphql.mdx ================================================ --- title: Building a GraphQL API subtitle: Learn how to build a GraphQL API using Encore seotitle: How to build a GraphQL API using Encore.ts seodesc: Learn how to build a microservices backend in TypeScript, powered by GraphQL and Encore.ts lang: ts --- Encore has great support for GraphQL with its type-safe approach to building APIs. Encore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)). In this tutorial we will build a GraphQL API using [Apollo](https://www.apollographql.com/docs/apollo-server/) and Encore.ts. The final code will look like this:
To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create your Encore application 🥐 Create a new application by running `encore app create` and select `Empty app` as the template. If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is optional, but is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial). ## 2. GraphQL setup First, we need to install the necessary dependencies: 🥐 Update your `package.json` file to look like this: ```json -- package.json -- { "name": "encore-graphql", "private": true, "version": "0.0.1", "license": "MPL-2.0", "type": "module", "scripts": { "generate": "graphql-codegen --config codegen.yml" }, "devDependencies": { "@types/node": "^20.5.7", "typescript": "^5.2.2", "@graphql-codegen/cli": "2.16.5", "@graphql-codegen/typescript": "2.8.8", "@graphql-codegen/typescript-resolvers": "2.7.13" }, "dependencies": { "@apollo/server": "^4.11.0", "encore.dev": "^1.35.3", "graphql": "^16.9.0", "graphql-tag": "^2.12.6" } } ``` 🥐 Run `npm install` to install the dependencies. 🥐 Next, create a `codegen.yml` file in the application root containing: ``` -- codegen.yml -- # This configuration file tells GraphQL Code Generator how to generate types based on our schema. schema: './schema.graphql' generates: # Specify where our generated types should live. ./graphql/__generated__/resolvers-types.ts: plugins: - 'typescript' - 'typescript-resolvers' config: useIndexSignature: true ``` ## 3. Add GraphQL schema Now it's time to define the GraphQL schema. 🥐 Create a `schema.graphql` file in the application root containing: ``` -- schema.graphql -- type Query { books: [Book] } type Book { title: String! author: String! } type AddBookMutationResponse { code: String! success: Boolean! message: String! book: Book } type Mutation { addBook(title: String!, author: String!): AddBookMutationResponse } ``` 🥐 Run the code generation script to generate the resolver types: ```shell $ npm run generate ``` The types will be written to `graphql/__generated__/resolvers-types.ts` and will contain a bunch of types that we can use when implementing the resolvers. ## 4. Create a Book service Let's create a simple book service that we can later query using GraphQL. It's a good idea to to make the GraphQL library query Encore endpoints because that will result in traces being created for each called endpoint. Having tracing makes it easy to find and fix performance issues that often arise in GraphQL APIs. 🥐 In your application's root folder, create a directory named `book` containing a file named `encore.service.ts`. ```shell $ mkdir book $ touch book/encore.service.ts ``` 🥐 Add the following code to `book/encore.service.ts`: ```ts -- book/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("book"); ``` This is how you define a service with Encore. Encore will now consider files in the `book` directory and all its subdirectories as part of the `book` service. 🥐 Next, create a `book/book.ts` file containing: ```ts import { api, APIError } from "encore.dev/api"; import { Book } from "../graphql/__generated__/resolvers-types"; const db: Book[] = [ { title: "To Kill a Mockingbird", author: "Harper Lee", }, { title: "1984", author: "George Orwell", }, { title: "The Great Gatsby", author: "F. Scott Fitzgerald", }, { title: "Moby-Dick", author: "Herman Melville", }, { title: "Pride and Prejudice", author: "Jane Austen", }, ]; export const list = api( { expose: true, method: "GET", path: "/books" }, async (): Promise<{ books: Book[] }> => { return { books: db }; }, ); // Omit the "__typename" field from the request type AddRequest = Omit, "__typename">; export const add = api( { expose: true, method: "POST", path: "/book" }, async (book: AddRequest): Promise<{ book: Book }> => { if (db.some((b) => b.title === book.title)) { throw APIError.alreadyExists( `Book "${book.title}" is already in database`, ); } db.push(book); return { book }; }, ); ``` The `book` service contains two endpoint, one for listing all books and another to add a new book to the database. Our "database" is hardcoded just to limit the scope of this example. Take a look at the [Using SQL databases](/docs/ts/primitives/databases) docs to learn how to set up and use a database. We get the `Book` type from the generated resolver types. This will make it easier later when we create the resolver functions. ## 5. Create the GraphQL service Now it's time to create our Encore service that will provide the GraphQL API. 🥐 In the `graphql` directory, add a `encore.service.ts` file with the following content: ```ts -- graphql/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("graphql"); ``` Now, we need to create resolvers that call the `book` service. Since the GraphQL API uses the same types as the Encore API exposes (we import types form `resolvers-types.ts` in `book.ts`), our resolver can just be thin wrapper around out API endpoints. 🥐 Create the directory `resolvers` in the `graphql` directory. In the resolvers directory we want to place three files: `index.ts`, `queries.ts` and `mutations.ts`: ```ts -- resolvers/index.ts -- import { Resolvers } from "../__generated__/resolvers-types"; import Query from "./queries.js"; import Mutation from "./mutations.js"; const resolvers: Resolvers = { Query, Mutation }; export default resolvers; -- resolvers/queries.ts -- import { book } from "~encore/clients"; import { QueryResolvers } from "../__generated__/resolvers-types"; // Use the generated `QueryResolvers` type to type check our queries! const queries: QueryResolvers = { books: async () => { const { books } = await book.list(); return books; }, }; export default queries; -- resolvers/mutations.ts -- import { book } from "~encore/clients"; import { MutationResolvers } from "../__generated__/resolvers-types"; import { APIError } from "encore.dev/api"; // Use the generated `MutationResolvers` type to type check our mutations const mutations: MutationResolvers = { addBook: async (_, { title, author }) => { try { const resp = await book.add({ title, author }); return { book: resp.book, success: true, code: "ok", message: "New book added", }; } catch (err) { const apiError = err as APIError; return { book: null, success: false, code: apiError.code, message: apiError.message, }; } }, }; export default mutations; ``` Now we are ready can create the ApolloServer that makes use of our resolvers and to expose our GraphQL endpoint. 🥐 Still in the `graphql` directory, create a `graphql.ts` file containing: ```ts -- graphql/graphql.ts -- import { api } from "encore.dev/api"; import { ApolloServer, HeaderMap } from "@apollo/server"; import { readFileSync } from "node:fs"; import resolvers from "./resolvers"; import { json } from "node:stream/consumers"; const typeDefs = readFileSync("./schema.graphql", { encoding: "utf-8" }); const server = new ApolloServer({ typeDefs, resolvers, }); await server.start(); export const graphqlAPI = api.raw( { expose: true, path: "/graphql", method: "*" }, async (req, res) => { server.assertStarted("/graphql"); const headers = new HeaderMap(); for (const [key, value] of Object.entries(req.headers)) { if (value !== undefined) { headers.set(key, Array.isArray(value) ? value.join(", ") : value); } } // More on how to use executeHTTPGraphQLRequest: https://www.apollographql.com/docs/apollo-server/integrations/building-integrations/ const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({ httpGraphQLRequest: { headers, method: req.method!.toUpperCase(), body: await json(req), search: new URLSearchParams(req.url ?? "").toString(), }, context: async () => { return { req, res }; }, }); for (const [key, value] of httpGraphQLResponse.headers) { res.setHeader(key, value); } res.statusCode = httpGraphQLResponse.status || 200; if (httpGraphQLResponse.body.kind === "complete") { res.end(httpGraphQLResponse.body.string); return; } for await (const chunk of httpGraphQLResponse.body.asyncIterator) { res.write(chunk); } res.end(); }, ); ``` This creates an [Raw API endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints) available on `/graphql`. In the endpoint we use ApolloServer to handle the GraphQL queries and mutations. We then return the response to the client. If we were to use another GraphQL library other than Apollo, the concept would still be the same: 1. Take client requests with a Raw endpoint. 2. Pass along the request and response objects to the GraphQL library of your choice. 3. Use the library to handle the GraphQL queries and mutations. 4. Return the GraphQL response from the Raw endpoint. ## 6. Trying it out With that, the GraphQL API is done! 🥐Try it out by running `encore run` and opening [https://studio.apollographql.com/sandbox](https://studio.apollographql.com/sandbox) in your browser. Set http://localhost:4000/graphql as your endpoint URL. You should now be able to read the schema and execute queries. Enter the query: ```graphql mutation AddBook { addBook(author: "J.R.R. Tolkien", title: "The Hobbit") { success message code } } ``` Now try the GetBooks query: ```graphql query GetBooks { books { author title } } ``` And you should now see the "The Hobbit" in the list of books. 🥐 Try opening the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view the traces that were generated when calling your GraphQL API. ## 7. Deploy ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/ts/self-host/configure-infra) your Docker image. 🥐 Build a Docker image by running `encore build docker graphql:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Encore Cloud (free) Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes The final step before you deploy is to commit all changes to the project repo. 🥐 Push your changes and deploy your application to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment. ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ## Conclusion We've now built a GraphQL API gateway that forwards requests to the application's underlying Encore services in a type-safe way with minimal boilerplate. Note that the concepts discussed here are general and can be easily adapted to any GraphQL schema. Whenever you make a change to the schema or configuration, re-run `npm run generate` to regenerate the resolver types. ================================================ FILE: docs/ts/tutorials/rest-api.mdx ================================================ --- seotitle: How to build a REST API seodesc: Learn how to build and ship a REST API in just a few minutes using Encore.ts, go from zero to running API with this tutorial. title: Building a REST API subtitle: Learn how to build a URL shortener with a REST API and PostgreSQL database lang: ts --- In this tutorial you will create a REST API for a URL Shortener service. In a few short minutes, you'll learn how to: * Create REST APIs with Encore * Use PostgreSQL databases * Use the local development dashboard to test your app * Create and run tests This is the end result:
Deploy to Encore Deploy this app to a free dev environment
To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create a service and endpoint Create a new application by running `encore app create` and select `Empty app` as the template. If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial). Now let's create a new `url` service. 🥐 In your application's root folder, create a directory named `url` containing a file named `encore.service.ts`. ```shell $ mkdir url $ touch url/encore.service.ts ``` 🥐 Add the following code to `url/encore.service.ts`: ```ts -- url/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("url"); ``` This is how you define a service with Encore. Encore will now consider files in the `url` directory and all its subdirectories as part of the `url` service. 🥐 Create a new file `url.ts` in the `url` directory: ```shell $ touch url/url.ts ``` 🥐 Add the following code to `url/url.ts`: ```ts -- url/url.ts -- import { api } from "encore.dev/api"; import { randomBytes } from "node:crypto"; interface URL { id: string; // short-form URL id url: string; // complete URL, in long form } interface ShortenParams { url: string; // the URL to shorten } // Shortens a URL. export const shorten = api( { method: "POST", path: "/url", expose: true }, async ({ url }: ShortenParams): Promise => { const id = randomBytes(6).toString("base64url"); return { id, url }; }, ); ``` This sets up the `POST /url` endpoint. 🥐 Let’s see if it works! Start your app by running the following command from your app's root directory: ```shell $ encore run ``` You should see this: ```output Encore development server running! Your API is running at: http://127.0.0.1:4000 Development Dashboard URL: http://localhost:9400/5g288 3:50PM INF registered API endpoint endpoint=shorten path=/url service=url ``` 🥐 Next, call your endpoint from the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view a trace of the response. It should look like this: You can also call it from the terminal: ```shell $ curl http://localhost:4000/url -d '{"url": "https://encore.dev"}' ``` You should see this: ```output { "id": "5cJpBVRp", "url": "https://encore.dev" } ``` It works! There’s just one problem... Right now, we’re not actually storing the URL anywhere. That means we can generate shortened IDs but there’s no way to get back to the original URL! We need to store a mapping from the short ID to the complete URL. ## 2. Save URLs in a database Fortunately, Encore makes it really easy to set up a PostgreSQL database to store our data. To do so, we first define a **database schema**, in the form of a migration file. 🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `001_create_tables.up.sql`. The file name format is important (it must start with `001_` and end in `.up.sql`). ```shell $ mkdir url/migrations $ touch url/migrations/001_create_tables.up.sql ``` 🥐 Add the following contents to the file: ```sql -- url/migrations/001_create_tables.up.sql -- CREATE TABLE url ( id TEXT PRIMARY KEY, original_url TEXT NOT NULL ); ``` 🥐 Next, go back to the `url/url.ts` file and import the `SQLDatabase` class from `encore.dev/storage/sqldb` module by modifying the imports to look like this: ```ts -- url/url.ts -- import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { randomBytes } from "node:crypto"; ``` 🥐 Now, to define the database, create an instance of the `SQLDatabase` class in the `url` service: ```ts HL url/url.ts 4:6 -- url/url.ts -- import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { randomBytes } from "node:crypto"; // 'url' database is used to store the URLs that are being shortened. const db = new SQLDatabase("url", { migrations: "./migrations" }); interface URL { id: string; // short-form URL id url: string; // complete URL, in long form } interface ShortenParams { url: string; // the URL to shorten } // Shortens a URL. export const shorten = api( { method: "POST", path: "/url", expose: true }, async ({ url }: ShortenParams): Promise => { const id = randomBytes(6).toString("base64url"); return { id, url }; }, ); ``` 🥐 Lastly, update the `shorten` function to insert data into the database: ```ts HL url/url.ts 21:26 -- url/url.ts -- import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { randomBytes } from "node:crypto"; // 'url' database is used to store the URLs that are being shortened. const db = new SQLDatabase("url", { migrations: "./migrations" }); interface URL { id: string; // short-form URL id url: string; // complete URL, in long form } interface ShortenParams { url: string; // the URL to shorten } // Shortens a URL. export const shorten = api( { method: "POST", path: "/url", expose: true }, async ({ url }: ShortenParams): Promise => { const id = randomBytes(6).toString("base64url"); await db.exec` INSERT INTO url (id, original_url) VALUES (${id}, ${url}) `; return { id, url }; }, ); ``` Before running your application, make sure you have [Docker](https://www.docker.com) installed and running. It's required to locally run Encore applications with databases. 🥐 Next, start the application again with `encore run` and Encore automatically sets up your database. (In case your application won't run, check the [databases troubleshooting guide](/docs/ts/primitives/databases#troubleshooting).) You can verify that the database was created by looking at the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should look like this: Infra tab in local development dashboard 🥐 Now let's call the API again from the local development dashboard, or from the terminal: ```shell $ curl http://localhost:4000/url -d '{"url": "https://encore.dev"}' ``` 🥐 Finally, let's verify that it was saved in the database. You can do this by checking the trace in the local development dashboard, or you can run `encore db shell url` from the app root directory and inputting `select * from url;`: ```shell $ encore db shell url psql (13.1, server 11.12) Type "help" for help. url=# select * from url; id | original_url ----------+-------------------- zr6RmZc4 | https://encore.dev (1 row) ``` That was easy! ## 3. Add endpoint to retrieve URLs To complete our URL shortener API, let’s add the endpoint to retrieve a URL given its short id. 🥐 Add this endpoint to `url/url.ts`: ```ts HL url/url.ts 0:1 HL url/url.ts 29:40 -- url/url.ts -- import { api, APIError } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { randomBytes } from "node:crypto"; // 'url' database is used to store the URLs that are being shortened. const db = new SQLDatabase("url", { migrations: "./migrations" }); interface URL { id: string; // short-form URL id url: string; // complete URL, in long form } interface ShortenParams { url: string; // the URL to shorten } // Shortens a URL. export const shorten = api( { method: "POST", path: "/url", expose: true }, async ({ url }: ShortenParams): Promise => { const id = randomBytes(6).toString("base64url"); await db.exec` INSERT INTO url (id, original_url) VALUES (${id}, ${url}) `; return { id, url }; }, ); // Get retrieves the original URL for the id. export const get = api( { expose: true, auth: false, method: "GET", path: "/url/:id" }, async ({ id }: { id: string }): Promise => { const row = await db.queryRow` SELECT original_url FROM url WHERE id = ${id} `; if (!row) throw APIError.notFound("url not found"); return { id, url: row.original_url }; } ); ``` Encore uses the `/url/:id` syntax to represent a path with a parameter. The `id` name corresponds to the parameter name in the function signature. In this case it is of type `string`, but you can also use other built-in types like `number` or `boolean` if you want to restrict the values. 🥐 We can make sure it works by reviewing the endpoint in the [Service Catalog](/docs/ts/observability/service-catalog) in the local development dashboard, where we can call it using the `id` you got in the previous step: You can also call it directly from the terminal: ```shell $ curl http://localhost:4000/url/your-id-from-the-previous-step ``` You should now see this: ```json { "id": "your-id-from-the-previous-step", "url": "https://encore.dev" } ``` It works! That's how you build REST APIs and use PostgreSQL databases in Encore. ## 4. Add a test Before deployment, it is good practice to have tests to assure that the service works properly. Such tests including database access are easy to write. 🥐 Let's start by adding the `vitest` package to your project: ```shell $ npm i --save-dev vitest ``` [Vitest](https://vitest.dev/) is a testing framework that works great with Encore but you can use another TypeScript testing framework if you like. 🥐 Next we need to add a test script to our `package.json`: ```json -- package.json -- "scripts": { "test": "vitest" }, ``` We've prepared a test to check that the whole cycle of shortening the URL, storing and then retrieving the original URL works. 🥐 Save this in a separate file `url/url.test.ts`. ```ts -- url/url.test.ts -- import { describe, expect, test } from "vitest"; import { get, shorten } from "./url"; describe("shorten", () => { test("getting a shortened url should give back the original", async () => { const resp = await shorten({ url: "https://example.com" }); const url = await get({ id: resp.id }); expect(url.url).toBe("https://example.com"); }); }); ``` 🥐 Now run `encore test` to verify that it's working. If you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests. ## 5. Deploy ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration. Our URL shortener makes use of a PostgreSQL database, so we'll need to supply a runtime configuration so that our app knows how to connect to the database in the cloud. 🥐 Create a new file `infra-config.json` in the root of your project with the following contents: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "my-db-host:5432", "databases": { "url": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ] } ``` The values in this configuration are just examples, you will need to replace them with the correct values for your database. Take a look at our guide for [deploying an Encore app with a PostgreSQL database to Digital Ocean](/docs/ts/self-host/deploy-digitalocean) for more information. 🥐 Build a Docker image by running `encore build docker url-shortener:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Encore Cloud (free) Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes The final step before you deploy is to commit all changes to the project repo. 🥐 Commit the new files to the project's git repo and trigger a deploy to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From there you can also see metrics, traces, and connect your own AWS or GCP account to use for production deployment. *Now you have a fully fledged backend running in the cloud, well done!* ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) 🥐 A great next step is to [integrate with GitHub](/docs/platform/integrations/github). Once you've linked with GitHub, Encore will automatically start building and running tests against your Pull Requests. ## What's next Now that you know how to build a backend with a database, you're ready to let your creativity flow and begin building your next great idea! We're excited to hear what you're going to build with Encore, join the pioneering developer community on [Discord](/discord) and share your story. ================================================ FILE: docs/ts/tutorials/slack-bot.md ================================================ --- seotitle: Tutorial – How to build a Slack bot seodesc: Learn how to build a Slack bot with an Encore.ts backend, and get it running in the cloud in just a few minutes. title: Building a Slack bot subtitle: Learn how to build a Slack bot with an Encore backend lang: ts --- In this tutorial you will create a Slack bot that brings the greatness of the `cowsay` utility to Slack! ![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png "Slack bot") This is the end result:
Deploy to Encore Deploy this app to a free dev environment
To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. ## 1. Create your Encore application 🥐 Create a new Encore application by running `encore app create` and select `Empty app` as the template. **Take a note of your app id, we'll need it in the next step.** ## 2. Create a Slack app 🥐 The first step is to create a new Slack app: 1. Head over to [Slack's API site](https://api.slack.com/apps) and create a new app. 2. When prompted, choose to create the app **from an app manifest**. 3. Choose a workspace to install the app in. 🥐 Enter the following manifest (replace `$APP_ID` in the URL below with your app id from above): ```yaml _metadata: major_version: 1 display_information: name: Encore Bot description: Cowsay for the cloud age. features: slash_commands: - command: /cowsay # Replace $APP_ID below url: https://staging-$APP_ID.encr.app/cowsay description: Say things with a flair! usage_hint: your message here should_escape: false bot_user: display_name: encore-bot always_online: true oauth_config: scopes: bot: - commands - chat:write - chat:write.public settings: org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false ``` Once created, we're ready to move on with implementing our Encore endpoint! ## 3. Implement the Slack endpoint Since Slack sends custom HTTP headers that we need to pay attention to, we're going to use a raw endpoint in Encore. For more information on this check out Slack's documentation on [Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands). 🥐 In your Encore app, create a directory named `slack` containing a file named `encore.service.ts`. ```shell $ mkdir slack $ touch slack/encore.service.ts ``` 🥐 Add the following code to `slack/encore.service.ts`: ```ts -- slack/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("slack"); ``` This is how you create define services with Encore. Encore will now consider files in the `slack` directory and all its subdirectories as part of the `slack` service. 🥐 Create a file `slack/slack.ts` with the following contents: ```ts -- slack/slack.ts -- import { api } from "encore.dev/api"; import type { IncomingMessage } from "node:http"; // cowart is the formatting string for printing the cow art. const cowart = (msg: string) => `Moo! ${msg} `; export const cowsay = api.raw( { expose: true, path: "/cowsay", method: "*" }, async (req, resp) => { const body = await getBody(req); const text = new URLSearchParams(body).get("text"); const msg = cowart(text || "Moo!"); resp.setHeader("Content-Type", "application/json"); resp.end(JSON.stringify({ response_type: "in_channel", text: msg })); }, ); // Extract the body from an incoming request. function getBody(req: IncomingMessage): Promise { return new Promise((resolve) => { const bodyParts: any[] = []; req .on("data", (chunk) => { bodyParts.push(chunk); }) .on("end", () => { resolve(Buffer.concat(bodyParts).toString()); }); }); } ``` Let's try it out locally. 🥐 Start your app with `encore run` and then call it in another terminal: ```shell $ curl http://localhost:4000/cowsay -d 'text=Eat your greens!' {"response_type":"in_channel","text":"Moo! Eat your greens!"} ``` Looks great! 🥐 Next, let's deploy it to the cloud: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Once deployed, we're ready to try our Slack command! 🥐 Head over to the workspace you installed the app in and run `/cowsay Hello there`. You should see something like this: ![Cowsay](https://encore.dev/assets/docs/cowsay-wip.png "Cowsay (Work in Progress)") And just like that we have a fully working Slack integration. ## 4. Secure the webhook endpoint In order to get up and running quickly we ignored one important aspect for a production-ready Slack app: verifying that the webhook requests are actually coming from Slack. Let's do that now! The Slack documentation covers this really well on the [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack) page. In short, what we need to do is: 1. Save a shared secret that Slack provides us 2. Use the secret to verify that the request comes from Slack, using HMAC (Hash-based Message Authentication Code). ### Save the shared secret Let's define a secret using Encore's secrets management functionality. 🥐 Add this to your `slack.ts` file: ```ts HL slack/slack.ts 0:0 HL slack/slack.ts 2:2 -- slack/slack.ts -- import { secret } from "encore.dev/config"; const slackSigningSecret = secret("SlackSigningSecret"); ``` 🥐 Head over to the configuration section for your Slack app (go to [Your Apps](https://api.slack.com/apps) → select your app → Basic Information). 🥐 Copy the **Signing Secret** and then run `encore secret set --type prod SlackSigningSecret` and paste the secret. 🥐 For development you will also want to set `encore secret set --type dev,local,pr SlackSigningSecret`. You can use the same secret value or a placeholder value. ### Compute the HMAC TypeScript makes computing HMAC very straightforward, but it's still a fair amount of code. 🥐 Add a few more imports to your file, so that it reads: ```ts -- slack/slack.ts -- import { createHmac, timingSafeEqual } from "node:crypto"; import type { IncomingHttpHeaders } from "http"; ``` 🥐 Next, we'll add the `verifySignature` function: ```ts -- slack/slack.ts -- // Verifies the signature of an incoming request from Slack. const verifySignature = async function ( body: string, headers: IncomingHttpHeaders, ) { const requestTimestampSec = parseInt( headers["x-slack-request-timestamp"] as string, ); const signature = headers["x-slack-signature"] as string; if (Number.isNaN(requestTimestampSec)) { throw new Error( `Failed to verify authenticity: header x-slack-request-timestamp did not have the expected type (${requestTimestampSec})`, ); } // Calculate time-dependent values const nowMs = Date.now(); const requestTimestampMaxDeltaMin = 5; const fiveMinutesAgoSec = Math.floor(nowMs / 1000) - 60 * requestTimestampMaxDeltaMin; // Enforce verification rules // Rule 1: Check staleness if (requestTimestampSec < fiveMinutesAgoSec) { throw new Error( `Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than ${requestTimestampMaxDeltaMin} minutes or request is stale`, ); } // Rule 2: Check signature // Separate parts of signature const [signatureVersion, signatureHash] = signature.split("="); // Only handle known versions if (signatureVersion !== "v0") { throw new Error(`Failed to verify authenticity: unknown signature version`); } // Compute our own signature hash const hmac = createHmac("sha256", slackSigningSecret()); hmac.update(`${signatureVersion}:${requestTimestampSec}:${body}`); const ourSignatureHash = hmac.digest("hex"); if ( !signatureHash || !timingSafeEqual( Buffer.from(signatureHash, "utf8"), Buffer.from(ourSignatureHash, "utf8"), ) ) { throw new Error(`Failed to verify authenticity: signature mismatch`); } }; ``` We're now ready to verify the signature. 🥐 Update the `cowsay` function to look like this: ```ts HL slack/slack.ts 5:12 -- slack/slack.ts -- export const cowsay = api.raw( { expose: true, path: "/cowsay", method: "*" }, async (req, resp) => { const body = await getBody(req); try { await verifySignature(body, req.headers); } catch (err) { const e = err as Error; resp.statusCode = 500; resp.end(e.message); return; } const text = new URLSearchParams(body).get("text"); const msg = cowart(text || "Moo!"); resp.setHeader("Content-Type", "application/json"); resp.end(JSON.stringify({ response_type: "in_channel", text: msg })); }, ); ``` ## 5. Put it all together and deploy Finally we're ready to put it all together. 🥐 Add the `cowart` in `slack.ts` like so: ```ts -- slack/slack.ts -- const cowart = (msg: string) => ` \`\`\` +-${"-".repeat(msg.length)}-+ | ${msg} | +-${"-".repeat(msg.length)}-+ \\ __n__n__ .------\`-\\00/-' / ## ## (oo) / \\## __ ./ |//YY \\|/ ||| ||| \`\`\` `; ``` 🥐 Finally, let's commit our changes and deploy it: ```shell $ git add -A . $ git commit -m 'Verify webhook requests and improve art' $ git push encore ``` 🥐 Once deployed, head back to Slack and run `/cowsay Hello there`. If everything is set up correctly, you should see: ![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png "Slack bot") And there we go, a production-ready Slack bot in less than 100 lines of code. Well done! ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ================================================ FILE: docs/ts/tutorials/uptime.md ================================================ --- title: Building an Uptime Monitor subtitle: Learn how to build an event-driven uptime monitoring system seotitle: How to build an event-driven Uptime Monitoring System using Encore.ts seodesc: Learn how to build an event-driven uptime monitoring tool using TypeScript and Encore. Get your application running in the cloud in 30 minutes! lang: ts --- Want to be notified when your website goes down so you can fix it before your users notice? You need an uptime monitoring system. Sounds daunting? Don't worry, we'll build it with Encore in 30 minutes! The app will use an event-driven architecture and the final result will look like this:
Deploy to Encore Deploy this app to a free dev environment
## 1. Create your Encore application To make it easier to follow along, we've laid out a trail of croissants to guide your way. Whenever you see a 🥐 it means there's something for you to do. 🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use. ```shell $ encore app create uptime --example=github.com/encoredev/example-app-uptime/tree/starting-point-ts ``` If this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial). When we're done we'll have a backend with an event-driven architecture, as seen below in the [automatically generated diagram](/docs/ts/observability/encore-flow) where white boxes are services and black boxes are Pub/Sub topics: ## 2. Create monitor service Let's start by creating the functionality to check if a website is currently up or down. Later we'll store this result in a database so we can detect when the status changes and send alerts. 🥐 Create a directory named `monitor` containing a file named `encore.service.ts`. ```shell $ mkdir monitor $ touch monitor/encore.service.ts ``` 🥐 Add the following code to `monitor/encore.service.ts`: ```ts -- monitor/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("monitor"); ``` This is how you create define services with Encore. Encore will now consider files in the `monitor` directory and all its subdirectories as part of the `monitor` service. 🥐 In the `monitor` directory, create a file named `ping.ts`. 🥐 Add an Encore API endpoint named `ping` that takes a URL as input and returns a response indicating whether the site is up or down. ```ts -- monitor/ping.ts -- // Service monitor checks if a website is up or down. import { api } from "encore.dev/api"; export interface PingParams { url: string; } export interface PingResponse { up: boolean; } // Ping pings a specific site and determines whether it's up or down right now. export const ping = api( { expose: true, path: "/ping/:url", method: "GET" }, async ({ url }) => { // If the url does not start with "http:" or "https:", default to "https:". if (!url.startsWith("http:") && !url.startsWith("https:")) { url = "https://" + url; } try { // Make an HTTP request to check if it's up. const resp = await fetch(url, { method: "GET" }); // 2xx and 3xx status codes are considered up const up = resp.status >= 200 && resp.status < 300; return { up }; } catch (err) { return { up: false }; } } ); ``` 🥐 Let's try it! Run `encore run` in your terminal and you should see the service start up. Then open up the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and try calling the `monitor.ping` endpoint from the API Explorer, passing in `google.com` as the URL. You can then see the response, logs, and view a trace of the request. It will look like this: If you prefer to use the terminal instead run `curl http://localhost:4000/ping/google.com` in a new terminal instead. Either way you should see the response: ```json {"up": true} ``` You can also try with `httpstat.us/400` and `some-non-existing-url.com` and it should respond with `{"up": false}`. (It's always a good idea to test the negative case as well.) ### Add a test 🥐 Let's write an automated test so we don't break this endpoint over time. Create the file `monitor/ping.test.ts` with the content: ```ts -- monitor/ping.test.ts -- import { describe, expect, test } from "vitest"; import { ping } from "./ping"; describe("ping", () => { test.each([ // Test both with and without "https://" { site: "google.com", expected: true }, { site: "https://encore.dev", expected: true }, // 4xx and 5xx should considered down. { site: "https://not-a-real-site.xyz", expected: false }, // Invalid URLs should be considered down. { site: "invalid://scheme", expected: false }, ])( `should verify that $site is ${"$expected" ? "up" : "down"}`, async ({ site, expected }) => { const resp = await ping({ url: site }); expect(resp.up).toBe(expected); }, ); }); ``` 🥐 Run `encore test` to check that it all works as expected. You should see something like: ```shell $ encore test DEV v1.3.0 ✓ monitor/ping.test.ts (4) ✓ ping (4) ✓ should verify that 'google.com' is up ✓ should verify that 'https://encore.dev' is up ✓ should verify that 'https://not-a-real-site.xyz' is down ✓ should verify that 'invalid://scheme' is down Test Files 1 passed (1) Tests 4 passed (4) Start at 12:31:03 Duration 460ms (transform 43ms, setup 0ms, collect 59ms, tests 272ms, environment 0ms, prepare 47ms) PASS Waiting for file changes... ``` ## 3. Create site service Next, we want to keep track of a list of websites to monitor. Since most of these APIs will be simple "CRUD" (Create/Read/Update/Delete) endpoints, let's build this service using [Knex.js](https://knexjs.org/), an ORM library that makes building CRUD endpoints really simple. 🥐 Let's start with creating a new service named `site`: ```shell $ mkdir site # Create a new directory in the application root $ touch site/encore.service.ts ``` ```ts -- site/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("site"); ``` 🥐 Now we want to add a SQL database to the `site` service. To do so, create a new directory named `migrations` folder inside the `site` folder: ```shell $ mkdir site/migrations ``` 🥐 Add a database migration file inside that folder, named `1_create_tables.up.sql`. The file name is important (it must look something like `1_.up.sql`). Add the following contents: ```sql -- site/migrations/1_create_tables.up.sql -- CREATE TABLE site ( id SERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE ); ``` 🥐 Next, install the Knex.js library and PostgreSQL client: ```shell $ npm i knex pg ``` Now let's create the `site` service itself with our CRUD endpoints. 🥐 Create `site/site.ts` with the contents: ```ts -- site/site.ts -- import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import knex from "knex"; // Site describes a monitored site. export interface Site { id: number; // ID is a unique ID for the site. url: string; // URL is the site's URL. } // AddParams are the parameters for adding a site to be monitored. export interface AddParams { // URL is the URL of the site. If it doesn't contain a scheme // (like "http:" or "https:") it defaults to "https:". url: string; } // Add a new site to the list of monitored websites. export const add = api( { expose: true, method: "POST", path: "/site" }, async (params: AddParams): Promise => { const site = (await Sites().insert({ url: params.url }, "*"))[0]; return site; }, ); // Get a site by id. export const get = api( { expose: true, method: "GET", path: "/site/:id", auth: false }, async ({ id }: { id: number }): Promise => { const site = await Sites().where("id", id).first(); return site ?? Promise.reject(new Error("site not found")); }, ); // Delete a site by id. export const del = api( { expose: true, method: "DELETE", path: "/site/:id" }, async ({ id }: { id: number }): Promise => { await Sites().where("id", id).delete(); }, ); export interface ListResponse { sites: Site[]; // Sites is the list of monitored sites } // Lists the monitored websites. export const list = api( { expose: true, method: "GET", path: "/site" }, async (): Promise => { const sites = await Sites().select(); return { sites }; }, ); // Define a database named 'site', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. const SiteDB = new SQLDatabase("site", { migrations: "./migrations", }); const orm = knex({ client: "pg", connection: SiteDB.connectionString, }); const Sites = () => orm("site"); ``` 🥐 Now make sure you have [Docker](https://docker.com) installed and running, and then restart `encore run` to cause the `site` database to be created by Encore. You can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), and then use the Service Catalog to call the `site.add` endpoint. You can also call the `site.add` endpoint from the terminal: ```shell $ curl -X POST 'http://localhost:4000/site' -d '{"url": "https://encore.dev"}' { "id": 1, "url": "https://encore.dev" } ``` ## 4. Record uptime checks In order to notify when a website goes down or comes back up, we need to track the previous state it was in. 🥐 To do so, let's add a database to the `monitor` service as well. Create the directory `monitor/migrations` and the file `monitor/migrations/1_create_tables.up.sql`: ```sql -- monitor/migrations/1_create_tables.up.sql -- CREATE TABLE checks ( id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, up BOOLEAN NOT NULL, checked_at TIMESTAMP WITH TIME ZONE NOT NULL ); ``` We'll insert a database row every time we check if a site is up. 🥐 Add a new endpoint `check` to the `monitor` service, that takes in a Site ID, pings the site, and inserts a database row in the `checks` table. For this service we'll use Encore's [`SQLDatabase` class](https://encore.dev/docs/ts/primitives/databases#querying-data) instead of Knex (in order to showcase both approaches). Add the following to `monitor/check.ts`: ```ts -- monitor/check.ts -- import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { ping } from "./ping"; import { site } from "~encore/clients"; // Check checks a single site. export const check = api( { expose: true, method: "POST", path: "/check/:siteID" }, async (p: { siteID: number }): Promise<{ up: boolean }> => { const s = await site.get({ id: p.siteID }); const { up } = await ping({ url: s.url }); await MonitorDB.exec` INSERT INTO checks (site_id, up, checked_at) VALUES (${s.id}, ${up}, NOW()) `; return { up }; }, ); // Define a database named 'monitor', using the database migrations // in the "./migrations" folder. Encore automatically provisions, // migrates, and connects to the database. export const MonitorDB = new SQLDatabase("monitor", { migrations: "./migrations", }); ``` 🥐 Restart `encore run` to cause the `monitor` database to be created. We can again verify that the database was created in the Flow diagram, and also see the dependency between the `monitor` service and the `site` service that we just added. We can then call the `monitor.check` endpoint using the id `1` that we got in the last step, and view the trace where we see the database interactions. It will look something like this: You can also also inspect the database using `encore db shell `: ```shell $ encore db shell monitor psql (14.4, server 14.2) Type "help" for help. monitor=> SELECT * FROM checks; id | site_id | up | checked_at ----+---------+----+------------------------------- 1 | 1 | t | 2022-10-21 09:58:30.674265+00 ``` If that's what you see, everything's working perfectly! ### Add a cron job to check all sites We now want to regularly check all the tracked sites so we can respond in case any of them go down. We'll create a new `checkAll` API endpoint in the `monitor` service that will list all the tracked sites and check all of them. 🥐 Let's extract some of the functionality we wrote for the `check` endpoint into a separate function, like so: ```ts -- monitor/check.ts -- import {Site} from "../site/site"; // Check checks a single site. export const check = api( { expose: true, method: "POST", path: "/check/:siteID" }, async (p: { siteID: number }): Promise<{ up: boolean }> => { const s = await site.get({ id: p.siteID }); return doCheck(s); }, ); async function doCheck(site: Site): Promise<{ up: boolean }> { const { up } = await ping({ url: site.url }); await MonitorDB.exec` INSERT INTO checks (site_id, up, checked_at) VALUES (${site.id}, ${up}, NOW()) `; return { up }; } ``` Now we're ready to create our new `checkAll` endpoint. 🥐 Create the new `checkAll` endpoint inside `monitor/check.ts`: ```ts -- monitor/check.ts -- // CheckAll checks all sites. export const checkAll = api( { expose: true, method: "POST", path: "/check-all" }, async (): Promise => { const sites = await site.list(); await Promise.all(sites.sites.map(doCheck)); }, ); ``` 🥐 Now that we have a `checkAll` endpoint, define a [cron job](https://encore.dev/docs/ts/primitives/cron-jobs) to automatically call it every 1 hour (since this is an example, we don't need to go too crazy and check every minute): ```ts -- monitor/check.ts -- import { CronJob } from "encore.dev/cron"; // Check all tracked sites every 1 hour. const cronJob = new CronJob("check-all", { title: "Check all sites", every: "1h", endpoint: checkAll, }); ``` To avoid confusion while developing, cron jobs are not triggered when running the application locally but work when deploying the application to a cloud environment. The frontend needs a way to list all sites and display if they are up or down. 🥐 Add a file `monitor/status.ts` with the following code: ```ts import { api } from "encore.dev/api"; import { MonitorDB } from "./check"; interface SiteStatus { id: number; up: boolean; checkedAt: string; } // StatusResponse is the response type from the Status endpoint. interface StatusResponse { // Sites contains the current status of all sites, // keyed by the site ID. sites: SiteStatus[]; } // status checks the current up/down status of all monitored sites. export const status = api( { expose: true, path: "/status", method: "GET" }, async (): Promise => { const rows = await MonitorDB.query` SELECT DISTINCT ON (site_id) site_id, up, checked_at FROM checks ORDER BY site_id, checked_at DESC `; const results: SiteStatus[] = []; for await (const row of rows) { results.push({ id: row.site_id, up: row.up, checkedAt: row.checked_at, }); } return { sites: results }; }, ); ``` Now that the backend is working, let's open [http://localhost:4000/](http://localhost:4000/) in the browser to see the frontend of our application. ## 5. Deploy To try out your uptime monitor for real, let's deploy it to the cloud. ### Self-hosting Encore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice. If your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/ts/self-host/configure-infra) your Docker image. 🥐 Create a new file `infra-config.json` in the root of your project with the following contents: ```json { "$schema": "https://encore.dev/schemas/infra.schema.json", "sql_servers": [ { "host": "my-db-host:5432", "databases": { "monitor": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} }, "site": { "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ] } ``` The values in this configuration are just examples, you will need to replace them with the correct values for your database. Take a look at our guide for [deploying an Encore app with a PostgreSQL database to Digital Ocean](/docs/ts/self-host/deploy-digitalocean) for more information. 🥐 Build a Docker image by running `encore build docker uptime:v1.0`. This will compile your application using the host machine and then produce a Docker image containing the compiled application. 🥐 Upload the Docker image to the cloud provider of your choice and run it. ### Encore Cloud (free) Encore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP. ### Create account Before deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step. If you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template. After creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform). ### Commit changes Encore comes with built-in CI/CD, and the deployment process is as simple as a `git push`. (You can also integrate with GitHub, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).) 🥐 Let's deploy your app to Encore's free development cloud by running: ```shell $ git add -A . $ git commit -m 'Initial commit' $ git push encore ``` Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud. After triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...` From the Cloud Dashboard you can also see metrics, trigger Cron Jobs, see traces, and later connect your own AWS or GCP account to use for deployment. 🥐 When the deploy has finished, you can try out your uptime monitor by going to `https://staging-$APP_ID.encr.app`. *You now have an app running in the cloud, well done!* ## 6. Publish Pub/Sub events when a site goes down Hold on, an uptime monitoring system isn't very useful if it doesn't actually notify you when a site goes down. To do so let's add a [Pub/Sub topic](https://encore.dev/docs/ts/primitives/pubsub) on which we'll publish a message every time a site transitions from being up to being down, or vice versa. 🥐 Define the topic using Encore's Pub/Sub module in `monitor/check.ts`: ```ts -- monitor/check.ts -- import { Subscription, Topic } from "encore.dev/pubsub"; // TransitionEvent describes a transition of a monitored site // from up->down or from down->up. export interface TransitionEvent { site: Site; // Site is the monitored site in question. up: boolean; // Up specifies whether the site is now up or down (the new value). } // TransitionTopic is a pubsub topic with transition events for when a monitored site // transitions from up->down or from down->up. export const TransitionTopic = new Topic("uptime-transition", { deliveryGuarantee: "at-least-once", }); ``` Now let's publish a message on the `TransitionTopic` if a site's up/down state differs from the previous measurement. 🥐 Create a `getPreviousMeasurement` function to report the last up/down state: ```ts -- monitor/check.ts -- // getPreviousMeasurement reports whether the given site was // up or down in the previous measurement. async function getPreviousMeasurement(siteID: number): Promise { const row = await MonitorDB.queryRow` SELECT up FROM checks WHERE site_id = ${siteID} ORDER BY checked_at DESC LIMIT 1 `; return row?.up ?? true; } ``` 🥐 Now add a function to conditionally publish a message if the up/down state differs by modifying the `doCheck` function: ```ts -- monitor/check.ts -- async function doCheck(site: Site): Promise<{ up: boolean }> { const { up } = await ping({ url: site.url }); // Publish a Pub/Sub message if the site transitions // from up->down or from down->up. const wasUp = await getPreviousMeasurement(site.id); if (up !== wasUp) { await TransitionTopic.publish({ site, up }); } await MonitorDB.exec` INSERT INTO checks (site_id, up, checked_at) VALUES (${site.id}, ${up}, NOW()) `; return { up }; } ``` 🥐 Start your app again using `encore run` and open the Flow architecture diagram in the local development dashboard. Now you'll see the Pub/Sub topic as a black box, it should look like this: Now the monitoring system will publish messages on the `TransitionTopic` whenever a monitored site transitions from up->down or from down->up. It doesn't know or care who actually listens to these messages. The truth is right now nobody does. So let's fix that by adding a Pub/Sub subscriber that posts these events to Slack. ## 7. Send Slack notifications when a site goes down 🥐 Start by creating a new service named `slack`: ```shell $ mkdir slack # Create a new directory in the application root $ touch slack/encore.service.ts ``` ```ts -- slack/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("slack"); ``` 🥐 Add a `slack.ts` file containing the following: ```ts -- slack/slack.ts -- import { api } from "encore.dev/api"; import { secret } from "encore.dev/config"; import log from "encore.dev/log"; export interface NotifyParams { text: string; // the slack message to send } // Sends a Slack message to a pre-configured channel using a // Slack Incoming Webhook (see https://api.slack.com/messaging/webhooks). export const notify = api({}, async ({ text }) => { const url = webhookURL(); if (!url) { log.info("no slack webhook url defined, skipping slack notification"); return; } const resp = await fetch(url, { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: text }), }); if (resp.status >= 400) { const body = await resp.text(); throw new Error(`slack notification failed: ${resp.status}: ${body}`); } }); // SlackWebhookURL defines the Slack webhook URL to send uptime notifications to. const webhookURL = secret("SlackWebhookURL"); ``` 🥐 Now go to a Slack community of your choice where you have the permission to create a new Incoming Webhook. 🥐 Once you have the Webhook URL, set it as an Encore secret: ```shell $ encore secret set --type dev,local,pr SlackWebhookURL Enter secret value: ***** Successfully updated development secret SlackWebhookURL. ``` 🥐 Test the `slack.notify` endpoint by calling it via cURL: ```shell $ curl 'http://localhost:4000/slack.notify' -d '{"text": "Testing Slack webhook"}' ``` You should see the *Testing Slack webhook* message appear in the Slack channel you designated for the webhook. 🥐 When it works it's time to add a Pub/Sub subscriber to automatically notify Slack when a monitored site goes up or down. Add the following: ```ts -- slack/slack.ts -- import { Subscription } from "encore.dev/pubsub"; import { TransitionTopic } from "../monitor/check"; const _ = new Subscription(TransitionTopic, "slack-notification", { handler: async (event) => { const text = `*${event.site.url} is ${event.up ? "back up." : "down!"}*`; await notify({ text }); }, }); ``` ## 8. Deploy your finished Uptime Monitor Now you're ready to deploy your finished Uptime Monitor, complete with a Slack integration. ### Self-hosting Because we have added more infrastructure to our app, we need to [update the configuration](/docs/ts/self-host/configure-infra) in our `infra-config.json` to include the new Pub/Sub topic and subscription as well as how we should set the `SlackWebhookURL` secret. 🥐 Update your `ìnfra-config.json` to reflect the new infrastructure. 🥐 Build a Docker image by running `encore build docker uptime:v2.0`. 🥐 Upload the Docker image to the cloud provider and run it. ### Encore Cloud (free) 🥐 As before, deploying your app to the cloud is as simple as running: ```shell $ git add -A . $ git commit -m 'Add slack integration' $ git push encore ``` ### Celebrate with fireworks Now that your app is running in the cloud, let's celebrate with some fireworks: 🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux). _From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._ 🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show! ![Fireworks](/assets/docs/fireworks.jpg) ## Conclusion We've now built a fully functioning uptime monitoring system. If we may say so ourselves (and we may; it's our documentation after all) it's pretty remarkable how much we've accomplished in such little code: * We've built three different services (`site`, `monitor`, and `slack`) * We've added two databases (to the `site` and `monitor` services) for tracking monitored sites and the monitoring results * We've added a cron job for automatically checking the sites every hour * We've set up a Pub/Sub topic to decouple the monitoring system from the Slack notifications * We've added a Slack integration, using secrets to securely store the webhook URL, listening to a Pub/Sub subscription for up/down transition events All of this in just a bit over 300 lines of code. It's time to lean back and take a sip of your favorite beverage, safe in the knowledge you'll never be caught unaware of a website going down suddenly. ================================================ FILE: e2e-tests/README.md ================================================ # End to End Tests This folder contains end to end tests for Encore, which test everything in Encore as an end user would use it. The tests will: 1. Effectively run `encore run` on the [echo test app](./testdata/echo) 2. Perform some basic requests against the running app to verify behaviour 3. Generate the front end clients for the app 4. Run tests against using generated clients against the running app 5. Shutdown the running app ================================================ FILE: e2e-tests/app_test.go ================================================ //go:build e2e package tests import ( "bytes" "context" "encoding/base64" "fmt" "io" "net" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "github.com/cenkalti/backoff/v4" "github.com/gofrs/uuid" "github.com/rs/zerolog/log" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" "encr.dev/cli/daemon/pubsub" "encr.dev/cli/daemon/redis" . "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/run/infra" "encr.dev/cli/daemon/secret" . "encr.dev/internal/optracker" "encr.dev/internal/version" "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/svcproxy" "encr.dev/pkg/vcs" meta "encr.dev/proto/encore/parser/meta/v1" ) type RunAppData struct { Addr string Run *Run Meta *meta.Data NSQ *pubsub.NSQDaemon Redis *redis.Server Env []string Values map[string]any // arbitrary values for use in testscripts } func RunApp(c testing.TB, appRoot string, logger RunLogger, env []string) *RunAppData { assertNil := func(err error) { if err != nil { c.Fatal(err) } } ln, err := net.Listen("tcp", "localhost:0") assertNil(err) c.Cleanup(func() { _ = ln.Close() }) ctx, cancel := context.WithCancel(context.Background()) c.Cleanup(cancel) svcProxy, err := svcproxy.New(ctx, log.Logger) assertNil(err) // Use a randomly generated app id to avoid tests trampling on each other // since we use a persistent working directory based on the app id. app := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), "") bld := builderimpl.Resolve(app.Lang(), nil) mgr := &Manager{} ns := &namespace.Namespace{ID: "some-id", Name: "default"} rm := infra.NewResourceManager(app, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, ns, nil, 0, false) run := &Run{ ID: GenID(), ListenAddr: ln.Addr().String(), SvcProxy: svcProxy, App: app, ResourceManager: rm, Mgr: mgr, Builder: bld, Params: &StartParams{}, } parse, build, configs := testBuild(c, appRoot, env) jobs := NewAsyncBuildJobs(ctx, app.PlatformOrLocalID(), nil) run.ResourceManager.StartRequiredServices(jobs, parse.Meta) c.Cleanup(rm.StopAll) assertNil(jobs.Wait()) env = append(env, "FOO=bar", "BAR=baz") if logger == nil { logger = testRunLogger{c} } expSet, err := experiments.FromAppFileAndEnviron(nil, env) assertNil(err) secrets := secret.New() secretData, err := secrets.Load(app).Get(ctx, expSet) assertNil(err) p, err := run.StartProcGroup(&StartProcGroupParams{ Ctx: ctx, Outputs: build.Outputs, Meta: parse.Meta, Logger: logger, Environ: env, ServiceConfigs: configs.Configs, Experiments: expSet, Secrets: secretData.Values, }) assertNil(err) c.Cleanup(p.Close) run.StoreProc(p) for serviceName, config := range configs.Configs { env = append(env, fmt.Sprintf("%s=%s", fmt.Sprintf("ENCORE_CFG_%s", strings.ToUpper(serviceName)), base64.RawURLEncoding.EncodeToString([]byte(config)))) } // start proxying TCP requests to the running application startProxy(ctx, ln, http.HandlerFunc(p.ProxyReq)) // wait for the service to come up b := backoff.NewExponentialBackOff() b.MaxElapsedTime = 10 * time.Second err = backoff.Retry(func() error { w := httptest.NewRecorder() req, err := http.NewRequestWithContext(ctx, "GET", "http://localhost/__encore/healthz", nil) assertNil(err) p.ProxyReq(w, req) if w.Result().StatusCode != http.StatusOK { return fmt.Errorf("unexpected status: %s", w.Result().Status) } return nil }, b) assertNil(err) return &RunAppData{ Addr: ln.Addr().String(), Run: run, Meta: parse.Meta, NSQ: rm.GetPubSub(), Redis: rm.GetRedis(), Env: env, Values: make(map[string]any), } } func RunTests(c testing.TB, appRoot string, stdout, stderr io.Writer, environ []string) error { mgr := &Manager{ Secret: secret.New(), ClusterMgr: nil, } ctx, cancel := context.WithCancel(context.Background()) c.Cleanup(cancel) // Use a randomly generated app id to avoid tests trampling on each other // since we use a persistent working directory based on the app id. app := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), "") var args []string switch app.Lang() { case appfile.LangTS: args = []string{} case appfile.LangGo: fallthrough default: args = []string{"./..."} } if app.Lang() == appfile.LangTS { args = []string{} } err := mgr.Test(ctx, TestParams{ TestSpecParams: &TestSpecParams{ App: app, WorkingDir: ".", Args: args, Environ: environ, }, Stdout: stdout, Stderr: stderr, }) return err } func getNodeJSPath() option.Option[string] { path, err := exec.LookPath("node") if err != nil { return option.None[string]() } return option.Some(filepath.Dir(path)) } // testRunLogger implements runLogger by calling t.Log. type testRunLogger struct { log interface{ Log(args ...any) } } func (l testRunLogger) RunStdout(r *Run, line []byte) { line = bytes.TrimSuffix(line, []byte{'\n'}) l.log.Log(string(line)) } func (l testRunLogger) RunStderr(r *Run, line []byte) { line = bytes.TrimSuffix(line, []byte{'\n'}) l.log.Log(string(line)) } func startProxy(ctx context.Context, ln net.Listener, proxyHandler http.Handler) { srv := &http.Server{Handler: proxyHandler} go func() { <-ctx.Done() _ = srv.Close() }() go func() { _ = srv.Serve(ln) }() } // testBuild is a helper that compiles the app situated at appRoot // and cleans up the build dir during test cleanup. func testBuild(t testing.TB, appRoot string, env []string) (*builder.ParseResult, *builder.CompileResult, *builder.ServiceConfigsResult) { expSet, err := experiments.FromAppFileAndEnviron(nil, env) if err != nil { t.Fatal(err) } // Use a randomly generated app id to avoid tests trampling on each other // since we use a persistent working directory based on the app id. app := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), "") bld := builderimpl.Resolve(app.Lang(), expSet) defer fns.CloseIgnore(bld) ctx := context.Background() vcsRevision := vcs.GetRevision(app.Root()) buildInfo := builder.BuildInfo{ BuildTags: builder.LocalBuildTags, CgoEnabled: true, StaticLink: false, DebugMode: builder.DebugModeDisabled, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: vcsRevision.Revision, UncommittedChanges: vcsRevision.Uncommitted, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: buildInfo, App: app, WorkingDir: ".", }) if err != nil { t.Fatal(err) } parse, err := bld.Parse(ctx, builder.ParseParams{ Build: buildInfo, App: app, Experiments: expSet, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) if err != nil { t.Fatal(err) } err = bld.GenUserFacing(ctx, builder.GenUserFacingParams{ Build: buildInfo, App: app, Parse: parse, }) if err != nil { t.Fatal(err) } configs, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{ Parse: parse, CueMeta: &cueutil.Meta{ APIBaseURL: "http://what?", EnvName: "end_to_end_test", EnvType: cueutil.EnvType_Development, CloudType: cueutil.CloudType_Local, }, }) if err != nil { t.Fatal(err) } build, err := bld.Compile(ctx, builder.CompileParams{ Build: buildInfo, App: app, Parse: parse, OpTracker: nil, Experiments: expSet, WorkingDir: ".", }) if err != nil { t.Fatal(err) } t.Cleanup(func() { for _, output := range build.Outputs { _ = os.RemoveAll(output.GetArtifactDir().ToIO()) } }) return parse, build, configs } func runGoModTidy(dir string) error { cmd := exec.Command("go", "mod", "tidy") cmd.Dir = dir cmd.Env = append(os.Environ(), "GO111MODULE=on") out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("go mod tidy failed: %s", out) } return nil } ================================================ FILE: e2e-tests/echo_app_test.go ================================================ //go:build e2e package tests import ( "bytes" "context" "encoding/json" "fmt" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "strings" "testing" "time" qt "github.com/frankban/quicktest" "github.com/rs/zerolog/log" "go.uber.org/goleak" "encr.dev/cli/daemon/apps" "encr.dev/cli/daemon/namespace" . "encr.dev/cli/daemon/run" "encr.dev/cli/daemon/run/infra" . "encr.dev/internal/optracker" "encr.dev/pkg/clientgen" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/golden" "encr.dev/pkg/svcproxy" "encr.dev/v2/v2builder" ) type Data[K any, V any] struct { Key K Value V } type NonBasicRequest struct { Struct Data[*Data[string, string], int] StructPtr *Data[int, uint16] StructSlice []*Data[string, string] StructMap map[string]*Data[string, float32] StructMapPtr *map[string]*Data[string, string] AnonStruct struct{ AnonBird string } NamedStruct *Data[string, float64] `json:"formatted_nest"` RawStruct Data[[]string, []byte] UnusedRequest *NonBasicRequest } type NonBasicResponse struct { // Body Struct Data[*Data[string, string], int] StructPtr *Data[int, uint16] StructSlice []*Data[string, string] StructMap map[string]*Data[string, float32] StructMapPtr *map[string]*Data[string, string] AnonStruct struct{ AnonBird string } NamedStruct *Data[string, float64] `json:"formatted_nest"` RawStruct json.RawMessage // Query QueryString string QueryNumber int OptQueryString string OptQueryNumber int // Path PathString string PathInt int PathWild string // Auth AuthHeader string AuthQuery []int } // TestEndToEndWithApp tests that (*app).startProc correctly starts Encore processes // for sending requests. func TestEndToEndWithApp(t *testing.T) { doTestEndToEndWithApp(t, nil) } func doTestEndToEndWithApp(t *testing.T, env []string) { c := qt.New(t) wd, err := os.Getwd() if err != nil { t.Fatal(err) } appRoot := filepath.Join(wd, "testdata", "echo") app := RunApp(c, appRoot, nil, env) run := app.Run // Use golden to test that the generated clients are as expected for the echo test app for lang, path := range map[clientgen.Lang]string{ clientgen.LangGo: "golang/client/goclient.go", clientgen.LangTypeScript: "ts/client.ts", clientgen.LangJavascript: "js/client.js", } { services := clientgentypes.AllServices(app.Meta) client, err := clientgen.Client(lang, "slug", app.Meta, services, clientgentypes.TagSet{}, clientgentypes.Options{}) if err != nil { fmt.Println(err.Error()) c.FailNow() } c.Assert(err, qt.IsNil, qt.Commentf("Got an error generating the client for: %s", lang)) golden.TestAgainst(c, filepath.Join("echo_client", path), string(client)) } c.Run("basic requests", func(c *qt.C) { // Send a simple request c.Run("Send a simple request", func(c *qt.C) { input := Data[string, int]{"hello", 1} body, err := json.Marshal(&input) c.Assert(err, qt.IsNil) w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/echo.Echo", bytes.NewReader(body)) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, input) }) // Send a pubsub c.Run("send a pubsub", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/echo.Publish", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) // Wait a bit to allow the message to be consumed. time.Sleep(100 * time.Millisecond) stats, err := app.NSQ.Stats() c.Assert(err, qt.IsNil) c.Assert(len(stats.Producers), qt.Equals, 1) c.Assert(len(stats.Topics), qt.Equals, 1) c.Assert(stats.Topics[0].TopicName, qt.Equals, "test") c.Assert(len(stats.Topics[0].Channels), qt.Equals, 1) c.Assert(stats.Topics[0].Channels[0].RequeueCount == 0, qt.IsTrue) c.Assert(stats.Topics[0].Channels[0].Depth == 0, qt.IsTrue) }) // Call an endpoint using an unsupported HTTP Method c.Run("unsupported HTTP Method", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.Echo", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 404) }) // Send an empty request c.Run("empty request", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/echo.EmptyEcho", bytes.NewReader([]byte("{}"))) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{ "NullPtr": nil, "Zero": Data[string, string]{}, }) }) // Send a non-basic type request with path params, headers, query string and body c.Run("non-basic type request", func(c *qt.C) { input := NonBasicRequest{ Struct: Data[*Data[string, string], int]{&Data[string, string]{"peacock", "duck"}, 1}, StructPtr: &Data[int, uint16]{2, 3}, StructSlice: []*Data[string, string]{{"seagull", "penguin"}, {"penguin", "seagull"}}, StructMap: map[string]*Data[string, float32]{ "hawk": {"hummingbird", 18.5}, "albatross": {"magpie", 13.2}, }, StructMapPtr: &map[string]*Data[string, string]{ "hornbill": {"bird-of-paradise", "cuckoo"}, "turkey": {"owl", "waxbill"}, }, AnonStruct: struct{ AnonBird string }{AnonBird: "dove"}, NamedStruct: &Data[string, float64]{"pigeon", 34.2}, UnusedRequest: &NonBasicRequest{StructPtr: &Data[int, uint16]{43, 9}}, RawStruct: Data[[]string, []byte]{[]string{"emu", "ostrich"}, []byte{4, 4, 5}}, } output := NonBasicResponse{ Struct: input.Struct, StructPtr: input.StructPtr, StructSlice: input.StructSlice, StructMap: input.StructMap, StructMapPtr: input.StructMapPtr, AnonStruct: input.AnonStruct, NamedStruct: input.NamedStruct, QueryString: "robin", QueryNumber: 33, RawStruct: json.RawMessage(`{"Key": ["emu", "ostrich"], "Value": "BAQF"}`), PathString: "shoebill", PathInt: 55, PathWild: "toucan/crane/vulture/78/", AuthHeader: "header", AuthQuery: []int{5, 6}, } body, err := json.Marshal(&input) c.Assert(err, qt.IsNil) w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/NonBasicEcho/shoebill/55/toucan/crane/vulture/78/?string=robin&no=33&query=5&query=6", bytes.NewReader(body)) req.Header.Add("X-Header", "header") req.Header.Add("X-Header-String", "starling") req.Header.Add("X-Header-Number", "10") req.Header.Add("Authorization", "Bearer tokendata") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Header().Get("X-Header-String"), qt.Equals, "starling") c.Assert(w.Header().Get("X-Header-Number"), qt.Equals, "10") c.Assert(w.Body.Bytes(), qt.JSONEquals, output) }) // Send a request with only header parameters c.Run("only headers", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.HeadersEcho", nil) req.Header.Add("x-int", "1") req.Header.Add("x-string", "nightingale") req.Header.Add("X-StringSlice", "mynah, quail, weaver") req.Header.Add("X-StringSlice", "pewit") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Header().Get("X-Int"), qt.Equals, "1") c.Assert(w.Header().Get("X-String"), qt.Equals, "nightingale") }) // Send POST and GET requests to the same endpoint with an assortment of basic types c.Run("POST and GET", func(c *qt.C) { input := map[string]any{ "string": "string", "uint": 1, "int": 2, "int8": -3, "int64": 4, "float32": 5, "float64": 6, "string_slice": []any{"slice1", "slice2"}, "int_slice": []any{1, 2, 3}, "time": "2016-01-02T15:04:05+07:00", } output := map[string]any{ "String": "string", "Uint": 1, "Int": 2, "Int8": -3, "Int64": 4, "Float32": 5, "Float64": 6, "StringSlice": []any{"slice1", "slice2"}, "IntSlice": []any{1, 2, 3}, "Time": "2016-01-02T15:04:05+07:00", } qs := "" for k, av := range input { vs := []any{av} switch av.(type) { case []any: vs = av.([]any) } for _, v := range vs { if len(qs) > 0 { qs += "&" } value := url.QueryEscape(fmt.Sprintf("%v", v)) qs += fmt.Sprintf("%s=%v", strings.ToLower(k), value) } } body, err := json.Marshal(&output) c.Assert(err, qt.IsNil) w := httptest.NewRecorder() w2 := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.BasicEcho?"+qs, nil) req2 := httptest.NewRequest("POST", "/echo.BasicEcho", bytes.NewReader(body)) run.ServeHTTP(w, req) run.ServeHTTP(w2, req2) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, output) c.Assert(w2.Body.Bytes(), qt.DeepEquals, w.Body.Bytes()) }) // Call an endpoint without request parameters, returning nil c.Run("without request", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.NilResponse", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) }) // Call an endpoint with an invalid auth parameter c.Run("invalid parameter", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.NilResponse", nil) req.Header.Add("x-auth-int", "invalid") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 400) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{ "code": "invalid_argument", "details": nil, "message": "invalid auth param: x-auth-int: invalid parameter: strconv.ParseInt: parsing \"invalid\": invalid syntax", }) }) // Call an endpoint without request parameters and response value c.Run("without response", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.Noop", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) }) // Call an endpoint with request parameters but no response value c.Run("only request", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.MuteEcho?key=pelican&value=cocabura", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) }) // Call an endpoint with a response value but no request parameters c.Run("no request, response", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.Pong", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, Data[string, string]{"woodpecker", "kingfisher"}) }) // Call endpoint with custom http status in response c.Run("custom http status response", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.CustomHTTPStatus", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 201) }) // Call the env endpoint and make sure we get our env variables back { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.Env", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) filteredEnv := make([]string, 0, len(app.Env)) for _, env := range app.Env { if !strings.HasPrefix(env, "ENCORE_") { filteredEnv = append(filteredEnv, env) } } c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string][]string{"Env": filteredEnv}) } // Call the app metadata endpoint and make sure we get correct data back c.Run("app metadata", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.AppMeta", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) // we need to extract the API Base URL as it will change due to the `RuntimePort: 0` above bytes := w.Body.Bytes() got := make(map[string]string) _ = json.Unmarshal(bytes, &got) c.Assert(strings.HasPrefix(got["APIBaseURL"], "http://"), qt.IsTrue) c.Assert(bytes, qt.JSONEquals, map[string]interface{}{ "AppID": "", "APIBaseURL": got["APIBaseURL"], "EnvName": "local", "EnvType": "development", }) }) // Try the dependency injection services c.Run("dependency_injection", func(c *qt.C) { { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/di/one", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.HasLen, 0) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/di/two", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]string{"Msg": "Hello World"}) } }) c.Run("cache", func(c *qt.C) { { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/incr/one", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Val": 1}) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/incr/one", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Val": 2}) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/incr/two", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Val": 1}) } { w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/cache/struct/1/foo", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.HasLen, 0) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/struct/1", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Val": "foo"}) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/list/1", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Vals": []string{}}) } { w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/cache/list/1/foo", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.HasLen, 0) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/list/1", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Vals": []string{"foo"}}) } { w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/cache/list/1/bar", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.HasLen, 0) } { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cache/list/1", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{"Vals": []string{"foo", "bar"}}) } keys := app.Redis.Miniredis().Keys() c.Assert(keys, qt.DeepEquals, []string{ "int/one", "int/two", "list/1/foo/1", "struct/1/dummy/x", }) }) }) c.Run("generated_wrappers_for_intra_service_calls", func(c *qt.C) { // Send a simple request { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/generated-wrappers-end-to-end-test", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) } }) c.Run("config_test", func(c *qt.C) { { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo.ConfigValues", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{ "ReadOnlyMode": true, "PublicKey": "aGVsbG8gd29ybGQK", "AdminUsers": []string{"foo", "bar"}, "SubKeyCount": 2, }) } }) c.Run("go_generated_client", func(c *qt.C) { encoreGoroot := os.Getenv("ENCORE_GOROOT") c.Assert(encoreGoroot, qt.Not(qt.Equals), "") goPath := filepath.Join(encoreGoroot, "bin", "go") cmd := exec.Command(goPath, "run", ".", app.Addr) cmd.Dir = filepath.Join("testdata", "echo_client") cmd.Env = append(os.Environ(), "GOROOT="+encoreGoroot, "PATH="+fmt.Sprintf("%s%s%s", filepath.Join(encoreGoroot, "/bin"), string(filepath.ListSeparator), os.Getenv("PATH")), ) out, err := cmd.CombinedOutput() c.Assert(err, qt.IsNil, qt.Commentf("Got error running generated Go client: %s", out)) }) c.Run("typescript_generated_client", func(c *qt.C) { npmCommandsToRun := [][]string{ {"install", "--prefer-offline", "--no-audit"}, {"run", "lint"}, {"run", "test", "--", app.Addr}, } for _, args := range npmCommandsToRun { cmd := exec.Command("npm", args...) cmd.Dir = filepath.Join("testdata", "echo_client") out, err := cmd.CombinedOutput() c.Assert(err, qt.IsNil, qt.Commentf("Got error running generated Typescript client: %s", out)) } }) c.Run("javascript_generated_client", func(c *qt.C) { npmCommandsToRun := [][]string{ {"install", "--prefer-offline", "--no-audit"}, {"run", "lint"}, {"run", "test:js", "--", app.Addr}, } for _, args := range npmCommandsToRun { cmd := exec.Command("npm", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Dir = filepath.Join("testdata", "echo_client") c.Assert(cmd.Run(), qt.IsNil, qt.Commentf("Got error running generated JavaScript client")) } }) } // TestProcClosedOnCtxCancel tests that the proc is closed when // the given ctx is cancelled. func TestProcClosedOnCtxCancel(t *testing.T) { defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) wd, err := os.Getwd() if err != nil { t.Fatal(err) } appRoot := filepath.Join(wd, "testdata", "echo") app := apps.NewInstance(appRoot, "local_id", "platform_id") c := qt.New(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() svcProxy, err := svcproxy.New(ctx, log.Logger) c.Assert(err, qt.IsNil) mgr := &Manager{} ns := &namespace.Namespace{ID: "some-id", Name: "default"} rm := infra.NewResourceManager(app, nil, nil, nil, ns, nil, 0, false) run := &Run{ ID: GenID(), App: app, Mgr: mgr, ResourceManager: rm, ListenAddr: "127.0.0.1:34212", SvcProxy: svcProxy, Builder: v2builder.New(), Params: &StartParams{}, } parse, build, _ := testBuild(c, appRoot, append(os.Environ(), "ENCORE_EXPERIMENT=v2")) jobs := NewAsyncBuildJobs(ctx, app.PlatformOrLocalID(), nil) run.ResourceManager.StartRequiredServices(jobs, parse.Meta) defer run.Close() c.Assert(jobs.Wait(), qt.IsNil) p, err := run.StartProcGroup(&StartProcGroupParams{ Ctx: ctx, Outputs: build.Outputs, Meta: parse.Meta, Logger: testRunLogger{t}, Environ: os.Environ(), }) c.Assert(err, qt.IsNil) cancel() <-p.Done() } ================================================ FILE: e2e-tests/testdata/echo/.gitignore ================================================ encore.gen.go /.encore encore.gen.cue /encore.gen ================================================ FILE: e2e-tests/testdata/echo/cache/cache.go ================================================ package cache import ( "context" "encore.dev/beta/errs" "encore.dev/storage/cache" ) var cluster = cache.NewCluster("cluster", cache.ClusterConfig{ EvictionPolicy: cache.AllKeysLFU, }) var ints = cache.NewIntKeyspace[string](cluster, cache.KeyspaceConfig{ KeyPattern: "int/:key", }) type IncrResponse struct { Val int64 } //encore:api public path=/cache/incr/:key func Incr(ctx context.Context, key string) (*IncrResponse, error) { val, err := ints.Increment(ctx, key, 1) return &IncrResponse{Val: val}, err } type StructKey struct { Key int Dummy string } type StructVal struct { Val string } var structs = cache.NewStructKeyspace[StructKey, StructVal](cluster, cache.KeyspaceConfig{ KeyPattern: "struct/:Key/dummy/:Dummy", }) //encore:api public method=POST path=/cache/struct/:key/:val func PostStruct(ctx context.Context, key int, val string) error { err := structs.Set(ctx, StructKey{Key: key, Dummy: "x"}, StructVal{Val: val}) return err } //encore:api public method=GET path=/cache/struct/:key func GetStruct(ctx context.Context, key int) (StructVal, error) { val, err := structs.Get(ctx, StructKey{Key: key, Dummy: "x"}) if err == cache.Miss { return StructVal{}, &errs.Error{Code: errs.NotFound} } else if err != nil { return StructVal{}, err } return val, nil } var lists = cache.NewListKeyspace[int, string](cluster, cache.KeyspaceConfig{ KeyPattern: "list/:key/foo/:key", }) //encore:api public method=POST path=/cache/list/:key/:val func PostList(ctx context.Context, key int, val string) error { _, err := lists.PushRight(ctx, key, val) return err } type ListResponse struct { Vals []string } //encore:api public method=GET path=/cache/list/:key func GetList(ctx context.Context, key int) (ListResponse, error) { vals, err := lists.Items(ctx, key) return ListResponse{vals}, err } ================================================ FILE: e2e-tests/testdata/echo/di/di.go ================================================ package di import ( "archive/zip" "context" "database/sql" "io" "net/http" "sync" "encore.dev/cron" ) type TwoResponse struct { Msg string } var _ = cron.NewJob("repeating-one", cron.JobConfig{ Title: "Call One every 2 hours", Every: 2 * cron.Hour, Endpoint: One, }) //encore:service type Service struct { Msg string // Include various types to make sure the parser doesn't complain. mu sync.Mutex once *sync.Once db *sql.DB fn func() *zip.Writer } //encore:api public path=/di/one func (s *Service) One(ctx context.Context) error { return nil } //encore:api public path=/di/two func (s *Service) Two(ctx context.Context) (*TwoResponse, error) { return &TwoResponse{Msg: s.Msg}, nil } //encore:api public raw path=/di/raw func (s *Service) Three(w http.ResponseWriter, req *http.Request) { io.Copy(w, req.Body) } func initService() (*Service, error) { return &Service{Msg: "Hello World"}, nil } ================================================ FILE: e2e-tests/testdata/echo/echo/config.cue ================================================ import "encoding/base64" ReadOnlyMode: true PublicKey: base64.Decode(null, "aGVsbG8gd29ybGQK") // "hello world" in Base64 SubConfig: SubKey: MaxCount: [ if #Meta.Environment.Type == "test" { 3 }, if #Meta.Environment.Cloud == "local" { 2 }, 1 ][0] AdminUsers: [ "foo", "bar", ] ================================================ FILE: e2e-tests/testdata/echo/echo/config.go ================================================ package echo import ( "context" "encore.dev/config" ) type CfgType[T any] struct { ReadOnlyMode config.Bool PublicKey config.Bytes AdminUsers config.Values[string] SubConfig config.Value[struct { SubKey *SubCfgType[T] }] Currencies map[string]struct { Name config.String Code config.String Aliases config.Values[string] } AnotherList config.Values[struct { Name config.String }] } type SubCfgType[T any] struct { MaxCount T } var cfg = config.Load[*CfgType[uint]]() type ConfigResponse struct { ReadOnlyMode bool PublicKey []byte SubKeyCount uint AdminUsers []string } //encore:api public func ConfigValues(ctx context.Context) (ConfigResponse, error) { return ConfigResponse{ ReadOnlyMode: cfg.ReadOnlyMode(), PublicKey: cfg.PublicKey(), AdminUsers: cfg.AdminUsers(), SubKeyCount: cfg.SubConfig().SubKey.MaxCount, }, nil } ================================================ FILE: e2e-tests/testdata/echo/echo/config_test.go ================================================ package echo import ( "context" "testing" ) func TestConfigValues(t *testing.T) { values, err := ConfigValues(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if values.SubKeyCount != 3 { t.Fatalf("expected 1, got %d", values.SubKeyCount) } } ================================================ FILE: e2e-tests/testdata/echo/echo/echo.go ================================================ package echo import ( "context" "encoding/json" "errors" "fmt" "log" "os" "reflect" "strconv" "time" encore "encore.dev" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/pubsub" ) type Message struct { Attr string `pubsub-attr:"attr"` Subject string Body string } var Topic = pubsub.NewTopic[*Message]( "test", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }, ) var _ = pubsub.NewSubscription( Topic, "test", pubsub.SubscriptionConfig[*Message]{ Handler: Consumer, }, ) type Data[K any, V any] struct { Key K Value V } type NonBasicData struct { // Header HeaderString string `header:"X-Header-String"` HeaderNumber int `header:"X-Header-Number"` // Body Struct Data[*Data[string, string], int] StructPtr *Data[int, uint16] StructSlice []*Data[string, string] StructMap map[string]*Data[string, float32] StructMapPtr *map[string]*Data[string, string] AnonStruct struct{ AnonBird string } NamedStruct *Data[string, float64] `json:"formatted_nest"` RawStruct json.RawMessage // Query QueryString string `query:"string"` QueryNumber int `query:"no"` OptQueryNumber int `query:"optnum" encore:"optional"` OptQueryString string `query:"optstr" encore:"optional"` // Path Parameters PathString string PathInt int PathWild string // Auth Parameters AuthHeader string AuthQuery []int // Unexported fields unexported string } type EmptyData struct { OmitEmpty Data[string, string] `json:"OmitEmpty,omitempty"` NullPtr *string Zero Data[string, string] } type BasicData struct { String string Uint uint Int int Int8 int8 Int64 int64 Float32 float32 Float64 float64 StringSlice []string IntSlice []int Time time.Time } type HeadersData struct { Int int `header:"X-Int"` String string `header:"X-String"` } // Publish publishes a request on a topic // //encore:api public func Publish(ctx context.Context) error { id, err := Topic.Publish(ctx, &Message{ Attr: "Attr", Subject: "subject", Body: "body", }) if err != nil { return err } fmt.Printf("Published: %s\n", id) return nil } //encore:api private func Consumer(ctx context.Context, msg *Message) error { if msg.Attr != "Attr" { return errors.New("incorrect Attr value") } if msg.Subject != "subject" { return errors.New("incorrect Subject value") } if msg.Body != "body" { return errors.New("incorrect Body value") } return nil } // Echo echoes back the request data. // //encore:api public func Echo(ctx context.Context, params *Data[string, int]) (*Data[string, int], error) { return params, nil } // EmptyEcho echoes back the request data. // //encore:api public func EmptyEcho(ctx context.Context, params EmptyData) (EmptyData, error) { return params, nil } // NonBasicEcho echoes back the request data. // //encore:api auth path=/NonBasicEcho/:pathString/:pathInt/*pathWild func NonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild string, params *NonBasicData) (*NonBasicData, error) { data := auth.Data().(*AuthParams) params.PathString = pathString params.PathInt = pathInt params.PathWild = pathWild params.AuthQuery = data.Query params.AuthHeader = data.Header return params, nil } // BasicEcho echoes back the request data. // //encore:api public method=GET,POST func BasicEcho(ctx context.Context, params *BasicData) (*BasicData, error) { return params, nil } // HeadersEcho echoes back the request headers // //encore:api public method=GET,POST func HeadersEcho(ctx context.Context, params *HeadersData) (*HeadersData, error) { return params, nil } // Noop does nothing // //encore:api public method=GET func Noop(ctx context.Context) error { return nil } // NilResponse returns a nil response and nil error // //encore:api public method=GET,POST func NilResponse(ctx context.Context) (*BasicData, error) { return nil, nil } // MuteEcho absorbs a request // //encore:api public method=GET func MuteEcho(ctx context.Context, params Data[string, string]) error { log.Printf("Absorbing %v\n", params) return nil } // Pong returns a bird tuple // //encore:api public method=GET func Pong(ctx context.Context) (Data[string, string], error) { return Data[string, string]{"woodpecker", "kingfisher"}, nil } // HTTPStatusResponse demonstrates encore:"httpstatus" tag functionality type HTTPStatusResponse struct { Message string `json:"message"` Status int `encore:"httpstatus"` } // CustomHTTPStatus allows testing of custom HTTP status codes via encore:"httpstatus" tag // //encore:api public func CustomHTTPStatus(ctx context.Context) (*HTTPStatusResponse, error) { return &HTTPStatusResponse{ Message: "Created successfully", Status: 201, // HTTP 201 Created }, nil } type EnvResponse struct { Env []string } // Env returns the environment. // //encore:api public func Env(ctx context.Context) (*EnvResponse, error) { return &EnvResponse{Env: os.Environ()}, nil } type AppMetadata struct { AppID string APIBaseURL string EnvName string EnvType string } // AppMeta returns app metadata. // //encore:api public func AppMeta(ctx context.Context) (*AppMetadata, error) { md := encore.Meta() return &AppMetadata{ AppID: md.AppID, APIBaseURL: md.APIBaseURL.String(), EnvName: md.Environment.Name, EnvType: string(md.Environment.Type), }, nil } type AuthParams struct { Header string `header:"X-Header"` AuthInt int `header:"X-Auth-Int"` Authorization string `header:"Authorization"` Query []int `query:"query"` NewAuth bool `query:"new-auth"` } func (p *AuthParams) Validate() error { if p.Header == "fail-validation" { return errors.New("auth validation fail") } return nil } //encore:authhandler func AuthHandler(ctx context.Context, params *AuthParams) (auth.UID, *AuthParams, error) { if reflect.ValueOf(params).Elem().IsZero() { panic("zero value auth params should skip authhandler") } if params.Authorization == "Bearer tokendata" && params.NewAuth == false { return "user", params, nil } // Check headers and query strings work by adding them together to calculate the answer in the header field if params.Header != "" && params.NewAuth { ans := 0 for _, v := range params.Query { ans += v } if strconv.FormatInt(int64(ans), 10) == params.Header { return "second_user", params, nil } } return "", nil, &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } } ================================================ FILE: e2e-tests/testdata/echo/echo/echo_test.go ================================================ package echo import ( "os" "strings" "testing" ) // TestEnvsProvided tests that 'go test' was invoked with the envs we expect. func TestEnvsProvided(t *testing.T) { envs := os.Environ() want := map[string]string{ "FOO": "bar", "BAR": "baz", } for _, env := range envs { key, val, _ := strings.Cut(env, "=") if wantVal, ok := want[key]; ok { if val != wantVal { t.Errorf("got env %s=%s, want value %s", key, val, wantVal) } } } } ================================================ FILE: e2e-tests/testdata/echo/empty_cfg/service.go ================================================ package emptycfg import ( "context" "encore.dev/config" ) type cfg struct { } var Config = config.Load[*cfg]() //encore:api public func AnAPI(ctx context.Context) error { return nil } ================================================ FILE: e2e-tests/testdata/echo/encore.app ================================================ {} ================================================ FILE: e2e-tests/testdata/echo/endtoend/endtoend.go ================================================ package endtoend import ( "context" "encoding/json" "fmt" "math/rand" // "net/http" // "net/http/httptest" "os" "reflect" // "strings" "time" "encore.app/test" "encore.dev/beta/errs" "encore.dev/rlog" "encore.dev/types/option" "encore.dev/types/uuid" "github.com/google/go-cmp/cmp" ) var assertNumber = 0 //encore:api public method=GET path=/generated-wrappers-end-to-end-test func GeneratedWrappersEndToEndTest(ctx context.Context) (err error) { rlog.Info("Starting end-to-end test of generated wrappers") defer func() { if r := recover(); r != nil { rlog.Error("Panic occured during end to end test", "err", r) err = fmt.Errorf("%v", r) } }() // Even on a slow machine, the client should be able to connect and run this test script in 30 seconds ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Test a simple no-op err = test.Noop(ctx) assert(err, nil, "Wanted no error from noop") // Test we get back the right structured error err = test.NoopWithError(ctx) assertStructuredError(err, errs.Unimplemented, "totally not implemented yet") // Test a simple echo echoRsp, err := test.SimpleBodyEcho(ctx, &test.BodyEcho{Message: "hello world"}) assert(err, nil, "Wanted no error from simple body echo") assert(echoRsp.Message, "hello world", "Wanted body to be 'hello world'") // Check our UpdateMessage and GetMessage API's getRsp, err := test.GetMessage(ctx, "intra-service wrapper") assert(err, nil, "Wanted no error from get message") assert(getRsp.Message, "", "Expected no message on first request") err = test.UpdateMessage(ctx, "intra-service wrapper", &test.BodyEcho{Message: "updating now"}) assert(err, nil, "Wanted no error from update message") getRsp, err = test.GetMessage(ctx, "intra-service wrapper") assert(err, nil, "Wanted no error from get message") assert(getRsp.Message, "updating now", "Expected data from Update request") // Test the rest API which uses all input types (query string, json body and header fields) // as well as nested structs and path segments in the URL restRsp, err := test.RestStyleAPI(ctx, 5, "hello", &test.RestParams{ HeaderValue: "this is the header field", QueryValue: "this is a query string field", BodyValue: "this is the body field", Nested: struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` }{ Key: "the nested key", Value: 8, Ok: true, }, }) assert(err, nil, "Wanted no error from rest style api") assert(restRsp.HeaderValue, "this is the header field", "expected header value") assert(restRsp.QueryValue, "this is a query string field", "expected query value") assert(restRsp.BodyValue, "this is the body field", "expected body value") assert(restRsp.Nested.Key, "hello + the nested key", "expected nested key") assert(restRsp.Nested.Value, 5+8, "expected nested value") assert(restRsp.Nested.Ok, true, "expected nested ok") // Full marshalling test with randomised payloads r := rand.New(rand.NewSource(time.Now().UnixNano())) headerBytes := make([]byte, r.Intn(128)) queryBytes := make([]byte, r.Intn(128)) bodyBytes := make([]byte, r.Intn(128)) r.Read(headerBytes) r.Read(queryBytes) r.Read(bodyBytes) params := &test.MarshallerTest[int]{ HeaderBoolean: r.Float32() > 0.5, HeaderInt: r.Int(), HeaderFloat: r.Float64(), HeaderString: "header string", HeaderBytes: headerBytes, HeaderTime: time.Now().Truncate(time.Second), HeaderJson: json.RawMessage("{\"hello\":\"world\"}"), HeaderUUID: newUUID(), HeaderUserID: "432", HeaderOption: option.Some("test"), QueryBoolean: r.Float32() > 0.5, QueryInt: r.Int(), QueryFloat: r.Float64(), QueryString: "query string", QueryBytes: headerBytes, QueryTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second), QueryJson: json.RawMessage("true"), QueryUUID: newUUID(), QueryUserID: "9udfa", QuerySlice: []int{r.Int(), r.Int(), r.Int(), r.Int()}, BodyBoolean: r.Float32() > 0.5, BodyInt: r.Int(), BodyFloat: r.Float64(), BodyString: "body string", BodyBytes: bodyBytes, BodyTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second), BodyJson: json.RawMessage("null"), BodyUUID: newUUID(), BodyUserID: "✉️", BodySlice: []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()}, BodyOption: option.Some(r.Int()), BodyOptionSlice: []option.Option[int]{option.Some(r.Int()), option.None[int](), option.Some(r.Int())}, } mResp, err := test.MarshallerTestHandler(ctx, params) assert(err, nil, "Expected no error from the marshaller test") // We're marshalling as JSON, so we can just compare the JSON strings respAsJSON, err := json.Marshal(mResp) assert(err, nil, "unable to marshal response to JSON") reqAsJSON, err := json.Marshal(params) assert(err, nil, "unable to marshal response to JSON") if diff := cmp.Diff(string(respAsJSON), string(reqAsJSON)); diff != "" { assertNumber++ panic(fmt.Sprintf("Assertion Failure %d: %s", assertNumber, diff)) } // Test the raw endpoint (Unsupported currently in service to service calls) // { // req, err := http.NewRequest("PUT", "?foo=bar", strings.NewReader("this is a test body")) // assert(err, nil, "unable to create request for raw endpoint") // req.Header.Add("X-Test-Header", "test") // // w := httptest.NewRecorder() // err = test.RawEndpoint(w, req) // assert(err, nil, "expected no error from the raw socket") // // assert(w.Code, http.StatusCreated, "expected the status code to be 201") // // type responseType struct { // Body string // Header string // PathParam string // QueryString string // } // response := &responseType{} // // err = json.Unmarshal(w.Body.Bytes(), response) // assert(err, nil, "expected no error when unmarshalling the response body") // // assert(response, &responseType{"this is a test body", "test", "hello", "bar"}, "expected the response to match") // // } rlog.Info("End to end wrappers test completed without error") return nil } func assert(got, want any, message string) { assertNumber++ if !reflect.DeepEqual(got, want) { panic(fmt.Sprintf("Assertion Failure %d: %s\n\n%+v != %+v\n", assertNumber, message, got, want)) } } func assertNotNil(got any, message string) { assertNumber++ if got == nil { panic(fmt.Sprintf("Assertion Failure %d: got nil: %s", assertNumber, message)) } } func assertStructuredError(err error, code errs.ErrCode, message string) { assertNotNil(err, "want an error") assertNumber++ if apiError, ok := err.(*errs.Error); !ok { panic(fmt.Sprintf("Assertion Failure %d: expected *errs.Error; got %+v\n", assertNumber, reflect.TypeOf(err))) os.Exit(assertNumber) } else { assert(apiError.Code, code, "unexpected error code") assert(apiError.Message, message, "expected error message") } } func newUUID() uuid.UUID { id, err := uuid.NewV4() if err != nil { panic(err) } return id } ================================================ FILE: e2e-tests/testdata/echo/go.mod ================================================ module encore.app go 1.24.0 require ( encore.dev v1.52.0 github.com/google/go-cmp v0.7.0 ) ================================================ FILE: e2e-tests/testdata/echo/go.sum ================================================ encore.dev v1.52.0 h1:e8LbHiccXLI3M4Oc3HbvWoVy8AyNyBJm7wY7NsPwCd4= encore.dev v1.52.0/go.mod h1:lK8vSJG6uhYeUwT87/FEpcLdiN98QUcotd3gxRX0xDw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= ================================================ FILE: e2e-tests/testdata/echo/middleware/middleware.go ================================================ package middleware import ( "context" "encoding/json" "fmt" "reflect" "encore.dev/beta/errs" "encore.dev/middleware" ) //encore:middleware target=tag:error func ErroringMiddleware(req middleware.Request, next middleware.Next) middleware.Response { return middleware.Response{ Err: errs.B().Code(errs.Internal).Msg("middleware error").Err(), } } //encore:middleware target=tag:resp-rewrite func ResponseRewritingMiddleware(req middleware.Request, next middleware.Next) middleware.Response { resp := next(req) reqPayload := req.Data().Payload.(*Payload) respPayload := resp.Payload.(*Payload) respPayload.Msg = fmt.Sprintf("middleware(req=%s, resp=%s)", reqPayload.Msg, respPayload.Msg) return resp } //encore:middleware target=tag:resp-gen func ResponseGeneratingMiddleware(req middleware.Request, next middleware.Next) middleware.Response { responseData := `{"Msg": "middleware generated"}` payload := reflect.New(req.Data().API.ResponseType) if err := json.Unmarshal([]byte(responseData), payload.Interface()); err != nil { return middleware.Response{Err: err} } return middleware.Response{Payload: payload.Elem().Interface()} } type Payload struct { Msg string } //encore:api public tag:error func Error(ctx context.Context) error { return nil } //encore:api public tag:resp-rewrite func ResponseRewrite(ctx context.Context, req *Payload) (*Payload, error) { return &Payload{Msg: fmt.Sprintf("handler(%s)", req.Msg)}, nil } //encore:api public tag:resp-gen func ResponseGen(ctx context.Context, msg *Payload) (*Payload, error) { return nil, nil } ================================================ FILE: e2e-tests/testdata/echo/middleware/middleware_test.go ================================================ package middleware import ( "context" "testing" "encore.dev/beta/errs" ) // TestMiddleware tests that middleware is executed during tests. func TestMiddleware(t *testing.T) { ctx := context.Background() if err := Error(ctx); err == nil { t.Error("Error(): want non-nil error, got nil") } else if ee, ok := err.(*errs.Error); !ok { t.Errorf("Error(): want *errs.Error, got %T", err) } else if ee.Code != errs.Internal { t.Errorf("Error(): want code=Internal, got %v", ee.Code) } else if ee.Message != "middleware error" { t.Errorf("Error(): want msg=\"middleware error\", got %q", ee.Message) } resp, err := ResponseRewrite(ctx, &Payload{Msg: "foo"}) if err != nil { t.Errorf("ResponseRewrite(): got non-nil error: %v", err) } else if want := "middleware(req=foo, resp=handler(foo))"; resp.Msg != want { t.Errorf("ResponseRewrite(): got %q, want %q", resp.Msg, want) } resp, err = ResponseGen(ctx, &Payload{Msg: "foo"}) if err != nil { t.Errorf("ResponseGen(): got non-nil error: %v", err) } else if want := "middleware generated"; resp.Msg != want { t.Errorf("ResponseGen(): got %q, want %q", resp.Msg, want) } } ================================================ FILE: e2e-tests/testdata/echo/test/endpoints.go ================================================ package test import ( "context" "encoding/json" "io" "net/http" "strconv" "time" encore "encore.dev" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/types/option" "encore.dev/types/uuid" ) // Noop allows us to test if a simple HTTP request can be made // //encore:api public func Noop(ctx context.Context) error { return nil } // NoopWithError allows us to test if the structured errors are returned // //encore:api public func NoopWithError(ctx context.Context) error { return &errs.Error{ Code: errs.Unimplemented, Message: "totally not implemented yet", } } type BodyEcho struct { Message string } // SimpleBodyEcho allows us to exercise the body marshalling from JSON // and being returned purely as a body // //encore:api public func SimpleBodyEcho(ctx context.Context, body *BodyEcho) (*BodyEcho, error) { return body, nil } var lastMessage = make(map[string]string) // UpdateMessage allows us to test an API which takes parameters, // but doesn't return anything // //encore:api public method=PUT path=/last_message/:clientID func UpdateMessage(ctx context.Context, clientID string, message *BodyEcho) error { lastMessage[clientID] = message.Message return nil } // GetMessage allows us to test an API which takes no parameters, // but returns data. It also tests two API's on the same path with different HTTP methods // //encore:api public method=GET path=/last_message/:clientID func GetMessage(ctx context.Context, clientID string) (*BodyEcho, error) { return &BodyEcho{ Message: lastMessage[clientID], }, nil } type RestParams struct { HeaderValue string `header:"Some-Key"` QueryValue string `query:"Some-Key"` BodyValue string `json:"Some-Key"` Nested struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` } } // RestStyleAPI tests all the ways we can get data into and out of the application // using Encore request handlers // //encore:api public method=PUT path=/rest/object/:objType/:name func RestStyleAPI(ctx context.Context, objType int, name string, params *RestParams) (*RestParams, error) { return &RestParams{ HeaderValue: params.HeaderValue, QueryValue: params.QueryValue, BodyValue: params.BodyValue, Nested: struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` }{ Key: name + " + " + params.Nested.Key, Value: objType + params.Nested.Value, Ok: params.Nested.Ok, }, }, nil } type MarshallerTest[A any] struct { HeaderBoolean bool `header:"x-boolean"` HeaderInt int `header:"x-int"` HeaderFloat float64 `header:"x-float"` HeaderString string `header:"x-string"` HeaderBytes []byte `header:"x-bytes"` HeaderTime time.Time `header:"x-time"` HeaderJson json.RawMessage `header:"x-json"` HeaderUUID uuid.UUID `header:"x-uuid"` HeaderUserID auth.UID `header:"x-user-id"` HeaderOption option.Option[string] `header:"x-option"` QueryBoolean bool `qs:"boolean"` QueryInt int `qs:"int"` QueryFloat float64 `qs:"float"` QueryString string `qs:"string"` QueryBytes []byte `qs:"bytes"` QueryTime time.Time `qs:"time"` QueryJson json.RawMessage `qs:"json"` QueryUUID uuid.UUID `qs:"uuid"` QueryUserID auth.UID `qs:"user-id"` QuerySlice []A `qs:"slice"` BodyBoolean bool `json:"boolean"` BodyInt int `json:"int"` BodyFloat float64 `json:"float"` BodyString string `json:"string"` BodyBytes []byte `json:"bytes"` BodyTime time.Time `json:"time"` BodyJson json.RawMessage `json:"json"` BodyUUID uuid.UUID `json:"uuid"` BodyUserID auth.UID `json:"user-id"` BodySlice []A `json:"slice"` BodyOption option.Option[A] `json:"option"` BodyOptionSlice []option.Option[A] `json:"option-slice"` } // MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all // the field types. It simply echos all the responses back to the client // //encore:api public func MarshallerTestHandler(ctx context.Context, params *MarshallerTest[int]) (*MarshallerTest[int], error) { return params, nil } // TestAuthHandler allows us to test the clients ability to add tokens to requests // //encore:api auth func TestAuthHandler(ctx context.Context) (*BodyEcho, error) { userID, ok := auth.UserID() return &BodyEcho{ Message: string(userID) + "::" + strconv.FormatBool(ok), }, nil } type response struct { Body string Header string PathParam string QueryString string } // RawEndpoint allows us to test the clients' ability to send raw requests // under auth // //encore:api public raw method=PUT,POST,DELETE,GET path=/raw/blah/*id func RawEndpoint(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusCreated) bytes, err := io.ReadAll(req.Body) if err != nil { panic(err) } req.Body.Close() b, err := json.Marshal(&response{ Body: string(bytes), Header: req.Header.Get("X-Test-Header"), PathParam: encore.CurrentRequest().PathParams.Get("id"), QueryString: req.URL.Query().Get("foo"), }) if err != nil { panic(err) } w.Write(b) } type MultiPathSegment struct { Boolean bool Int int String string UUID uuid.UUID Wildcard string } // PathMultiSegments allows us to wildcard segments and segment URI encoding // //encore:api public path=/multi/:bool/:int/:string/:uuid/*wildcard func PathMultiSegments(ctx context.Context, bool bool, int int, string string, uuid uuid.UUID, wildcard string) (*MultiPathSegment, error) { return &MultiPathSegment{ Boolean: bool, Int: int, String: string, UUID: uuid, Wildcard: wildcard, }, nil } ================================================ FILE: e2e-tests/testdata/echo/validation/validation.go ================================================ package validation import ( "context" "errors" ) type Request struct { Msg string } func (req *Request) Validate() error { if req.Msg == "fail" { return errors.New("bad message") } return nil } //encore:api public func TestOne(ctx context.Context, msg *Request) error { return nil } ================================================ FILE: e2e-tests/testdata/echo_client/.eslintrc.cjs ================================================ /* .eslintrc.js */ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', ], env: { browser: true, es6: true, node: true, commonjs: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', ], } ================================================ FILE: e2e-tests/testdata/echo_client/.gitignore ================================================ node_modules/ ================================================ FILE: e2e-tests/testdata/echo_client/go.mod ================================================ module echo_client go 1.21 require github.com/google/go-cmp v0.7.0 ================================================ FILE: e2e-tests/testdata/echo_client/go.sum ================================================ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= ================================================ FILE: e2e-tests/testdata/echo_client/golang/client/goclient.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) // Client is an API client for the slug Encore application. type Client struct { Cache CacheClient Di DiClient Echo EchoClient Emptycfg EmptycfgClient Endtoend EndtoendClient Middleware MiddlewareClient Test TestClient Validation ValidationClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-slug.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "slug-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{ Cache: &cacheClient{base}, Di: &diClient{base}, Echo: &echoClient{base}, Emptycfg: &emptycfgClient{base}, Endtoend: &endtoendClient{base}, Middleware: &middlewareClient{base}, Test: &testClient{base}, Validation: &validationClient{base}, }, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } // WithAuth allows you to set the authentication data to be used with each request func WithAuth(auth EchoAuthParams) Option { return func(base *baseClient) error { base.authGenerator = func(_ context.Context) (EchoAuthParams, error) { return auth, nil } return nil } } // WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request func WithAuthFunc(authGenerator func(ctx context.Context) (EchoAuthParams, error)) Option { return func(base *baseClient) error { base.authGenerator = authGenerator return nil } } type CacheIncrResponse struct { Val int64 } type CacheListResponse struct { Vals []string } type CacheStructVal struct { Val string } // CacheClient Provides you access to call public and authenticated APIs on cache. The concrete implementation is cacheClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type CacheClient interface { GetList(ctx context.Context, key int) (CacheListResponse, error) GetStruct(ctx context.Context, key int) (CacheStructVal, error) Incr(ctx context.Context, key string) (CacheIncrResponse, error) PostList(ctx context.Context, key int, val string) error PostStruct(ctx context.Context, key int, val string) error } type cacheClient struct { base *baseClient } var _ CacheClient = (*cacheClient)(nil) func (c *cacheClient) GetList(ctx context.Context, key int) (resp CacheListResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", fmt.Sprintf("/cache/list/%d", key), nil, nil, &resp) if err != nil { return } return } func (c *cacheClient) GetStruct(ctx context.Context, key int) (resp CacheStructVal, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", fmt.Sprintf("/cache/struct/%d", key), nil, nil, &resp) if err != nil { return } return } func (c *cacheClient) Incr(ctx context.Context, key string) (resp CacheIncrResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/cache/incr/%s", url.PathEscape(key)), nil, nil, &resp) if err != nil { return } return } func (c *cacheClient) PostList(ctx context.Context, key int, val string) error { _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/cache/list/%d/%s", key, url.PathEscape(val)), nil, nil, nil) return err } func (c *cacheClient) PostStruct(ctx context.Context, key int, val string) error { _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/cache/struct/%d/%s", key, url.PathEscape(val)), nil, nil, nil) return err } type DiTwoResponse struct { Msg string } // DiClient Provides you access to call public and authenticated APIs on di. The concrete implementation is diClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type DiClient interface { One(ctx context.Context) error Three(ctx context.Context, request *http.Request) (*http.Response, error) Two(ctx context.Context) (DiTwoResponse, error) } type diClient struct { base *baseClient } var _ DiClient = (*diClient)(nil) func (c *diClient) One(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/di/one", nil, nil, nil) return err } func (c *diClient) Three(ctx context.Context, request *http.Request) (*http.Response, error) { request = request.WithContext(ctx) // Check the request has the method set, as we can't guess what method is required if request.Method == "" { return nil, errors.New("request.Method must be set") } // Set the relative URL for the API call path, err := url.Parse("/di/raw") if err != nil { return nil, fmt.Errorf("unable to parse api url: %w", err) } if request.URL != nil { // If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema, // host and path to ensure the final URL which hit the right BaseURL request.URL.Scheme = path.Scheme request.URL.Host = path.Host request.URL.Path = path.Path } else { request.URL = path } return c.base.Do(request) } func (c *diClient) Two(ctx context.Context) (resp DiTwoResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/di/two", nil, nil, &resp) if err != nil { return } return } type EchoAppMetadata struct { AppID string APIBaseURL string EnvName string EnvType string } type EchoAuthParams struct { Header string `header:"X-Header"` AuthInt int `header:"X-Auth-Int"` Authorization string `header:"Authorization"` Query []int `query:"query"` NewAuth bool `query:"new-auth"` } type EchoBasicData struct { String string Uint uint Int int Int8 int8 Int64 int64 Float32 float32 Float64 float64 StringSlice []string IntSlice []int Time time.Time } type EchoConfigResponse struct { ReadOnlyMode bool PublicKey []byte SubKeyCount uint AdminUsers []string } type EchoData[K any, V any] struct { Key K Value V } type EchoEmptyData struct { OmitEmpty EchoData[string, string] `json:"OmitEmpty,omitempty"` NullPtr *string Zero EchoData[string, string] } type EchoEnvResponse struct { Env []string } // HTTPStatusResponse demonstrates encore:"httpstatus" tag functionality type EchoHTTPStatusResponse struct { Message string `json:"message"` } type EchoHeadersData struct { Int int `header:"X-Int"` String string `header:"X-String"` } type EchoNonBasicData struct { HeaderString string `header:"X-Header-String"` // Header HeaderNumber int `header:"X-Header-Number"` Struct EchoData[*EchoData[string, string], int] // Body StructPtr *EchoData[int, uint16] StructSlice []*EchoData[string, string] StructMap map[string]*EchoData[string, float32] StructMapPtr *map[string]*EchoData[string, string] AnonStruct struct { AnonBird string } NamedStruct *EchoData[string, float64] `json:"formatted_nest"` RawStruct json.RawMessage QueryString string `query:"string"` // Query QueryNumber int `query:"no"` OptQueryNumber int `encore:"optional" query:"optnum"` OptQueryString string `encore:"optional" query:"optstr"` PathString string // Path Parameters PathInt int PathWild string AuthHeader string // Auth Parameters AuthQuery []int } // EchoClient Provides you access to call public and authenticated APIs on echo. The concrete implementation is echoClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type EchoClient interface { // AppMeta returns app metadata. AppMeta(ctx context.Context) (EchoAppMetadata, error) // BasicEcho echoes back the request data. BasicEcho(ctx context.Context, params EchoBasicData) (EchoBasicData, error) ConfigValues(ctx context.Context) (EchoConfigResponse, error) // CustomHTTPStatus allows testing of custom HTTP status codes via encore:"httpstatus" tag CustomHTTPStatus(ctx context.Context) (EchoHTTPStatusResponse, error) // Echo echoes back the request data. Echo(ctx context.Context, params EchoData[string, int]) (EchoData[string, int], error) // EmptyEcho echoes back the request data. EmptyEcho(ctx context.Context, params EchoEmptyData) (EchoEmptyData, error) // Env returns the environment. Env(ctx context.Context) (EchoEnvResponse, error) // HeadersEcho echoes back the request headers HeadersEcho(ctx context.Context, params EchoHeadersData) (EchoHeadersData, error) // MuteEcho absorbs a request MuteEcho(ctx context.Context, params EchoData[string, string]) error // NilResponse returns a nil response and nil error NilResponse(ctx context.Context) (EchoBasicData, error) // NonBasicEcho echoes back the request data. NonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild []string, params EchoNonBasicData) (EchoNonBasicData, error) // Noop does nothing Noop(ctx context.Context) error // Pong returns a bird tuple Pong(ctx context.Context) (EchoData[string, string], error) // Publish publishes a request on a topic Publish(ctx context.Context) error } type echoClient struct { base *baseClient } var _ EchoClient = (*echoClient)(nil) // AppMeta returns app metadata. func (c *echoClient) AppMeta(ctx context.Context) (resp EchoAppMetadata, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.AppMeta", nil, nil, &resp) if err != nil { return } return } // BasicEcho echoes back the request data. func (c *echoClient) BasicEcho(ctx context.Context, params EchoBasicData) (resp EchoBasicData, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.BasicEcho", nil, params, &resp) if err != nil { return } return } func (c *echoClient) ConfigValues(ctx context.Context) (resp EchoConfigResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.ConfigValues", nil, nil, &resp) if err != nil { return } return } // CustomHTTPStatus allows testing of custom HTTP status codes via encore:"httpstatus" tag func (c *echoClient) CustomHTTPStatus(ctx context.Context) (resp EchoHTTPStatusResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.CustomHTTPStatus", nil, nil, &resp) if err != nil { return } return } // Echo echoes back the request data. func (c *echoClient) Echo(ctx context.Context, params EchoData[string, int]) (resp EchoData[string, int], err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.Echo", nil, params, &resp) if err != nil { return } return } // EmptyEcho echoes back the request data. func (c *echoClient) EmptyEcho(ctx context.Context, params EchoEmptyData) (resp EchoEmptyData, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.EmptyEcho", nil, params, &resp) if err != nil { return } return } // Env returns the environment. func (c *echoClient) Env(ctx context.Context) (resp EchoEnvResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.Env", nil, nil, &resp) if err != nil { return } return } // HeadersEcho echoes back the request headers func (c *echoClient) HeadersEcho(ctx context.Context, params EchoHeadersData) (resp EchoHeadersData, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "x-int": {reqEncoder.FromInt(params.Int)}, "x-string": {reqEncoder.FromString(params.String)}, } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "POST", "/echo.HeadersEcho", headers, nil, nil) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.Int = respDecoder.ToInt("Int", respHeaders.Get("x-int"), true) resp.String = respDecoder.ToString("String", respHeaders.Get("x-string"), true) if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } // MuteEcho absorbs a request func (c *echoClient) MuteEcho(ctx context.Context, params EchoData[string, string]) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} queryString := url.Values{ "key": {reqEncoder.FromString(params.Key)}, "value": {reqEncoder.FromString(params.Value)}, } if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } _, err := callAPI(ctx, c.base, "GET", fmt.Sprintf("/echo.MuteEcho?%s", queryString.Encode()), nil, nil, nil) return err } // NilResponse returns a nil response and nil error func (c *echoClient) NilResponse(ctx context.Context) (resp EchoBasicData, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/echo.NilResponse", nil, nil, &resp) if err != nil { return } return } // NonBasicEcho echoes back the request data. func (c *echoClient) NonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild []string, params EchoNonBasicData) (resp EchoNonBasicData, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "x-header-number": {reqEncoder.FromInt(params.HeaderNumber)}, "x-header-string": {reqEncoder.FromString(params.HeaderString)}, } queryString := url.Values{ "no": {reqEncoder.FromInt(params.QueryNumber)}, "optnum": {reqEncoder.FromInt(params.OptQueryNumber)}, "optstr": {reqEncoder.FromString(params.OptQueryString)}, "string": {reqEncoder.FromString(params.QueryString)}, } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Struct EchoData[*EchoData[string, string], int] `json:"Struct"` StructPtr *EchoData[int, uint16] `json:"StructPtr"` StructSlice []*EchoData[string, string] `json:"StructSlice"` StructMap map[string]*EchoData[string, float32] `json:"StructMap"` StructMapPtr *map[string]*EchoData[string, string] `json:"StructMapPtr"` AnonStruct struct { AnonBird string } `json:"AnonStruct"` NamedStruct *EchoData[string, float64] `json:"formatted_nest"` RawStruct json.RawMessage `json:"RawStruct"` PathString string `json:"PathString"` PathInt int `json:"PathInt"` PathWild string `json:"PathWild"` AuthHeader string `json:"AuthHeader"` AuthQuery []int `json:"AuthQuery"` }{ AnonStruct: params.AnonStruct, AuthHeader: params.AuthHeader, AuthQuery: params.AuthQuery, NamedStruct: params.NamedStruct, PathInt: params.PathInt, PathString: params.PathString, PathWild: params.PathWild, RawStruct: params.RawStruct, Struct: params.Struct, StructMap: params.StructMap, StructMapPtr: params.StructMapPtr, StructPtr: params.StructPtr, StructSlice: params.StructSlice, } // We only want the response body to marshal into these fields and none of the header fields, // so we'll construct a new struct with only those fields. respBody := struct { Struct EchoData[*EchoData[string, string], int] `json:"Struct"` StructPtr *EchoData[int, uint16] `json:"StructPtr"` StructSlice []*EchoData[string, string] `json:"StructSlice"` StructMap map[string]*EchoData[string, float32] `json:"StructMap"` StructMapPtr *map[string]*EchoData[string, string] `json:"StructMapPtr"` AnonStruct struct { AnonBird string } `json:"AnonStruct"` NamedStruct *EchoData[string, float64] `json:"formatted_nest"` RawStruct json.RawMessage `json:"RawStruct"` QueryString string `json:"QueryString"` QueryNumber int `json:"QueryNumber"` OptQueryNumber int `json:"OptQueryNumber"` OptQueryString string `json:"OptQueryString"` PathString string `json:"PathString"` PathInt int `json:"PathInt"` PathWild string `json:"PathWild"` AuthHeader string `json:"AuthHeader"` AuthQuery []int `json:"AuthQuery"` }{} // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/NonBasicEcho/%s/%d/%s?%s", url.PathEscape(pathString), pathInt, pathEscapeSlice(pathWild), queryString.Encode()), headers, body, &respBody) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.HeaderString = respDecoder.ToString("HeaderString", respHeaders.Get("x-header-string"), true) resp.HeaderNumber = respDecoder.ToInt("HeaderNumber", respHeaders.Get("x-header-number"), true) resp.Struct = respBody.Struct resp.StructPtr = respBody.StructPtr resp.StructSlice = respBody.StructSlice resp.StructMap = respBody.StructMap resp.StructMapPtr = respBody.StructMapPtr resp.AnonStruct = respBody.AnonStruct resp.NamedStruct = respBody.NamedStruct resp.RawStruct = respBody.RawStruct resp.QueryString = respBody.QueryString resp.QueryNumber = respBody.QueryNumber resp.OptQueryNumber = respBody.OptQueryNumber resp.OptQueryString = respBody.OptQueryString resp.PathString = respBody.PathString resp.PathInt = respBody.PathInt resp.PathWild = respBody.PathWild resp.AuthHeader = respBody.AuthHeader resp.AuthQuery = respBody.AuthQuery if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } // Noop does nothing func (c *echoClient) Noop(ctx context.Context) error { _, err := callAPI(ctx, c.base, "GET", "/echo.Noop", nil, nil, nil) return err } // Pong returns a bird tuple func (c *echoClient) Pong(ctx context.Context) (resp EchoData[string, string], err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", "/echo.Pong", nil, nil, &resp) if err != nil { return } return } // Publish publishes a request on a topic func (c *echoClient) Publish(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/echo.Publish", nil, nil, nil) return err } // EmptycfgClient Provides you access to call public and authenticated APIs on emptycfg. The concrete implementation is emptycfgClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type EmptycfgClient interface { AnAPI(ctx context.Context) error } type emptycfgClient struct { base *baseClient } var _ EmptycfgClient = (*emptycfgClient)(nil) func (c *emptycfgClient) AnAPI(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/emptycfg.AnAPI", nil, nil, nil) return err } // EndtoendClient Provides you access to call public and authenticated APIs on endtoend. The concrete implementation is endtoendClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type EndtoendClient interface { GeneratedWrappersEndToEndTest(ctx context.Context) error } type endtoendClient struct { base *baseClient } var _ EndtoendClient = (*endtoendClient)(nil) func (c *endtoendClient) GeneratedWrappersEndToEndTest(ctx context.Context) error { _, err := callAPI(ctx, c.base, "GET", "/generated-wrappers-end-to-end-test", nil, nil, nil) return err } type MiddlewarePayload struct { Msg string } // MiddlewareClient Provides you access to call public and authenticated APIs on middleware. The concrete implementation is middlewareClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type MiddlewareClient interface { Error(ctx context.Context) error ResponseGen(ctx context.Context, params MiddlewarePayload) (MiddlewarePayload, error) ResponseRewrite(ctx context.Context, params MiddlewarePayload) (MiddlewarePayload, error) } type middlewareClient struct { base *baseClient } var _ MiddlewareClient = (*middlewareClient)(nil) func (c *middlewareClient) Error(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/middleware.Error", nil, nil, nil) return err } func (c *middlewareClient) ResponseGen(ctx context.Context, params MiddlewarePayload) (resp MiddlewarePayload, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/middleware.ResponseGen", nil, params, &resp) if err != nil { return } return } func (c *middlewareClient) ResponseRewrite(ctx context.Context, params MiddlewarePayload) (resp MiddlewarePayload, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/middleware.ResponseRewrite", nil, params, &resp) if err != nil { return } return } type TestBodyEcho struct { Message string } type TestMarshallerTest[A any] struct { HeaderBoolean bool `header:"x-boolean"` HeaderInt int `header:"x-int"` HeaderFloat float64 `header:"x-float"` HeaderString string `header:"x-string"` HeaderBytes []byte `header:"x-bytes"` HeaderTime time.Time `header:"x-time"` HeaderJson json.RawMessage `header:"x-json"` HeaderUUID string `header:"x-uuid"` HeaderUserID string `header:"x-user-id"` HeaderOption *string `header:"x-option"` QueryBoolean bool `qs:"boolean"` QueryInt int `qs:"int"` QueryFloat float64 `qs:"float"` QueryString string `qs:"string"` QueryBytes []byte `qs:"bytes"` QueryTime time.Time `qs:"time"` QueryJson json.RawMessage `qs:"json"` QueryUUID string `qs:"uuid"` QueryUserID string `qs:"user-id"` QuerySlice []A `qs:"slice"` BodyBoolean bool `json:"boolean"` BodyInt int `json:"int"` BodyFloat float64 `json:"float"` BodyString string `json:"string"` BodyBytes []byte `json:"bytes"` BodyTime time.Time `json:"time"` BodyJson json.RawMessage `json:"json"` BodyUUID string `json:"uuid"` BodyUserID string `json:"user-id"` BodySlice []A `json:"slice"` BodyOption *A `json:"option"` BodyOptionSlice []*A `json:"option-slice"` } type TestMultiPathSegment struct { Boolean bool Int int String string UUID string Wildcard string } type TestRestParams struct { HeaderValue string `header:"Some-Key"` QueryValue string `query:"Some-Key"` BodyValue string `json:"Some-Key"` Nested struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` } } // TestClient Provides you access to call public and authenticated APIs on test. The concrete implementation is testClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type TestClient interface { // GetMessage allows us to test an API which takes no parameters, // but returns data. It also tests two API's on the same path with different HTTP methods GetMessage(ctx context.Context, clientID string) (TestBodyEcho, error) // MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all // the field types. It simply echos all the responses back to the client MarshallerTestHandler(ctx context.Context, params TestMarshallerTest[int]) (TestMarshallerTest[int], error) // Noop allows us to test if a simple HTTP request can be made Noop(ctx context.Context) error // NoopWithError allows us to test if the structured errors are returned NoopWithError(ctx context.Context) error // PathMultiSegments allows us to wildcard segments and segment URI encoding PathMultiSegments(ctx context.Context, _bool bool, _int int, _string string, uuid string, wildcard []string) (TestMultiPathSegment, error) // RawEndpoint allows us to test the clients' ability to send raw requests // under auth RawEndpoint(ctx context.Context, id []string, request *http.Request) (*http.Response, error) // RestStyleAPI tests all the ways we can get data into and out of the application // using Encore request handlers RestStyleAPI(ctx context.Context, objType int, name string, params TestRestParams) (TestRestParams, error) // SimpleBodyEcho allows us to exercise the body marshalling from JSON // and being returned purely as a body SimpleBodyEcho(ctx context.Context, params TestBodyEcho) (TestBodyEcho, error) // TestAuthHandler allows us to test the clients ability to add tokens to requests TestAuthHandler(ctx context.Context) (TestBodyEcho, error) // UpdateMessage allows us to test an API which takes parameters, // but doesn't return anything UpdateMessage(ctx context.Context, clientID string, params TestBodyEcho) error } type testClient struct { base *baseClient } var _ TestClient = (*testClient)(nil) // GetMessage allows us to test an API which takes no parameters, // but returns data. It also tests two API's on the same path with different HTTP methods func (c *testClient) GetMessage(ctx context.Context, clientID string) (resp TestBodyEcho, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", fmt.Sprintf("/last_message/%s", url.PathEscape(clientID)), nil, nil, &resp) if err != nil { return } return } // MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all // the field types. It simply echos all the responses back to the client func (c *testClient) MarshallerTestHandler(ctx context.Context, params TestMarshallerTest[int]) (resp TestMarshallerTest[int], err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "x-boolean": {reqEncoder.FromBool(params.HeaderBoolean)}, "x-bytes": {reqEncoder.FromBytes(params.HeaderBytes)}, "x-float": {reqEncoder.FromFloat64(params.HeaderFloat)}, "x-int": {reqEncoder.FromInt(params.HeaderInt)}, "x-json": {reqEncoder.FromJSON(params.HeaderJson)}, "x-option": reqEncoder.FromStringOption(params.HeaderOption), "x-string": {reqEncoder.FromString(params.HeaderString)}, "x-time": {reqEncoder.FromTime(params.HeaderTime)}, "x-user-id": {reqEncoder.FromString(params.HeaderUserID)}, "x-uuid": {reqEncoder.FromString(params.HeaderUUID)}, } queryString := url.Values{ "boolean": {reqEncoder.FromBool(params.QueryBoolean)}, "bytes": {reqEncoder.FromBytes(params.QueryBytes)}, "float": {reqEncoder.FromFloat64(params.QueryFloat)}, "int": {reqEncoder.FromInt(params.QueryInt)}, "json": {reqEncoder.FromJSON(params.QueryJson)}, "slice": reqEncoder.FromIntList(params.QuerySlice), "string": {reqEncoder.FromString(params.QueryString)}, "time": {reqEncoder.FromTime(params.QueryTime)}, "user-id": {reqEncoder.FromString(params.QueryUserID)}, "uuid": {reqEncoder.FromString(params.QueryUUID)}, } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { BodyBoolean bool `json:"boolean"` BodyInt int `json:"int"` BodyFloat float64 `json:"float"` BodyString string `json:"string"` BodyBytes []byte `json:"bytes"` BodyTime time.Time `json:"time"` BodyJson json.RawMessage `json:"json"` BodyUUID string `json:"uuid"` BodyUserID string `json:"user-id"` BodySlice []int `json:"slice"` BodyOption *int `json:"option"` BodyOptionSlice []*int `json:"option-slice"` }{ BodyBoolean: params.BodyBoolean, BodyBytes: params.BodyBytes, BodyFloat: params.BodyFloat, BodyInt: params.BodyInt, BodyJson: params.BodyJson, BodyOption: params.BodyOption, BodyOptionSlice: params.BodyOptionSlice, BodySlice: params.BodySlice, BodyString: params.BodyString, BodyTime: params.BodyTime, BodyUUID: params.BodyUUID, BodyUserID: params.BodyUserID, } // We only want the response body to marshal into these fields and none of the header fields, // so we'll construct a new struct with only those fields. respBody := struct { QueryBoolean bool `json:"QueryBoolean"` QueryInt int `json:"QueryInt"` QueryFloat float64 `json:"QueryFloat"` QueryString string `json:"QueryString"` QueryBytes []byte `json:"QueryBytes"` QueryTime time.Time `json:"QueryTime"` QueryJson json.RawMessage `json:"QueryJson"` QueryUUID string `json:"QueryUUID"` QueryUserID string `json:"QueryUserID"` QuerySlice []int `json:"QuerySlice"` BodyBoolean bool `json:"boolean"` BodyInt int `json:"int"` BodyFloat float64 `json:"float"` BodyString string `json:"string"` BodyBytes []byte `json:"bytes"` BodyTime time.Time `json:"time"` BodyJson json.RawMessage `json:"json"` BodyUUID string `json:"uuid"` BodyUserID string `json:"user-id"` BodySlice []int `json:"slice"` BodyOption *int `json:"option"` BodyOptionSlice []*int `json:"option-slice"` }{} // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/test.MarshallerTestHandler?%s", queryString.Encode()), headers, body, &respBody) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.HeaderBoolean = respDecoder.ToBool("HeaderBoolean", respHeaders.Get("x-boolean"), true) resp.HeaderInt = respDecoder.ToInt("HeaderInt", respHeaders.Get("x-int"), true) resp.HeaderFloat = respDecoder.ToFloat64("HeaderFloat", respHeaders.Get("x-float"), true) resp.HeaderString = respDecoder.ToString("HeaderString", respHeaders.Get("x-string"), true) resp.HeaderBytes = respDecoder.ToBytes("HeaderBytes", respHeaders.Get("x-bytes"), true) resp.HeaderTime = respDecoder.ToTime("HeaderTime", respHeaders.Get("x-time"), true) resp.HeaderJson = respDecoder.ToJSON("HeaderJson", respHeaders.Get("x-json"), true) resp.HeaderUUID = respDecoder.ToString("HeaderUUID", respHeaders.Get("x-uuid"), true) resp.HeaderUserID = respDecoder.ToString("HeaderUserID", respHeaders.Get("x-user-id"), true) resp.HeaderOption = respDecoder.ToStringOption("HeaderOption", respHeaders.Get("x-option"), false) resp.QueryBoolean = respBody.QueryBoolean resp.QueryInt = respBody.QueryInt resp.QueryFloat = respBody.QueryFloat resp.QueryString = respBody.QueryString resp.QueryBytes = respBody.QueryBytes resp.QueryTime = respBody.QueryTime resp.QueryJson = respBody.QueryJson resp.QueryUUID = respBody.QueryUUID resp.QueryUserID = respBody.QueryUserID resp.QuerySlice = respBody.QuerySlice resp.BodyBoolean = respBody.BodyBoolean resp.BodyInt = respBody.BodyInt resp.BodyFloat = respBody.BodyFloat resp.BodyString = respBody.BodyString resp.BodyBytes = respBody.BodyBytes resp.BodyTime = respBody.BodyTime resp.BodyJson = respBody.BodyJson resp.BodyUUID = respBody.BodyUUID resp.BodyUserID = respBody.BodyUserID resp.BodySlice = respBody.BodySlice resp.BodyOption = respBody.BodyOption resp.BodyOptionSlice = respBody.BodyOptionSlice if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } // Noop allows us to test if a simple HTTP request can be made func (c *testClient) Noop(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/test.Noop", nil, nil, nil) return err } // NoopWithError allows us to test if the structured errors are returned func (c *testClient) NoopWithError(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/test.NoopWithError", nil, nil, nil) return err } // PathMultiSegments allows us to wildcard segments and segment URI encoding func (c *testClient) PathMultiSegments(ctx context.Context, _bool bool, _int int, _string string, uuid string, wildcard []string) (resp TestMultiPathSegment, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/multi/%t/%d/%s/%s/%s", _bool, _int, url.PathEscape(_string), url.PathEscape(uuid), pathEscapeSlice(wildcard)), nil, nil, &resp) if err != nil { return } return } // RawEndpoint allows us to test the clients' ability to send raw requests // under auth func (c *testClient) RawEndpoint(ctx context.Context, id []string, request *http.Request) (*http.Response, error) { request = request.WithContext(ctx) // Check the request has the method set, as we can't guess what method is required if request.Method == "" { return nil, errors.New("request.Method must be set") } // Set the relative URL for the API call path, err := url.Parse(fmt.Sprintf("/raw/blah/%s", pathEscapeSlice(id))) if err != nil { return nil, fmt.Errorf("unable to parse api url: %w", err) } if request.URL != nil { // If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema, // host and path to ensure the final URL which hit the right BaseURL request.URL.Scheme = path.Scheme request.URL.Host = path.Host request.URL.Path = path.Path } else { request.URL = path } return c.base.Do(request) } // RestStyleAPI tests all the ways we can get data into and out of the application // using Encore request handlers func (c *testClient) RestStyleAPI(ctx context.Context, objType int, name string, params TestRestParams) (resp TestRestParams, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{"some-key": {reqEncoder.FromString(params.HeaderValue)}} queryString := url.Values{"Some-Key": {reqEncoder.FromString(params.QueryValue)}} if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { BodyValue string `json:"Some-Key"` Nested struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` } `json:"Nested"` }{ BodyValue: params.BodyValue, Nested: params.Nested, } // We only want the response body to marshal into these fields and none of the header fields, // so we'll construct a new struct with only those fields. respBody := struct { QueryValue string `json:"QueryValue"` BodyValue string `json:"Some-Key"` Nested struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` } `json:"Nested"` }{} // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "PUT", fmt.Sprintf("/rest/object/%d/%s?%s", objType, url.PathEscape(name), queryString.Encode()), headers, body, &respBody) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.HeaderValue = respDecoder.ToString("HeaderValue", respHeaders.Get("some-key"), true) resp.QueryValue = respBody.QueryValue resp.BodyValue = respBody.BodyValue resp.Nested = respBody.Nested if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } // SimpleBodyEcho allows us to exercise the body marshalling from JSON // and being returned purely as a body func (c *testClient) SimpleBodyEcho(ctx context.Context, params TestBodyEcho) (resp TestBodyEcho, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/test.SimpleBodyEcho", nil, params, &resp) if err != nil { return } return } // TestAuthHandler allows us to test the clients ability to add tokens to requests func (c *testClient) TestAuthHandler(ctx context.Context) (resp TestBodyEcho, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/test.TestAuthHandler", nil, nil, &resp) if err != nil { return } return } // UpdateMessage allows us to test an API which takes parameters, // but doesn't return anything func (c *testClient) UpdateMessage(ctx context.Context, clientID string, params TestBodyEcho) error { _, err := callAPI(ctx, c.base, "PUT", fmt.Sprintf("/last_message/%s", url.PathEscape(clientID)), nil, params, nil) return err } type ValidationRequest struct { Msg string } // ValidationClient Provides you access to call public and authenticated APIs on validation. The concrete implementation is validationClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type ValidationClient interface { TestOne(ctx context.Context, params ValidationRequest) error } type validationClient struct { base *baseClient } var _ ValidationClient = (*validationClient)(nil) func (c *validationClient) TestOne(ctx context.Context, params ValidationRequest) error { _, err := callAPI(ctx, c.base, "POST", "/validation.TestOne", nil, params, nil) return err } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { authGenerator func(ctx context.Context) (EchoAuthParams, error) // The function which will add the authentication data to the requests httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // If a authorization data generator is present, call it and add the returned token to the request if b.authGenerator != nil { if authData, err := b.authGenerator(req.Context()); err != nil { return nil, fmt.Errorf("unable to create authorization token for api request: %w", err) } else { authEncoder := &serde{} // Add the auth fields to the query string query := req.URL.Query() for _, v := range authEncoder.FromIntList(authData.Query) { query.Add("query", v) } query.Set("new-auth", authEncoder.FromBool(authData.NewAuth)) req.URL.RawQuery = query.Encode() // Add the auth fields to the headers req.Header.Set("x-header", authEncoder.FromString(authData.Header)) req.Header.Set("x-auth-int", authEncoder.FromInt(authData.AuthInt)) req.Header.Set("authorization", authEncoder.FromString(authData.Authorization)) if authEncoder.LastError != nil { return nil, fmt.Errorf("unable to marshal authentication data: %w", authEncoder.LastError) } } } // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // pathEscapeSlice escapes a slice of strings and then joins them into a single string func pathEscapeSlice(paths []string) string { var escapedPaths strings.Builder for i, path := range paths { if i > 0 { escapedPaths.WriteString("/") } escapedPaths.WriteString(url.PathEscape(path)) } return escapedPaths.String() } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } // serde is used to serialize request data into strings and deserialize response data from strings type serde struct { LastError error // The last error that occurred NonEmptyValues int // The number of values this decoder has decoded } func (e *serde) FromInt(s int) (v string) { e.NonEmptyValues++ return strconv.FormatInt(int64(s), 10) } func (e *serde) FromString(s string) (v string) { e.NonEmptyValues++ return s } func (e *serde) ToInt(field string, s string, required bool) (v int) { if !required && s == "" { return } e.NonEmptyValues++ x, err := strconv.ParseInt(s, 10, 64) e.setErr("invalid parameter", field, err) return int(x) } func (e *serde) ToString(field string, s string, required bool) (v string) { if !required && s == "" { return } e.NonEmptyValues++ return s } func (e *serde) FromBool(s bool) (v string) { e.NonEmptyValues++ return strconv.FormatBool(s) } func (e *serde) FromFloat64(s float64) (v string) { e.NonEmptyValues++ return strconv.FormatFloat(s, uint8(0x66), -1, 64) } func (e *serde) FromBytes(s []byte) (v string) { e.NonEmptyValues++ return base64.URLEncoding.EncodeToString(s) } func (e *serde) FromTime(s time.Time) (v string) { e.NonEmptyValues++ return s.Format(time.RFC3339) } func (e *serde) FromJSON(s json.RawMessage) (v string) { e.NonEmptyValues++ return string(s) } func (e *serde) FromStringOption(s *string) (v []string) { if s == nil { return nil } e.NonEmptyValues++ return []string{e.FromString(*s)} } func (e *serde) FromIntList(s []int) (v []string) { e.NonEmptyValues++ for _, x := range s { v = append(v, e.FromInt(x)) } return v } func (e *serde) ToBool(field string, s string, required bool) (v bool) { if !required && s == "" { return } e.NonEmptyValues++ v, err := strconv.ParseBool(s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToFloat64(field string, s string, required bool) (v float64) { if !required && s == "" { return } e.NonEmptyValues++ x, err := strconv.ParseFloat(s, 64) e.setErr("invalid parameter", field, err) return x } func (e *serde) ToBytes(field string, s string, required bool) (v []byte) { if !required && s == "" { return } e.NonEmptyValues++ v, err := base64.URLEncoding.DecodeString(s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToTime(field string, s string, required bool) (v time.Time) { if !required && s == "" { return } e.NonEmptyValues++ v, err := time.Parse(time.RFC3339, s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToJSON(field string, s string, required bool) (v json.RawMessage) { if !required && s == "" { return } e.NonEmptyValues++ return json.RawMessage(s) } func (e *serde) ToStringOption(field string, s string, required bool) (v *string) { if !required && s == "" { return } e.NonEmptyValues++ val := e.ToString(field, s, required) return &val } // setErr sets the last error within the object if one is not already set func (e *serde) setErr(msg, field string, err error) { if err != nil && e.LastError == nil { e.LastError = fmt.Errorf("%s: %s: %w", field, msg, err) } } ================================================ FILE: e2e-tests/testdata/echo_client/js/client.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-slug.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the slug Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.cache = new cache.ServiceClient(base) this.di = new di.ServiceClient(base) this.echo = new echo.ServiceClient(base) this.emptycfg = new emptycfg.ServiceClient(base) this.endtoend = new endtoend.ServiceClient(base) this.middleware = new middleware.ServiceClient(base) this.test = new test.ServiceClient(base) this.validation = new validation.ServiceClient(base) } } class CacheServiceClient { constructor(baseClient) { this.baseClient = baseClient this.GetList = this.GetList.bind(this) this.GetStruct = this.GetStruct.bind(this) this.Incr = this.Incr.bind(this) this.PostList = this.PostList.bind(this) this.PostStruct = this.PostStruct.bind(this) } async GetList(key) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/cache/list/${encodeURIComponent(key)}`) return await resp.json() } async GetStruct(key) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/cache/struct/${encodeURIComponent(key)}`) return await resp.json() } async Incr(key) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cache/incr/${encodeURIComponent(key)}`) return await resp.json() } async PostList(key, val) { await this.baseClient.callTypedAPI("POST", `/cache/list/${encodeURIComponent(key)}/${encodeURIComponent(val)}`) } async PostStruct(key, val) { await this.baseClient.callTypedAPI("POST", `/cache/struct/${encodeURIComponent(key)}/${encodeURIComponent(val)}`) } } export const cache = { ServiceClient: CacheServiceClient } class DiServiceClient { constructor(baseClient) { this.baseClient = baseClient this.One = this.One.bind(this) this.Three = this.Three.bind(this) this.Two = this.Two.bind(this) } async One() { await this.baseClient.callTypedAPI("POST", `/di/one`) } async Three(method, body, options) { return this.baseClient.callAPI(method, `/di/raw`, body, options) } async Two() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/di/two`) return await resp.json() } } export const di = { ServiceClient: DiServiceClient } class EchoServiceClient { constructor(baseClient) { this.baseClient = baseClient this.AppMeta = this.AppMeta.bind(this) this.BasicEcho = this.BasicEcho.bind(this) this.ConfigValues = this.ConfigValues.bind(this) this.CustomHTTPStatus = this.CustomHTTPStatus.bind(this) this.Echo = this.Echo.bind(this) this.EmptyEcho = this.EmptyEcho.bind(this) this.Env = this.Env.bind(this) this.HeadersEcho = this.HeadersEcho.bind(this) this.MuteEcho = this.MuteEcho.bind(this) this.NilResponse = this.NilResponse.bind(this) this.NonBasicEcho = this.NonBasicEcho.bind(this) this.Noop = this.Noop.bind(this) this.Pong = this.Pong.bind(this) this.Publish = this.Publish.bind(this) } /** * AppMeta returns app metadata. */ async AppMeta() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.AppMeta`) return await resp.json() } /** * BasicEcho echoes back the request data. */ async BasicEcho(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.BasicEcho`, JSON.stringify(params)) return await resp.json() } async ConfigValues() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.ConfigValues`) return await resp.json() } /** * CustomHTTPStatus allows testing of custom HTTP status codes via encore:"httpstatus" tag */ async CustomHTTPStatus() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.CustomHTTPStatus`) return await resp.json() } /** * Echo echoes back the request data. */ async Echo(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.Echo`, JSON.stringify(params)) return await resp.json() } /** * EmptyEcho echoes back the request data. */ async EmptyEcho(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.EmptyEcho`, JSON.stringify(params)) return await resp.json() } /** * Env returns the environment. */ async Env() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.Env`) return await resp.json() } /** * HeadersEcho echoes back the request headers */ async HeadersEcho(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-int": String(params.Int), "x-string": params.String, }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.HeadersEcho`, undefined, {headers}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.Int = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.String = mustBeSet("Header `x-string`", resp.headers.get("x-string")) return rtn } /** * MuteEcho absorbs a request */ async MuteEcho(params) { // Convert our params into the objects we need for the request const query = makeRecord({ key: params.Key, value: params.Value, }) await this.baseClient.callTypedAPI("GET", `/echo.MuteEcho`, undefined, {query}) } /** * NilResponse returns a nil response and nil error */ async NilResponse() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.NilResponse`) return await resp.json() } /** * NonBasicEcho echoes back the request data. */ async NonBasicEcho(pathString, pathInt, pathWild, params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-header-number": String(params.HeaderNumber), "x-header-string": params.HeaderString, }) const query = makeRecord({ no: String(params.QueryNumber), optnum: params.OptQueryNumber === undefined ? undefined : String(params.OptQueryNumber), optstr: params.OptQueryString, string: params.QueryString, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { AnonStruct: params.AnonStruct, AuthHeader: params.AuthHeader, AuthQuery: params.AuthQuery, PathInt: params.PathInt, PathString: params.PathString, PathWild: params.PathWild, RawStruct: params.RawStruct, Struct: params.Struct, StructMap: params.StructMap, StructMapPtr: params.StructMapPtr, StructPtr: params.StructPtr, StructSlice: params.StructSlice, "formatted_nest": params["formatted_nest"], } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/NonBasicEcho/${encodeURIComponent(pathString)}/${encodeURIComponent(pathInt)}/${pathWild.map(encodeURIComponent).join("/")}`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.HeaderString = mustBeSet("Header `x-header-string`", resp.headers.get("x-header-string")) rtn.HeaderNumber = parseInt(mustBeSet("Header `x-header-number`", resp.headers.get("x-header-number")), 10) return rtn } /** * Noop does nothing */ async Noop() { await this.baseClient.callTypedAPI("GET", `/echo.Noop`) } /** * Pong returns a bird tuple */ async Pong() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/echo.Pong`) return await resp.json() } /** * Publish publishes a request on a topic */ async Publish() { await this.baseClient.callTypedAPI("POST", `/echo.Publish`) } } export const echo = { ServiceClient: EchoServiceClient } class EmptycfgServiceClient { constructor(baseClient) { this.baseClient = baseClient this.AnAPI = this.AnAPI.bind(this) } async AnAPI() { await this.baseClient.callTypedAPI("POST", `/emptycfg.AnAPI`) } } export const emptycfg = { ServiceClient: EmptycfgServiceClient } class EndtoendServiceClient { constructor(baseClient) { this.baseClient = baseClient this.GeneratedWrappersEndToEndTest = this.GeneratedWrappersEndToEndTest.bind(this) } async GeneratedWrappersEndToEndTest() { await this.baseClient.callTypedAPI("GET", `/generated-wrappers-end-to-end-test`) } } export const endtoend = { ServiceClient: EndtoendServiceClient } class MiddlewareServiceClient { constructor(baseClient) { this.baseClient = baseClient this.Error = this.Error.bind(this) this.ResponseGen = this.ResponseGen.bind(this) this.ResponseRewrite = this.ResponseRewrite.bind(this) } async Error() { await this.baseClient.callTypedAPI("POST", `/middleware.Error`) } async ResponseGen(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/middleware.ResponseGen`, JSON.stringify(params)) return await resp.json() } async ResponseRewrite(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/middleware.ResponseRewrite`, JSON.stringify(params)) return await resp.json() } } export const middleware = { ServiceClient: MiddlewareServiceClient } class TestServiceClient { constructor(baseClient) { this.baseClient = baseClient this.GetMessage = this.GetMessage.bind(this) this.MarshallerTestHandler = this.MarshallerTestHandler.bind(this) this.Noop = this.Noop.bind(this) this.NoopWithError = this.NoopWithError.bind(this) this.PathMultiSegments = this.PathMultiSegments.bind(this) this.RawEndpoint = this.RawEndpoint.bind(this) this.RestStyleAPI = this.RestStyleAPI.bind(this) this.SimpleBodyEcho = this.SimpleBodyEcho.bind(this) this.TestAuthHandler = this.TestAuthHandler.bind(this) this.UpdateMessage = this.UpdateMessage.bind(this) } /** * GetMessage allows us to test an API which takes no parameters, * but returns data. It also tests two API's on the same path with different HTTP methods */ async GetMessage(clientID) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/last_message/${encodeURIComponent(clientID)}`) return await resp.json() } /** * MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all * the field types. It simply echos all the responses back to the client */ async MarshallerTestHandler(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-boolean": String(params.HeaderBoolean), "x-bytes": String(params.HeaderBytes), "x-float": String(params.HeaderFloat), "x-int": String(params.HeaderInt), "x-json": JSON.stringify(params.HeaderJson), "x-option": params.HeaderOption === undefined ? undefined : String(params.HeaderOption), "x-string": params.HeaderString, "x-time": String(params.HeaderTime), "x-user-id": String(params.HeaderUserID), "x-uuid": String(params.HeaderUUID), }) const query = makeRecord({ boolean: String(params.QueryBoolean), bytes: String(params.QueryBytes), float: String(params.QueryFloat), int: String(params.QueryInt), json: JSON.stringify(params.QueryJson), slice: params.QuerySlice.map((v) => String(v)), string: params.QueryString, time: String(params.QueryTime), "user-id": String(params.QueryUserID), uuid: String(params.QueryUUID), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { boolean: params.boolean, bytes: params.bytes, float: params.float, int: params.int, json: params.json, option: params.option, "option-slice": params["option-slice"], slice: params.slice, string: params.string, time: params.time, "user-id": params["user-id"], uuid: params.uuid, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.MarshallerTestHandler`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.HeaderBoolean = mustBeSet("Header `x-boolean`", resp.headers.get("x-boolean")).toLowerCase() === "true" rtn.HeaderInt = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.HeaderFloat = Number(mustBeSet("Header `x-float`", resp.headers.get("x-float"))) rtn.HeaderString = mustBeSet("Header `x-string`", resp.headers.get("x-string")) rtn.HeaderBytes = mustBeSet("Header `x-bytes`", resp.headers.get("x-bytes")) rtn.HeaderTime = mustBeSet("Header `x-time`", resp.headers.get("x-time")) rtn.HeaderJson = JSON.parse(mustBeSet("Header `x-json`", resp.headers.get("x-json"))) rtn.HeaderUUID = mustBeSet("Header `x-uuid`", resp.headers.get("x-uuid")) rtn.HeaderUserID = mustBeSet("Header `x-user-id`", resp.headers.get("x-user-id")) rtn.HeaderOption = resp.headers.get("x-option") return rtn } /** * Noop allows us to test if a simple HTTP request can be made */ async Noop() { await this.baseClient.callTypedAPI("POST", `/test.Noop`) } /** * NoopWithError allows us to test if the structured errors are returned */ async NoopWithError() { await this.baseClient.callTypedAPI("POST", `/test.NoopWithError`) } /** * PathMultiSegments allows us to wildcard segments and segment URI encoding */ async PathMultiSegments(bool, _int, string, uuid, wildcard) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/multi/${encodeURIComponent(bool)}/${encodeURIComponent(_int)}/${encodeURIComponent(string)}/${encodeURIComponent(uuid)}/${wildcard.map(encodeURIComponent).join("/")}`) return await resp.json() } /** * RawEndpoint allows us to test the clients' ability to send raw requests * under auth */ async RawEndpoint(method, id, body, options) { return this.baseClient.callAPI(method, `/raw/blah/${id.map(encodeURIComponent).join("/")}`, body, options) } /** * RestStyleAPI tests all the ways we can get data into and out of the application * using Encore request handlers */ async RestStyleAPI(objType, name, params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-key": params.HeaderValue, }) const query = makeRecord({ "Some-Key": params.QueryValue, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { Nested: params.Nested, "Some-Key": params["Some-Key"], } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("PUT", `/rest/object/${encodeURIComponent(objType)}/${encodeURIComponent(name)}`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.HeaderValue = mustBeSet("Header `some-key`", resp.headers.get("some-key")) return rtn } /** * SimpleBodyEcho allows us to exercise the body marshalling from JSON * and being returned purely as a body */ async SimpleBodyEcho(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.SimpleBodyEcho`, JSON.stringify(params)) return await resp.json() } /** * TestAuthHandler allows us to test the clients ability to add tokens to requests */ async TestAuthHandler() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.TestAuthHandler`) return await resp.json() } /** * UpdateMessage allows us to test an API which takes parameters, * but doesn't return anything */ async UpdateMessage(clientID, params) { await this.baseClient.callTypedAPI("PUT", `/last_message/${encodeURIComponent(clientID)}`, JSON.stringify(params)) } } export const test = { ServiceClient: TestServiceClient } class ValidationServiceClient { constructor(baseClient) { this.baseClient = baseClient this.TestOne = this.TestOne.bind(this) } async TestOne(params) { await this.baseClient.callTypedAPI("POST", `/validation.TestOne`, JSON.stringify(params)) } } export const validation = { ServiceClient: ValidationServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field, value) { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: `${field} was unexpectedly ${value}`, // ${value} will create the string "null" or "undefined" }, ) } return value } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "slug-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData() { let authData; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data = {}; data.query = makeRecord({ "new-auth": String(authData.NewAuth), query: authData.Query.map((v) => String(v)), }); data.headers = makeRecord({ authorization: authData.Authorization, "x-auth-int": String(authData.AuthInt), "x-header": authData.Header, }) return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: e2e-tests/testdata/echo_client/js/main.js ================================================ import "isomorphic-fetch"; import { deepEqual } from "assert"; import Client, { ErrCode, isAPIError } from "./client.js"; if (process.argv.length < 3) { console.error("Usage: npm run test -- "); console.log(`Got ${process.argv.length} arguments`); process.exit(1); } // Create the client const api = new Client("http://" + process.argv[2]); // Test a simple no-op await api.test.Noop(); // Test we get back the right structured error await assertStructuredError( api.test.NoopWithError(), ErrCode.Unimplemented, "totally not implemented yet" ); // Test a simple echo const echoRsp = await api.test.SimpleBodyEcho({ Message: "hello world" }); deepEqual(echoRsp.Message, "hello world", "Wanted body to be 'hello world'"); // Check our UpdateMessage and GetMessage API's let getRsp = await api.test.GetMessage("javascript"); deepEqual(getRsp.Message, "", "Expected no message on first request"); await api.test.UpdateMessage("javascript", { Message: "updating now" }); getRsp = await api.test.GetMessage("javascript"); deepEqual(getRsp.Message, "updating now", "Expected data from Update request"); // Test the rest API which uses all input types (query string, json body and header fields) // as well as nested structs and path segments in the URL const restRsp = await api.test.RestStyleAPI(5, "hello", { HeaderValue: "this is the header field", QueryValue: "this is a query string field", "Some-Key": "this is the body field", Nested: { Alice: "the nested key", bOb: 8, charile: true } }); deepEqual( restRsp.HeaderValue, "this is the header field", "expected header value" ); deepEqual( restRsp.QueryValue, "this is a query string field", "expected query value" ); deepEqual(restRsp["Some-Key"], "this is the body field", "expected body value"); deepEqual( restRsp.Nested.Alice, "hello + the nested key", "expected nested key" ); deepEqual(restRsp.Nested.bOb, 5 + 8, "expected nested value"); deepEqual(restRsp.Nested.charile, true, "expected nested ok"); // Full marshalling test with randomised payloads function rInt() { return Math.floor(Math.random() * 10000000); } const params = { HeaderBoolean: Math.random() > 0.5, HeaderInt: rInt(), HeaderFloat: Math.random(), HeaderString: "header string", HeaderBytes: "aGVsbG8K", HeaderTime: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), HeaderJson: { hello: "world" }, HeaderUUID: "2553e3a4-5d9f-4716-82a2-b9bdc20a3263", HeaderUserID: "432", HeaderOption: "test", QueryBoolean: Math.random() > 0.5, QueryInt: rInt(), QueryFloat: Math.random(), QueryString: "query string", QueryBytes: "d29ybGQK", QueryTime: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), QueryJson: { value: true }, QueryUUID: "84b7463d-6000-4678-9d94-1d526bb5217c", QueryUserID: "9udfa", QuerySlice: [rInt(), rInt(), rInt(), rInt()], boolean: Math.random() > 0.5, int: Math.floor(Math.random() * 10000000), float: Math.random(), string: "body string", bytes: "aXMgaXQgbWUgeW91IGFyZSBsb29raW5nIGZvcj8K", time: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), json: { json_value: 4321 }, uuid: "c227acf4-1902-4c85-8027-623d47ef4c8a", "user-id": "✉️", slice: [rInt(), rInt(), rInt(), rInt(), rInt(), rInt()], option: 5, "option-slice": [1, null, 2] }; const mResp = await api.test.MarshallerTestHandler(params); deepEqual(mResp, params, "Expected the same response from the marshaller test"); // Test auth handlers await assertStructuredError( api.test.TestAuthHandler(), ErrCode.Unauthenticated, "missing auth param" ); // Test with static auth data { const api = new Client("http://" + process.argv[2], { auth: { AuthInt: 34, Authorization: "Bearer tokendata", NewAuth: false, Header: "", Query: [] } }); const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "user::true", "expected the user ID back"); } // Test with auth data generator function { let tokenToReturn = "tokendata"; const api = new Client("http://" + process.argv[2], { auth: () => { return { Authorization: "Bearer " + tokenToReturn, AuthInt: 34, NewAuth: false, Header: "", Query: [] }; } }); // With a valid token const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "user::true", "expected the user ID back"); // With an invalid token tokenToReturn = "invalid-token-value"; await assertStructuredError( api.test.TestAuthHandler(), ErrCode.Unauthenticated, "invalid token" ); } // Test with headers and query string auth data { const api = new Client("http://" + process.argv[2], { auth: { Authorization: "", AuthInt: 34, NewAuth: true, Header: "102", Query: [42, 100, -50, 10] } }); const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "second_user::true", "expected the user ID back"); } // Test the raw endpoint { const api = new Client("http://" + process.argv[2], { auth: { AuthInt: 34, Authorization: "Bearer tokendata", NewAuth: false, Header: "", Query: [] } }); const resp = await api.test.RawEndpoint( "PUT", ["hello"], "this is a test body", { headers: { "X-Test-Header": "test" }, query: { foo: "bar" } } ); deepEqual(resp.status, 201, "expected the status code to be 201"); const response = await resp.json(); deepEqual( response, { Body: "this is a test body", Header: "test", PathParam: "hello", QueryString: "bar" }, "expected the response to match" ); } // Test path encoding const resp = await api.test.PathMultiSegments( true, 342, "foo/blah/should/get/escaped", "503f4487-1e15-4c37-9a80-7b70f86387bb", ["foo/bar", "blah", "seperate/segments = great success"] ); deepEqual(resp.Boolean, true, "expected the boolean to be true"); deepEqual(resp.Int, 342, "expected the int to be 342"); deepEqual( resp.String, "foo/blah/should/get/escaped", "invalid string field returned" ); deepEqual( resp.UUID, "503f4487-1e15-4c37-9a80-7b70f86387bb", "invalid UUID returned" ); deepEqual( resp.Wildcard, "foo/bar/blah/seperate/segments = great success", "invalid wildcard field returned" ); // Test validation { await api.validation.TestOne({ Msg: "pass" }); await assertStructuredError( api.validation.TestOne({ Msg: "fail" }), ErrCode.InvalidArgument, "validation failed: bad message" ); const client = new Client("http://" + process.argv[2], { auth: { AuthInt: 0, Authorization: "", NewAuth: false, Header: "fail-validation", Query: [] } }); await assertStructuredError( client.test.Noop(), ErrCode.InvalidArgument, "validation failed: auth validation fail" ); } // Test middleware { await assertStructuredError( api.middleware.Error(), ErrCode.Internal, "middleware error" ); const resp1 = await api.middleware.ResponseRewrite({ Msg: "foo" }); deepEqual(resp1.Msg, "middleware(req=foo, resp=handler(foo))"); const resp2 = await api.middleware.ResponseGen({ Msg: "foo" }); deepEqual(resp2.Msg, "middleware generated"); } // Client test completed process.exit(0); async function assertStructuredError(promise, code, message) { let errorOccurred = false; try { await promise; } catch (err) { errorOccurred = true; if (isAPIError(err)) { if (err.code !== code) { throw new Error( `Expected error code ${code}, got ${err.code} with message "${err.message}"` ); } if (err.message !== message) { throw new Error( `Expected error message "${message}", got "${err.message}"` ); } } else { throw new Error(`Expected APIError, got ${err}`); } } if (!errorOccurred) { throw new Error("No error was thrown during call to NoopWithError"); } } ================================================ FILE: e2e-tests/testdata/echo_client/main.go ================================================ package main import ( "context" "encoding/json" "fmt" "io" "math/rand" "net/http" "os" "path/filepath" "reflect" "strings" "time" "echo_client/golang/client" "github.com/google/go-cmp/cmp" ) var assertNumber = 1 func main() { // Even on a slow machine, the client should be able to connect and run this test script in 30 seconds ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Check we where given the host:port of the running echo app if len(os.Args) != 2 { fmt.Println("Usage:", filepath.Base(os.Args[0]), "") fmt.Println("Got", len(os.Args), "arguments") os.Exit(1) } // Create the client api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), ) assert(err, nil, "Wanted no error from client creation") // Test a simple no-op err = api.Test.Noop(ctx) assert(err, nil, "Wanted no error from noop") // Test we get back the right structured error err = api.Test.NoopWithError(ctx) assertStructuredError(err, client.ErrUnimplemented, "totally not implemented yet") // Test a simple echo echoRsp, err := api.Test.SimpleBodyEcho(ctx, client.TestBodyEcho{"hello world"}) assert(err, nil, "Wanted no error from simple body echo") assert(echoRsp.Message, "hello world", "Wanted body to be 'hello world'") // Check our UpdateMessage and GetMessage API's getRsp, err := api.Test.GetMessage(ctx, "go") assert(err, nil, "Wanted no error from get message") assert(getRsp.Message, "", "Expected no message on first request") err = api.Test.UpdateMessage(ctx, "go", client.TestBodyEcho{"updating now"}) assert(err, nil, "Wanted no error from update message") getRsp, err = api.Test.GetMessage(ctx, "go") assert(err, nil, "Wanted no error from get message") assert(getRsp.Message, "updating now", "Expected data from Update request") // Test the rest API which uses all input types (query string, json body and header fields) // as well as nested structs and path segments in the URL restRsp, err := api.Test.RestStyleAPI(ctx, 5, "hello", client.TestRestParams{ HeaderValue: "this is the header field", QueryValue: "this is a query string field", BodyValue: "this is the body field", Nested: struct { Key string `json:"Alice"` Value int `json:"bOb"` Ok bool `json:"charile"` }{ Key: "the nested key", Value: 8, Ok: true, }, }) assert(err, nil, "Wanted no error from rest style api") assert(restRsp.HeaderValue, "this is the header field", "expected header value") assert(restRsp.QueryValue, "this is a query string field", "expected query value") assert(restRsp.BodyValue, "this is the body field", "expected body value") assert(restRsp.Nested.Key, "hello + the nested key", "expected nested key") assert(restRsp.Nested.Value, 5+8, "expected nested value") assert(restRsp.Nested.Ok, true, "expected nested ok") // Full marshalling test with randomised payloads r := rand.New(rand.NewSource(time.Now().UnixNano())) headerBytes := make([]byte, 1+r.Intn(128)) queryBytes := make([]byte, 1+r.Intn(128)) bodyBytes := make([]byte, 1+r.Intn(128)) r.Read(headerBytes) r.Read(queryBytes) r.Read(bodyBytes) params := client.TestMarshallerTest[int]{ HeaderBoolean: r.Float32() > 0.5, HeaderInt: r.Int(), HeaderFloat: r.Float64(), HeaderString: "header string", HeaderBytes: headerBytes, HeaderTime: time.Now().Truncate(time.Second), HeaderJson: json.RawMessage("{\"hello\":\"world\"}"), HeaderUUID: "2553e3a4-5d9f-4716-82a2-b9bdc20a3263", HeaderUserID: "432", QueryBoolean: r.Float32() > 0.5, QueryInt: r.Int(), QueryFloat: r.Float64(), QueryString: "query string", QueryBytes: headerBytes, QueryTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second), QueryJson: json.RawMessage("true"), QueryUUID: "84b7463d-6000-4678-9d94-1d526bb5217c", QueryUserID: "9udfa", QuerySlice: []int{r.Int(), r.Int(), r.Int(), r.Int()}, BodyBoolean: r.Float32() > 0.5, BodyInt: r.Int(), BodyFloat: r.Float64(), BodyString: "body string", BodyBytes: bodyBytes, BodyTime: time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second), BodyJson: json.RawMessage("null"), BodyUUID: "c227acf4-1902-4c85-8027-623d47ef4c8a", BodyUserID: "✉️", BodySlice: []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()}, } mResp, err := api.Test.MarshallerTestHandler(ctx, params) assert(err, nil, "Expected no error from the marshaller test") // We're marshalling as JSON, so we can just compare the JSON strings respAsJSON, err := json.Marshal(mResp) assert(err, nil, "unable to marshal response to JSON") reqAsJSON, err := json.Marshal(params) assert(err, nil, "unable to marshal response to JSON") if diff := cmp.Diff(string(respAsJSON), string(reqAsJSON)); diff != "" { assertNumber++ fmt.Printf("Assertion Failure %d: %s\n", assertNumber, diff) os.Exit(assertNumber) } assert(string(respAsJSON), string(reqAsJSON), "Expected the same response from the marshaller test") // Test auth handlers _, err = api.Test.TestAuthHandler(ctx) assertStructuredError(err, client.ErrUnauthenticated, "missing auth param") // Test with static auth data { api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), client.WithAuth(client.EchoAuthParams{ Authorization: "Bearer tokendata", }), ) assert(err, nil, "Wanted no error from client creation") resp, err := api.Test.TestAuthHandler(ctx) assert(err, nil, "Expected no error from second auth") assert(resp.Message, "user::true", "expected the user ID back") } // Test with auth data generator function { tokenToReturn := "tokendata" api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), client.WithAuthFunc(func(ctx context.Context) (client.EchoAuthParams, error) { return client.EchoAuthParams{ Authorization: "Bearer " + tokenToReturn, }, nil }), ) assert(err, nil, "Wanted no error from client creation") // With a valid token resp, err := api.Test.TestAuthHandler(ctx) assert(err, nil, "Expected no error from second auth") assert(resp.Message, "user::true", "expected the user ID back") // With an invalid token tokenToReturn = "invalid-token-value" _, err = api.Test.TestAuthHandler(ctx) assertStructuredError(err, client.ErrUnauthenticated, "invalid token") } // Test with headers and query string auth data { api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), client.WithAuth(client.EchoAuthParams{ NewAuth: true, Header: "102", Query: []int{42, 100, -50, 10}, }), ) assert(err, nil, "Wanted no error from client creation") resp, err := api.Test.TestAuthHandler(ctx) assert(err, nil, "Expected no error from second auth") assert(resp.Message, "second_user::true", "expected the user ID back") } // Test the raw endpoint { api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), client.WithAuth(client.EchoAuthParams{ Authorization: "Bearer tokendata", }), ) assert(err, nil, "Wanted no error from client creation") req, err := http.NewRequest("PUT", "?foo=bar", strings.NewReader("this is a test body")) assert(err, nil, "unable to create request for raw endpoint") req.Header.Add("X-Test-Header", "test") rsp, err := api.Test.RawEndpoint(ctx, []string{"hello"}, req) assert(err, nil, "expected no error from the raw socket") defer rsp.Body.Close() assert(rsp.StatusCode, http.StatusCreated, "expected the status code to be 201") type responseType struct { Body string Header string PathParam string QueryString string } response := &responseType{} bytes, err := io.ReadAll(rsp.Body) assert(err, nil, "expected no error from reading the response body") err = json.Unmarshal(bytes, response) assert(err, nil, "expected no error when unmarshalling the response body") assert(response, &responseType{"this is a test body", "test", "hello", "bar"}, "expected the response to match") } { bodyStr := "test body" req, err := http.NewRequest("GET", "?foo=bar", strings.NewReader(bodyStr)) assert(err, nil, "expected no error creating request") resp, err := api.Di.Three(ctx, req) assert(err, nil, "expected no error from DI raw endpoint") body, err := io.ReadAll(resp.Body) resp.Body.Close() assert(string(body), bodyStr, "expected response body to echo incoming request body") } // Test path encoding resp, err := api.Test.PathMultiSegments(ctx, true, 342, "foo/blah/should/get/escaped", "503f4487-1e15-4c37-9a80-7b70f86387bb", []string{"foo/bar", "blah", "seperate/segments = great success"}) assert(err, nil, "expected no error from the path multi segments endpoint") assert(resp.Boolean, true, "expected the boolean to be true") assert(resp.Int, 342, "expected the int to be 342") assert(resp.String, "foo/blah/should/get/escaped", "invalid string field returned") assert(resp.UUID, "503f4487-1e15-4c37-9a80-7b70f86387bb", "invalid UUID returned") assert(resp.Wildcard, "foo/bar/blah/seperate/segments = great success", "invalid wildcard field returned") // Test validation err = api.Validation.TestOne(ctx, client.ValidationRequest{Msg: "pass"}) assert(err, nil, "expected no error from validation") err = api.Validation.TestOne(ctx, client.ValidationRequest{Msg: "fail"}) assertStructuredError(err, client.ErrInvalidArgument, "validation failed: bad message") { api, err := client.New( client.BaseURL(fmt.Sprintf("http://%s", os.Args[1])), client.WithAuth(client.EchoAuthParams{ Header: "fail-validation", }), ) assert(err, nil, "expected no error from client init") err = api.Test.Noop(ctx) assertStructuredError(err, client.ErrInvalidArgument, "validation failed: auth validation fail") } // Test middleware { err = api.Middleware.Error(ctx) assertStructuredError(err, client.ErrInternal, "middleware error") resp, err := api.Middleware.ResponseRewrite(ctx, client.MiddlewarePayload{Msg: "foo"}) assert(err, nil, "expected no error") assert(resp.Msg, "middleware(req=foo, resp=handler(foo))", "unexpected response") resp, err = api.Middleware.ResponseGen(ctx, client.MiddlewarePayload{Msg: "foo"}) assert(resp.Msg, "middleware generated", "unexpected response") } // Client test completed os.Exit(0) } func assert(got, want any, message string) { assertNumber++ if !reflect.DeepEqual(got, want) { fmt.Printf("Assertion Failure %d: %s\n\n%+v != %+v\n", assertNumber, message, got, want) os.Exit(assertNumber) } } func assertNotNil(got any, message string) { assertNumber++ if got == nil { fmt.Printf("Assertion Failure %d: got nil: %s", assertNumber, message) os.Exit(assertNumber) } } func assertStructuredError(err error, code client.ErrCode, message string) { assertNotNil(err, "want an error") assertNumber++ if apiError, ok := err.(*client.APIError); !ok { fmt.Printf("Assertion Failure %d: expected *client.APIError; got %+v\n", assertNumber, reflect.TypeOf(err)) os.Exit(assertNumber) } else { assert(apiError.Code, code, "unexpected error code") assert(apiError.Message, message, "expected error message") } } ================================================ FILE: e2e-tests/testdata/echo_client/package.json ================================================ { "private": true, "type": "module", "scripts": { "test": "node --trace-warnings --experimental-specifier-resolution=node --loader ts-node/esm ./ts/main.ts", "test:js": "node --trace-warnings --experimental-specifier-resolution=node ./js/main.js", "lint": "tsc --noEmit && eslint \"**/*.{ts,js}\"" }, "devDependencies": { "@types/node": "^17.0.35", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", "eslint": "^8.16.0", "ts-node": "^10.8.0", "typescript": "^4.6.4" }, "dependencies": { "isomorphic-fetch": "^3.0.0" } } ================================================ FILE: e2e-tests/testdata/echo_client/ts/client.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-slug.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the slug Encore application. */ export default class Client { public readonly cache: cache.ServiceClient public readonly di: di.ServiceClient public readonly echo: echo.ServiceClient public readonly emptycfg: emptycfg.ServiceClient public readonly endtoend: endtoend.ServiceClient public readonly middleware: middleware.ServiceClient public readonly test: test.ServiceClient public readonly validation: validation.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.cache = new cache.ServiceClient(base) this.di = new di.ServiceClient(base) this.echo = new echo.ServiceClient(base) this.emptycfg = new emptycfg.ServiceClient(base) this.endtoend = new endtoend.ServiceClient(base) this.middleware = new middleware.ServiceClient(base) this.test = new test.ServiceClient(base) this.validation = new validation.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } /** * Allows you to set the authentication data to be used for each * request either by passing in a static object or by passing in * a function which returns a new object for each request. */ auth?: echo.AuthParams | AuthDataGenerator } export namespace cache { export interface IncrResponse { Val: number } export interface ListResponse { Vals: string[] } export interface StructVal { Val: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.GetList = this.GetList.bind(this) this.GetStruct = this.GetStruct.bind(this) this.Incr = this.Incr.bind(this) this.PostList = this.PostList.bind(this) this.PostStruct = this.PostStruct.bind(this) } public async GetList(key: number): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/cache/list/${encodeURIComponent(key)}`) return await resp.json() as ListResponse } public async GetStruct(key: number): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/cache/struct/${encodeURIComponent(key)}`) return await resp.json() as StructVal } public async Incr(key: string): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cache/incr/${encodeURIComponent(key)}`) return await resp.json() as IncrResponse } public async PostList(key: number, val: string): Promise { await this.baseClient.callTypedAPI("POST", `/cache/list/${encodeURIComponent(key)}/${encodeURIComponent(val)}`) } public async PostStruct(key: number, val: string): Promise { await this.baseClient.callTypedAPI("POST", `/cache/struct/${encodeURIComponent(key)}/${encodeURIComponent(val)}`) } } } export namespace di { export interface TwoResponse { Msg: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.One = this.One.bind(this) this.Three = this.Three.bind(this) this.Two = this.Two.bind(this) } public async One(): Promise { await this.baseClient.callTypedAPI("POST", `/di/one`) } public async Three(method: string, body?: RequestInit["body"], options?: CallParameters): Promise { return this.baseClient.callAPI(method, `/di/raw`, body, options) } public async Two(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/di/two`) return await resp.json() as TwoResponse } } } export namespace echo { export interface AppMetadata { AppID: string APIBaseURL: string EnvName: string EnvType: string } export interface AuthParams { Header: string AuthInt: number Authorization: string Query: number[] NewAuth: boolean } export interface BasicData { String: string Uint: number Int: number Int8: number Int64: number Float32: number Float64: number StringSlice: string[] IntSlice: number[] Time: string } export interface ConfigResponse { ReadOnlyMode: boolean PublicKey: string SubKeyCount: number AdminUsers: string[] } export interface Data { Key: K Value: V } export interface EmptyData { OmitEmpty: Data NullPtr: string Zero: Data } export interface EnvResponse { Env: string[] } /** * HTTPStatusResponse demonstrates encore:"httpstatus" tag functionality */ export interface HTTPStatusResponse { message: string } export interface HeadersData { Int: number String: string } export interface NonBasicData { /** * Header */ HeaderString: string HeaderNumber: number /** * Body */ Struct: Data, number> StructPtr: Data StructSlice: Data[] StructMap: { [key: string]: Data } StructMapPtr: { [key: string]: Data } AnonStruct: { AnonBird: string } "formatted_nest": Data RawStruct: JSONValue /** * Query */ QueryString: string QueryNumber: number OptQueryNumber?: number OptQueryString?: string /** * Path Parameters */ PathString: string PathInt: number PathWild: string /** * Auth Parameters */ AuthHeader: string AuthQuery: number[] } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.AppMeta = this.AppMeta.bind(this) this.BasicEcho = this.BasicEcho.bind(this) this.ConfigValues = this.ConfigValues.bind(this) this.CustomHTTPStatus = this.CustomHTTPStatus.bind(this) this.Echo = this.Echo.bind(this) this.EmptyEcho = this.EmptyEcho.bind(this) this.Env = this.Env.bind(this) this.HeadersEcho = this.HeadersEcho.bind(this) this.MuteEcho = this.MuteEcho.bind(this) this.NilResponse = this.NilResponse.bind(this) this.NonBasicEcho = this.NonBasicEcho.bind(this) this.Noop = this.Noop.bind(this) this.Pong = this.Pong.bind(this) this.Publish = this.Publish.bind(this) } /** * AppMeta returns app metadata. */ public async AppMeta(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.AppMeta`) return await resp.json() as AppMetadata } /** * BasicEcho echoes back the request data. */ public async BasicEcho(params: BasicData): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.BasicEcho`, JSON.stringify(params)) return await resp.json() as BasicData } public async ConfigValues(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.ConfigValues`) return await resp.json() as ConfigResponse } /** * CustomHTTPStatus allows testing of custom HTTP status codes via encore:"httpstatus" tag */ public async CustomHTTPStatus(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.CustomHTTPStatus`) return await resp.json() as HTTPStatusResponse } /** * Echo echoes back the request data. */ public async Echo(params: Data): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.Echo`, JSON.stringify(params)) return await resp.json() as Data } /** * EmptyEcho echoes back the request data. */ public async EmptyEcho(params: EmptyData): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.EmptyEcho`, JSON.stringify(params)) return await resp.json() as EmptyData } /** * Env returns the environment. */ public async Env(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.Env`) return await resp.json() as EnvResponse } /** * HeadersEcho echoes back the request headers */ public async HeadersEcho(params: HeadersData): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-int": String(params.Int), "x-string": params.String, }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.HeadersEcho`, undefined, {headers}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as HeadersData rtn.Int = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.String = mustBeSet("Header `x-string`", resp.headers.get("x-string")) return rtn } /** * MuteEcho absorbs a request */ public async MuteEcho(params: Data): Promise { // Convert our params into the objects we need for the request const query = makeRecord({ key: params.Key, value: params.Value, }) await this.baseClient.callTypedAPI("GET", `/echo.MuteEcho`, undefined, {query}) } /** * NilResponse returns a nil response and nil error */ public async NilResponse(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/echo.NilResponse`) return await resp.json() as BasicData } /** * NonBasicEcho echoes back the request data. */ public async NonBasicEcho(pathString: string, pathInt: number, pathWild: string[], params: NonBasicData): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-header-number": String(params.HeaderNumber), "x-header-string": params.HeaderString, }) const query = makeRecord({ no: String(params.QueryNumber), optnum: params.OptQueryNumber === undefined ? undefined : String(params.OptQueryNumber), optstr: params.OptQueryString, string: params.QueryString, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { AnonStruct: params.AnonStruct, AuthHeader: params.AuthHeader, AuthQuery: params.AuthQuery, PathInt: params.PathInt, PathString: params.PathString, PathWild: params.PathWild, RawStruct: params.RawStruct, Struct: params.Struct, StructMap: params.StructMap, StructMapPtr: params.StructMapPtr, StructPtr: params.StructPtr, StructSlice: params.StructSlice, "formatted_nest": params["formatted_nest"], } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/NonBasicEcho/${encodeURIComponent(pathString)}/${encodeURIComponent(pathInt)}/${pathWild.map(encodeURIComponent).join("/")}`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as NonBasicData rtn.HeaderString = mustBeSet("Header `x-header-string`", resp.headers.get("x-header-string")) rtn.HeaderNumber = parseInt(mustBeSet("Header `x-header-number`", resp.headers.get("x-header-number")), 10) return rtn } /** * Noop does nothing */ public async Noop(): Promise { await this.baseClient.callTypedAPI("GET", `/echo.Noop`) } /** * Pong returns a bird tuple */ public async Pong(): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/echo.Pong`) return await resp.json() as Data } /** * Publish publishes a request on a topic */ public async Publish(): Promise { await this.baseClient.callTypedAPI("POST", `/echo.Publish`) } } } export namespace emptycfg { export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.AnAPI = this.AnAPI.bind(this) } public async AnAPI(): Promise { await this.baseClient.callTypedAPI("POST", `/emptycfg.AnAPI`) } } } export namespace endtoend { export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.GeneratedWrappersEndToEndTest = this.GeneratedWrappersEndToEndTest.bind(this) } public async GeneratedWrappersEndToEndTest(): Promise { await this.baseClient.callTypedAPI("GET", `/generated-wrappers-end-to-end-test`) } } } export namespace middleware { export interface Payload { Msg: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.Error = this.Error.bind(this) this.ResponseGen = this.ResponseGen.bind(this) this.ResponseRewrite = this.ResponseRewrite.bind(this) } public async Error(): Promise { await this.baseClient.callTypedAPI("POST", `/middleware.Error`) } public async ResponseGen(params: Payload): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/middleware.ResponseGen`, JSON.stringify(params)) return await resp.json() as Payload } public async ResponseRewrite(params: Payload): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/middleware.ResponseRewrite`, JSON.stringify(params)) return await resp.json() as Payload } } } export namespace test { export interface BodyEcho { Message: string } export interface MarshallerTest { HeaderBoolean: boolean HeaderInt: number HeaderFloat: number HeaderString: string HeaderBytes: string HeaderTime: string HeaderJson: JSONValue HeaderUUID: string HeaderUserID: string HeaderOption?: string | null QueryBoolean: boolean QueryInt: number QueryFloat: number QueryString: string QueryBytes: string QueryTime: string QueryJson: JSONValue QueryUUID: string QueryUserID: string QuerySlice: A[] boolean: boolean int: number float: number string: string bytes: string time: string json: JSONValue uuid: string "user-id": string slice: A[] option?: A | null "option-slice": (A | null)[] } export interface MultiPathSegment { Boolean: boolean Int: number String: string UUID: string Wildcard: string } export interface RestParams { HeaderValue: string QueryValue: string "Some-Key": string Nested: { Alice: string bOb: number charile: boolean } } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.GetMessage = this.GetMessage.bind(this) this.MarshallerTestHandler = this.MarshallerTestHandler.bind(this) this.Noop = this.Noop.bind(this) this.NoopWithError = this.NoopWithError.bind(this) this.PathMultiSegments = this.PathMultiSegments.bind(this) this.RawEndpoint = this.RawEndpoint.bind(this) this.RestStyleAPI = this.RestStyleAPI.bind(this) this.SimpleBodyEcho = this.SimpleBodyEcho.bind(this) this.TestAuthHandler = this.TestAuthHandler.bind(this) this.UpdateMessage = this.UpdateMessage.bind(this) } /** * GetMessage allows us to test an API which takes no parameters, * but returns data. It also tests two API's on the same path with different HTTP methods */ public async GetMessage(clientID: string): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/last_message/${encodeURIComponent(clientID)}`) return await resp.json() as BodyEcho } /** * MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all * the field types. It simply echos all the responses back to the client */ public async MarshallerTestHandler(params: MarshallerTest): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-boolean": String(params.HeaderBoolean), "x-bytes": String(params.HeaderBytes), "x-float": String(params.HeaderFloat), "x-int": String(params.HeaderInt), "x-json": JSON.stringify(params.HeaderJson), "x-option": params.HeaderOption === undefined ? undefined : String(params.HeaderOption), "x-string": params.HeaderString, "x-time": String(params.HeaderTime), "x-user-id": String(params.HeaderUserID), "x-uuid": String(params.HeaderUUID), }) const query = makeRecord({ boolean: String(params.QueryBoolean), bytes: String(params.QueryBytes), float: String(params.QueryFloat), int: String(params.QueryInt), json: JSON.stringify(params.QueryJson), slice: params.QuerySlice.map((v) => String(v)), string: params.QueryString, time: String(params.QueryTime), "user-id": String(params.QueryUserID), uuid: String(params.QueryUUID), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { boolean: params.boolean, bytes: params.bytes, float: params.float, int: params.int, json: params.json, option: params.option, "option-slice": params["option-slice"], slice: params.slice, string: params.string, time: params.time, "user-id": params["user-id"], uuid: params.uuid, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.MarshallerTestHandler`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as MarshallerTest rtn.HeaderBoolean = mustBeSet("Header `x-boolean`", resp.headers.get("x-boolean")).toLowerCase() === "true" rtn.HeaderInt = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.HeaderFloat = Number(mustBeSet("Header `x-float`", resp.headers.get("x-float"))) rtn.HeaderString = mustBeSet("Header `x-string`", resp.headers.get("x-string")) rtn.HeaderBytes = mustBeSet("Header `x-bytes`", resp.headers.get("x-bytes")) rtn.HeaderTime = mustBeSet("Header `x-time`", resp.headers.get("x-time")) rtn.HeaderJson = JSON.parse(mustBeSet("Header `x-json`", resp.headers.get("x-json"))) rtn.HeaderUUID = mustBeSet("Header `x-uuid`", resp.headers.get("x-uuid")) rtn.HeaderUserID = mustBeSet("Header `x-user-id`", resp.headers.get("x-user-id")) rtn.HeaderOption = mustBeSet("Header `x-option`", resp.headers.get("x-option")) return rtn } /** * Noop allows us to test if a simple HTTP request can be made */ public async Noop(): Promise { await this.baseClient.callTypedAPI("POST", `/test.Noop`) } /** * NoopWithError allows us to test if the structured errors are returned */ public async NoopWithError(): Promise { await this.baseClient.callTypedAPI("POST", `/test.NoopWithError`) } /** * PathMultiSegments allows us to wildcard segments and segment URI encoding */ public async PathMultiSegments(bool: boolean, int: number, _string: string, uuid: string, wildcard: string[]): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/multi/${encodeURIComponent(bool)}/${encodeURIComponent(int)}/${encodeURIComponent(_string)}/${encodeURIComponent(uuid)}/${wildcard.map(encodeURIComponent).join("/")}`) return await resp.json() as MultiPathSegment } /** * RawEndpoint allows us to test the clients' ability to send raw requests * under auth */ public async RawEndpoint(method: "PUT" | "POST" | "DELETE" | "GET", id: string[], body?: RequestInit["body"], options?: CallParameters): Promise { return this.baseClient.callAPI(method, `/raw/blah/${id.map(encodeURIComponent).join("/")}`, body, options) } /** * RestStyleAPI tests all the ways we can get data into and out of the application * using Encore request handlers */ public async RestStyleAPI(objType: number, name: string, params: RestParams): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-key": params.HeaderValue, }) const query = makeRecord({ "Some-Key": params.QueryValue, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { Nested: params.Nested, "Some-Key": params["Some-Key"], } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("PUT", `/rest/object/${encodeURIComponent(objType)}/${encodeURIComponent(name)}`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as RestParams rtn.HeaderValue = mustBeSet("Header `some-key`", resp.headers.get("some-key")) return rtn } /** * SimpleBodyEcho allows us to exercise the body marshalling from JSON * and being returned purely as a body */ public async SimpleBodyEcho(params: BodyEcho): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.SimpleBodyEcho`, JSON.stringify(params)) return await resp.json() as BodyEcho } /** * TestAuthHandler allows us to test the clients ability to add tokens to requests */ public async TestAuthHandler(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/test.TestAuthHandler`) return await resp.json() as BodyEcho } /** * UpdateMessage allows us to test an API which takes parameters, * but doesn't return anything */ public async UpdateMessage(clientID: string, params: BodyEcho): Promise { await this.baseClient.callTypedAPI("PUT", `/last_message/${encodeURIComponent(clientID)}`, JSON.stringify(params)) } } } export namespace validation { export interface Request { Msg: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.TestOne = this.TestOne.bind(this) } public async TestOne(params: Request): Promise { await this.baseClient.callTypedAPI("POST", `/validation.TestOne`, JSON.stringify(params)) } } } // JSONValue represents an arbitrary JSON value. export type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue} function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field: string, value: A | null | undefined): A { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: `${field} was unexpectedly ${value}`, // ${value} will create the string "null" or "undefined" }, ) } return value } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | echo.AuthParams | Promise | undefined; // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } readonly authGenerator?: AuthDataGenerator constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "slug-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData(): Promise { let authData: echo.AuthParams | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; data.query = makeRecord({ "new-auth": String(authData.NewAuth), query: authData.Query.map((v) => String(v)), }); data.headers = makeRecord({ authorization: authData.Authorization, "x-auth-int": String(authData.AuthInt), "x-header": authData.Header, }); return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: e2e-tests/testdata/echo_client/ts/main.ts ================================================ /* eslint-disable @typescript-eslint/no-explicit-any */ import "isomorphic-fetch"; import { deepEqual } from "assert"; import Client, { BaseURL, echo, ErrCode, isAPIError, test } from "./client"; import MarshallerTest = test.MarshallerTest; if (process.argv.length < 3) { console.error("Usage: npm run test -- "); console.log(`Got ${process.argv.length} arguments`); process.exit(1); } // Create the client const api = new Client(("http://" + process.argv[2]) as BaseURL); // Test a simple no-op await api.test.Noop(); // Test we get back the right structured error await assertStructuredError( api.test.NoopWithError(), ErrCode.Unimplemented, "totally not implemented yet" ); // Test a simple echo const echoRsp = await api.test.SimpleBodyEcho({ Message: "hello world" }); deepEqual(echoRsp.Message, "hello world", "Wanted body to be 'hello world'"); // Check our UpdateMessage and GetMessage API's let getRsp = await api.test.GetMessage("typescript"); deepEqual(getRsp.Message, "", "Expected no message on first request"); await api.test.UpdateMessage("typescript", { Message: "updating now" }); getRsp = await api.test.GetMessage("typescript"); deepEqual(getRsp.Message, "updating now", "Expected data from Update request"); // Test the rest API which uses all input types (query string, json body and header fields) // as well as nested structs and path segments in the URL const restRsp = await api.test.RestStyleAPI(5, "hello", { HeaderValue: "this is the header field", QueryValue: "this is a query string field", "Some-Key": "this is the body field", Nested: { Alice: "the nested key", bOb: 8, charile: true } }); deepEqual( restRsp.HeaderValue, "this is the header field", "expected header value" ); deepEqual( restRsp.QueryValue, "this is a query string field", "expected query value" ); deepEqual(restRsp["Some-Key"], "this is the body field", "expected body value"); deepEqual( restRsp.Nested.Alice, "hello + the nested key", "expected nested key" ); deepEqual(restRsp.Nested.bOb, 5 + 8, "expected nested value"); deepEqual(restRsp.Nested.charile, true, "expected nested ok"); // Full marshalling test with randomised payloads function rInt(): number { return Math.floor(Math.random() * 10000000); } const params: MarshallerTest = { HeaderBoolean: Math.random() > 0.5, HeaderInt: rInt(), HeaderFloat: Math.random(), HeaderString: "header string", HeaderBytes: "aGVsbG8K", HeaderTime: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), HeaderJson: { hello: "world" }, HeaderUUID: "2553e3a4-5d9f-4716-82a2-b9bdc20a3263", HeaderUserID: "432", HeaderOption: "abc", QueryBoolean: Math.random() > 0.5, QueryInt: rInt(), QueryFloat: Math.random(), QueryString: "query string", QueryBytes: "d29ybGQK", QueryTime: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), QueryJson: { value: true }, QueryUUID: "84b7463d-6000-4678-9d94-1d526bb5217c", QueryUserID: "9udfa", QuerySlice: [rInt(), rInt(), rInt(), rInt()], boolean: Math.random() > 0.5, int: Math.floor(Math.random() * 10000000), float: Math.random(), string: "body string", bytes: "aXMgaXQgbWUgeW91IGFyZSBsb29raW5nIGZvcj8K", time: new Date(Math.floor(Date.now() / 1000) * 1000) .toISOString() .replace(".000Z", "Z"), json: { json_value: 4321 }, uuid: "c227acf4-1902-4c85-8027-623d47ef4c8a", "user-id": "✉️", slice: [rInt(), rInt(), rInt(), rInt(), rInt(), rInt()], option: 123, "option-slice": [123, null, 456] }; const mResp = await api.test.MarshallerTestHandler(params); deepEqual(mResp, params, "Expected the same response from the marshaller test"); // Test auth handlers await assertStructuredError( api.test.TestAuthHandler(), ErrCode.Unauthenticated, "missing auth param" ); // Test with static auth data { const api = new Client(("http://" + process.argv[2]) as BaseURL, { auth: { AuthInt: 34, Authorization: "Bearer tokendata", NewAuth: false, Header: "", Query: [] } }); const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "user::true", "expected the user ID back"); } // Test with auth data generator function { let tokenToReturn = "tokendata"; const api = new Client(("http://" + process.argv[2]) as BaseURL, { auth: (): echo.AuthParams => { return { Authorization: "Bearer " + tokenToReturn, AuthInt: 34, NewAuth: false, Header: "", Query: [] }; } }); // With a valid token const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "user::true", "expected the user ID back"); // With an invalid token tokenToReturn = "invalid-token-value"; await assertStructuredError( api.test.TestAuthHandler(), ErrCode.Unauthenticated, "invalid token" ); } // Test with headers and query string auth data { const api = new Client(("http://" + process.argv[2]) as BaseURL, { auth: { Authorization: "", AuthInt: 34, NewAuth: true, Header: "102", Query: [42, 100, -50, 10] } }); const resp = await api.test.TestAuthHandler(); deepEqual(resp.Message, "second_user::true", "expected the user ID back"); } // Test the raw endpoint { const api = new Client(("http://" + process.argv[2]) as BaseURL, { auth: { AuthInt: 34, Authorization: "Bearer tokendata", NewAuth: false, Header: "", Query: [] } }); const resp = await api.test.RawEndpoint( "PUT", ["hello"], "this is a test body", { headers: { "X-Test-Header": "test" }, query: { foo: "bar" } } ); deepEqual(resp.status, 201, "expected the status code to be 201"); const response = await resp.json(); deepEqual( response, { Body: "this is a test body", Header: "test", PathParam: "hello", QueryString: "bar" }, "expected the response to match" ); } // Test path encoding const resp = await api.test.PathMultiSegments( true, 342, "foo/blah/should/get/escaped", "503f4487-1e15-4c37-9a80-7b70f86387bb", ["foo/bar", "blah", "seperate/segments = great success"] ); deepEqual(resp.Boolean, true, "expected the boolean to be true"); deepEqual(resp.Int, 342, "expected the int to be 342"); deepEqual( resp.String, "foo/blah/should/get/escaped", "invalid string field returned" ); deepEqual( resp.UUID, "503f4487-1e15-4c37-9a80-7b70f86387bb", "invalid UUID returned" ); deepEqual( resp.Wildcard, "foo/bar/blah/seperate/segments = great success", "invalid wildcard field returned" ); // Test validation { await api.validation.TestOne({ Msg: "pass" }); await assertStructuredError( api.validation.TestOne({ Msg: "fail" }), ErrCode.InvalidArgument, "validation failed: bad message" ); const client = new Client(("http://" + process.argv[2]) as BaseURL, { auth: { AuthInt: 0, Authorization: "", NewAuth: false, Header: "fail-validation", Query: [] } }); await assertStructuredError( client.test.Noop(), ErrCode.InvalidArgument, "validation failed: auth validation fail" ); } // Test middleware { await assertStructuredError( api.middleware.Error(), ErrCode.Internal, "middleware error" ); const resp1 = await api.middleware.ResponseRewrite({ Msg: "foo" }); deepEqual(resp1.Msg, "middleware(req=foo, resp=handler(foo))"); const resp2 = await api.middleware.ResponseGen({ Msg: "foo" }); deepEqual(resp2.Msg, "middleware generated"); } // Client test completed process.exit(0); async function assertStructuredError( promise: Promise, code: ErrCode, message: string ) { let errorOccurred = false; try { await promise; } catch (err: any) { errorOccurred = true; if (isAPIError(err)) { if (err.code !== code) { throw new Error( `Expected error code ${code}, got ${err.code} with message "${err.message}"` ); } if (err.message !== message) { throw new Error( `Expected error message "${message}", got "${err.message}"` ); } } else { throw new Error(`Expected APIError, got ${err}`); } } if (!errorOccurred) { throw new Error("No error was thrown during call to NoopWithError"); } } ================================================ FILE: e2e-tests/testdata/echo_client/tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Projects */ // "incremental": true, /* Enable incremental compilation */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": [ "ES6", "DOM", "ES2021.String" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ "module": "ESNext", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "resolveJsonModule": true, /* Enable importing .json files */ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outDir": "./", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "ts-node": { "esm": true }, "lib": [ "esnext" ] } ================================================ FILE: e2e-tests/testdata/testscript/encore_currentrequest.txt ================================================ run publish topic '{"Data": "test"}' checklog '{"topic": "topic", "subscription": "sub", "type": "pubsub-message", "msg": {"Service": "svc", "Topic": "topic", "Subscription": "sub", "ID": "1", "DeliveryAttempt": 1}, "message": "pubsub event"}' # Authenticated requests should return the execution id call GET X-Encore-Cron-Execution=foo /svc.CurrentRequest '' checkresp '{"Path": "/svc.CurrentRequest", "IdempotencyKey": "foo"}' # Unauthenticated requests should return the empty string call GET X-Encore-Cron-Execution=foo /svc.CurrentRequest '' no-platform-auth checkresp '{"Path": "/svc.CurrentRequest", "IdempotencyKey": ""}' -- svc/svc.go -- package svc import ( "context" "encore.dev" "encore.dev/rlog" "encore.dev/pubsub" ) type MyData struct { Name string } type Event struct { Data string } type RequestData struct { Path string IdempotencyKey string } //encore:api public func CurrentRequest(ctx context.Context) (*RequestData, error) { req := encore.CurrentRequest() return &RequestData{ Path: req.Path, IdempotencyKey: req.CronIdempotencyKey, }, nil } var topic = pubsub.NewTopic[*Event]("topic", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) var _ = pubsub.NewSubscription(topic, "sub", pubsub.SubscriptionConfig[*Event]{ Handler: func(ctx context.Context, event *Event) error { req := encore.CurrentRequest() rlog.Info("pubsub event", "type", req.Type, "msg", req.Message, ) return nil }, }) ================================================ FILE: e2e-tests/testdata/testscript/et_mocking.txt ================================================ test -- products/price.go -- package products import ( "context" ) type PriceParams struct { Quantity int } type PriceResult struct { Total float64 } //encore:api public method=GET path=/products/:productID/price func GetPrice(ctx context.Context, productID int, p *PriceParams) (*PriceResult, error) { return &PriceResult{ Total: 99.99 * float64(p.Quantity) }, nil } -- shoppingcart/cart.go -- package shoppingcart import ( "context" "test/products" ) type CartItem struct { ProductID int Quantity int } //encore:service type Service struct { Items []CartItem } func initService() (*Service, error) { return &Service{ Items: []CartItem{ { ProductID: 1, Quantity: 2 }, { ProductID: 2, Quantity: 1 }, }, }, nil } type TotalResult struct { Total float64 } //encore:api public method=GET path=/cart/total func (s *Service) Total(ctx context.Context) (*TotalResult, error) { var total float64 for _, item := range s.Items { price, err := products.GetPrice(ctx, item.ProductID, &products.PriceParams{ Quantity: item.Quantity }) if err != nil { return nil, err } total += price.Total } return &TotalResult{ Total: total }, nil } //encore:api private func (s *Service) Empty(ctx context.Context) error { s.Items = []CartItem{} return nil } -- shoppingcart/cart_test.go -- package shoppingcart import ( "context" "math" "testing" "encore.dev/et" "test/products" ) func callAndExpect(t *testing.T, total float64) { resp, err := Total(context.Background()) if err != nil { t.Fatal(err) } if math.Abs(resp.Total - total) > 0.001 { t.Fatalf("expected total to be %f, got %f", total, resp.Total) } } func TestTotal_NoMocking(t *testing.T) { t.Parallel() callAndExpect(t, 299.97) } func TestTotal_WithMockingOfProductsEndpoint(t *testing.T) { t.Parallel() et.MockEndpoint(products.GetPrice, func(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) { return &products.PriceResult{ Total: 20 * float64(p.Quantity) }, nil }) callAndExpect(t, 60.0) } func TestTotal_WithMockingOfServiceMethod(t *testing.T) { t.Parallel() et.MockEndpoint(Total, func(ctx context.Context) (*TotalResult, error) { return &TotalResult{ Total: 100.0 }, nil }) callAndExpect(t, 100.0) } func TestTotal_WithMockingOfServiceObjectWithDifferentInstance(t *testing.T) { t.Parallel() et.MockService("shoppingcart", &Service{ Items: []CartItem{ { ProductID: 1, Quantity: 5 }, }, }) callAndExpect(t, 499.95) } func TestTotal_WithMockingOfServiceWithMockObject(t *testing.T) { t.Parallel() et.MockService[products.Interface]("products", &mockProducts{}) callAndExpect(t, 303.0) } type mockProducts struct{} func (m *mockProducts) GetPrice(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) { return &products.PriceResult{ Total: float64(productID) + float64(p.Quantity * 100) }, nil } func TestTotal_UsingServiceIsolation(t *testing.T) { t.Parallel() callAndExpect(t, 299.97) // These don't run with parallel so we can test the isolation t.Run("emptied in isolation", func(t *testing.T) { et.EnableServiceInstanceIsolation() Empty(context.Background()) callAndExpect(t, 0.0) }) t.Run("non isolated still has items", func(t *testing.T) { callAndExpect(t, 299.97) }) } func TestTotal_RemovingMockServices(t *testing.T) { t.Parallel() et.MockService[products.Interface]("products", &mockProducts{}) t.Run("remove mock", func(t *testing.T) { et.MockService[products.Interface]("products", nil) callAndExpect(t, 299.97) }) callAndExpect(t, 303.0) } func TestTotal_RemovingMockEndpoints(t *testing.T) { t.Parallel() et.MockEndpoint(products.GetPrice, func(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) { return &products.PriceResult{ Total: 20 * float64(p.Quantity) }, nil }) t.Run("remove mock", func(t *testing.T) { et.MockEndpoint(products.GetPrice, nil) callAndExpect(t, 299.97) }) callAndExpect(t, 60.0) } ================================================ FILE: e2e-tests/testdata/testscript/et_override_user.txt ================================================ test -- svc/svc_test.go -- package svc import ( "context" "testing" "encore.dev/beta/auth" "encore.dev/et" ) func TestOverrideAuthInfo(t *testing.T) { curr, _ := auth.UserID() if curr != "" { t.Fatalf("got uid %q, want %q", curr, "") } et.OverrideAuthInfo("foo", nil) curr, _ = auth.UserID() if curr != "foo" { t.Fatalf("got uid %q, want %q", curr, "foo") } } func TestOverrideAuthInfo_ResetBetweenTests(t *testing.T) { curr, _ := auth.UserID() if curr != "" { t.Fatalf("got uid %q, want %q", curr, "") } } func TestOverrideAuthInfo_PropagatesToAPICalls(t *testing.T) { resp, err := GetUser(context.Background()) if err != nil { t.Fatal(err) } else if resp.UserID != "" { t.Fatalf("got uid %q, want %q", resp.UserID, "") } et.OverrideAuthInfo("foo", nil) resp, err = GetUser(context.Background()) if err != nil { t.Fatal(err) } else if resp.UserID != "foo" { t.Fatalf("got uid %q, want %q", resp.UserID, "foo") } } func TestOverrideAuthInfo_APICallOptsOverride(t *testing.T) { et.OverrideAuthInfo("foo", nil) ctx := auth.WithContext(context.Background(), "bar", nil) resp, err := GetUser(ctx) if err != nil { t.Fatal(err) } else if resp.UserID != "bar" { t.Fatalf("got uid %q, want %q", resp.UserID, "bar") } } -- svc/svc.go -- package svc import ( "context" "encore.dev/beta/auth" ) type Response struct { UserID auth.UID } //encore:api public func GetUser(ctx context.Context) (*Response, error) { uid, _ := auth.UserID() return &Response{UserID: uid}, nil } ================================================ FILE: e2e-tests/testdata/testscript/et_override_user_authdata.txt ================================================ test -- svc/svc_test.go -- package svc import ( "context" "testing" "encore.dev/beta/auth" "encore.dev/et" ) func TestOverrideAuthInfo(t *testing.T) { curr, _ := auth.Data().(*AuthData) if curr != nil { t.Fatalf("got data %+v, want nil", curr) } et.OverrideAuthInfo("foo", &AuthData{"email"}) curr = auth.Data().(*AuthData) if curr == nil || curr.Email != "email" { t.Fatalf("got data %+v, want %q", curr, &AuthData{"email"}) } } func TestOverrideAuthInfo_ResetBetweenTests(t *testing.T) { curr := auth.Data() if curr != nil { t.Fatalf("got data %+v, want nil", curr) } } func TestOverrideAuthInfo_PropagatesToAPICalls(t *testing.T) { resp, err := GetUser(context.Background()) if err != nil { t.Fatal(err) } else if resp.Data != nil { t.Fatalf("got data %+v, want nil", resp.Data) } et.OverrideAuthInfo("foo", &AuthData{"email"}) resp, err = GetUser(context.Background()) if err != nil { t.Fatal(err) } else if resp.Data == nil || resp.Data.Email != "email" { t.Fatalf("got data %+v, want %+v", resp.Data, &AuthData{"email"}) } } func TestOverrideAuthInfo_APICallOptsOverride(t *testing.T) { et.OverrideAuthInfo("foo", &AuthData{"email"}) ctx := auth.WithContext(context.Background(), "bar", &AuthData{"email2"}) resp, err := GetUser(ctx) if err != nil { t.Fatal(err) } else if resp.Data == nil || resp.Data.Email != "email2" { t.Fatalf("got data %+v, want %+v", resp.Data, &AuthData{"email2"}) } } -- svc/svc.go -- package svc import ( "context" "encore.dev/beta/auth" ) type Response struct { UserID auth.UID Data *AuthData } //encore:api public func GetUser(ctx context.Context) (*Response, error) { uid, _ := auth.UserID() data, _ := auth.Data().(*AuthData) return &Response{UserID: uid, Data: data}, nil } type AuthData struct { Email string } //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, *AuthData, error) { return "hello", &AuthData{Email: "hello@example.org"}, nil } ================================================ FILE: e2e-tests/testdata/testscript/experiment_local_secrets_override.txtar ================================================ env ENCORE_EXPERIMENT=local-secrets-override run call GET /svc.GetFoo checkresp '{"Data": "bar"}' -- svc/svc.go -- package svc import "context" type Resp struct { Data string } var secrets struct { Foo string } //encore:api public func GetFoo(ctx context.Context) (*Resp, error) { return &Resp{Data: secrets.Foo}, nil } -- .secrets.local.cue -- Foo: "bar" ================================================ FILE: e2e-tests/testdata/testscript/fallback_routes.txt ================================================ run call GET /v1/regular '' checkresp '{"Message":"Hello, Encore world!"}' call GET /v1/regular/foo '' checkresp '{"fallback": true}' call POST /v1/regular '' checkresp '{"fallback": true}' call GET /v2/regular '' checkresp '{"fallback": true}' call GET / '' checkresp '{"fallback": true}' -- svc/svc.go -- package svc import ( "context" "net/http" ) //encore:api public raw path=/!fallback func Fallback(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"fallback": true}`)) } type Response struct { Message string } //encore:api public method=GET path=/v1/regular func Regular(ctx context.Context) (*Response, error) { return &Response{Message: "Hello, Encore world!"}, nil } ================================================ FILE: e2e-tests/testdata/testscript/graceful_shutdown.txt ================================================ run shutdown checklog '{"message": "shutting down"}' -- svc/svc.go -- package svc import ( "encore.dev/shutdown" "encore.dev/rlog" ) //encore:service type Service struct{} func (s *Service) Shutdown(p shutdown.Progress) error { rlog.Info("shutting down") return nil } ================================================ FILE: e2e-tests/testdata/testscript/pubsub_method_handler.txt ================================================ run publish pointer '{"Data": "test"}' publish non-pointer '{"Data": "test"}' checklog '{"topic": "pointer", "subscription": "pointer", "event": {"Data": "test"}, "message": "pointer method"}' checklog '{"topic": "non-pointer", "subscription": "non-pointer", "event": {"Data": "test"}, "message": "non-pointer method"}' -- svc/svc.go -- package svc import ( "context" "encore.dev/rlog" "encore.dev/pubsub" ) //encore:service type Service struct{} func (s *Service) PointerMethod(ctx context.Context, event *Event) error { rlog.Info("pointer method", "event", event) return nil } func (s Service) NonPointerMethod(ctx context.Context, event *Event) error { rlog.Info("non-pointer method", "event", event) return nil } type Event struct { Data string } var Pointer = pubsub.NewTopic[*Event]("pointer", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) var NonPointer = pubsub.NewTopic[*Event]("non-pointer", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) var _ = pubsub.NewSubscription(Pointer, "pointer", pubsub.SubscriptionConfig[*Event]{ Handler: pubsub.MethodHandler((*Service).PointerMethod), }, ) var _ = pubsub.NewSubscription(NonPointer, "non-pointer", pubsub.SubscriptionConfig[*Event]{ Handler: pubsub.MethodHandler(Service.NonPointerMethod), }, ) ================================================ FILE: e2e-tests/testdata/testscript/pubsub_ref.txt ================================================ test -- svc/svc.go -- package svc import ( "context" "encore.dev/pubsub" ) type Msg struct{Message string} var Topic = pubsub.NewTopic[Msg]("topic", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce}) //encore:api func Dummy(context.Context) error { return nil } -- svc/svc_test.go -- package svc import ( "context" "testing" "encore.dev/et" "encore.dev/pubsub" ) func TestRefPublish(t *testing.T) { ref := pubsub.TopicRef[pubsub.Publisher[Msg]](Topic) ref.Publish(context.Background(), Msg{Message: "test"}) msgs := et.Topic(Topic).PublishedMessages() if len(msgs) != 1 || msgs[0].Message != "test" { t.Fatalf("got %v, want %v", msgs, []Msg{{Message: "test"}}) } meta := ref.Meta() want := pubsub.TopicMeta{ Name: "topic", Config: pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }, } if meta != want { t.Fatalf("got meta %v, want %v", meta, want) } } ================================================ FILE: e2e-tests/testdata/testscript/ts_hello.txt ================================================ run call GET /hello/world '' checkresp '{"message": "Hello world"}' shutdown -- encore.app -- {"lang": "typescript"} -- package.json -- { "name": "encore-ts-testapp", "private": true, "version": "0.0.1", "description": "Encore Typescript Test app", "license": "MPL-2.0", "type": "module", "scripts": { "test": "vitest" }, "devDependencies": { "@types/node": "^22.5.7", "typescript": "^5.4", "vitest": "^3.1.3" }, "dependencies": { "encore.dev": "1.50.0" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.13.0" } } -- tsconfig.json -- { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "target": "ES2022", "module": "ES2022", "types": ["node"], "paths": { "~encore/*": ["./encore.gen/*"] }, "composite": true, "strict": true, "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "isolatedModules": true, "sourceMap": true, "declaration": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true } } -- myservice/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("myservice"); -- myservice/api.ts -- import { api } from "encore.dev/api"; export const hello = api( { expose: true, method: "GET", path: "/hello/:name" }, async ({ name }: { name: string }): Promise<{ message: string }> => { return { message: `Hello ${name}` }; } ); ================================================ FILE: e2e-tests/testdata/testscript/ts_worker_pooling.txt ================================================ run call GET /hello/world '' checkresp '{"message": "Hello world"}' shutdown -- encore.app -- { "lang": "typescript", "build": { "worker_pooling": true } } -- package.json -- { "name": "encore-ts-testapp", "private": true, "version": "0.0.1", "description": "Encore Typescript Test app", "license": "MPL-2.0", "type": "module", "scripts": { "test": "vitest" }, "devDependencies": { "@types/node": "^22.5.7", "typescript": "^5.4", "vitest": "^3.1.3" }, "dependencies": { "encore.dev": "1.50.0" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.13.0" } } -- tsconfig.json -- { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "target": "ES2022", "module": "ES2022", "types": ["node"], "paths": { "~encore/*": ["./encore.gen/*"] }, "composite": true, "strict": true, "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "isolatedModules": true, "sourceMap": true, "declaration": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true } } -- myservice/encore.service.ts -- import { Service } from "encore.dev/service"; export default new Service("myservice"); -- myservice/api.ts -- import { api } from "encore.dev/api"; export const hello = api( { expose: true, method: "GET", path: "/hello/:name" }, async ({ name }: { name: string }): Promise<{ message: string }> => { return { message: `Hello ${name}` }; } ); ================================================ FILE: e2e-tests/testdata/tsapp/.gitignore ================================================ encore.gen.go encore.gen.cue /.encore /encore.gen node_modules ================================================ FILE: e2e-tests/testdata/tsapp/encore.app ================================================ {"lang": "typescript"} ================================================ FILE: e2e-tests/testdata/tsapp/package.json ================================================ { "name": "encore-ts-testapp", "private": true, "version": "0.0.1", "description": "Encore Typescript Test app", "license": "MPL-2.0", "type": "module", "scripts": { "test": "vitest" }, "devDependencies": { "@types/node": "^22.5.7", "typescript": "^5.4", "vitest": "^3.1.3" }, "dependencies": { "encore.dev": "file:/Users/andre/src/github.com/encoredev/encore/runtimes/js/encore.dev" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.13.0" } } ================================================ FILE: e2e-tests/testdata/tsapp/service1/api.test.ts ================================================ import { describe, expect, it } from "vitest"; import { hello } from "./api"; describe("get", () => { it("should combine string with parameter value", async () => { const resp = await hello({ name: "world" }); expect(resp.message).toBe("Hello world"); }); }); ================================================ FILE: e2e-tests/testdata/tsapp/service1/api.ts ================================================ import { api, HttpStatus } from "encore.dev/api"; import { APICallMeta, currentRequest } from "encore.dev"; import { service2 } from "~encore/clients"; import log from "encore.dev/log"; export const hello = api( { expose: true, method: "GET", path: "/hello/:name" }, async ({ name }: { name: string }): Promise<{ message: string }> => { return { message: `Hello ${name}` }; } ); // Endpoint that demonstrates middleware data access export const middlewareDemo = api( { expose: true, method: "GET", path: "/middleware-test", tags: ["mwtest"] }, async (): Promise<{ message: string; middlewareMsg: string; }> => { const req = currentRequest() as APICallMeta; return { message: "Hello", middlewareMsg: req.middlewareData?.customMsg || "Not set" }; } ); // Service-to-service call: Get greeting from service2 export const getGreetingViaService2 = api( { expose: true, method: "POST", path: "/get-greeting" }, async (req: { name: string; style?: "formal" | "casual" | "excited"; }): Promise<{ message: string; greeting: string; }> => { try { const result = await service2.greet({ name: req.name, style: req.style || "formal" }); return { message: "Greeting retrieved successfully via service-to-service call", greeting: result.greeting }; } catch (error) { log.error("Failed to get greeting via service call", { error }); throw new Error( "Service-to-service call failed: " + (error as Error).message ); } } ); // Endpoint with custom HTTP status export const customStatus = api( { expose: true, method: "GET", path: "/test-custom-status" }, async (): Promise<{ message: string; status: HttpStatus; }> => { return { message: "I accept!", status: 202 }; } ); ================================================ FILE: e2e-tests/testdata/tsapp/service1/encore.service.ts ================================================ import { Service } from "encore.dev/service"; import { middleware } from "encore.dev/api"; // Middleware to add custom data to request const dataEnricher = middleware(async (req, next) => { // Add custom data that endpoints can access req.data.customMsg = "Hello from middleware!"; const resp = await next(req); resp.status = 201; resp.header.add("x-test-header", "hello"); return resp; }); export default new Service("service1", { middlewares: [middleware({ target: { tags: ["mwtest"] } }, dataEnricher)] }); ================================================ FILE: e2e-tests/testdata/tsapp/service2/api.test.ts ================================================ import { describe, expect, it } from "vitest"; import { testApiError } from "./api"; describe("errors", () => { it("api error with no details with cause", async () => { await expect( testApiError({ variant: "no-details-with-cause" }) ).rejects.toThrow( expect.objectContaining({ message: "the error", details: undefined, code: "canceled", cause: expect.objectContaining({ message: "this is the cause" }) }) ); }); it("api error with no details no cause", async () => { await expect( testApiError({ variant: "no-details-no-cause" }) ).rejects.toThrow( expect.objectContaining({ message: "the error", details: undefined, code: "canceled", cause: undefined }) ); }); it("api error with details no cause", async () => { await expect( testApiError({ variant: "with-details-no-cause" }) ).rejects.toThrow( expect.objectContaining({ message: "the error", details: { a: "detail" }, code: "canceled", cause: undefined }) ); }); it("api error with details with cause", async () => { await expect( testApiError({ variant: "with-details-with-cause" }) ).rejects.toThrow( expect.objectContaining({ message: "the error", details: { a: "detail" }, code: "canceled", cause: expect.objectContaining({ message: "this is the cause" }) }) ); }); }); ================================================ FILE: e2e-tests/testdata/tsapp/service2/api.ts ================================================ import { api, HttpStatus } from "encore.dev/api"; import { IsEmail, MaxLen, MinLen } from "encore.dev/validate"; import log from "encore.dev/log"; import { APIError } from "encore.dev/api"; interface GreetingRequest { name: string; } interface GreetingResponse { greeting: string; timestamp: Date; } interface MessageRequest { message: string & MinLen<3> & MaxLen<1000>; recipient?: string & IsEmail; } interface MessageResponse { message: string; } // Generate different greeting styles export const greet = api( { expose: true, method: "POST", path: "/greet" }, async (req: GreetingRequest): Promise => { let greeting = `Hey ${req.name}! How's it going?`; return { greeting, timestamp: new Date() }; } ); export const testInputValidation = api( { expose: true, method: "POST", path: "/test-validation" }, async (req: MessageRequest): Promise => { return { message: `Message processed` }; } ); export const testApiError = api( { expose: true, method: "GET", path: "/test-api-error/:variant" }, async ({ variant }: { variant: string }): Promise<{ message: string }> => { switch (variant) { case "no-details-no-cause": throw APIError.canceled("the error"); case "with-details-no-cause": throw APIError.canceled("the error").withDetails({ a: "detail" }); case "no-details-with-cause": throw APIError.canceled("the error", new Error("this is the cause")); case "with-details-with-cause": throw APIError.canceled( "the error", new Error("this is the cause") ).withDetails({ a: "detail" }); default: return { message: "Hello there" }; } } ); export const testOtherError = api( { expose: true, method: "GET", path: "/test-other-error" }, async (): Promise<{ message: string }> => { throw new Error("This is a test error"); } ); ================================================ FILE: e2e-tests/testdata/tsapp/service2/encore.service.ts ================================================ import { Service } from "encore.dev/service"; export default new Service("service2"); ================================================ FILE: e2e-tests/testdata/tsapp/tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { /* Basic Options */ "lib": ["ES2022"], "target": "ES2022", "module": "ES2022", "types": ["node"], "paths": { "~encore/*": ["./encore.gen/*"] }, /* Workspace Settings */ "composite": true, /* Strict Type-Checking Options */ "strict": true, /* Module Resolution Options */ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "isolatedModules": true, "sourceMap": true, "declaration": true, /* Advanced Options */ "forceConsistentCasingInFileNames": true, "skipLibCheck": true } } ================================================ FILE: e2e-tests/testdata/tsapp/vite.config.ts ================================================ /// import { defineConfig } from "vite"; import path from "path"; export default defineConfig({ resolve: { alias: { "~encore": path.resolve(__dirname, "./encore.gen") } } }); ================================================ FILE: e2e-tests/testscript_test.go ================================================ //go:build e2e package tests import ( "bufio" "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "time" "github.com/nsqio/go-nsq" ts "github.com/rogpeppe/go-internal/testscript" "encr.dev/cli/daemon/run" "encr.dev/pkg/golden" ) // headerRe matches valid headers in the form "Header=value". var headerRe = regexp.MustCompile(`^([A-Z][A-Za-z0-9-]*)=([^ ]*)$`) func TestRun(t *testing.T) { doRun(t, nil) } func doRun(t *testing.T, experiments []string) { runtimePath := os.Getenv("ENCORE_RUNTIMES_PATH") goroot := os.Getenv("ENCORE_GOROOT") if testing.Short() { t.Skip("skipping in short mode") } else if runtimePath == "" || goroot == "" { t.Skipf("skipping due to missing ENCORE_RUNTIMES_PATH=%q or ENCORE_GOROOT=%q", runtimePath, goroot) } home := t.TempDir() ts.Run(t, ts.Params{ Dir: "testdata/testscript", Setup: func(e *ts.Env) error { e.Setenv("ENCORE_RUNTIMES_PATH", runtimePath) e.Setenv("ENCORE_GOROOT", goroot) e.Setenv("EXTRA_EXPERIMENTS", strings.Join(experiments, ",")) e.Setenv("HOME", home) e.Setenv("GOFLAGS", "-modcacherw") gomod := []byte("module test\n\ngo 1.21.0\n\nrequire encore.dev v1.52.0") if err := os.WriteFile(filepath.Join(e.WorkDir, "go.mod"), gomod, 0755); err != nil { return err } if err := runGoModTidy(e.WorkDir); err != nil { return err } initVals(e) return nil }, Cmds: map[string]func(ts *ts.TestScript, neg bool, args []string){ "run": func(ts *ts.TestScript, neg bool, args []string) { log := &testscriptLogger{ts: ts} exp := ts.Getenv("ENCORE_EXPERIMENT") if extra := ts.Getenv("EXTRA_EXPERIMENTS"); extra != "" { if exp != "" { exp += "," } exp += extra } env := []string{"ENCORE_EXPERIMENT=" + exp} if nodePath, ok := getNodeJSPath().Get(); ok { env = append(env, "PATH="+nodePath) } app := RunApp(getTB(ts), getWorkdir(ts), log, env) setVal(ts, "app", app) setVal(ts, "log", log) }, "shutdown": func(ts *ts.TestScript, neg bool, args []string) { app := getVal[*RunAppData](ts, "app") app.Run.ProcGroup().Close() }, "test": func(ts *ts.TestScript, neg bool, args []string) { log := &testscriptLogger{ts: ts} exp := ts.Getenv("ENCORE_EXPERIMENT") if extra := ts.Getenv("EXTRA_EXPERIMENTS"); extra != "" { if exp != "" { exp += "," } exp += extra } err := RunTests(getTB(ts), getWorkdir(ts), &log.stdout, &log.stderr, []string{"ENCORE_EXPERIMENT=" + exp}) _, _ = os.Stdout.Write(log.stdout.Bytes()) _, _ = os.Stderr.Write(log.stderr.Bytes()) if !neg && err != nil { ts.Fatalf("tests failed: %v", err) } else if neg && err == nil { ts.Fatalf("tests unexpectedly passed: %v", err) } setVal(ts, "log", log) }, "call": func(ts *ts.TestScript, neg bool, args []string) { usage := func() { ts.Fatalf("usage: call [Header=value...] [data] [platform-auth]") } if len(args) < 2 { usage() } method := args[0] args = args[1:] headers := make(map[string]string) for i, arg := range args { m := headerRe.FindStringSubmatch(arg) if m != nil { headers[m[1]] = m[2] } else { args = args[i:] break } } if len(args) == 0 { usage() } app := getVal[*RunAppData](ts, "app") url := "http://" + app.Addr + args[0] disablePlatformAuth := false var body io.Reader if n := len(args); n > 1 { val := args[1] if val == "no-platform-auth" { disablePlatformAuth = true } else { body = strings.NewReader(val) if n > 2 { if args[2] == "no-platform-auth" { disablePlatformAuth = true } else { ts.Fatalf("unexpected argument %q", args[2]) } } } } req := httptest.NewRequest(method, url, body) for k, v := range headers { ts.Logf("setting %s=%v", k, v) req.Header.Set(k, v) } if disablePlatformAuth { req.Header.Set(run.TestHeaderDisablePlatformAuth, "true") } w := httptest.NewRecorder() app.Run.ServeHTTP(w, req) respBody := w.Body.Bytes() _, _ = os.Stdout.Write(respBody) if w.Code != http.StatusOK && !neg { ts.Fatalf("unexpected status code: %v: %s", w.Code, respBody) } else if w.Code == http.StatusOK && neg { ts.Fatalf("unexpected status code: %v: %s", w.Code, respBody) } app.Values["call_resp"] = respBody }, "publish": func(ts *ts.TestScript, neg bool, args []string) { if len(args) != 2 { ts.Fatalf("usage: publish ") } topicName := args[0] data := args[1] app := getVal[*RunAppData](ts, "app") id, _ := app.Values["publish_id"].(int) id++ app.Values["publish_id"] = id msgData, _ := json.Marshal(messageWrapper{ ID: strconv.Itoa(id), Attributes: nil, Data: json.RawMessage(data), }) prod, err := nsq.NewProducer(app.NSQ.Addr(), nsq.NewConfig()) if err != nil { ts.Fatalf("unable to create producer: %v", err) } prod.SetLoggerLevel(nsq.LogLevelMax) if err := prod.Publish(topicName, msgData); err != nil { ts.Fatalf("unable to publish: %v", err) } // Wait for the message to get processed for i := 0; i < 100; i++ { stats, err := app.NSQ.Stats() if err != nil { ts.Fatalf("unable to get nsq stats: %v", err) } for _, topic := range stats.Topics { if topic.TopicName == topicName { if topic.Depth == 0 { break } ts.Logf("waiting for %q queue to be processed, depth: %d", topic.TopicName, topic.Depth) time.Sleep(100 * time.Millisecond) } } } }, "checklog": func(ts *ts.TestScript, neg bool, args []string) { if len(args) != 1 { ts.Fatalf("usage: checklog ") } time.Sleep(100 * time.Millisecond) var want []jsonObj var pattern jsonObj err := json.Unmarshal([]byte(args[0]), &pattern) if err != nil { if strings.Contains(args[0], "{") { ts.Fatalf("checklog pattern not valid log line: %v", err) } fn := ts.ReadFile(args[0]) scanner := bufio.NewScanner(strings.NewReader(fn)) for scanner.Scan() { var ln jsonObj if err := json.Unmarshal(scanner.Bytes(), &ln); err != nil { ts.Fatalf("invalid log line in checklog script: %s: %v", scanner.Bytes(), err) } want = append(want, ln) } } else { want = []jsonObj{pattern} } log := getVal[*testscriptLogger](ts, "log") stderr := log.stderr.String() scanner := bufio.NewScanner(strings.NewReader(stderr)) seen := make([]bool, len(want)) for scanner.Scan() { var got jsonObj if err := json.Unmarshal(scanner.Bytes(), &got); err == nil { for i, ln := range want { if !seen[i] && gotLogLine(got, ln) { seen[i] = true } } } } for i, ln := range want { if !neg && !seen[i] { ts.Fatalf("unable to find log line: %v", ln) } else if neg && seen[i] { ts.Fatalf("found log line: %v", ln) } } }, "checkresp": func(ts *ts.TestScript, neg bool, args []string) { if len(args) != 1 { ts.Fatalf("usage: checkresp ") } var want jsonObj err := json.Unmarshal([]byte(args[0]), &want) if err != nil { if strings.Contains(args[0], "{") { ts.Fatalf("checkresp pattern not valid log line: %v", err) } fn := ts.ReadFile(args[0]) if err := json.Unmarshal([]byte(fn), &want); err != nil { ts.Fatalf("invalid json object in checkresp script: %s: %v", fn, err) } } app := getVal[*RunAppData](ts, "app") var got jsonObj if err := json.Unmarshal(app.Values["call_resp"].([]byte), &got); err != nil { ts.Fatalf("unable to parse response: %v", err) } match := gotLogLine(got, want) if !neg && !match { ts.Fatalf("response does not match: got %s, want %s", got, want) } else if neg && match { ts.Fatalf("log line unexpectedly matched") } }, }, }) } func TestMain(m *testing.M) { golden.Setup() os.Exit(ts.RunMain(m, nil)) } // messageWrapper is the data structure for an NSQ message. // It must be synchronized with the nsq/topic.go file in the runtime. type messageWrapper struct { ID string Attributes map[string]string Data json.RawMessage } type jsonObj = map[string]any func gotLogLine(got, want any) bool { switch want := want.(type) { case map[string]any: got, ok := got.(map[string]any) if !ok { return false } for k, v := range want { if !gotLogLine(got[k], v) { return false } } return true default: return got == want } } type testscriptLogger struct { ts *ts.TestScript stdout, stderr bytes.Buffer } func (l *testscriptLogger) RunStdout(r *run.Run, line []byte) { l.ts.Logf("%s", line) l.stdout.Write(line) } func (l *testscriptLogger) RunStderr(r *run.Run, line []byte) { l.ts.Logf("%s", line) l.stderr.Write(line) } func initVals(e *ts.Env) { e.Values["vars"] = map[string]any{ "tb": e.T().(testing.TB), "wd": e.WorkDir, } } func getTB(ts *ts.TestScript) testing.TB { return getVal[testing.TB](ts, "tb") } func getWorkdir(ts *ts.TestScript) string { return getVal[string](ts, "wd") } func setVal(ts *ts.TestScript, key string, val any) { ts.Value("vars").(map[string]any)[key] = val } func getVal[T any](ts *ts.TestScript, key string) T { return ts.Value("vars").(map[string]any)[key].(T) } ================================================ FILE: e2e-tests/ts_app_test.go ================================================ //go:build e2e package tests import ( "encoding/json" "net/http/httptest" "os" "path/filepath" "strings" "testing" qt "github.com/frankban/quicktest" ) func TestTSEndToEndWithApp(t *testing.T) { c := qt.New(t) wd, err := os.Getwd() if err != nil { t.Fatal(err) } nodePath, ok := getNodeJSPath().Get() if !ok { c.Fatal("Could not find nodejs binary, it is needed to run typescript apps") } appRoot := filepath.Join(wd, "testdata", "tsapp") app := RunApp(c, appRoot, nil, []string{"PATH=" + nodePath}) run := app.Run // Test basic hello endpoint c.Run("typescript hello endpoint", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/hello/world", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) c.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]string{ "message": "Hello world", }) }) // Test middleware functionality c.Run("middleware demo endpoint", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/middleware-test", nil) run.ServeHTTP(w, req) // status modified by mw c.Assert(w.Code, qt.Equals, 201) // header set by mw c.Assert(w.Header().Get("X-Test-Header"), qt.Equals, "hello") var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) // Verify middleware data is present c.Assert(response["message"], qt.Equals, "Hello") c.Assert(response["middlewareMsg"], qt.Equals, "Hello from middleware!") }) // Test custom HTTP status - 404 Not Found c.Run("custom HTTP status", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test-custom-status", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 202) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["message"], qt.Equals, "I accept!") }) c.Run("service2 greeting endpoint", func(c *qt.C) { requestBody := `{"name": "Bob"}` w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/greet", strings.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["greeting"], qt.Equals, "Hey Bob! How's it going?") }) c.Run("service-to-service greeting call", func(c *qt.C) { requestBody := `{"name": "Charlie"}` w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/get-greeting", strings.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["message"], qt.Equals, "Greeting retrieved successfully via service-to-service call") c.Assert(response["greeting"], qt.Equals, "Hey Charlie! How's it going?") }) c.Run("service2 input validation - valid", func(c *qt.C) { requestBody := `{"message": "Hello world", "recipient": "test@example.com"}` w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/test-validation", strings.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 200) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["message"], qt.Equals, "Message processed") }) c.Run("service2 input validation - to short", func(c *qt.C) { requestBody := `{"message": "Hi"}` w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/test-validation", strings.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") run.ServeHTTP(w, req) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["message"], qt.Contains, "message: length too short") // Should return validation error c.Assert(w.Code, qt.Equals, 400) }) c.Run("service2 input validation - invalid email", func(c *qt.C) { requestBody := `{"message": "Valid message", "recipient": "not-an-email"}` w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/test-validation", strings.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") run.ServeHTTP(w, req) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["message"], qt.Contains, "value is not an email") // Should return validation error c.Assert(w.Code, qt.Equals, 400) }) // Test error handling c.Run("service2 api errors", func(c *qt.C) { expected := map[string]map[string]interface{}{ "no-details-no-cause": { "code": "canceled", "details": nil, "internal_message": nil, "message": "the error", }, "with-details-no-cause": { "code": "canceled", "details": map[string]interface{}{ "a": "detail", }, "internal_message": nil, "message": "the error", }, "no-details-with-cause": { "code": "canceled", "details": nil, "internal_message": nil, "message": "the error: this is the cause", }, "with-details-with-cause": { "code": "canceled", "details": map[string]interface{}{ "a": "detail", }, "internal_message": nil, "message": "the error: this is the cause", }, } for path, expected := range expected { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test-api-error/"+path, nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 499) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["code"], qt.Equals, expected["code"]) if expected["details"] == nil { c.Assert(response["details"], qt.IsNil) } else { c.Assert(response["details"], qt.DeepEquals, expected["details"]) } c.Assert(response["internal_message"], qt.Equals, expected["internal_message"]) c.Assert(response["message"], qt.Equals, expected["message"]) } }) c.Run("service2 other errors", func(c *qt.C) { w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test-other-error/", nil) run.ServeHTTP(w, req) c.Assert(w.Code, qt.Equals, 500) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) c.Assert(err, qt.IsNil) c.Assert(response["code"], qt.Equals, "internal") c.Assert(response["details"], qt.IsNil) c.Assert(response["internal_message"], qt.Equals, "This is a test error") c.Assert(response["message"], qt.Equals, "an internal error occurred") }) // Run TypeScript tests c.Run("run TypeScript tests", func(c *qt.C) { err := RunTests(c.TB, appRoot, os.Stdout, os.Stderr, nil) c.Assert(err, qt.IsNil) }) } ================================================ FILE: go.mod ================================================ module encr.dev go 1.24.0 toolchain go1.24.9 require ( cloud.google.com/go/storage v1.43.0 cuelang.org/go v0.4.3 encore.dev v1.1.0 github.com/agnivade/levenshtein v1.1.1 github.com/alecthomas/chroma v0.10.0 github.com/alicebob/miniredis/v2 v2.23.0 github.com/bep/debounce v1.2.1 github.com/bluele/gcache v0.0.2 github.com/briandowns/spinner v1.19.0 github.com/cenkalti/backoff/v4 v4.2.1 github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.7.1 github.com/cockroachdb/errors v1.11.1 github.com/dave/jennifer v1.7.0 github.com/evanw/esbuild v0.19.8 github.com/fatih/color v1.15.0 github.com/fatih/structtag v1.2.0 github.com/frankban/quicktest v1.14.6 github.com/fsnotify/fsnotify v1.8.0 github.com/getkin/kin-openapi v0.115.0 github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-migrate/migrate/v4 v4.15.2 github.com/golang/protobuf v1.5.4 github.com/google/btree v1.1.3 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.6 github.com/google/renameio/v2 v2.0.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hasura/go-graphql-client v0.12.1 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgproto3/v2 v2.3.3 github.com/jackc/pgx/v5 v5.7.4 github.com/json-iterator/go v1.1.12 github.com/julienschmidt/httprouter v1.3.0 github.com/jwalton/go-supportscolor v1.1.0 github.com/knadh/koanf/parsers/toml/v2 v2.1.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/providers/rawbytes v0.1.0 github.com/knadh/koanf/v2 v2.1.2 github.com/lib/pq v1.10.9 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora/v3 v3.0.0 github.com/mattn/go-runewidth v0.0.15 github.com/mattn/go-sqlite3 v1.14.18 github.com/modern-go/reflect2 v1.0.2 github.com/nsqio/go-nsq v1.1.0 github.com/nsqio/nsq v1.2.1 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/diskv v2.0.1+incompatible github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/rogpeppe/go-internal v1.14.1 github.com/rs/xid v1.6.0 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/sqlc-dev/sqlc v1.29.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a go.encore.dev/platform-sdk v1.1.0 go.uber.org/goleak v1.3.0 go4.org v0.0.0-20230225012048-214862532bf5 golang.org/x/crypto v0.39.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/mod v0.25.0 golang.org/x/net v0.41.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 golang.org/x/text v0.26.0 golang.org/x/tools v0.34.0 google.golang.org/api v0.200.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 gotest.tools/v3 v3.5.1 mvdan.cc/sh/v3 v3.12.0 sigs.k8s.io/yaml v1.3.0 ) require ( cel.dev/expr v0.19.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/fmstephe/unsafeutil v1.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) require ( cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.2.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/algolia/algoliasearch-client-go/v3 v3.31.4 github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/docker/cli v28.2.2+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.2.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/proto v1.9.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/getsentry/sentry-go v0.25.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.4 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.24.1 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mark3labs/mcp-go v0.27.0 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/nsqio/go-diskqueue v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect github.com/pingcap/log v1.1.0 // indirect github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/riza-io/grpc-go v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.62.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.9.1 // indirect modernc.org/sqlite v1.37.0 // indirect nhooyr.io/websocket v1.8.10 // indirect ) // The implementation of the `encore.dev` runtime, is in this repo // along side the Encore CLI replace encore.dev => ./runtimes/go ================================================ FILE: go.sum ================================================ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/spanner v1.28.0/go.mod h1:7m6mtQZn/hMbMfx62ct5EWrGND4DNqkXyrmBPRS+OJo= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= github.com/algolia/algoliasearch-client-go/v3 v3.31.4 h1:UJhx6AhZCYf0qZygDz2c1x1+1q2q2sfzsRaQM6yswWk= github.com/algolia/algoliasearch-client-go/v3 v3.31.4/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw= github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/timer_metrics v1.0.0/go.mod h1:87z4/LSg3f++tMqZwZlsLwPuJu6xloyJ7Qm40NyEkLs= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dhui/dktest v0.3.10 h1:0frpeeoM9pHouHjhLeZDuDTJ0PqjDTrycaHaMmkJAo8= github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/proto v1.9.2 h1:YX2MPuUfUi/h8v+yt4WD8cdj6bt9P3475d2zrL0iogM= github.com/emicklei/proto v1.9.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanw/esbuild v0.19.8 h1:IJ1CRsv3i4dkjPLo6NEGTMI0DDba7jOlPc6JFzIxtl4= github.com/evanw/esbuild v0.19.8/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fmstephe/unsafeutil v1.0.0 h1:hWKjyW7jOL7rfCiBgX61tGy742pZ3C3VpHcGwTAgB2w= github.com/fmstephe/unsafeutil v1.0.0/go.mod h1:00y9QPGpX2A5iB0UmPDtnSpO4c2XsRQu3dQYuGL8+RA= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU= github.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hasura/go-graphql-client v0.12.1 h1:tL+BCoyubkYYyaQ+tJz+oPe/pSxYwOJHwe5SSqqi6WI= github.com/hasura/go-graphql-client v0.12.1/go.mod h1:F4N4kR6vY8amio3gEu3tjSZr8GPOXJr3zj72DKixfLE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jwalton/go-supportscolor v1.1.0 h1:HsXFJdMPjRUAx8cIW6g30hVSFYaxh9yRQwEWgkAR7lQ= github.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/toml/v2 v2.1.0 h1:EUdIKIeezfDj6e1ABDhIjhbURUpyrP1HToqW6tz8R0I= github.com/knadh/koanf/parsers/toml/v2 v2.1.0/go.mod h1:0KtwfsWJt4igUTQnsn0ZjFWVrP80Jv7edTBRbQFd2ho= github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc= github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= github.com/mreiferson/go-options v1.0.0/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsqio/go-diskqueue v1.0.0/go.mod h1:INuJIxl4ayUsyoNtHL5+9MFPDfSZ0zY93hNY6vhBRsI= github.com/nsqio/go-diskqueue v1.1.0 h1:r0dJ0DMXT3+2mOq+79cvCjnhoBxyGC2S9O+OjQrpe4Q= github.com/nsqio/go-diskqueue v1.1.0/go.mod h1:INuJIxl4ayUsyoNtHL5+9MFPDfSZ0zY93hNY6vhBRsI= github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= github.com/nsqio/nsq v1.2.1 h1:ZVjANYLnX1vPLmuSNCOdiw4nNPnzWgAC4t8wFhznMqU= github.com/nsqio/nsq v1.2.1/go.mod h1:vXbwehoIygyVoX44oLFaN7MA0xrmudeuborDpMPiLTY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk= github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0= github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4 h1:t0R4wRdWPe9ZD+qsJRaidE1gN1CyM7d3IaKxJDyypQI= github.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4/go.mod h1:lqKDuJp+gFrjIzf8LR/daIFsmcKkP6fmczWBs/7n39k= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs= github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.encore.dev/platform-sdk v1.1.0 h1:0BYLt7ZAoPje3KMLee6/gA2FECHwzi1sKgp3SqC+QRo= go.encore.dev/platform-sdk v1.1.0/go.mod h1:ImcJU8p0V3bSXb+d++Ni/8hFDwVZaFpUAAySTY2x6FY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.200.0 h1:0ytfNWn101is6e9VBoct2wrGDjOi5vn7jw5KtaQgDrU= google.golang.org/api v0.200.0/go.mod h1:Tc5u9kcbjO7A8SwGlYj4IiVifJU01UqXtEgDMYmBmV8= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk= google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: go_llm_instructions.txt ================================================ If the user asks you questions, you should assume you are Corey and act accordingly. Corey is a helpful AI coding assistant created by Encore. Acts as the world's most proficient developers would. Always knowledgeable of latest best practices. Provides concise, clear, efficient coding solutions with friendly, approachable manners. Defaults to Encore.go for backend development. Has knowledge of distributed systems, Encore.go, Go(Golang), TypeScript, React, Next.js, and modern development. - Always think through the problem and plan before responding - Work iteratively with the user to achieve desired outcome - Optimize solutions for user's needs and goals Corey MUST write valid Go code using state-of-the-art Go v1.22+ features and best practices. Encore uses a monorepo design where one app contains the entire backend. Enables distributed tracing and Encore Flow through unified application model. Supports both monolith and microservices architectures with monolith-style developer experience. Directory structure: /app-name encore.app service1/ migrations/ 1_create_table.up.sql service1.go service1_test.go service2/ service2.go Sub-packages are internal to services, cannot define APIs, used for helpers and code organization. For large apps, group related services into system directories (logical groupings with no special runtime behavior): /app-name encore.app system1/ service1/ service2/ system2/ service3/ Create type-safe APIs from regular Go functions using //encore:api annotation. Access controls: - public: Accessible to anyone on the internet - private: Only accessible within app and via cron jobs - auth: Public but requires valid authentication Function signatures: func Foo(ctx context.Context, p *Params) (*Response, error) // full func Foo(ctx context.Context) (*Response, error) // response only func Foo(ctx context.Context, p *Params) error // request only func Foo(ctx context.Context) error // minimal Request/response data locations: - header: Use `header` tag for HTTP headers - query: Default for GET/HEAD/DELETE, uses snake_case, supports basic types/slices - body: Default for other methods, uses `json` tag, supports complex types Path parameters: Use :name for variables, *name for wildcards. Place at end of path. Sensitive data: - Field level: `encore:"sensitive"` tag, auto-redacted in tracing - Endpoint level: Add `sensitive` to //encore:api annotation Type support by location: - headers/path: bool, numeric, string, time.Time, UUID, json.RawMessage - query: All above plus lists - body: All types including structs, maps, pointers A service is defined by creating at least one API within a Go package. Package name becomes service name. //encore:service annotation enables custom initialization and graceful shutdown: type Service struct { // Dependencies here } func initService() (*Service, error) { // Initialization code } //encore:api public func (s *Service) MyAPI(ctx context.Context) error { // API implementation } Graceful shutdown via Shutdown method: func (s *Service) Shutdown(force context.Context) - Graceful phase: Several seconds for completion - Forced phase: When force context canceled, terminate immediately For lower-level HTTP access (webhooks, WebSockets): //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { // Process raw HTTP request } //encore:api public raw method=POST path=/webhook/:id func Webhook(w http.ResponseWriter, req *http.Request) { id := encore.CurrentRequest().PathParams.Get("id") } Encore treats SQL databases as logical resources with native PostgreSQL support. Create database: var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{ Migrations: "./migrations", }) Migration naming: number_description.up.sql (e.g., 1_create_table.up.sql) Migrations folder structure: service/ migrations/ 1_create_table.up.sql 2_add_field.up.sql service.go Data operations: // Insert _, err := tododb.Exec(ctx, ` INSERT INTO todo_item (id, title, done) VALUES ($1, $2, $3) `, id, title, done) // Query err := tododb.QueryRow(ctx, ` SELECT id, title, done FROM todo_item LIMIT 1 `).Scan(&item.ID, &item.Title, &item.Done) // Use errors.Is(err, sqldb.ErrNoRows) for no results CLI commands: - encore db shell database-name [--env=name] - Opens psql shell - encore db conn-uri database-name [--env=name] - Outputs connection string - encore db proxy [--env=name] - Sets up local connection proxy For existing databases, create dedicated package with lazy connection pool: package externaldb import ( "context" "fmt" "github.com/jackc/pgx/v4/pgxpool" "go4.org/syncutil" ) func Get(ctx context.Context) (*pgxpool.Pool, error) { err := once.Do(func() error { var err error pool, err = setup(ctx) return err }) return pool, err } var ( once syncutil.Once pool *pgxpool.Pool ) var secrets struct { ExternalDBPassword string } func setup(ctx context.Context) (*pgxpool.Pool, error) { connString := fmt.Sprintf("postgresql://%s:%s@hostname:port/dbname?sslmode=require", "user", secrets.ExternalDBPassword) return pgxpool.Connect(ctx, connString) } Works with Cassandra, DynamoDB, BigTable, MongoDB, Neo4j, and other services. Default: per-service databases for isolation. To share, reference using sqldb.Named: // In report service, access todo service's database: var todoDB = sqldb.Named("todo") //encore:api method=GET path=/report/todo func CountCompletedTodos(ctx context.Context) (*ReportResponse, error) { var report ReportResponse err := todoDB.QueryRow(ctx,` SELECT COUNT(*) FROM todo_item WHERE completed = TRUE `).Scan(&report.Total) return &report, err } Declarative periodic tasks. Does not run locally or in Preview Environments. import "encore.dev/cron" var _ = cron.NewJob("welcome-email", cron.JobConfig{ Title: "Send welcome emails", Every: 2 * cron.Hour, Endpoint: SendWelcomeEmail, }) //encore:api private func SendWelcomeEmail(ctx context.Context) error { return nil } Scheduling options: - Every: Must divide 24 hours evenly (e.g., 10 * cron.Minute, 6 * cron.Hour) - Schedule: Cron expressions (e.g., "0 4 15 * *" for 4am UTC on 15th) Requirements: Endpoints must be idempotent, no request parameters, signature func(context.Context) error or func(context.Context) (*T, error) Redis-based distributed caching system. import "encore.dev/storage/cache" var MyCacheCluster = cache.NewCluster("my-cache-cluster", cache.ClusterConfig{ EvictionPolicy: cache.AllKeysLRU, }) // Keyspace with type safety var RequestsPerUser = cache.NewIntKeyspace[auth.UID](cluster, cache.KeyspaceConfig{ KeyPattern: "requests/:key", DefaultExpiry: cache.ExpireIn(10 * time.Second), }) // Structured keys type MyKey struct { UserID auth.UID ResourcePath string } var ResourceRequestsPerUser = cache.NewIntKeyspace[MyKey](cluster, cache.KeyspaceConfig{ KeyPattern: "requests/:UserID/:ResourcePath", DefaultExpiry: cache.ExpireIn(10 * time.Second), }) Supports strings, integers, floats, structs, sets, and ordered lists. Cloud-agnostic API compatible with S3, GCS, and S3-compatible services. var ProfilePictures = objects.NewBucket("profile-pictures", objects.BucketConfig{ Versioned: false, }) // Public bucket with CDN var PublicAssets = objects.NewBucket("public-assets", objects.BucketConfig{ Public: true, }) Operations: Upload, Download, List, Remove, Attrs, Exists Bucket references for permissions: type myPerms interface { objects.Downloader objects.Uploader } ref := objects.BucketRef[myPerms](bucket) Asynchronous event broadcasting with automatic infrastructure provisioning. type SignupEvent struct{ UserID int } var Signups = pubsub.NewTopic[*SignupEvent]("signups", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) // Publishing messageID, err := Signups.Publish(ctx, &SignupEvent{UserID: id}) // Topic reference signupRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](Signups) // Subscribing var _ = pubsub.NewSubscription( user.Signups, "send-welcome-email", pubsub.SubscriptionConfig[*SignupEvent]{ Handler: SendWelcomeEmail, }, ) // Method handler with dependency injection var _ = pubsub.NewSubscription( user.Signups, "send-welcome-email", pubsub.SubscriptionConfig[*SignupEvent]{ Handler: pubsub.MethodHandler((*Service).SendWelcomeEmail), }, ) Delivery guarantees: - AtLeastOnce: Handlers must be idempotent - ExactlyOnce: Stronger guarantees (AWS: 300 msg/sec, GCP: 3000+ msg/sec) Ordering: Use OrderingAttribute matching pubsub-attr tag Testing: msgs := et.Topic(Signups).PublishedMessages() assert.Len(t, msgs, 1) Built-in secrets manager for API keys, passwords, private keys. var secrets struct { SSHPrivateKey string GitHubAPIToken string } func callGitHub(ctx context.Context) { req.Header.Add("Authorization", "token " + secrets.GitHubAPIToken) } CLI management: - encore secret set --type production secret-name - encore secret set --type development secret-name - encore secret set --env env-name secret-name (environment-specific override) Types: production (prod), development (dev), preview (pr), local Local override via .secrets.local.cue: GitHubAPIToken: "my-local-override-token" Call APIs like regular functions with automatic type checking: import "encore.app/hello" //encore:api public func MyOtherAPI(ctx context.Context) error { resp, err := hello.Ping(ctx, &hello.PingParams{Name: "World"}) if err == nil { log.Println(resp.Message) // "Hello, World!" } return err } Structured errors via encore.dev/beta/errs package. return &errs.Error{ Code: errs.NotFound, Message: "sprocket not found", } // Returns HTTP 404 {"code": "not_found", "message": "sprocket not found"} Wrapping: errs.Wrap(err, msg, metaPairs...) errs.WrapCode(err, code, msg, metaPairs...) Builder pattern: eb := errs.B().Meta("board_id", params.ID) return eb.Code(errs.NotFound).Msg("board not found").Err() Error codes: OK(200), Canceled(499), Unknown(500), InvalidArgument(400), DeadlineExceeded(504), NotFound(404), AlreadyExists(409), PermissionDenied(403), ResourceExhausted(429), FailedPrecondition(400), Aborted(409), OutOfRange(400), Unimplemented(501), Internal(500), Unavailable(503), DataLoss(500), Unauthenticated(401) Inspection: errs.Code(err), errs.Meta(err), errs.Details(err) Flexible auth with different access levels. import "encore.dev/beta/auth" // Basic //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, error) { // Validate token and return user ID } // With user data type Data struct { Username string } //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) { // Return user ID and custom data } // Structured auth params type MyAuthParams struct { SessionCookie *http.Cookie `cookie:"session"` ClientID string `query:"client_id"` Authorization string `header:"Authorization"` } //encore:authhandler func AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) { // Process structured auth params } Usage: auth.Data(), auth.UserID() Override for testing: auth.WithContext(ctx, auth.UID("my-user-id"), &MyAuthData{}) Error handling: return "", &errs.Error{ Code: errs.Unauthenticated, Message: "invalid token", } Environment-specific config using CUE files. package mysvc import "encore.dev/config" type SomeConfigType struct { ReadOnly config.Bool Example config.String } var cfg *SomeConfigType = config.Load[*SomeConfigType]() CUE tags for constraints: type FooBar { A int `cue:">100"` B int `cue:"A-50"` C int `cue:"A+B"` } Config types: config.String, config.Bool, config.Int, config.Float64, config.Time, config.UUID, config.Value[T], config.Values[T] Meta values: - APIBaseURL, Environment.Name, Environment.Type (production/development/ephemeral/test), Environment.Cloud (aws/gcp/encore/local) Testing: et.SetCfg(cfg.SendEmails, true) CUE patterns: - Defaults: value: type | *default_value - Switch: array with conditionals, take [0] Configure in encore.app file: - debug: Enable CORS debug logging - allow_headers: Additional accepted headers ("*" allows all) - expose_headers: Additional exposed headers - allow_origins_without_credentials: Defaults to ["*"] - allow_origins_with_credentials: For authenticated requests, supports wildcards like "https://*.example.com" Access app and request info via encore.dev package. // Application metadata meta := encore.Meta() // meta.AppID, meta.APIBaseURL, meta.Environment, meta.Build, meta.Deploy // Request metadata req := encore.CurrentRequest() // req.Service, req.Endpoint, req.Path, req.StartTime // Cloud-specific behavior switch encore.Meta().Environment.Cloud { case encore.CloudAWS: return writeIntoRedshift(ctx, action, user) case encore.CloudGCP: return writeIntoBigQuery(ctx, action, user) } Reusable code running before/after API requests. //encore:middleware global target=all func ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response { payload := req.Data().Payload if validator, ok := payload.(interface { Validate() error }); ok { if err := validator.Validate(); err != nil { err = errs.WrapCode(err, errs.InvalidArgument, "validation failed") return middleware.Response{Err: err} } } return next(req) } // With dependency injection //encore:middleware target=all func (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response { // Implementation } // Tag-based targeting //encore:middleware target=tag:cache func CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response { // ... } //encore:api public method=GET path=/user/:id tag:cache func GetUser(ctx context.Context, id string) (*User, error) { // Implementation } Ordering: Global before service-specific, lexicographic by filename. Built-in mocking for isolated testing. // Mock endpoint for single test func Test_Something(t *testing.T) { t.Parallel() et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) { return &products.PriceResponse{Price: 100}, nil }) } // Mock endpoint for all tests in package func TestMain(m *testing.M) { et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) { return &products.PriceResponse{Price: 100}, nil }) os.Exit(m.Run()) } // Mock entire service et.MockService("products", &products.Service{ SomeField: "a testing value", }) // Type-safe service mocking et.MockService[products.Interface]("products", &myMockObject{}) Run tests with: encore test ./... Supports all standard go test flags. Built-in tracing at localhost:9400. Database testing: - Automatic setup in separate cluster, optimized for speed - Temporary databases: et.NewTestDatabase() creates isolated, fully migrated DB Service structs: Lazy initialization, instance sharing between tests - Isolate with: et.EnableServiceInstanceIsolation() Automatic request validation via Validate() method. type MyRequest struct { Email string } func (r *MyRequest) Validate() error { if !isValidEmail(r.Email) { return &errs.Error{Code: errs.InvalidArgument, Message: "invalid email"} } return nil } Validation runs after deserialization, before handler. Non-errs.Error errors become InvalidArgument (HTTP 400). Enable in encore.app: { "id": "my-app-id", "build": { "cgo_enabled": true } } Uses Ubuntu builder with gcc. Libraries must support static linking. Implement Clerk authentication: package auth import "github.com/clerkinc/clerk-sdk-go/clerk" type Service struct { client clerk.Client } func initService() (*Service, error) { client, err := clerk.NewClient(secrets.ClientSecretKey) if err != nil { return nil, err } return &Service{client: client}, nil } type UserData struct { ID string Username *string FirstName *string LastName *string ProfileImageURL string PrimaryEmailAddressID *string EmailAddresses []clerk.EmailAddress } //encore:authhandler func (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) { // Token verification and user data retrieval } Set secrets: - encore secret set --prod ClientSecretKey - encore secret set --dev ClientSecretKey Add dependencies as struct fields for easy testing: package email //encore:service type Service struct { sendgridClient *sendgrid.Client } func initService() (*Service, error) { client, err := sendgrid.NewClient() if err != nil { return nil, err } return &Service{sendgridClient: client}, nil } //encore:api private func (s *Service) Send(ctx context.Context, p *SendParams) error { // Use s.sendgridClient } // For testing, use interface type sendgridClient interface { SendEmail(...) } func TestFoo(t *testing.T) { svc := &Service{sendgridClient: &myMockClient{}} // Test } Transactional outbox pattern for database + Pub/Sub consistency. var SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */) ref := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic) ref = outbox.Bind(ref, outbox.TxPersister(tx)) Required schema: CREATE TABLE outbox ( id BIGSERIAL PRIMARY KEY, topic TEXT NOT NULL, data JSONB NOT NULL, inserted_at TIMESTAMPTZ NOT NULL ); CREATE INDEX outbox_topic_idx ON outbox (topic, id); Relay setup: type Service struct { signupsRef pubsub.Publisher[*SignupEvent] } func initService() (*Service, error) { relay := outbox.NewRelay(outbox.SQLDBStore(db)) signupsRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic) outbox.RegisterTopic(relay, signupsRef) go relay.PollForMessage(context.Background(), -1) return &Service{signupsRef: signupsRef}, nil } Supports: encore.dev/storage/sqldb, database/sql, github.com/jackc/pgx/v5 - Hello World: https://github.com/encoredev/examples/tree/main/hello-world - URL Shortener: https://github.com/encoredev/examples/tree/main/url-shortener - Uptime Monitor: https://github.com/encoredev/examples/tree/main/uptime Execution: - encore run [--debug] [--watch=true] - Run application - encore test ./... [go test flags] - Test application - encore check - Check for compile-time errors App management: - encore app clone [app-id] [directory] - Clone app - encore app create [name] - Create new app - encore app init [name] - Create from existing repo - encore app link [app-id] - Link app with server Authentication: - encore auth login/logout/signup/whoami Daemon: - encore daemon - Restart daemon - encore daemon env - Output environment info Database: - encore db shell database-name [--env=name] - psql shell (--write, --admin, --superuser) - encore db conn-uri database-name [--env=name] - Connection string - encore db proxy [--env=name] - Local proxy - encore db reset [service-names...] - Reset databases Code generation: - encore gen client [app-id] [--env=name] [--lang=lang] - Generate API client Languages: go, typescript, javascript, openapi Logging: - encore logs [--env=prod] [--json] - Stream logs Kubernetes: - encore k8s configure --env=ENV_NAME - Update kubectl config Secrets: - encore secret set --type TYPE secret-name (types: production, development, preview, local) - encore secret set --env env-name secret-name - encore secret list [keys...] - encore secret archive/unarchive id Version: - encore version - Report version - encore version update - Check and apply updates VPN: - encore vpn start/status/stop Build: - encore build docker [--base string] [--push] - Build Docker image ================================================ FILE: internal/conf/conf.go ================================================ // Package conf writes and reads the Encore configuration file for the user. package conf import ( "context" "encoding/json" "errors" "fmt" "io/fs" "net/http" "os" "path/filepath" "runtime" "strings" "time" "golang.org/x/oauth2" "encr.dev/internal/goldfish" "encr.dev/pkg/xos" ) var ErrInvalidRefreshToken = errors.New("invalid refresh token") var ErrNotLoggedIn = errors.New("not logged in: run 'encore auth login' first") // These can be overwritten using // `go build -ldflags "-X encr.dev/cli/internal/conf.defaultPlatformURL=https://api.encore.dev"`. var ( defaultPlatformURL = "https://api.encore.cloud" defaultDevDashURL = "https://devdash.encore.dev" defaultConfigDirectory = "encore" ) // APIBaseURL is the base URL for communicating with the Encore Platform. var APIBaseURL = (func() string { if u := os.Getenv("ENCORE_PLATFORM_API_URL"); u != "" { return u } return defaultPlatformURL })() // WSBaseURL is the base URL for communicating with the Encore Platform over WebSocket. var WSBaseURL = (func() string { return strings.Replace(APIBaseURL, "http", "ws", -1) // "https" becomes "wss" })() // DevDashURL is the base URL to retrieve the dev dashboard code from. var DevDashURL = (func() string { if u := os.Getenv("ENCORE_DEVDASH_URL"); u != "" { return u } return defaultDevDashURL })() // CacheDevDash reports whether or not the dev dash contents should be cached. var CacheDevDash = (func() bool { return !strings.Contains(DevDashURL, "localhost") })() // DevDaemon reports whether or not the daemon is running in development mode. var DevDaemon = (func() bool { return os.Getenv("ENCORE_DAEMON_DEV") != "" })() // Dir reports the directory where Encore's configuration is stored. func Dir() (string, error) { dir := os.Getenv("ENCORE_CONFIG_DIR") if dir == "" { d, err := os.UserConfigDir() if err != nil { return "", err } dir = filepath.Join(d, defaultConfigDirectory) } return dir, nil } // CacheDir reports the base directory for storing data which can be cached // and deleted at any time by the user without affecting the Encore daemon. // // The directory may or may not exist already. func CacheDir() (string, error) { dir := os.Getenv("ENCORE_CACHE_DIR") if dir == "" { d, err := os.UserCacheDir() if err != nil { return "", err } dir = filepath.Join(d, defaultConfigDirectory, "cache") } if !filepath.IsAbs(dir) { return "", fmt.Errorf("ENCORE_CACHE_DIR must be absolute, got %q", dir) } return dir, nil } // DataDir reports the base directory for storing data, like database volumes. // The directory may or may not exist already. func DataDir() (string, error) { dir := os.Getenv("ENCORE_DATA_DIR") if dir == "" { d, err := os.UserCacheDir() if err != nil { return "", err } dir = filepath.Join(d, defaultConfigDirectory, "data") } if !filepath.IsAbs(dir) { return "", fmt.Errorf("ENCORE_DATA_DIR must be absolute, got %q", dir) } return dir, nil } // Config represents the stored Encore configuration. type Config struct { oauth2.Token Actor string `json:"actor,omitempty"` // The ID of either the user or app authenticated Email string `json:"email,omitempty"` // non-zero if logged in as a user AppSlug string `json:"app_slug,omitempty"` // non-zero if logged in as an app WireGuard struct { PublicKey string `json:"pub,omitempty"` PrivateKey string `json:"priv,omitempty"` } `json:"wg,omitempty"` } // Write persists the configuration for the user. func Write(cfg *Config) (err error) { defer func() { if err != nil { err = fmt.Errorf("conf.Write: %v", err) } }() dir, err := Dir() if err != nil { return err } path := filepath.Join(dir, ".auth_token") if data, err := json.Marshal(cfg); err != nil { return err } else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } else if err := xos.WriteFile(path, data, 0600); err != nil { return err } return nil } func Logout() error { dir, err := Dir() if err != nil { return err } path := filepath.Join(dir, ".auth_token") if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { return err } DefaultTokenSource = NewTokenSource() AuthClient = oauth2.NewClient(nil, DefaultTokenSource) return nil } func CurrentUser() (*Config, error) { dir, err := Dir() if err != nil { return nil, fmt.Errorf("conf.CurrentUser: %w", err) } conf, err := readConf(dir) if err != nil { return nil, fmt.Errorf("conf.CurrentUser: %w", err) } return conf, nil } func OriginalUser(configDir string) (cfg *Config, err error) { if runtime.GOOS == "windows" { // Windows does not have the notion of a root user, so just use CurrentUser return CurrentUser() } if configDir == "" { var err error configDir, err = Dir() if err != nil { return nil, err } } return readConf(configDir) } func readConf(configDir string) (*Config, error) { path := filepath.Join(configDir, ".auth_token") data, err := os.ReadFile(path) if err != nil { return nil, err } var conf Config if err := json.Unmarshal(data, &conf); err != nil { return nil, err } return &conf, nil } func NewTokenSource() *TokenSource { ts := &TokenSource{} ts.token = goldfish.New(1*time.Second, ts.readTokenFromConfig) ts.cfg = &oauth2.Config{ Endpoint: oauth2.Endpoint{ TokenURL: APIBaseURL + "/login/oauth:refresh-token", }, } return ts } // TokenSource implements oauth2.TokenSource by looking up the // current logged in user's API Token. type TokenSource struct { token *goldfish.Cache[*oauth2.Token] cfg *oauth2.Config } // Token implements oauth2.TokenSource. func (ts *TokenSource) Token() (*oauth2.Token, error) { baseToken, err := ts.token.Get() if err != nil { return nil, err } // Use the built-in token source to simplify the logic of // refreshing the token as necessary. fetch := ts.cfg.TokenSource(context.Background(), baseToken) token, err := fetch.Token() if err != nil { var re *oauth2.RetrieveError if errors.As(err, &re) && re.Response.StatusCode == 422 { // The refresh token is invalid. Log the user out to reset the token. _ = Logout() return nil, ErrInvalidRefreshToken } } else if token.AccessToken != baseToken.AccessToken { // The token has changed, so update the config. cfg, err := CurrentUser() if err != nil { return nil, err } cfg.Token = *token if err := Write(cfg); err != nil { return nil, err } } return token, err } // readTokenFromConfig reads the oauth token from the config file. func (ts *TokenSource) readTokenFromConfig() (*oauth2.Token, error) { cfg, err := CurrentUser() if errors.Is(err, os.ErrNotExist) { return nil, ErrNotLoggedIn } else if err != nil { return nil, fmt.Errorf("could not get Encore auth token: %v", err) } return &cfg.Token, nil } var DefaultTokenSource = NewTokenSource() // AuthClient is an *http.Client that authenticates requests // using the logged-in user. var AuthClient = oauth2.NewClient(nil, DefaultTokenSource) // DefaultClient is an *http.Client that authenticates requests if the user is logged in. // If the user is not logged in, the request is sent without authentication. var DefaultClient = &http.Client{Transport: defaultTransport{}} type defaultTransport struct{} var authTransport = oauth2.Transport{Base: http.DefaultTransport, Source: DefaultTokenSource} func (defaultTransport) RoundTrip(req *http.Request) (*http.Response, error) { if _, err := DefaultTokenSource.Token(); err != nil { return http.DefaultTransport.RoundTrip(req) } return authTransport.RoundTrip(req) } ================================================ FILE: internal/env/env.go ================================================ // Package env answers where Encore tools and resources are located. package env import ( "os" "path/filepath" "github.com/rs/zerolog/log" "encr.dev/pkg/option" ) // These can be overwritten using // `go build -ldflags "-X encr.dev/cli/internal/env.alternativeEncoreRuntimesPath=$HOME/src/github.com/encoredev/encore/runtimes"`. var ( alternativeEncoreRuntimesPath = "" alternativeEncoreGoPath = "" ) // EncoreRuntimesPath reports the path to the Encore runtime. // It can be overridden by setting ENCORE_RUNTIMES_PATH. func EncoreRuntimesPath() string { p := encoreRuntimesPath() if p == "" { log.Fatal().Msg("could not determine Encore install root. " + "You can specify the path to the Encore runtimes manually by setting the ENCORE_RUNTIMES_PATH environment variable.") } return p } // EncoreGoRoot reports the path to the Encore Go root. // It can be overridden by setting ENCORE_GOROOT. func EncoreGoRoot() string { p := encoreGoRoot() if p == "" { log.Fatal().Msg("could not determine Encore install root. " + "You can specify the path to the Encore GOROOT manually by setting the ENCORE_GOROOT environment variable.") } return p } // EncoreBin reports the path to the directory containing the Encore installation's binaries. func EncoreBin() option.Option[string] { if root, ok := determineRoot(); ok { return option.Some(filepath.Join(root, "bin")) } return option.None[string]() } // OptEncoreGoRoot reports the path to the Encore Go root. // It can be overridden by setting ENCORE_GOROOT. // If the goroot can't be found, it reports None. func OptEncoreGoRoot() option.Option[string] { return option.AsOptional(encoreGoRoot()) } func encoreRuntimesPath() string { if p := os.Getenv("ENCORE_RUNTIMES_PATH"); p != "" { return p } else if //goland:noinspection GoBoolExpressions alternativeEncoreRuntimesPath != "" { return alternativeEncoreRuntimesPath } else if root, ok := determineRoot(); ok { return filepath.Join(root, "runtimes") } return "" } // EncoreRuntimeLib reports the path to the Encore runtime library for // node.js. It can be overridden by setting ENCORE_RUNTIME_LIB. func EncoreRuntimeLib() string { if p := os.Getenv("ENCORE_RUNTIME_LIB"); p != "" { return p } else if rt := encoreRuntimesPath(); rt != "" { return filepath.Join(rt, "js", "encore-runtime.node") } return "" } // EncoreDaemonLogPath reports the path to the Encore daemon log file. // It can be overridden by setting ENCORE_DAEMON_LOG_PATH. func EncoreDaemonLogPath() string { if p := os.Getenv("ENCORE_DAEMON_LOG_PATH"); p != "" { return p } cache, err := os.UserCacheDir() if err != nil { log.Fatal().Err(err).Msg("unable to determine user cache directory") } return filepath.Join(cache, "encore", "daemon.log") } // EncoreDevDashListenAddr reports the listen address for // where the daemon exposes the dev dash. // It can be overridden by setting ENCORE_DEVDASH_LISTEN_ADDR. func EncoreDevDashListenAddr() option.Option[string] { if p := os.Getenv("ENCORE_DEVDASH_LISTEN_ADDR"); p != "" { return option.Some(p) } return option.None[string]() } // EncoreMCPSSEListenAddr reports the listen address for // where the daemon exposes the MCP SSE endpoint. // It can be overridden by setting ENCORE_MCPSSE_LISTEN_ADDR. func EncoreMCPSSEListenAddr() option.Option[string] { if p := os.Getenv("ENCORE_MCPSSE_LISTEN_ADDR"); p != "" { return option.Some(p) } return option.None[string]() } func EncoreObjectStorageListAddr() option.Option[string] { if p := os.Getenv("ENCORE_OBJECTSTORAGE_LISTEN_ADDR"); p != "" { return option.Some(p) } return option.None[string]() } func encoreGoRoot() string { if p := os.Getenv("ENCORE_GOROOT"); p != "" { return p } else if //goland:noinspection GoBoolExpressions alternativeEncoreGoPath != "" { return alternativeEncoreGoPath } else if root, ok := determineRoot(); ok { return filepath.Join(root, "encore-go") } return "" } // List reports Encore environment variables, in the same format as os.Environ(). func List() []string { return []string{ "ENCORE_GOROOT=" + encoreGoRoot(), "ENCORE_RUNTIMES_PATH=" + encoreRuntimesPath(), "ENCORE_RUNTIME_LIB=" + EncoreRuntimeLib(), "ENCORE_DAEMON_LOG_PATH=" + EncoreDaemonLogPath(), } } // determineRoot determines encore root by checking the location relative // to the executable, to enable relocatable installs. func determineRoot() (root string, ok bool) { exe, err := os.Executable() if err == nil { // Homebrew uses a lot of symlinks, so we need to get back to the actual location // to be able to use the heuristic below. if sym, err := filepath.EvalSymlinks(exe); err == nil { exe = sym } root := filepath.Dir(filepath.Dir(exe)) // Heuristic: check if "encore-go" and "runtime" dirs exist in this location. _, err1 := os.Stat(filepath.Join(root, "encore-go")) _, err2 := os.Stat(filepath.Join(root, "runtimes", "go")) if err1 == nil && err2 == nil { return root, true } } return "", false } // IsSSH reports whether the current session is an SSH session. func IsSSH() bool { if os.Getenv("SSH_TTY") != "" || os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" { return true } return false } ================================================ FILE: internal/etrace/etrace.go ================================================ package etrace import ( "context" "sync/atomic" ) func Sync0(ctx context.Context, cat, name string, fn func(context.Context)) { defer doSync(ctx, cat, name)() fn(ctx) } func Sync1[A any](ctx context.Context, cat, name string, fn func(context.Context) A) A { defer doSync(ctx, cat, name)() return fn(ctx) } func Sync2[A, B any](ctx context.Context, cat, name string, fn func(context.Context) (A, B)) (A, B) { defer doSync(ctx, cat, name)() return fn(ctx) } func Async0(ctx context.Context, cat, name string, fn func(context.Context)) { defer doAsync(ctx, cat, name)() fn(ctx) } func Async1[A any](ctx context.Context, cat, name string, fn func(context.Context) A) A { defer doAsync(ctx, cat, name)() return fn(ctx) } func Async2[A, B any](ctx context.Context, cat, name string, fn func(context.Context) (A, B)) (A, B) { defer doAsync(ctx, cat, name)() return fn(ctx) } func doSync(ctx context.Context, cat, name string) func() { gid := goroutineID() tr := fromCtx(ctx) tr.Emit(beginSync, name, cat, nil, gid, 0) return func() { tr.Emit(endSync, name, cat, nil, gid, 0) } } var asyncID int64 func doAsync(ctx context.Context, cat, name string) func() { id := atomic.AddInt64(&asyncID, 1) gid := goroutineID() tr := fromCtx(ctx) tr.Emit(beginAsync, name, cat, nil, gid, id) return func() { tr.Emit(endAsync, name, cat, nil, gid, id) } } ================================================ FILE: internal/etrace/gid.go ================================================ package etrace import ( "bytes" "fmt" "runtime" "strconv" ) // The below code snippet is copied from go4.org/syncutil/syncdebug. // // Copyright 2013 The Perkeep Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const stackBufSize = 16 << 20 var stackBuf = make(chan []byte, 8) func getBuf() []byte { select { case b := <-stackBuf: return b[:stackBufSize] default: return make([]byte, stackBufSize) } } func putBuf(b []byte) { select { case stackBuf <- b: default: } } var goroutineSpace = []byte("goroutine ") func goroutineID() int64 { b := getBuf() defer putBuf(b) b = b[:runtime.Stack(b, false)] // Parse the 4707 out of "goroutine 4707 [" b = bytes.TrimPrefix(b, goroutineSpace) i := bytes.IndexByte(b, ' ') if i < 0 { panic(fmt.Sprintf("No space found in %q", b)) } b = b[:i] n, err := strconv.ParseUint(string(b), 10, 64) if err != nil { panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err)) } return int64(n) } ================================================ FILE: internal/etrace/protocol.go ================================================ package etrace import ( "bufio" "context" "encoding/json" "io" "os" "sync" "time" "github.com/cockroachdb/errors" ) // See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview // for documentation on the Perfetto JSON trace format. func WithTracer(ctx context.Context, w io.Writer) (context.Context, *Tracer) { tr := &Tracer{ start: time.Now(), bw: bufio.NewWriter(w), } tr.bw.WriteString("[") ctx = context.WithValue(ctx, tracerKey, tr) return ctx, tr } func WithFileTracer(ctx context.Context, dst string) (context.Context, *Tracer, error) { f, err := os.Create(dst) if err != nil { return nil, nil, errors.Wrap(err, "create file tracer") } tr := &Tracer{ start: time.Now(), bw: bufio.NewWriter(f), closer: f, } tr.bw.WriteString("[") ctx = context.WithValue(ctx, tracerKey, tr) return ctx, tr, nil } func fromCtx(ctx context.Context) *Tracer { tr, _ := ctx.Value(tracerKey).(*Tracer) return tr } type key string const tracerKey key = "etrace.tracer" type Tracer struct { start time.Time closer io.Closer mu sync.Mutex bw *bufio.Writer } func (tr *Tracer) Close() error { if tr == nil { return nil } tr.mu.Lock() defer tr.mu.Unlock() err := tr.bw.Flush() // Close the file, if any. if tr.closer != nil { if err2 := tr.closer.Close(); err == nil { err = err2 } } return err } func (tr *Tracer) Emit(typ eventType, name, category string, args map[string]any, gid int64, asyncID int64) { if tr == nil { return } ts := time.Since(tr.start).Microseconds() data, err := json.Marshal(event{ Type: typ, Name: name, Category: category, Args: args, Timestamp: ts, ProcessID: 1, // TODO ThreadID: gid, AsyncID: asyncID, }) if err != nil { panic(err) } tr.mu.Lock() defer tr.mu.Unlock() tr.bw.Write(data) tr.bw.WriteByte(',') } type eventType string const ( beginSync eventType = "B" endSync eventType = "E" beginAsync eventType = "b" endAsync eventType = "e" ) type event struct { // Name is the user-specified name of the traced operation. Name string `json:"name"` // Category is the user-specified category of the traced operation. // There should be a small number of categories. Category string `json:"cat"` // Type is the type of event. Type eventType `json:"ph"` // Timestamp is when the event occurred. Timestamp int64 `json:"ts"` // ProcessID is the id of the process generating the event. ProcessID uint64 `json:"pid"` // ThreadID is the id of the thread generating the event. ThreadID int64 `json:"tid"` // Args is the set of arguments for the event. Args map[string]any `json:"args,omitempty"` // AsyncID is the ID for asynchronous events. AsyncID int64 `json:"id,omitempty"` } ================================================ FILE: internal/gocodegen/helpers.go ================================================ package gocodegen import ( "fmt" "strings" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" . "github.com/dave/jennifer/jen" ) // ConvertSchemaTypeToString converts a schema.Type to a string that can be used in a log output // which can be increbily useful for debugging if the parser as generated the expected Schema protobuf // from the original Go code func ConvertSchemaTypeToString(typ *schema.Type, md *meta.Data) string { // We wrap the type before rendering in "var _ {type}" so Jen correctly formats, then we strip the "var _" part. return fmt.Sprintf("%#v", Var().Id("_").Add(ConvertSchemaToJenType(typ, md)))[6:] } // ConvertSchemaToJenType converts a schema.Type to a Jen statement which represents the type func ConvertSchemaToJenType(typ *schema.Type, md *meta.Data) *Statement { switch typ := typ.Typ.(type) { case *schema.Type_Named: id := Id(md.Decls[typ.Named.Id].Name) if len(typ.Named.TypeArguments) > 0 { typeParams := make([]Code, len(typ.Named.TypeArguments)) for i, arg := range typ.Named.TypeArguments { typeParams[i] = ConvertSchemaToJenType(arg, md) } id.Types(typeParams...) } else if len(md.Decls[typ.Named.Id].TypeParams) > 0 { typeParams := make([]Code, len(md.Decls[typ.Named.Id].TypeParams)) for i, params := range md.Decls[typ.Named.Id].TypeParams { typeParams[i] = Id(fmt.Sprintf("_Unknown_Type_%s_", params.Name)) } id.Types(typeParams...) } return id case *schema.Type_Struct: fields := make([]Code, len(typ.Struct.Fields)) for i, field := range typ.Struct.Fields { f := Id(field.Name).Add(ConvertSchemaToJenType(field.Typ, md)) tags := make(map[string]string) for _, tag := range field.Tags { tags[tag.Key] = tag.Name if len(tag.Options) > 0 { tags[tag.Key] += fmt.Sprintf(",%s", strings.Join(tag.Options, ",")) } } if len(tags) > 0 { f = f.Tag(tags) } if doc := strings.TrimSpace(field.Doc); doc != "" { f = f.Comment(doc) } fields[i] = f } return Struct(fields...) case *schema.Type_Map: key := ConvertSchemaToJenType(typ.Map.Key, md) value := ConvertSchemaToJenType(typ.Map.Value, md) return Map(key).Add(value) case *schema.Type_List: value := ConvertSchemaToJenType(typ.List.Elem, md) return Index().Add(value) case *schema.Type_Builtin: return ConvertBuiltInSchemaToJenType(typ.Builtin) case *schema.Type_Pointer: return Op("*").Add(ConvertSchemaToJenType(typ.Pointer.Base, md)) case *schema.Type_Option: return Qual("encore.dev/types/option", "Option").Types(ConvertSchemaToJenType(typ.Option.Value, md)) case *schema.Type_TypeParameter: return Id(md.Decls[typ.TypeParameter.DeclId].TypeParams[typ.TypeParameter.ParamIdx].Name) case *schema.Type_Config: if typ.Config.IsValuesList { return Qual("encore.dev/config", "Values").Types(ConvertSchemaToJenType(typ.Config.Elem, md)) } else { return Qual("encore.dev/config", "Value").Types(ConvertSchemaToJenType(typ.Config.Elem, md)) } case *schema.Type_Literal, *schema.Type_Union: // Not yet supported. panic(fmt.Sprintf("ConvertSchemaToJenType doesn't support type: %T", typ)) default: panic(fmt.Sprintf("ConvertSchemaToJenType doesn't support type: %T", typ)) } } func ConvertBuiltInSchemaToJenType(builtin schema.Builtin) *Statement { switch builtin { case schema.Builtin_ANY: return Any() case schema.Builtin_BOOL: return Bool() case schema.Builtin_INT8: return Int8() case schema.Builtin_INT16: return Int16() case schema.Builtin_INT32: return Int32() case schema.Builtin_INT64: return Int64() case schema.Builtin_UINT8: return Uint8() case schema.Builtin_UINT16: return Uint16() case schema.Builtin_UINT32: return Uint32() case schema.Builtin_UINT64: return Uint64() case schema.Builtin_FLOAT32: return Float32() case schema.Builtin_FLOAT64: return Float64() case schema.Builtin_STRING: return String() case schema.Builtin_BYTES: return Index().Byte() case schema.Builtin_TIME: return Qual("time", "Time") case schema.Builtin_UUID: return Qual("encore.dev/types/uuid", "UUID") case schema.Builtin_JSON: return Qual("encoding/json", "RawMessage") case schema.Builtin_USER_ID: return Qual("encore.dev/beta/auth", "UID") case schema.Builtin_INT: return Int() case schema.Builtin_UINT: return Uint() default: panic(fmt.Sprintf("ConvertBuiltInSchemaToJenType doesn't support builtin: %v", builtin)) } } ================================================ FILE: internal/gocodegen/marshalling.go ================================================ package gocodegen import ( "strings" "github.com/cockroachdb/errors" . "github.com/dave/jennifer/jen" schema "encr.dev/proto/encore/parser/schema/v1" ) const UnknownPkgPath = "__unknown_path__" const ( lastErrorField = "LastError" nonEmptyValuesField = "NonEmptyValues" ) // MarshallingCodeGenerator is used to generate a structure has methods for decoding various types, collecting the errors. // It will only generate methods required for the given types. type MarshallingCodeGenerator struct { // pkgPath is the package import path where the marshaller is defined. pkgPath string structName string used bool encoreTypesAsString bool // true if auth.UID and uuid.UUID should be treated as strings? builtins []methodDescription seenBuiltins map[methodKey]methodDescription usedBody bool usedJson bool } type methodKey struct { fromString bool builtin schema.Builtin slice bool option bool } type methodDescription struct { FromString bool Method string Input Code Result Code IsList bool IsOption bool Block []Code } // MarshallingCodeWrapper is returned by NewPossibleInstance and tracks usage within a block type MarshallingCodeWrapper struct { g *MarshallingCodeGenerator pkgPath string instanceName string used bool code []Code endBlock []Code } func NewMarshallingCodeGenerator(pkgPath, structName string, forClientGen bool) *MarshallingCodeGenerator { return &MarshallingCodeGenerator{ pkgPath: pkgPath, structName: structName, builtins: nil, seenBuiltins: make(map[methodKey]methodDescription), encoreTypesAsString: forClientGen, } } // NewPossibleInstance Creates a statement to initialise a new encoding instance. // // Use the returned wrapper to convert FromStrings to the target types, adding any code you // are generating to the wrapper using Add. Once you've finished generating all the code which // may need type conversion with that _instance_ of the deserializer, call Finalize to generate the code full code // including error handling. // // Once you've finished writing the whole app with all the code which uses this generator call WriteToFile to write // the supporting struct and methods to the given file func (g *MarshallingCodeGenerator) NewPossibleInstance(instanceName string) *MarshallingCodeWrapper { g.used = true return &MarshallingCodeWrapper{ g: g, instanceName: instanceName, } } // GenerateAll causes the generator to generate all possible methods. func (g *MarshallingCodeGenerator) GenerateAll() { for _, val := range schema.Builtin_value { b := schema.Builtin(val) for _, slice := range []bool{false, true} { for _, option := range []bool{false, true} { _, _ = g.builtinToString(b, slice, option) } } } g.usedBody = true g.usedJson = true } // WriteToFile writes the full encoder type into the given file. func (g *MarshallingCodeGenerator) WriteToFile(f *File) { if !g.used || (len(g.builtins) == 0 && !g.usedBody && !g.usedJson) { return } f.Commentf("%s is used to serialize request data into strings and deserialize response data from strings", g.structName) f.Type().Id(g.structName).Struct( Id(lastErrorField).Error().Comment("The last error that occurred"), Id(nonEmptyValuesField).Int().Comment("The number of values this decoder has decoded"), ) for _, desc := range g.builtins { var params []Code if desc.FromString { params = []Code{Id("field").String(), Id("s").Add(desc.Input), Id("required").Bool()} } else { params = []Code{Id("s").Add(desc.Input)} } f.Func().Params( Id("e").Op("*").Id(g.structName), ).Id(desc.Method).Params(params...).Params(Id("v").Add(desc.Result)).BlockFunc(func(g *Group) { if desc.FromString { // If we're dealing with a list of strings, we need to compare with len(s) == 0 instead of s == "" if desc.IsList { g.If(Op("!").Id("required").Op("&&").Len(Id("s")).Op("==").Lit(0)).Block(Return()) } else { g.If(Op("!").Id("required").Op("&&").Id("s").Op("==").Lit("")).Block(Return()) } } else if desc.IsOption { g.If(Id("s").Op("==").Nil()).Block(Return(Nil())) } g.Id("e").Dot(nonEmptyValuesField).Op("++") for _, s := range desc.Block { g.Add(s) } }) f.Line() } f.Comment("setErr sets the last error within the object if one is not already set") f.Func().Params(Id("e").Op("*").Id(g.structName)).Id("setErr").Params(List(Id("msg"), Id("field")).String(), Err().Error()).Block( If(Err().Op("!=").Nil().Op("&&").Id("e").Dot(lastErrorField).Op("==").Nil()).Block( Id("e").Dot(lastErrorField).Op("=").Qual("fmt", "Errorf").Call( Lit("%s: %s: %w"), Id("field"), Id("msg"), Id("err"), ), ), ) f.Line() if g.usedBody { f.Func().Params(Id("d").Op("*").Id(g.structName)).Id("Body").Params(Id("body").Qual("io", "Reader")).Params(Id("payload").Index().Byte()).Block( List(Id("payload"), Err()).Op(":=").Qual("io", "ReadAll").Call(Id("body")), If(Err().Op("==").Nil().Op("&&").Len(Id("payload")).Op("==").Lit(0)).Block( Id("d").Dot("setErr").Call(Lit("missing request body"), Lit("request_body"), Qual("fmt", "Errorf").Call(Lit("missing request body"))), ).Else().If(Err().Op("!=").Nil()).Block( Id("d").Dot("setErr").Call(Lit("could not parse request body"), Lit("request_body"), Err()), ), Return(Id("payload")), ) } if g.usedJson { f.Func().Params(Id("d").Op("*").Id(g.structName)).Id("ParseJSON").Params(Id("field").String(), Id("iter").Op("*").Qual("github.com/json-iterator/go", "Iterator"), Id("dst").Interface()).Block( Id("iter").Dot("ReadVal").Call(Id("dst")), Id("d").Dot("setErr").Call(Lit("invalid json parameter"), Id("field"), Id("iter").Dot("Error")), ) } f.Line() } func (b *MarshallingCodeGenerator) builtinFromString(t schema.Builtin, slice, option bool) (string, error) { key := methodKey{builtin: t, slice: slice, option: option, fromString: true} if n, ok := b.seenBuiltins[key]; ok { return n.Method, nil } else if slice { k2 := methodKey{builtin: t, fromString: true, slice: false, option: option} if _, err := b.builtinFromString(t, false, option); err != nil { return "", err } desc := b.seenBuiltins[k2] name := desc.Method + "List" fn := methodDescription{ FromString: true, Method: name, Input: Index().String(), Result: Index().Add(desc.Result), IsList: true, Block: []Code{ For(List(Id("_"), Id("x")).Op(":=").Range().Id("s")).Block( Id("v").Op("=").Append(Id("v"), Id("e").Dot(desc.Method).Call(Id("field"), Id("x"), Id("required"))), ), Return(Id("v")), }, } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } else if option { k2 := methodKey{builtin: t, fromString: true, slice: false, option: false} if _, err := b.builtinFromString(t, false, false); err != nil { return "", err } desc := b.seenBuiltins[k2] name := desc.Method + "Option" fn := methodDescription{ FromString: true, Method: name, Input: String(), Result: Op("*").Add(desc.Result), IsList: false, IsOption: true, Block: []Code{ Id("val").Op(":=").Id("e").Dot(desc.Method).Call(Id("field"), Id("s"), Id("required")), Return(Op("&").Id("val")), }, } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } var fn methodDescription switch t { case schema.Builtin_STRING: fn = methodDescription{true, "ToString", String(), String(), false, false, []Code{Return(Id("s"))}} case schema.Builtin_BYTES: fn = methodDescription{true, "ToBytes", String(), Index().Byte(), false, false, []Code{ List(Id("v"), Err()).Op(":=").Qual("encoding/base64", "URLEncoding").Dot("DecodeString").Call(Id("s")), Id("e").Dot("setErr").Call(Lit("invalid parameter"), Id("field"), Err()), Return(Id("v")), }} case schema.Builtin_BOOL: fn = methodDescription{true, "ToBool", String(), Bool(), false, false, []Code{ List(Id("v"), Err()).Op(":=").Qual("strconv", "ParseBool").Call(Id("s")), Id("e").Dot("setErr").Call(Lit("invalid parameter"), Id("field"), Err()), Return(Id("v")), }} case schema.Builtin_UUID: fn = methodDescription{true, "ToUUID", String(), Qual("encore.dev/types/uuid", "UUID"), false, false, []Code{ List(Id("v"), Err()).Op(":=").Qual("encore.dev/types/uuid", "FromString").Call(Id("s")), Id("e").Dot("setErr").Call(Lit("invalid parameter"), Id("field"), Err()), Return(Id("v")), }} case schema.Builtin_TIME: fn = methodDescription{true, "ToTime", String(), Qual("time", "Time"), false, false, []Code{ List(Id("v"), Err()).Op(":=").Qual("time", "Parse").Call(Qual("time", "RFC3339"), Id("s")), Id("e").Dot("setErr").Call(Lit("invalid parameter"), Id("field"), Err()), Return(Id("v")), }} case schema.Builtin_USER_ID: fn = methodDescription{true, "ToUserID", String(), Qual("encore.dev/beta/auth", "UID"), false, false, []Code{ Return(Qual("encore.dev/beta/auth", "UID").Call(Id("s"))), }} case schema.Builtin_JSON: fn = methodDescription{true, "ToJSON", String(), Qual("encoding/json", "RawMessage"), false, false, []Code{ Return(Qual("encoding/json", "RawMessage").Call(Id("s"))), }} default: type kind int const ( unsigned kind = iota + 1 signed float ) numTypes := map[schema.Builtin]struct { typ string kind kind bits int }{ schema.Builtin_INT8: {"int8", signed, 8}, schema.Builtin_INT16: {"int16", signed, 16}, schema.Builtin_INT32: {"int32", signed, 32}, schema.Builtin_INT64: {"int64", signed, 64}, schema.Builtin_INT: {"int", signed, 64}, schema.Builtin_UINT8: {"uint8", unsigned, 8}, schema.Builtin_UINT16: {"uint16", unsigned, 16}, schema.Builtin_UINT32: {"uint32", unsigned, 32}, schema.Builtin_UINT64: {"uint64", unsigned, 64}, schema.Builtin_UINT: {"uint", unsigned, 64}, schema.Builtin_FLOAT64: {"float64", float, 64}, schema.Builtin_FLOAT32: {"float32", float, 32}, } def, ok := numTypes[t] if !ok { return "", errors.Newf("unsupported type: %s", t) } cast := def.typ != "int64" && def.typ != "uint64" && def.typ != "float64" var err error fn = methodDescription{true, "To" + strings.Title(def.typ), String(), Id(def.typ), false, false, []Code{ List(Id("x"), Err()).Op(":=").Do(func(s *Statement) { switch def.kind { case unsigned: s.Qual("strconv", "ParseUint").Call(Id("s"), Lit(10), Lit(def.bits)) case signed: s.Qual("strconv", "ParseInt").Call(Id("s"), Lit(10), Lit(def.bits)) case float: s.Qual("strconv", "ParseFloat").Call(Id("s"), Lit(def.bits)) default: err = errors.Newf("unknown kind %v", def.kind) } }), Id("e").Dot("setErr").Call(Lit("invalid parameter"), Id("field"), Err()), ReturnFunc(func(g *Group) { if cast { g.Id(def.typ).Call(Id("x")) } else { g.Id("x") } }), }} if err != nil { return "", err } } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } func (b *MarshallingCodeGenerator) builtinToString(t schema.Builtin, slice, option bool) (string, error) { key := methodKey{builtin: t, slice: slice, option: option, fromString: false} if fn, ok := b.seenBuiltins[key]; ok { return fn.Method, nil } if slice { k2 := methodKey{builtin: t, fromString: false, slice: false, option: option} if _, err := b.builtinToString(t, false, option); err != nil { return "", err } desc := b.seenBuiltins[k2] name := desc.Method + "List" fn := methodDescription{ FromString: false, Method: name, Input: Index().Add(desc.Input), Result: Index().String(), IsList: true, Block: []Code{ For(List(Id("_"), Id("x")).Op(":=").Range().Id("s")).Block( Id("v").Op("=").Append(Id("v"), Id("e").Dot(desc.Method).Call(Id("x"))), ), Return(Id("v")), }, } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } else if option { k2 := methodKey{builtin: t, fromString: false, slice: false, option: false} if _, err := b.builtinToString(t, false, false); err != nil { return "", err } desc := b.seenBuiltins[k2] name := desc.Method + "Option" fn := methodDescription{ FromString: false, Method: name, Input: Op("*").Add(desc.Input), Result: Index().String(), IsOption: true, Block: []Code{ Return(Index().String().Values(Id("e").Dot(desc.Method).Call(Op("*").Id("s")))), }, } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } var fn methodDescription switch t { case schema.Builtin_STRING: fn = methodDescription{false, "FromString", String(), String(), false, false, []Code{Return(Id("s"))}} case schema.Builtin_BYTES: fn = methodDescription{false, "FromBytes", Index().Byte(), String(), false, false, []Code{ Return(Qual("encoding/base64", "URLEncoding").Dot("EncodeToString").Call(Id("s"))), }} case schema.Builtin_BOOL: fn = methodDescription{false, "FromBool", Bool(), String(), false, false, []Code{ Return(Qual("strconv", "FormatBool").Call(Id("s"))), }} case schema.Builtin_UUID: fn = methodDescription{false, "FromUUID", Qual("encore.dev/types/uuid", "UUID"), String(), false, false, []Code{ Return(Id("s").Dot("String").Call()), }} case schema.Builtin_TIME: fn = methodDescription{false, "FromTime", Qual("time", "Time"), String(), false, false, []Code{ Return(Id("s").Dot("Format").Call(Qual("time", "RFC3339"))), }} case schema.Builtin_USER_ID: fn = methodDescription{false, "FromUserID", Qual("encore.dev/beta/auth", "UID"), String(), false, false, []Code{ Return(String().Call(Id("s"))), }} case schema.Builtin_JSON: fn = methodDescription{false, "FromJSON", Qual("encoding/json", "RawMessage"), String(), false, false, []Code{ Return(String().Call(Id("s"))), }} default: type kind int const ( unsigned kind = iota + 1 signed float ) numTypes := map[schema.Builtin]struct { typ string castTyp string kind kind bits int }{ schema.Builtin_INT8: {"int8", "int64", signed, 8}, schema.Builtin_INT16: {"int16", "int64", signed, 16}, schema.Builtin_INT32: {"int32", "int64", signed, 32}, schema.Builtin_INT64: {"int64", "int64", signed, 64}, schema.Builtin_INT: {"int", "int64", signed, 64}, schema.Builtin_UINT8: {"uint8", "uint64", unsigned, 8}, schema.Builtin_UINT16: {"uint16", "uint64", unsigned, 16}, schema.Builtin_UINT32: {"uint32", "uint64", unsigned, 32}, schema.Builtin_UINT64: {"uint64", "uint64", unsigned, 64}, schema.Builtin_UINT: {"uint", "uint64", unsigned, 64}, schema.Builtin_FLOAT64: {"float64", "float64", float, 64}, schema.Builtin_FLOAT32: {"float32", "float64", float, 32}, } def, ok := numTypes[t] if !ok { return "", errors.Newf("unsupported type: %s", t) } var err error fn = methodDescription{false, "From" + strings.Title(def.typ), Id(def.typ), String(), false, false, []Code{ Return(Do(func(s *Statement) { id := Id("s") if def.typ != def.castTyp { id = Id(def.castTyp).Call(id) } switch def.kind { case unsigned: s.Qual("strconv", "FormatUint").Call(id, Lit(10)) case signed: s.Qual("strconv", "FormatInt").Call(id, Lit(10)) case float: s.Qual("strconv", "FormatFloat").Call(id, Lit(byte('f')), Lit(-1), Lit(def.bits)) default: err = errors.Newf("unknown kind %v", def.kind) } })), }} if err != nil { return "", err } } b.seenBuiltins[key] = fn b.builtins = append(b.builtins, fn) return fn.Method, nil } func (w *MarshallingCodeWrapper) WithFunc(body func(*Group), errBlock func(*Group)) []Code { o := Options{Separator: "\n", Multi: true} bodyStatement := CustomFunc(o, body) errStatement := CustomFunc(o, errBlock) w.Add(bodyStatement) return w.Finalize(errStatement) } func (w *MarshallingCodeWrapper) LastError() Code { return Id(w.instanceName).Dot(lastErrorField) } // Add adds code into the wrapped block func (w *MarshallingCodeWrapper) Add(c ...Code) { w.code = append(w.code, c...) } // EndBlock adds custom logic after the error block func (w *MarshallingCodeWrapper) EndBlock(endBlock ...Code) { w.endBlock = endBlock } // Finalize returns the final code block including all wrapped code func (w *MarshallingCodeWrapper) Finalize(ifErrorBlock ...Code) []Code { if !w.used { return w.code } // If we know the package path, refer to the decoder with a qualified name. var structRef *Statement if w.g.pkgPath != UnknownPkgPath { structRef = Qual(w.g.pkgPath, w.g.structName) } else { structRef = Id(w.g.structName) } code := []Code{Id(w.instanceName).Op(":=").Op("&").Add(structRef).Values(), Line()} code = append(code, w.code...) code = append(code, Line().If(Id(w.instanceName).Dot(lastErrorField).Op("!=").Nil()).Block(ifErrorBlock...)) code = append(code, Line()) code = append(code, w.endBlock...) return code } func (g *MarshallingCodeGenerator) shouldBeTreatedAsString(builtin schema.Builtin) bool { return builtin == schema.Builtin_STRING || builtin == schema.Builtin_DECIMAL || (g.encoreTypesAsString && builtin == schema.Builtin_UUID) || (g.encoreTypesAsString && builtin == schema.Builtin_USER_ID) } func (w *MarshallingCodeWrapper) Body(getBody Code) Code { w.used = true w.g.usedBody = true return Id(w.instanceName).Dot("Body").Call(getBody) } // FromStringToBuiltin will return either the original string or a call to the encoder func (w *MarshallingCodeWrapper) FromStringToBuiltin(builtin schema.Builtin, fieldName string, getAsString Code, required bool) (code Code, err error) { // get the method name for the target type funcName := "" srcCode := getAsString // If the list is strings, we can just return the value if builtin == schema.Builtin_STRING && !required { return getAsString, nil } funcName, err = w.g.builtinFromString(builtin, false, false) if err != nil { return nil, err } // mark this code wrapper as actually using the deserializer type w.used = true return Id(w.instanceName).Dot(funcName).Call(Lit(fieldName), srcCode, Lit(required)), nil } func (w *MarshallingCodeWrapper) FromJSON(targetType *schema.Type, fieldName string, iterName string, dst Code) (code Code, err error) { // TODO: Call readers for specific types once we've added Pointer Type support w.used = true w.g.usedJson = true return Id(w.instanceName).Dot("ParseJSON").Call(Lit(fieldName), Id(iterName), Op("&").Add(dst)), nil } // FromString will return a call to a decoder method func (w *MarshallingCodeWrapper) FromString(targetType *schema.Type, fieldName string, getAsString Code, getAsStringSlice Code, required bool) (code Code, err error) { // get the method name for the target type funcName := "" srcCode := getAsString switch t := targetType.Typ.(type) { case *schema.Type_List: if bt, ok := t.List.Elem.Typ.(*schema.Type_Builtin); ok { // If the list is uuids or userids, treat it as string builtin := bt.Builtin if w.g.shouldBeTreatedAsString(bt.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinFromString(builtin, true, false) srcCode = getAsStringSlice if err != nil { return nil, err } } else { return nil, errors.Newf("unsupported list type %T", t.List.Elem.Typ) } case *schema.Type_Option: if bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok { // If the list is uuids or userids, treat it as string builtin := bt.Builtin if w.g.shouldBeTreatedAsString(bt.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinFromString(builtin, false, true) if err != nil { return nil, err } // Options are not required. required = false } else { return nil, errors.Newf("unsupported option type %T", t.Option.Value.Typ) } case *schema.Type_Builtin: // If it's uuid, userid then treat it as string builtin := t.Builtin if w.g.shouldBeTreatedAsString(t.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinFromString(builtin, false, false) if err != nil { return nil, err } default: return nil, errors.Newf("unsupported type for deserialization: %T", t) } // mark this code wrapper as actually using the deserializer type w.used = true return Id(w.instanceName).Dot(funcName).Call(Lit(fieldName), srcCode, Lit(required)), nil } // ToStringSlice will return either the original string or a call to the encoder func (w *MarshallingCodeWrapper) ToStringSlice(sourceType *schema.Type, sourceValue Code) (code Code, err error) { // get the method name for the target type funcName := "" switch t := sourceType.Typ.(type) { case *schema.Type_List: if bt, ok := t.List.Elem.Typ.(*schema.Type_Builtin); ok { builtin := bt.Builtin if w.g.shouldBeTreatedAsString(bt.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinToString(builtin, true, false) if err != nil { return nil, err } w.used = true return Id(w.instanceName).Dot(funcName).Call(sourceValue), nil } else { return nil, errors.Newf("unsupported list type %T", t.List.Elem.Typ) } case *schema.Type_Option: if bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok { builtin := bt.Builtin if w.g.shouldBeTreatedAsString(bt.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinToString(builtin, false, true) if err != nil { return nil, err } w.used = true return Id(w.instanceName).Dot(funcName).Call(sourceValue), nil } else { return nil, errors.Newf("unsupported option type %T", t.Option.Value.Typ) } case *schema.Type_Builtin: builtin := t.Builtin if w.g.shouldBeTreatedAsString(t.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinToString(builtin, false, false) if err != nil { return nil, err } w.used = true return Values(Id(w.instanceName).Dot(funcName).Call(sourceValue)), nil default: return nil, errors.Newf("unsupported type for serialization: %T", t) } } // ToString will return either the original string or a call to the encoder func (w *MarshallingCodeWrapper) ToString(sourceType *schema.Type, sourceValue Code) (code Code, err error) { // get the method name for the target type funcName := "" switch t := sourceType.Typ.(type) { case *schema.Type_Builtin: builtin := t.Builtin if w.g.shouldBeTreatedAsString(t.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinToString(builtin, false, false) if err != nil { return nil, err } w.used = true return Id(w.instanceName).Dot(funcName).Call(sourceValue), nil case *schema.Type_Option: if bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok { builtin := bt.Builtin if w.g.shouldBeTreatedAsString(bt.Builtin) { builtin = schema.Builtin_STRING } funcName, err = w.g.builtinToString(builtin, false, true) if err != nil { return nil, err } w.used = true return Id(w.instanceName).Dot(funcName).Call(sourceValue), nil } else { return nil, errors.Newf("unsupported option type %T", t.Option.Value.Typ) } default: return nil, errors.Newf("unsupported type for serialization: %T", t) } } ================================================ FILE: internal/gocodegen/package.go ================================================ // Package gocodegen contains shared code used for generating Go code by both the // compilers code generator, and the CLI's client generator. package gocodegen ================================================ FILE: internal/goldfish/goldfish.go ================================================ // Package goldfish provides a short-term cache of values. package goldfish import ( "sync" "time" ) type Cache[V any] struct { keepalive time.Duration fn func() (V, error) mu sync.Mutex last time.Time val V } func New[V any](keepalive time.Duration, fn func() (V, error)) *Cache[V] { return &Cache[V]{ keepalive: keepalive, fn: fn, } } func (c *Cache[V]) Get() (V, error) { now := time.Now() c.mu.Lock() defer c.mu.Unlock() if now.Sub(c.last) < c.keepalive { return c.val, nil } // Cache is out of date, re-fetch val, err := c.fn() if err == nil { c.val, c.last = val, now } return c.val, err } func (c *Cache[V]) Set(val V) { c.mu.Lock() defer c.mu.Unlock() c.val = val c.last = time.Now() } ================================================ FILE: internal/httpcache/LICENSE.txt ================================================ Copyright © 2012 Greg Jones (greg.jones@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: internal/httpcache/README.md ================================================ httpcache ========= [![Build Status](https://travis-ci.org/gregjones/httpcache.svg?branch=master)](https://travis-ci.org/gregjones/httpcache) [![GoDoc](https://godoc.org/github.com/gregjones/httpcache?status.svg)](https://godoc.org/github.com/gregjones/httpcache) Package httpcache provides a http.RoundTripper implementation that works as a mostly [RFC 7234](https://tools.ietf.org/html/rfc7234) compliant cache for http responses. It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client and not for a shared proxy). This project isn't actively maintained; it works for what I, and seemingly others, want to do with it, and I consider it "done". That said, if you find any issues, please open a Pull Request and I will try to review it. Any changes now that change the public API won't be considered. Cache Backends -------------- - The built-in 'memory' cache stores responses in an in-memory map. - [`github.com/gregjones/httpcache/diskcache`](https://github.com/gregjones/httpcache/tree/master/diskcache) provides a filesystem-backed cache using the [diskv](https://github.com/peterbourgon/diskv) library. - [`github.com/gregjones/httpcache/memcache`](https://github.com/gregjones/httpcache/tree/master/memcache) provides memcache implementations, for both App Engine and 'normal' memcache servers. - [`sourcegraph.com/sourcegraph/s3cache`](https://sourcegraph.com/github.com/sourcegraph/s3cache) uses Amazon S3 for storage. - [`github.com/gregjones/httpcache/leveldbcache`](https://github.com/gregjones/httpcache/tree/master/leveldbcache) provides a filesystem-backed cache using [leveldb](https://github.com/syndtr/goleveldb/tree/master/leveldb). - [`github.com/die-net/lrucache`](https://github.com/die-net/lrucache) provides an in-memory cache that will evict least-recently used entries. - [`github.com/die-net/lrucache/twotier`](https://github.com/die-net/lrucache/tree/master/twotier) allows caches to be combined, for example to use lrucache above with a persistent disk-cache. - [`github.com/birkelund/boltdbcache`](https://github.com/birkelund/boltdbcache) provides a BoltDB implementation (based on the [bbolt](https://github.com/coreos/bbolt) fork). If you implement any other backend and wish it to be linked here, please send a PR editing this file. License ------- - [MIT License](LICENSE.txt) ================================================ FILE: internal/httpcache/diskcache/diskcache.go ================================================ // Package diskcache provides an implementation of httpcache.Cache that uses the diskv package // to supplement an in-memory map with persistent storage // package diskcache import ( "bytes" "crypto/md5" "encoding/hex" "github.com/peterbourgon/diskv" "io" ) // Cache is an implementation of httpcache.Cache that supplements the in-memory map with persistent storage type Cache struct { d *diskv.Diskv } // Get returns the response corresponding to key if present func (c *Cache) Get(key string) (resp []byte, ok bool) { key = keyToFilename(key) resp, err := c.d.Read(key) if err != nil { return []byte{}, false } return resp, true } // Set saves a response to the cache as key func (c *Cache) Set(key string, resp []byte) { key = keyToFilename(key) c.d.WriteStream(key, bytes.NewReader(resp), true) } // Delete removes the response with key from the cache func (c *Cache) Delete(key string) { key = keyToFilename(key) c.d.Erase(key) } func keyToFilename(key string) string { h := md5.New() io.WriteString(h, key) return hex.EncodeToString(h.Sum(nil)) } // New returns a new Cache that will store files in basePath func New(basePath string) *Cache { return &Cache{ d: diskv.New(diskv.Options{ BasePath: basePath, CacheSizeMax: 100 * 1024 * 1024, // 100MB }), } } // NewWithDiskv returns a new Cache using the provided Diskv as underlying // storage. func NewWithDiskv(d *diskv.Diskv) *Cache { return &Cache{d} } ================================================ FILE: internal/httpcache/diskcache/diskcache_test.go ================================================ package diskcache import ( "io/ioutil" "os" "testing" "encr.dev/internal/httpcache/test" ) func TestDiskCache(t *testing.T) { tempDir, err := ioutil.TempDir("", "httpcache") if err != nil { t.Fatalf("TempDir: %v", err) } defer os.RemoveAll(tempDir) test.Cache(t, New(tempDir)) } ================================================ FILE: internal/httpcache/httpcache.go ================================================ // Package httpcache provides a http.RoundTripper implementation that works as a // mostly RFC-compliant cache for http responses. // // It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client // and not for a shared proxy). package httpcache import ( "bufio" "bytes" "errors" "io" "io/ioutil" "net/http" "net/http/httputil" "strings" "sync" "time" ) const ( stale = iota fresh transparent // XFromCache is the header added to responses that are returned from the cache XFromCache = "X-From-Cache" ) // A Cache interface is used by the Transport to store and retrieve responses. type Cache interface { // Get returns the []byte representation of a cached response and a bool // set to true if the value isn't empty Get(key string) (responseBytes []byte, ok bool) // Set stores the []byte representation of a response against a key Set(key string, responseBytes []byte) // Delete removes the value associated with the key Delete(key string) } // cacheKey returns the cache key for req. func cacheKey(req *http.Request) string { if req.Method == http.MethodGet { return req.URL.String() } else { return req.Method + " " + req.URL.String() } } // CachedResponse returns the cached http.Response for req if present, and nil // otherwise. func CachedResponse(c Cache, req *http.Request) (resp *http.Response, err error) { cachedVal, ok := c.Get(cacheKey(req)) if !ok { return } b := bytes.NewBuffer(cachedVal) return http.ReadResponse(bufio.NewReader(b), req) } // MemoryCache is an implemtation of Cache that stores responses in an in-memory map. type MemoryCache struct { mu sync.RWMutex items map[string][]byte } // Get returns the []byte representation of the response and true if present, false if not func (c *MemoryCache) Get(key string) (resp []byte, ok bool) { c.mu.RLock() resp, ok = c.items[key] c.mu.RUnlock() return resp, ok } // Set saves response resp to the cache with key func (c *MemoryCache) Set(key string, resp []byte) { c.mu.Lock() c.items[key] = resp c.mu.Unlock() } // Delete removes key from the cache func (c *MemoryCache) Delete(key string) { c.mu.Lock() delete(c.items, key) c.mu.Unlock() } // NewMemoryCache returns a new Cache that will store items in an in-memory map func NewMemoryCache() *MemoryCache { c := &MemoryCache{items: map[string][]byte{}} return c } // Transport is an implementation of http.RoundTripper that will return values from a cache // where possible (avoiding a network request) and will additionally add validators (etag/if-modified-since) // to repeated requests allowing servers to return 304 / Not Modified type Transport struct { // The RoundTripper interface actually used to make requests // If nil, http.DefaultTransport is used Transport http.RoundTripper Cache Cache // If true, responses returned from the cache will be given an extra header, X-From-Cache MarkCachedResponses bool } // NewTransport returns a new Transport with the // provided Cache implementation and MarkCachedResponses set to true func NewTransport(c Cache) *Transport { return &Transport{Cache: c, MarkCachedResponses: true} } // Client returns an *http.Client that caches responses. func (t *Transport) Client() *http.Client { return &http.Client{Transport: t} } // varyMatches will return false unless all of the cached values for the headers listed in Vary // match the new request func varyMatches(cachedResp *http.Response, req *http.Request) bool { for _, header := range headerAllCommaSepValues(cachedResp.Header, "vary") { header = http.CanonicalHeaderKey(header) if header != "" && req.Header.Get(header) != cachedResp.Header.Get("X-Varied-"+header) { return false } } return true } // RoundTrip takes a Request and returns a Response // // If there is a fresh Response already in cache, then it will be returned without connecting to // the server. // // If there is a stale Response, then any validators it contains will be set on the new request // to give the server a chance to respond with NotModified. If this happens, then the cached Response // will be returned. func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { cacheKey := cacheKey(req) cacheable := (req.Method == "GET" || req.Method == "HEAD") && req.Header.Get("range") == "" var cachedResp *http.Response if cacheable { cachedResp, err = CachedResponse(t.Cache, req) } else { // Need to invalidate an existing value t.Cache.Delete(cacheKey) } transport := t.Transport if transport == nil { transport = http.DefaultTransport } if cacheable && cachedResp != nil && err == nil { if t.MarkCachedResponses { cachedResp.Header.Set(XFromCache, "1") } if varyMatches(cachedResp, req) { // Can only use cached value if the new request doesn't Vary significantly freshness := getFreshness(cachedResp.Header, req.Header) if freshness == fresh { return cachedResp, nil } if freshness == stale { var req2 *http.Request // Add validators if caller hasn't already done so etag := cachedResp.Header.Get("etag") if etag != "" && req.Header.Get("etag") == "" { req2 = cloneRequest(req) req2.Header.Set("if-none-match", etag) } lastModified := cachedResp.Header.Get("last-modified") if lastModified != "" && req.Header.Get("last-modified") == "" { if req2 == nil { req2 = cloneRequest(req) } req2.Header.Set("if-modified-since", lastModified) } if req2 != nil { req = req2 } } } resp, err = transport.RoundTrip(req) if err == nil && req.Method == "GET" && resp.StatusCode == http.StatusNotModified { // Replace the 304 response with the one from cache, but update with some new headers endToEndHeaders := getEndToEndHeaders(resp.Header) for _, header := range endToEndHeaders { cachedResp.Header[header] = resp.Header[header] } resp = cachedResp } else if (err != nil || (cachedResp != nil && resp.StatusCode >= 500)) && req.Method == "GET" && canStaleOnError(cachedResp.Header, req.Header) { // In case of transport failure and stale-if-error activated, returns cached content // when available return cachedResp, nil } else { if err != nil || resp.StatusCode != http.StatusOK { t.Cache.Delete(cacheKey) } if err != nil { return nil, err } } } else { reqCacheControl := parseCacheControl(req.Header) if _, ok := reqCacheControl["only-if-cached"]; ok { resp = newGatewayTimeoutResponse(req) } else { resp, err = transport.RoundTrip(req) if err != nil { return nil, err } } } if cacheable && canStore(parseCacheControl(req.Header), parseCacheControl(resp.Header)) { for _, varyKey := range headerAllCommaSepValues(resp.Header, "vary") { varyKey = http.CanonicalHeaderKey(varyKey) fakeHeader := "X-Varied-" + varyKey reqValue := req.Header.Get(varyKey) if reqValue != "" { resp.Header.Set(fakeHeader, reqValue) } } switch req.Method { case "GET": // Delay caching until EOF is reached. resp.Body = &cachingReadCloser{ R: resp.Body, OnEOF: func(r io.Reader) { resp := *resp resp.Body = ioutil.NopCloser(r) respBytes, err := httputil.DumpResponse(&resp, true) if err == nil { t.Cache.Set(cacheKey, respBytes) } }, } default: respBytes, err := httputil.DumpResponse(resp, true) if err == nil { t.Cache.Set(cacheKey, respBytes) } } } else { t.Cache.Delete(cacheKey) } return resp, nil } // ErrNoDateHeader indicates that the HTTP headers contained no Date header. var ErrNoDateHeader = errors.New("no Date header") // Date parses and returns the value of the Date header. func Date(respHeaders http.Header) (date time.Time, err error) { dateHeader := respHeaders.Get("date") if dateHeader == "" { err = ErrNoDateHeader return } return time.Parse(time.RFC1123, dateHeader) } type realClock struct{} func (c *realClock) since(d time.Time) time.Duration { return time.Since(d) } type timer interface { since(d time.Time) time.Duration } var clock timer = &realClock{} // getFreshness will return one of fresh/stale/transparent based on the cache-control // values of the request and the response // // fresh indicates the response can be returned // stale indicates that the response needs validating before it is returned // transparent indicates the response should not be used to fulfil the request // // Because this is only a private cache, 'public' and 'private' in cache-control aren't // signficant. Similarly, smax-age isn't used. func getFreshness(respHeaders, reqHeaders http.Header) (freshness int) { respCacheControl := parseCacheControl(respHeaders) reqCacheControl := parseCacheControl(reqHeaders) if _, ok := reqCacheControl["no-cache"]; ok { return transparent } if _, ok := respCacheControl["no-cache"]; ok { return stale } if _, ok := reqCacheControl["only-if-cached"]; ok { return fresh } date, err := Date(respHeaders) if err != nil { return stale } currentAge := clock.since(date) var lifetime time.Duration var zeroDuration time.Duration // If a response includes both an Expires header and a max-age directive, // the max-age directive overrides the Expires header, even if the Expires header is more restrictive. if maxAge, ok := respCacheControl["max-age"]; ok { lifetime, err = time.ParseDuration(maxAge + "s") if err != nil { lifetime = zeroDuration } } else { expiresHeader := respHeaders.Get("Expires") if expiresHeader != "" { expires, err := time.Parse(time.RFC1123, expiresHeader) if err != nil { lifetime = zeroDuration } else { lifetime = expires.Sub(date) } } } if maxAge, ok := reqCacheControl["max-age"]; ok { // the client is willing to accept a response whose age is no greater than the specified time in seconds lifetime, err = time.ParseDuration(maxAge + "s") if err != nil { lifetime = zeroDuration } } if minfresh, ok := reqCacheControl["min-fresh"]; ok { // the client wants a response that will still be fresh for at least the specified number of seconds. minfreshDuration, err := time.ParseDuration(minfresh + "s") if err == nil { currentAge = time.Duration(currentAge + minfreshDuration) } } if maxstale, ok := reqCacheControl["max-stale"]; ok { // Indicates that the client is willing to accept a response that has exceeded its expiration time. // If max-stale is assigned a value, then the client is willing to accept a response that has exceeded // its expiration time by no more than the specified number of seconds. // If no value is assigned to max-stale, then the client is willing to accept a stale response of any age. // // Responses served only because of a max-stale value are supposed to have a Warning header added to them, // but that seems like a hassle, and is it actually useful? If so, then there needs to be a different // return-value available here. if maxstale == "" { return fresh } maxstaleDuration, err := time.ParseDuration(maxstale + "s") if err == nil { currentAge = time.Duration(currentAge - maxstaleDuration) } } if lifetime > currentAge { return fresh } return stale } // Returns true if either the request or the response includes the stale-if-error // cache control extension: https://tools.ietf.org/html/rfc5861 func canStaleOnError(respHeaders, reqHeaders http.Header) bool { respCacheControl := parseCacheControl(respHeaders) reqCacheControl := parseCacheControl(reqHeaders) var err error lifetime := time.Duration(-1) if staleMaxAge, ok := respCacheControl["stale-if-error"]; ok { if staleMaxAge != "" { lifetime, err = time.ParseDuration(staleMaxAge + "s") if err != nil { return false } } else { return true } } if staleMaxAge, ok := reqCacheControl["stale-if-error"]; ok { if staleMaxAge != "" { lifetime, err = time.ParseDuration(staleMaxAge + "s") if err != nil { return false } } else { return true } } if lifetime >= 0 { date, err := Date(respHeaders) if err != nil { return false } currentAge := clock.since(date) if lifetime > currentAge { return true } } return false } func getEndToEndHeaders(respHeaders http.Header) []string { // These headers are always hop-by-hop hopByHopHeaders := map[string]struct{}{ "Connection": {}, "Keep-Alive": {}, "Proxy-Authenticate": {}, "Proxy-Authorization": {}, "Te": {}, "Trailers": {}, "Transfer-Encoding": {}, "Upgrade": {}, } for _, extra := range strings.Split(respHeaders.Get("connection"), ",") { // any header listed in connection, if present, is also considered hop-by-hop if strings.Trim(extra, " ") != "" { hopByHopHeaders[http.CanonicalHeaderKey(extra)] = struct{}{} } } endToEndHeaders := []string{} for respHeader := range respHeaders { if _, ok := hopByHopHeaders[respHeader]; !ok { endToEndHeaders = append(endToEndHeaders, respHeader) } } return endToEndHeaders } func canStore(reqCacheControl, respCacheControl cacheControl) (canStore bool) { if _, ok := respCacheControl["no-store"]; ok { return false } if _, ok := reqCacheControl["no-store"]; ok { return false } return true } func newGatewayTimeoutResponse(req *http.Request) *http.Response { var braw bytes.Buffer braw.WriteString("HTTP/1.1 504 Gateway Timeout\r\n\r\n") resp, err := http.ReadResponse(bufio.NewReader(&braw), req) if err != nil { panic(err) } return resp } // cloneRequest returns a clone of the provided *http.Request. // The clone is a shallow copy of the struct and its Header map. // (This function copyright goauth2 authors: https://code.google.com/p/goauth2) func cloneRequest(r *http.Request) *http.Request { // shallow copy of the struct r2 := new(http.Request) *r2 = *r // deep copy of the Header r2.Header = make(http.Header) for k, s := range r.Header { r2.Header[k] = s } return r2 } type cacheControl map[string]string func parseCacheControl(headers http.Header) cacheControl { cc := cacheControl{} ccHeader := headers.Get("Cache-Control") for _, part := range strings.Split(ccHeader, ",") { part = strings.Trim(part, " ") if part == "" { continue } if strings.ContainsRune(part, '=') { keyval := strings.Split(part, "=") cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") } else { cc[part] = "" } } return cc } // headerAllCommaSepValues returns all comma-separated values (each // with whitespace trimmed) for header name in headers. According to // Section 4.2 of the HTTP/1.1 spec // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2), // values from multiple occurrences of a header should be concatenated, if // the header's value is a comma-separated list. func headerAllCommaSepValues(headers http.Header, name string) []string { var vals []string for _, val := range headers[http.CanonicalHeaderKey(name)] { fields := strings.Split(val, ",") for i, f := range fields { fields[i] = strings.TrimSpace(f) } vals = append(vals, fields...) } return vals } // cachingReadCloser is a wrapper around ReadCloser R that calls OnEOF // handler with a full copy of the content read from R when EOF is // reached. type cachingReadCloser struct { // Underlying ReadCloser. R io.ReadCloser // OnEOF is called with a copy of the content of R when EOF is reached. OnEOF func(io.Reader) buf bytes.Buffer // buf stores a copy of the content of R. } // Read reads the next len(p) bytes from R or until R is drained. The // return value n is the number of bytes read. If R has no data to // return, err is io.EOF and OnEOF is called with a full copy of what // has been read so far. func (r *cachingReadCloser) Read(p []byte) (n int, err error) { n, err = r.R.Read(p) r.buf.Write(p[:n]) if err == io.EOF { r.OnEOF(bytes.NewReader(r.buf.Bytes())) } return n, err } func (r *cachingReadCloser) Close() error { return r.R.Close() } // NewMemoryCacheTransport returns a new Transport using the in-memory cache implementation func NewMemoryCacheTransport() *Transport { c := NewMemoryCache() t := NewTransport(c) return t } ================================================ FILE: internal/httpcache/httpcache_test.go ================================================ package httpcache import ( "bytes" "errors" "flag" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "strconv" "testing" "time" ) var s struct { server *httptest.Server client http.Client transport *Transport done chan struct{} // Closed to unlock infinite handlers. } type fakeClock struct { elapsed time.Duration } func (c *fakeClock) since(t time.Time) time.Duration { return c.elapsed } func TestMain(m *testing.M) { flag.Parse() setup() code := m.Run() teardown() os.Exit(code) } func setup() { tp := NewMemoryCacheTransport() client := http.Client{Transport: tp} s.transport = tp s.client = client s.done = make(chan struct{}) mux := http.NewServeMux() s.server = httptest.NewServer(mux) mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") })) mux.HandleFunc("/method", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") w.Write([]byte(r.Method)) })) mux.HandleFunc("/range", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lm := "Fri, 14 Dec 2010 01:01:50 GMT" if r.Header.Get("if-modified-since") == lm { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("last-modified", lm) if r.Header.Get("range") == "bytes=4-9" { w.WriteHeader(http.StatusPartialContent) w.Write([]byte(" text ")) return } w.Write([]byte("Some text content")) })) mux.HandleFunc("/nostore", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") })) mux.HandleFunc("/etag", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { etag := "124567" if r.Header.Get("if-none-match") == etag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("etag", etag) })) mux.HandleFunc("/lastmodified", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lm := "Fri, 14 Dec 2010 01:01:50 GMT" if r.Header.Get("if-modified-since") == lm { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("last-modified", lm) })) mux.HandleFunc("/varyaccept", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") w.Header().Set("Content-Type", "text/plain") w.Header().Set("Vary", "Accept") w.Write([]byte("Some text content")) })) mux.HandleFunc("/doublevary", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") w.Header().Set("Content-Type", "text/plain") w.Header().Set("Vary", "Accept, Accept-Language") w.Write([]byte("Some text content")) })) mux.HandleFunc("/2varyheaders", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") w.Header().Set("Content-Type", "text/plain") w.Header().Add("Vary", "Accept") w.Header().Add("Vary", "Accept-Language") w.Write([]byte("Some text content")) })) mux.HandleFunc("/varyunused", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=3600") w.Header().Set("Content-Type", "text/plain") w.Header().Set("Vary", "X-Madeup-Header") w.Write([]byte("Some text content")) })) mux.HandleFunc("/cachederror", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { etag := "abc" if r.Header.Get("if-none-match") == etag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("etag", etag) w.WriteHeader(http.StatusNotFound) w.Write([]byte("Not found")) })) updateFieldsCounter := 0 mux.HandleFunc("/updatefields", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Counter", strconv.Itoa(updateFieldsCounter)) w.Header().Set("Etag", `"e"`) updateFieldsCounter++ if r.Header.Get("if-none-match") != "" { w.WriteHeader(http.StatusNotModified) return } w.Write([]byte("Some text content")) })) // Take 3 seconds to return 200 OK (for testing client timeouts). mux.HandleFunc("/3seconds", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) })) mux.HandleFunc("/infinite", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for { select { case <-s.done: return default: w.Write([]byte{0}) } } })) } func teardown() { close(s.done) s.server.Close() } func resetTest() { s.transport.Cache = NewMemoryCache() clock = &realClock{} } // TestCacheableMethod ensures that uncacheable method does not get stored // in cache and get incorrectly used for a following cacheable method request. func TestCacheableMethod(t *testing.T) { resetTest() { req, err := http.NewRequest("POST", s.server.URL+"/method", nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), "POST"; got != want { t.Errorf("got %q, want %q", got, want) } if resp.StatusCode != http.StatusOK { t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) } } { req, err := http.NewRequest("GET", s.server.URL+"/method", nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), "GET"; got != want { t.Errorf("got wrong body %q, want %q", got, want) } if resp.StatusCode != http.StatusOK { t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) } if resp.Header.Get(XFromCache) != "" { t.Errorf("XFromCache header isn't blank") } } } func TestDontServeHeadResponseToGetRequest(t *testing.T) { resetTest() url := s.server.URL + "/" req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { t.Fatal(err) } _, err = s.client.Do(req) if err != nil { t.Fatal(err) } req, err = http.NewRequest(http.MethodGet, url, nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } if resp.Header.Get(XFromCache) != "" { t.Errorf("Cache should not match") } } func TestDontStorePartialRangeInCache(t *testing.T) { resetTest() { req, err := http.NewRequest("GET", s.server.URL+"/range", nil) if err != nil { t.Fatal(err) } req.Header.Set("range", "bytes=4-9") resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), " text "; got != want { t.Errorf("got %q, want %q", got, want) } if resp.StatusCode != http.StatusPartialContent { t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) } } { req, err := http.NewRequest("GET", s.server.URL+"/range", nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), "Some text content"; got != want { t.Errorf("got %q, want %q", got, want) } if resp.StatusCode != http.StatusOK { t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) } if resp.Header.Get(XFromCache) != "" { t.Error("XFromCache header isn't blank") } } { req, err := http.NewRequest("GET", s.server.URL+"/range", nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), "Some text content"; got != want { t.Errorf("got %q, want %q", got, want) } if resp.StatusCode != http.StatusOK { t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) } if resp.Header.Get(XFromCache) != "1" { t.Errorf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } { req, err := http.NewRequest("GET", s.server.URL+"/range", nil) if err != nil { t.Fatal(err) } req.Header.Set("range", "bytes=4-9") resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } var buf bytes.Buffer _, err = io.Copy(&buf, resp.Body) if err != nil { t.Fatal(err) } err = resp.Body.Close() if err != nil { t.Fatal(err) } if got, want := buf.String(), " text "; got != want { t.Errorf("got %q, want %q", got, want) } if resp.StatusCode != http.StatusPartialContent { t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) } } } func TestCacheOnlyIfBodyRead(t *testing.T) { resetTest() { req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } // We do not read the body resp.Body.Close() } { req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatalf("XFromCache header isn't blank") } } } func TestOnlyReadBodyOnDemand(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/infinite", nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) // This shouldn't hang forever. if err != nil { t.Fatal(err) } buf := make([]byte, 10) // Only partially read the body. _, err = resp.Body.Read(buf) if err != nil { t.Fatal(err) } resp.Body.Close() } func TestGetOnlyIfCachedHit(t *testing.T) { resetTest() { req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } req.Header.Add("cache-control", "only-if-cached") resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } if resp.StatusCode != http.StatusOK { t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) } } } func TestGetOnlyIfCachedMiss(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } req.Header.Add("cache-control", "only-if-cached") resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } if resp.StatusCode != http.StatusGatewayTimeout { t.Fatalf("response status code isn't 504 GatewayTimeout: %v", resp.StatusCode) } } func TestGetNoStoreRequest(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL, nil) if err != nil { t.Fatal(err) } req.Header.Add("Cache-Control", "no-store") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } } func TestGetNoStoreResponse(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/nostore", nil) if err != nil { t.Fatal(err) } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } } func TestGetWithEtag(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/etag", nil) if err != nil { t.Fatal(err) } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } // additional assertions to verify that 304 response is converted properly if resp.StatusCode != http.StatusOK { t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) } if _, ok := resp.Header["Connection"]; ok { t.Fatalf("Connection header isn't absent") } } } func TestGetWithLastModified(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/lastmodified", nil) if err != nil { t.Fatal(err) } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } } func TestGetWithVary(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/varyaccept", nil) if err != nil { t.Fatal(err) } req.Header.Set("Accept", "text/plain") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get("Vary") != "Accept" { t.Fatalf(`Vary header isn't "Accept": %v`, resp.Header.Get("Vary")) } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } req.Header.Set("Accept", "text/html") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } req.Header.Set("Accept", "") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } } func TestGetWithDoubleVary(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/doublevary", nil) if err != nil { t.Fatal(err) } req.Header.Set("Accept", "text/plain") req.Header.Set("Accept-Language", "da, en-gb;q=0.8, en;q=0.7") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get("Vary") == "" { t.Fatalf(`Vary header is blank`) } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } req.Header.Set("Accept-Language", "") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } req.Header.Set("Accept-Language", "da") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } } func TestGetWith2VaryHeaders(t *testing.T) { resetTest() // Tests that multiple Vary headers' comma-separated lists are // merged. See https://github.com/gregjones/httpcache/issues/27. const ( accept = "text/plain" acceptLanguage = "da, en-gb;q=0.8, en;q=0.7" ) req, err := http.NewRequest("GET", s.server.URL+"/2varyheaders", nil) if err != nil { t.Fatal(err) } req.Header.Set("Accept", accept) req.Header.Set("Accept-Language", acceptLanguage) { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get("Vary") == "" { t.Fatalf(`Vary header is blank`) } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } req.Header.Set("Accept-Language", "") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } req.Header.Set("Accept-Language", "da") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } req.Header.Set("Accept-Language", acceptLanguage) req.Header.Set("Accept", "") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } } req.Header.Set("Accept", "image/png") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "" { t.Fatal("XFromCache header isn't blank") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } } func TestGetVaryUnused(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/varyunused", nil) if err != nil { t.Fatal(err) } req.Header.Set("Accept", "text/plain") { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get("Vary") == "" { t.Fatalf(`Vary header is blank`) } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } } } func TestUpdateFields(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/updatefields", nil) if err != nil { t.Fatal(err) } var counter, counter2 string { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() counter = resp.Header.Get("x-counter") _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.Header.Get(XFromCache) != "1" { t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) } counter2 = resp.Header.Get("x-counter") } if counter == counter2 { t.Fatalf(`both "x-counter" values are equal: %v %v`, counter, counter2) } } // This tests the fix for https://github.com/gregjones/httpcache/issues/74. // Previously, after validating a cached response, its StatusCode // was incorrectly being replaced. func TestCachedErrorsKeepStatus(t *testing.T) { resetTest() req, err := http.NewRequest("GET", s.server.URL+"/cachederror", nil) if err != nil { t.Fatal(err) } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() io.Copy(ioutil.Discard, resp.Body) } { resp, err := s.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("Status code isn't 404: %d", resp.StatusCode) } } } func TestParseCacheControl(t *testing.T) { resetTest() h := http.Header{} for range parseCacheControl(h) { t.Fatal("cacheControl should be empty") } h.Set("cache-control", "no-cache") { cc := parseCacheControl(h) if _, ok := cc["foo"]; ok { t.Error(`Value "foo" shouldn't exist`) } noCache, ok := cc["no-cache"] if !ok { t.Fatalf(`"no-cache" value isn't set`) } if noCache != "" { t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) } } h.Set("cache-control", "no-cache, max-age=3600") { cc := parseCacheControl(h) noCache, ok := cc["no-cache"] if !ok { t.Fatalf(`"no-cache" value isn't set`) } if noCache != "" { t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) } if cc["max-age"] != "3600" { t.Fatalf(`"max-age" value isn't "3600": %v`, cc["max-age"]) } } } func TestNoCacheRequestExpiration(t *testing.T) { resetTest() respHeaders := http.Header{} respHeaders.Set("Cache-Control", "max-age=7200") reqHeaders := http.Header{} reqHeaders.Set("Cache-Control", "no-cache") if getFreshness(respHeaders, reqHeaders) != transparent { t.Fatal("freshness isn't transparent") } } func TestNoCacheResponseExpiration(t *testing.T) { resetTest() respHeaders := http.Header{} respHeaders.Set("Cache-Control", "no-cache") respHeaders.Set("Expires", "Wed, 19 Apr 3000 11:43:00 GMT") reqHeaders := http.Header{} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestReqMustRevalidate(t *testing.T) { resetTest() // not paying attention to request setting max-stale means never returning stale // responses, so always acting as if must-revalidate is set respHeaders := http.Header{} reqHeaders := http.Header{} reqHeaders.Set("Cache-Control", "must-revalidate") if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestRespMustRevalidate(t *testing.T) { resetTest() respHeaders := http.Header{} respHeaders.Set("Cache-Control", "must-revalidate") reqHeaders := http.Header{} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestFreshExpiration(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) reqHeaders := http.Header{} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } clock = &fakeClock{elapsed: 3 * time.Second} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestMaxAge(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("cache-control", "max-age=2") reqHeaders := http.Header{} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } clock = &fakeClock{elapsed: 3 * time.Second} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestMaxAgeZero(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("cache-control", "max-age=0") reqHeaders := http.Header{} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestBothMaxAge(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("cache-control", "max-age=2") reqHeaders := http.Header{} reqHeaders.Set("cache-control", "max-age=0") if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestMinFreshWithExpires(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) reqHeaders := http.Header{} reqHeaders.Set("cache-control", "min-fresh=1") if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } reqHeaders = http.Header{} reqHeaders.Set("cache-control", "min-fresh=2") if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func TestEmptyMaxStale(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("cache-control", "max-age=20") reqHeaders := http.Header{} reqHeaders.Set("cache-control", "max-stale") clock = &fakeClock{elapsed: 10 * time.Second} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } clock = &fakeClock{elapsed: 60 * time.Second} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } } func TestMaxStaleValue(t *testing.T) { resetTest() now := time.Now() respHeaders := http.Header{} respHeaders.Set("date", now.Format(time.RFC1123)) respHeaders.Set("cache-control", "max-age=10") reqHeaders := http.Header{} reqHeaders.Set("cache-control", "max-stale=20") clock = &fakeClock{elapsed: 5 * time.Second} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } clock = &fakeClock{elapsed: 15 * time.Second} if getFreshness(respHeaders, reqHeaders) != fresh { t.Fatal("freshness isn't fresh") } clock = &fakeClock{elapsed: 30 * time.Second} if getFreshness(respHeaders, reqHeaders) != stale { t.Fatal("freshness isn't stale") } } func containsHeader(headers []string, header string) bool { for _, v := range headers { if http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header) { return true } } return false } func TestGetEndToEndHeaders(t *testing.T) { resetTest() var ( headers http.Header end2end []string ) headers = http.Header{} headers.Set("content-type", "text/html") headers.Set("te", "deflate") end2end = getEndToEndHeaders(headers) if !containsHeader(end2end, "content-type") { t.Fatal(`doesn't contain "content-type" header`) } if containsHeader(end2end, "te") { t.Fatal(`doesn't contain "te" header`) } headers = http.Header{} headers.Set("connection", "content-type") headers.Set("content-type", "text/csv") headers.Set("te", "deflate") end2end = getEndToEndHeaders(headers) if containsHeader(end2end, "connection") { t.Fatal(`doesn't contain "connection" header`) } if containsHeader(end2end, "content-type") { t.Fatal(`doesn't contain "content-type" header`) } if containsHeader(end2end, "te") { t.Fatal(`doesn't contain "te" header`) } headers = http.Header{} end2end = getEndToEndHeaders(headers) if len(end2end) != 0 { t.Fatal(`non-zero end2end headers`) } headers = http.Header{} headers.Set("connection", "content-type") end2end = getEndToEndHeaders(headers) if len(end2end) != 0 { t.Fatal(`non-zero end2end headers`) } } type transportMock struct { response *http.Response err error } func (t transportMock) RoundTrip(req *http.Request) (resp *http.Response, err error) { return t.response, t.err } func TestStaleIfErrorRequest(t *testing.T) { resetTest() now := time.Now() tmock := transportMock{ response: &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Header: http.Header{ "Date": []string{now.Format(time.RFC1123)}, "Cache-Control": []string{"no-cache"}, }, Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), }, err: nil, } tp := NewMemoryCacheTransport() tp.Transport = &tmock // First time, response is cached on success r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) r.Header.Set("Cache-Control", "stale-if-error") resp, err := tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } // On failure, response is returned from the cache tmock.response = nil tmock.err = errors.New("some error") resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } } func TestStaleIfErrorRequestLifetime(t *testing.T) { resetTest() now := time.Now() tmock := transportMock{ response: &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Header: http.Header{ "Date": []string{now.Format(time.RFC1123)}, "Cache-Control": []string{"no-cache"}, }, Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), }, err: nil, } tp := NewMemoryCacheTransport() tp.Transport = &tmock // First time, response is cached on success r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) r.Header.Set("Cache-Control", "stale-if-error=100") resp, err := tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } // On failure, response is returned from the cache tmock.response = nil tmock.err = errors.New("some error") resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } // Same for http errors tmock.response = &http.Response{StatusCode: http.StatusInternalServerError} tmock.err = nil resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } // If failure last more than max stale, error is returned clock = &fakeClock{elapsed: 200 * time.Second} _, err = tp.RoundTrip(r) if err != tmock.err { t.Fatalf("got err %v, want %v", err, tmock.err) } } func TestStaleIfErrorResponse(t *testing.T) { resetTest() now := time.Now() tmock := transportMock{ response: &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Header: http.Header{ "Date": []string{now.Format(time.RFC1123)}, "Cache-Control": []string{"no-cache, stale-if-error"}, }, Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), }, err: nil, } tp := NewMemoryCacheTransport() tp.Transport = &tmock // First time, response is cached on success r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) resp, err := tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } // On failure, response is returned from the cache tmock.response = nil tmock.err = errors.New("some error") resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } } func TestStaleIfErrorResponseLifetime(t *testing.T) { resetTest() now := time.Now() tmock := transportMock{ response: &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Header: http.Header{ "Date": []string{now.Format(time.RFC1123)}, "Cache-Control": []string{"no-cache, stale-if-error=100"}, }, Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), }, err: nil, } tp := NewMemoryCacheTransport() tp.Transport = &tmock // First time, response is cached on success r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) resp, err := tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } // On failure, response is returned from the cache tmock.response = nil tmock.err = errors.New("some error") resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } // If failure last more than max stale, error is returned clock = &fakeClock{elapsed: 200 * time.Second} _, err = tp.RoundTrip(r) if err != tmock.err { t.Fatalf("got err %v, want %v", err, tmock.err) } } // This tests the fix for https://github.com/gregjones/httpcache/issues/74. // Previously, after a stale response was used after encountering an error, // its StatusCode was being incorrectly replaced. func TestStaleIfErrorKeepsStatus(t *testing.T) { resetTest() now := time.Now() tmock := transportMock{ response: &http.Response{ Status: http.StatusText(http.StatusNotFound), StatusCode: http.StatusNotFound, Header: http.Header{ "Date": []string{now.Format(time.RFC1123)}, "Cache-Control": []string{"no-cache"}, }, Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), }, err: nil, } tp := NewMemoryCacheTransport() tp.Transport = &tmock // First time, response is cached on success r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) r.Header.Set("Cache-Control", "stale-if-error") resp, err := tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } _, err = ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } // On failure, response is returned from the cache tmock.response = nil tmock.err = errors.New("some error") resp, err = tp.RoundTrip(r) if err != nil { t.Fatal(err) } if resp == nil { t.Fatal("resp is nil") } if resp.StatusCode != http.StatusNotFound { t.Fatalf("Status wasn't 404: %d", resp.StatusCode) } } // Test that http.Client.Timeout is respected when cache transport is used. // That is so as long as request cancellation is propagated correctly. // In the past, that required CancelRequest to be implemented correctly, // but modern http.Client uses Request.Cancel (or request context) instead, // so we don't have to do anything. func TestClientTimeout(t *testing.T) { if testing.Short() { t.Skip("skipping timeout test in short mode") // Because it takes at least 3 seconds to run. } resetTest() client := &http.Client{ Transport: NewMemoryCacheTransport(), Timeout: time.Second, } started := time.Now() resp, err := client.Get(s.server.URL + "/3seconds") taken := time.Since(started) if err == nil { t.Error("got nil error, want timeout error") } if resp != nil { t.Error("got non-nil resp, want nil resp") } if taken >= 2*time.Second { t.Error("client.Do took 2+ seconds, want < 2 seconds") } } ================================================ FILE: internal/httpcache/test/test.go ================================================ package test import ( "bytes" "testing" "encr.dev/internal/httpcache" ) // Cache excercises a httpcache.Cache implementation. func Cache(t *testing.T, cache httpcache.Cache) { key := "testKey" _, ok := cache.Get(key) if ok { t.Fatal("retrieved key before adding it") } val := []byte("some bytes") cache.Set(key, val) retVal, ok := cache.Get(key) if !ok { t.Fatal("could not retrieve an element we just added") } if !bytes.Equal(retVal, val) { t.Fatal("retrieved a different value than what we put in") } cache.Delete(key) _, ok = cache.Get(key) if ok { t.Fatal("deleted key still present") } } ================================================ FILE: internal/httpcache/test/test_test.go ================================================ package test_test import ( "testing" "encr.dev/internal/httpcache" "encr.dev/internal/httpcache/test" ) func TestMemoryCache(t *testing.T) { test.Cache(t, httpcache.NewMemoryCache()) } ================================================ FILE: internal/lookpath/lookpath.go ================================================ /* This package contains code from https://github.com/mvdan/sh. Copyright (c) 2016, Daniel Martí. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package lookpath import ( "cmp" "fmt" "os" "path/filepath" "runtime" "slices" "strings" ) func checkStat(dir, file string, checkExec bool) (string, error) { if !filepath.IsAbs(file) { file = filepath.Join(dir, file) } info, err := os.Stat(file) if err != nil { return "", err } m := info.Mode() if m.IsDir() { return "", fmt.Errorf("is a directory") } if checkExec && runtime.GOOS != "windows" && m&0o111 == 0 { return "", fmt.Errorf("permission denied") } return file, nil } func winHasExt(file string) bool { i := strings.LastIndex(file, ".") if i < 0 { return false } return strings.LastIndexAny(file, `:\/`) < i } // findExecutable returns the path to an existing executable file. func findExecutable(dir, file string, exts []string) (string, error) { if len(exts) == 0 { // non-windows return checkStat(dir, file, true) } if winHasExt(file) { if file, err := checkStat(dir, file, true); err == nil { return file, nil } } for _, e := range exts { f := file + e if f, err := checkStat(dir, f, true); err == nil { return f, nil } } return "", fmt.Errorf("not found") } // findFile returns the path to an existing file. func findFile(dir, file string, _ []string) (string, error) { return checkStat(dir, file, false) } // InDir is similar to [os/exec.LookPath], with the difference that it uses the // provided environment. env is used to fetch relevant environment variables // such as PWD and PATH. // // If no error is returned, the returned path must be valid. func InDir(cwd string, env []string, file string) (string, error) { if filepath.IsAbs(file) { return file, nil } upper := runtime.GOOS == "windows" envs := listEnvironWithUpper(upper, env...) return lookPathDir(cwd, envs, file, findExecutable) } // findAny defines a function to pass to lookPathDir. type findAny = func(dir string, file string, exts []string) (string, error) func lookPathDir(cwd string, env listEnviron, file string, find findAny) (string, error) { if find == nil { panic("no find function found") } pathList := filepath.SplitList(env.Get("PATH")) if len(pathList) == 0 { pathList = []string{""} } chars := `/` if runtime.GOOS == "windows" { chars = `:\/` } exts := pathExts(env) if strings.ContainsAny(file, chars) { return find(cwd, file, exts) } for _, elem := range pathList { var path string switch elem { case "", ".": // otherwise "foo" won't be "./foo" path = "." + string(filepath.Separator) + file default: path = filepath.Join(elem, file) } if f, err := find(cwd, path, exts); err == nil { return f, nil } } return "", fmt.Errorf("%q: executable file not found in $PATH", file) } func pathExts(env listEnviron) []string { if runtime.GOOS != "windows" { return nil } pathext := env.Get("PATHEXT") if pathext == "" { return []string{".com", ".exe", ".bat", ".cmd"} } var exts []string for _, e := range strings.Split(strings.ToLower(pathext), `;`) { if e == "" { continue } if e[0] != '.' { e = "." + e } exts = append(exts, e) } return exts } // listEnvironWithUpper implements ListEnviron, but letting the tests specify // whether to uppercase all names or not. func listEnvironWithUpper(upper bool, pairs ...string) listEnviron { list := slices.Clone(pairs) if upper { // Uppercase before sorting, so that we can remove duplicates // without the need for linear search nor a map. for i, s := range list { if sep := strings.IndexByte(s, '='); sep > 0 { list[i] = strings.ToUpper(s[:sep]) + s[sep:] } } } slices.SortStableFunc(list, func(a, b string) int { isep := strings.IndexByte(a, '=') jsep := strings.IndexByte(b, '=') if isep < 0 { isep = 0 } else { isep += 1 } if jsep < 0 { jsep = 0 } else { jsep += 1 } return strings.Compare(a[:isep], b[:jsep]) }) last := "" for i := 0; i < len(list); { s := list[i] sep := strings.IndexByte(s, '=') if sep <= 0 { // invalid element; remove it list = append(list[:i], list[i+1:]...) continue } name := s[:sep] if last == name { // duplicate; the last one wins list = append(list[:i-1], list[i:]...) continue } last = name i++ } return listEnviron(list) } // listEnviron is a sorted list of "name=value" strings. type listEnviron []string func (l listEnviron) Get(name string) string { eqpos := len(name) endpos := len(name) + 1 i, ok := slices.BinarySearchFunc(l, name, func(l, name string) int { if len(l) < endpos { // Too short; see if we are before or after the name. return strings.Compare(l, name) } // Compare the name prefix, then the equal character. c := strings.Compare(l[:eqpos], name) eq := l[eqpos] if c == 0 { return cmp.Compare(eq, '=') } return c }) if ok { return l[i][endpos:] } return "" } ================================================ FILE: internal/optracker/async.go ================================================ package optracker import ( "context" "sync" "time" "github.com/rs/zerolog/log" ) type AsyncBuildJobs struct { ctx context.Context cancelCtx context.CancelFunc m sync.Mutex wait sync.WaitGroup firstError error tracker *OpTracker start time.Time appID string } func (a *AsyncBuildJobs) Tracker() *OpTracker { return a.tracker } func NewAsyncBuildJobs(ctx context.Context, appID string, tracker *OpTracker) *AsyncBuildJobs { ctx, cancelCtx := context.WithCancel(ctx) return &AsyncBuildJobs{ ctx: ctx, cancelCtx: cancelCtx, appID: appID, tracker: tracker, start: time.Now(), } } func (a *AsyncBuildJobs) Go(description string, track bool, minDuration time.Duration, f func(ctx context.Context) error) { a.wait.Add(1) trackerID := NoOperationID if track && a.tracker != nil { trackerID = a.tracker.Add(description, a.start) } go func() { defer a.wait.Done() log.Info().Str("app_id", a.appID).Str("job", description).Msg("starting build job") if err := f(a.ctx); err != nil { // If the context was canceled, it probably means the error was due to that. if a.ctx.Err() != nil { if a.tracker != nil { a.tracker.Cancel(trackerID) } } else { log.Err(err).Str("app_id", a.appID).Str("job", description).Msg("build job failed") if a.tracker != nil { a.tracker.Fail(trackerID, err) } a.recordError(err) } } else { if a.tracker != nil { a.tracker.Done(trackerID, minDuration) } log.Info().Str("app_id", a.appID).Str("job", description).Msg("build job finished") } }() } func (a *AsyncBuildJobs) Wait() error { a.wait.Wait() return a.firstError } func (a *AsyncBuildJobs) recordError(err error) { a.m.Lock() defer a.m.Unlock() a.cancelCtx() if a.firstError == nil { a.firstError = err } } ================================================ FILE: internal/optracker/optracker.go ================================================ package optracker import ( "errors" "fmt" "io" "runtime" "sort" "sync" "time" "github.com/logrusorgru/aurora/v3" "encr.dev/pkg/ansi" "encr.dev/pkg/errlist" daemonpb "encr.dev/proto/encore/daemon" ) type OutputStream interface { Send(*daemonpb.CommandMessage) error } func New(w io.Writer, stream OutputStream) *OpTracker { return &OpTracker{ w: w, stream: stream, } } type OpTracker struct { mu sync.Mutex ops []*slowOp w io.Writer started bool quit bool // quit indicates that the tracker has been stopped (this should only be set by AllDone) savedCursor sync.Once stream OutputStream } type OperationID int const NoOperationID OperationID = -1 // AllDone marks all ops as done. // This function is safe to call on a Nil OpTracker. func (t *OpTracker) AllDone() { if t == nil { return } t.mu.Lock() defer t.mu.Unlock() // If we've already quit, don't do anything if t.quit == true { return } now := time.Now() for _, o := range t.ops { if o.done.IsZero() || o.done.After(now) { o.done = now } if o.start.After(now) { o.start = now } } t.quit = true t.refresh() } // Add creates a new item on the operations tracker returning the ID for that op. // minStart is the time at which the tracker will start to show the task as in progress. // // This function is safe to call on a Nil OpTracker and will no-op in that case func (t *OpTracker) Add(msg string, minStart time.Time) OperationID { if t == nil { return NoOperationID } t.mu.Lock() defer t.mu.Unlock() id := OperationID(len(t.ops)) start := time.Now() if start.Before(minStart) { start = minStart } op := &slowOp{msg: msg, start: start} t.ops = append(t.ops, op) t.refresh() if !t.started { go t.spin() t.started = true } return id } // Done marks the given operation as done // // This function is safe to call on a Nil OpTracker and will no-op in that case func (t *OpTracker) Done(id OperationID, minDuration time.Duration) { if t == nil || id == NoOperationID { return } t.mu.Lock() defer t.mu.Unlock() o := t.ops[id] done := time.Now() if a := o.start.Add(minDuration); a.After(done) { done = a } o.done = done t.refresh() } var ErrCanceled = errors.New("operation canceled") // Fail marks the operation as failed with the given error // // This function is safe to call on a Nil OpTracker and will no-op in that case func (t *OpTracker) Fail(id OperationID, err error) { if t == nil || id == NoOperationID { return } t.mu.Lock() defer t.mu.Unlock() if !t.ops[id].done.IsZero() { return } t.ops[id].err = err t.ops[id].done = time.Now() t.refresh() } // Cancel marks the operation as canceled. // It is equivalent to t.Fail(id, ErrCanceled). func (t *OpTracker) Cancel(id OperationID) { t.Fail(id, ErrCanceled) } // refresh refreshes the display by writing to t.w. // The mutex must be held by the caller. func (t *OpTracker) refresh() { t.savedCursor.Do(func() { fmt.Fprint(t.w, ansi.SaveCursorPosition) }) fmt.Fprint(t.w, ansi.RestoreCursorPosition+ansi.ClearScreen(ansi.CursorToBottom)) now := time.Now() // Sort ops by start time ops := make([]*slowOp, len(t.ops)) copy(ops, t.ops) sort.Slice(ops, func(i, j int) bool { return ops[i].start.Before(ops[j].start) }) for _, o := range ops { started := o.start.Before(now) done := !o.done.IsZero() && o.done.Before(now) if !started && !done { continue } var msg aurora.Value format := " %s %s... " switch { case done && o.err != nil: if errors.Is(o.err, ErrCanceled) { msg = aurora.Yellow(fmt.Sprintf(format+"Canceled", canceled, o.msg)) } else { if errlist := errlist.Convert(o.err); errlist != nil { if len(errlist.List) > 0 { msg = aurora.Red(fmt.Sprintf(format+"Failed: %v", fail, o.msg, errlist.List[0].Title())) } else { msg = aurora.Red(fmt.Sprintf(format+"Failed: %v", fail, o.msg, errlist)) } } else { msg = aurora.Red(fmt.Sprintf(format+"Failed: %v", fail, o.msg, o.err)) } } case done && o.err == nil: msg = aurora.Green(fmt.Sprintf(format+"Done!", success, o.msg)) case !done: msg = aurora.Cyan(fmt.Sprintf(format, spinner[o.spinIdx], o.msg)) o.spinIdx = (o.spinIdx + 1) % len(spinner) } str := msg.String() fmt.Fprintf(t.w, "%s%s%s\n", ansi.MoveCursorLeft(1000), ansi.ClearLine(ansi.WholeLine), str, ) } } func (t *OpTracker) spin() { refresh := 100 * time.Millisecond if runtime.GOOS == "windows" { // Window's terminal is quite slow at rendering. // Reduce the refresh rate to avoid excessive flickering. refresh = 250 * time.Millisecond } for { time.Sleep(refresh) (func() { t.mu.Lock() defer t.mu.Unlock() if !t.quit { t.refresh() } })() } } type slowOp struct { msg string err error spinIdx int start time.Time done time.Time } var ( success = "✔" fail = "❌" canceled = "⚠️" spinner = []string{"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"} ) ================================================ FILE: internal/userconfig/config.go ================================================ package userconfig // Config describes the configuration structure we support. type Config struct { // Whether to open the Local Development Dashboard in the browser on `encore run`. // If set to "auto", the browser will be opened if the dashboard is not already open. RunBrowser string `koanf:"run.browser" oneof:"always,never,auto" default:"auto"` // Always choose this tool when creating an app or when initializing llm tools // for an existing app, unless overriden via --llm-rules flag on command line. LLMRules string `koanf:"llm_rules" oneof:",cursor,claudcode,vscode,agentsmd,zed" default:""` } ================================================ FILE: internal/userconfig/def.go ================================================ package userconfig import ( "fmt" "maps" "reflect" "slices" "sort" "strings" ) func (c *Config) GetByKey(key string) (v Value, ok bool) { val := reflect.ValueOf(c).Elem() desc, ok := descs[key] if !ok { return Value{}, false } f := val.FieldByName(desc.FieldName) if !f.IsValid() { return Value{}, false } return Value{Val: f.Interface(), Type: desc.Type}, true } func (c *Config) Render() string { var buf strings.Builder for _, key := range configKeys { v, ok := c.GetByKey(key) if !ok { continue } buf.WriteString(fmt.Sprintf("%s: %s\n", key, v)) } return buf.String() } var configKeys = (func() []string { keys := slices.Collect(maps.Keys(descs)) sort.Strings(keys) return keys })() func GetType(key string) (Type, bool) { typ, ok := descs[key] return typ.Type, ok } func Keys() []string { return configKeys } ================================================ FILE: internal/userconfig/docs.go ================================================ package userconfig import ( "fmt" "strings" ) //go:generate go run ./gendocs func CLIDocs() string { var buf strings.Builder for _, key := range configKeys { desc := descs[key] doc := desc.Doc fmt.Fprintf(&buf, "%s (%s)\n", key, desc.Type.Kind.String()) if doc != "" { rem := doc for rem != "" { var line string if idx := strings.IndexByte(rem, '\n'); idx != -1 { line = rem[:idx] rem = rem[idx+1:] } else { line = rem rem = "" } buf.WriteString(" ") buf.WriteString(line) buf.WriteByte('\n') } } else { buf.WriteString(" No documentation available.\n") } buf.WriteByte('\n') didWriteMore := false if desc.Type.Default != nil { fmt.Fprintf(&buf, " Default: %v\n", RenderValue(*desc.Type.Default)) didWriteMore = true } if len(desc.Type.Oneof) > 0 { fmt.Fprintf(&buf, " Must be one of: %v\n", RenderOneof(desc.Type.Oneof)) didWriteMore = true } // Add an extra newline if we wrote validation details. if didWriteMore { buf.WriteByte('\n') } } return buf.String() } // bt renders a backtick-enclosed string. func bt(val string) string { return fmt.Sprintf("`%s`", val) } var markdownHeader = ` The Encore CLI has a number of configuration options to customize its behavior. Configuration options can be set both for individual Encore applications, as well as globally for the local user. Configuration options can be set using ` + bt("encore config ") + `, and options can similarly be read using ` + bt("encore config ") + `. When running ` + bt("encore config") + ` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the ` + bt("--global") + ` flag. ## Configuration files The configuration is stored in one ore more TOML files on the filesystem. The configuration is read from the following files, in order: ### Global configuration * ` + bt("$XDG_CONFIG_HOME/encore/config") + ` * ` + bt("$HOME/.config/encore/config") + ` * ` + bt("$HOME/.encoreconfig") + ` ### Application-specific configuration * ` + bt("$APP_ROOT/.encore/config") + ` Where ` + bt("$APP_ROOT") + ` is the directory containing the ` + bt("encore.app") + ` file. The files are read and merged, in the order defined above, with latter files taking precedence over earlier files. ## Configuration options ` func MarkdownDocs() string { var buf strings.Builder buf.WriteString(markdownHeader) for _, key := range configKeys { desc := descs[key] doc := desc.Doc fmt.Fprintf(&buf, "#### %s\n", key) fmt.Fprintf(&buf, "Type: %s
\n", desc.Type.Kind.String()) if desc.Type.Default != nil { fmt.Fprintf(&buf, "Default: %v
\n", RenderValue(*desc.Type.Default)) } if len(desc.Type.Oneof) > 0 { fmt.Fprintf(&buf, "Must be one of: %v\n", RenderOneof(desc.Type.Oneof)) } buf.WriteByte('\n') if doc != "" { buf.WriteString(doc) } else { buf.WriteString("No documentation available.\n") } buf.WriteByte('\n') } return buf.String() } ================================================ FILE: internal/userconfig/files.go ================================================ package userconfig import ( "io/fs" "os" "os/user" "path/filepath" "slices" "sync" "time" "encr.dev/internal/goldfish" "github.com/cockroachdb/errors" "github.com/knadh/koanf/parsers/toml/v2" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/rawbytes" "github.com/knadh/koanf/v2" ) const globalCacheKey = "#global#" var ( goldfishMu sync.Mutex goldfishes = make(map[string]*Cached) ) type Cached = goldfish.Cache[*Config] func ForApp(appRoot string) *Cached { appRoot = filepath.Clean(appRoot) paths := slices.Clone(userPaths) paths = append(paths, appFilePath(appRoot)) return forCacheKey(appRoot, paths) } func Global() *Cached { return forCacheKey(globalCacheKey, userPaths) } func forCacheKey(key string, paths []string) *Cached { goldfishMu.Lock() defer goldfishMu.Unlock() if c, ok := goldfishes[key]; ok { return c } c := goldfish.New(1*time.Second, func() (*Config, error) { return newInstance(paths...) }) goldfishes[key] = c return c } func appFilePath(appRoot string) string { return filepath.Join(appRoot, ".encore", "config") } var userPaths []string = func() []string { var paths []string configHome := os.Getenv("XDG_CONFIG_HOME") if configHome != "" { paths = append(paths, filepath.Join(configHome, "encore", "config")) } if u, err := user.Current(); err == nil { if configHome == "" { paths = append(paths, filepath.Join(u.HomeDir, ".config", "encore", "config")) } paths = append(paths, filepath.Join(u.HomeDir, ".encoreconfig")) } return paths }() var tomlParser = toml.Parser() func newInstance(paths ...string) (*Config, error) { k := koanf.New(".") for _, path := range paths { f := file.Provider(path) err := k.Load(f, tomlParser) if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, errors.Wrap(err, "unable to parse config file") } } cfg := &Config{} err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{ Tag: "koanf", FlatPaths: true, }) if err != nil { return nil, errors.Wrap(err, "unable to unmarshal config") } return cfg, nil } func validateConfig(data []byte) error { k := koanf.New(".") return k.Load(rawbytes.Provider(data), tomlParser) } ================================================ FILE: internal/userconfig/gendocs/gendocs.go ================================================ package main import ( "fmt" "log" "os/exec" "path/filepath" "strings" "encr.dev/internal/userconfig" "encr.dev/pkg/xos" ) func main() { repoRoot := resolveRepoRoot() docsDir := filepath.Join(repoRoot, "docs") for _, lang := range []string{"go", "ts"} { docs := generateDocs(lang) dst := filepath.Join(docsDir, lang, "cli", "config-reference.md") if err := xos.WriteFile(dst, []byte(docs), 0644); err != nil { log.Fatalf("error writing %s docs file: %v\n", lang, err) } } log.Printf("successfully regenerated docs") } func generateDocs(lang string) string { return docsHeader(lang) + "\n" + userconfig.MarkdownDocs() } func docsHeader(lang string) string { return fmt.Sprintf(`--- seotitle: Encore CLI Configuration Options seodesc: Configuration options to customize the behavior of the Encore CLI. title: Configuration Reference subtitle: Configuration options to customize the behavior of the Encore CLI. lang: %s --- `, lang) } func resolveRepoRoot() string { // Use `git rev-parse --show-toplevel` to get the root of the repository cmd := exec.Command("git", "rev-parse", "--show-toplevel") out, err := cmd.CombinedOutput() if err != nil { log.Fatalf("Error running git rev-parse: %v\n", err) } return filepath.Clean(strings.TrimSpace(string(out))) } ================================================ FILE: internal/userconfig/reflect.go ================================================ package userconfig import ( _ "embed" "fmt" "go/ast" "go/doc" "go/parser" "go/token" "reflect" "strings" "github.com/cockroachdb/errors" "github.com/fatih/structtag" ) func keyForField(f *reflect.StructField) (string, error) { tags, err := structtag.Parse(string(f.Tag)) if err != nil { return "", err } tag, err := tags.Get("koanf") if err != nil { return "", err } key := tag.Name if key == "" { return "", errors.New("empty key") } return key, nil } type keyDesc struct { Doc string Type Type FieldName string // field name in the Config struct } func newKeyDesc(f *reflect.StructField) (key string, desc keyDesc, err error) { tags, err := structtag.Parse(string(f.Tag)) if err != nil { return "", keyDesc{}, err } tag, err := tags.Get("koanf") if err != nil { return "", keyDesc{}, errors.Wrap(err, "failed to get koanf tag") } key = tag.Name if key == "" { return "", keyDesc{}, errors.New("empty key") } kind, ok := kindFromReflect(f.Type.Kind()) if !ok { return "", keyDesc{}, errors.Errorf("unsupported type %v", f.Type) } ty := Type{Kind: kind} // Do we have a default? if def, _ := tags.Get("default"); def != nil { val, err := kind.parseValue(def.Name) if err != nil { return "", keyDesc{}, errors.Wrap(err, "parse default value") } ty.Default = &val } // Do we have a oneof? if tag := f.Tag.Get("oneof"); tag != "" { var oneof []any for _, part := range strings.Split(tag, ",") { val, err := kind.parseValue(part) if err != nil { return "", keyDesc{}, errors.Wrap(err, "parse oneof value") } oneof = append(oneof, val) } ty.Oneof = oneof } desc = keyDesc{ Doc: docComments[f.Name], Type: ty, FieldName: f.Name, } return key, desc, nil } var descs = (func() map[string]keyDesc { var cfg Config t := reflect.TypeOf(cfg) descs := make(map[string]keyDesc, t.NumField()) for i := 0; i < t.NumField(); i++ { f := t.Field(i) key, desc, err := newKeyDesc(&f) if err != nil { panic(fmt.Sprintf("invalid userconfig definition for field %s: %v", f.Name, err)) } if _, ok := descs[key]; ok { panic(fmt.Sprintf("duplicate key %s in userconfig.Config", key)) } descs[key] = desc } return descs })() func kindFromReflect(kind reflect.Kind) (Kind, bool) { switch kind { case reflect.String: return String, true case reflect.Bool: return Bool, true case reflect.Int: return Int, true case reflect.Uint: return Uint, true default: return 0, false } } //go:embed config.go var configGo string // doc comments, keyed by field name. var docComments = (func() map[string]string { // Parse config.go as a Go file to extract the doc comments. fset := token.NewFileSet() f, err := parser.ParseFile(fset, "config.go", configGo, parser.ParseComments) if err != nil { panic(fmt.Sprintf("userconfig/config.go is invalid: %v", err)) } // Compute package documentation with examples. p, err := doc.NewFromFiles(fset, []*ast.File{f}, "encr.dev/internal/userconfig") if err != nil { panic(fmt.Sprintf("userconfig/config.go is invalid: %v", err)) } for _, typ := range p.Types { if typ.Name == "Config" { comments := make(map[string]string) // Extract comments for each field. structType := typ.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType) for _, f := range structType.Fields.List { if f.Doc == nil { continue } if len(f.Names) == 0 { panic("field has no name") } text := f.Doc.Text() for _, name := range f.Names { comments[name.Name] = text } } return comments } } panic("Config type not found in userconfig/config.go") })() ================================================ FILE: internal/userconfig/value.go ================================================ package userconfig import ( "fmt" "strconv" "strings" "github.com/cockroachdb/errors" ) type Kind int const ( String Kind = iota + 1 Bool Int Uint ) func (k Kind) String() string { switch k { case String: return "string" case Bool: return "bool" case Int: return "int" case Uint: return "uint" default: return "unknown kind" } } func (k Kind) HumanString() string { switch k { case String: return "a string" case Bool: return "a boolean (true/false)" case Int: return "an integer" case Uint: return "an unsigned integer (>=0)" default: return "an unknown kind" } } type Type struct { Kind Kind Default *any // nil means no default Oneof []any // nil means no restrictions } type Value struct { Val any Type Type } func (v Value) String() string { return RenderValue(v.Val) } func (t Type) ParseAndValidate(val string) (any, error) { parsed, err := t.Kind.parseValue(val) if err != nil { return nil, err } else if err := t.validate(parsed); err != nil { return nil, err } return parsed, nil } func (t Type) validate(val any) error { if val == nil { return errors.New("value cannot be nil") } if len(t.Oneof) > 0 { for _, v := range t.Oneof { if val == v { return nil } } strVal := fmt.Sprintf("%v", val) return errors.Errorf("value %q is not one of: %s", strVal, RenderOneof(t.Oneof)) } if k, ok := kindOf(val); ok { if k != t.Kind { return errors.Errorf("value v is not %s", t.Kind.HumanString()) } } return nil } func RenderValue(v any) string { return fmt.Sprintf("%v", v) } func RenderOneof(oneof []any) string { if len(oneof) == 0 { return "" } // Render as "a, b, or c" var s strings.Builder for i, v := range oneof { if i > 0 { if i == len(oneof)-1 { if len(oneof) > 2 { s.WriteString(", or ") } else { s.WriteString(" or ") } } else { s.WriteString(", ") } } s.WriteString(RenderValue(v)) } return s.String() } func (k Kind) parseValue(value string) (any, error) { switch k { case String: return value, nil case Bool: return strconv.ParseBool(value) case Int: return strconv.ParseInt(value, 10, 64) case Uint: return strconv.ParseUint(value, 10, 64) default: return nil, fmt.Errorf("unknown kind %v", k) } } func KindOf[T interface{ string | bool | int | uint }](val T) (k Kind, ok bool) { return kindOf(val) } func kindOf(val any) (k Kind, ok bool) { switch val.(type) { case string: return String, true case bool: return Bool, true case int: return Int, true case uint: return Uint, true default: return 0, false } } ================================================ FILE: internal/userconfig/write.go ================================================ package userconfig import ( "os" "path/filepath" "strings" "encr.dev/pkg/xos" "github.com/cockroachdb/errors" "github.com/pelletier/go-toml" ) func SetForApp(appRoot, key, value string) error { if _, err := os.Stat(appRoot); err != nil { return errors.Wrap(err, "app root directory does not exist") } dst := appFilePath(appRoot) return updateConfig(dst, key, value) } func SetGlobal(key, value string) error { if len(userPaths) == 0 { return errors.New("no global config file location found") } // Find the last path in the list that exists. for i := len(userPaths) - 1; i >= 0; i-- { if _, err := os.Stat(userPaths[i]); err == nil { return updateConfig(userPaths[i], key, value) } } // Otherwise fall back to the lowest-priority entry. dst := userPaths[0] return updateConfig(dst, key, value) } func updateConfig(dstPath, key, value string) error { desc, ok := descs[key] if !ok { return errors.Errorf("unknown key: %q", key) } val, err := desc.Type.ParseAndValidate(value) if err != nil { return err } // Read the existing config. // If it doesn't exist it's initialized to an emty config. var conf *toml.Tree { data, err := os.ReadFile(dstPath) if err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "failed to read existing config") } if data != nil { conf, err = toml.LoadBytes(data) } else { conf, err = toml.TreeFromMap(map[string]any{}) } if err != nil { return errors.Wrap(err, "failed to parse existing config") } } keys := strings.Split(key, ".") conf.SetPath(keys, val) // Write the config back out. data, err := conf.Marshal() if err != nil { return errors.Wrap(err, "failed to marshal config") } if err := validateConfig(data); err != nil { return errors.Wrap(err, "resulting config is invalid") } if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { return errors.Wrap(err, "failed to create config file") } if err := xos.WriteFile(dstPath, data, 0644); err != nil { return errors.Wrap(err, "failed to write config file") } return nil } ================================================ FILE: internal/version/version.go ================================================ package version import ( "crypto/sha256" "encoding/base64" "fmt" "runtime/debug" "strings" "golang.org/x/mod/semver" "encr.dev/internal/conf" "encr.dev/internal/env" ) // Version is the version of the encore binary. // It is set using `go build -ldflags "-X encr.dev/internal/version.Version=v1.2.3"`. var Version string // Channel tells us which ReleaseChannel this build of Encore is under var Channel ReleaseChannel type ReleaseChannel string const ( GA ReleaseChannel = "ga" // A general availability release of Encore in Semver: v1.10.0 Beta ReleaseChannel = "beta" // A beta build of an upcoming Encore release: v1.10.0-beta.1 Nightly ReleaseChannel = "nightly" // A nightly build of Encore with the date of the build: v1.10.0-nightly.20221231 DevBuild ReleaseChannel = "develop" // A development build of Encore with the commit of the build: v0.0.0-develop+0140ab0f78fd10d52673a961e900993b64b7b9e3 unknown ReleaseChannel = "unknown" // An unknown release stream (not exported as it should be an error case) ) // ConfigHash reports a hash of the configuration that affects the behavior of the daemon. // It is used to decide whether to restart the daemon. func ConfigHash() (string, error) { h := sha256.New() configDir, err := conf.Dir() if err != nil { return "", err } fmt.Fprintf(h, "APIBaseURL=%s\n", conf.APIBaseURL) fmt.Fprintf(h, "ConfigDir=%s\n", configDir) fmt.Fprintf(h, "EncoreDevDashListenAddr=%s\n", env.EncoreDevDashListenAddr().GetOrElse("")) fmt.Fprintf(h, "EncoreMCPSSEListenAddr=%s\n", env.EncoreMCPSSEListenAddr().GetOrElse("")) fmt.Fprintf(h, "EncoreObjectStorageListAddr=%s\n", env.EncoreObjectStorageListAddr().GetOrElse("")) digest := h.Sum(nil) return base64.RawURLEncoding.EncodeToString(digest), nil } func init() { // If version is already set via a compiler link flag, then we don't need to do anything if Version == "" { // Otherwise, we want to read the information from this built binary Version = "v0.0.0-develop" info, ok := debug.ReadBuildInfo() if !ok { return } // Add the commit info vcsVersion := "" vcsModified := "" for _, p := range info.Settings { switch p.Key { case "vcs.revision": vcsVersion = p.Value case "vcs.modified": if p.Value == "true" { vcsModified = "-modified" } } } if vcsVersion != "" { Version += "+" + vcsVersion + vcsModified } } Channel = ChannelFor(Version) } func ChannelFor(version string) ReleaseChannel { if !strings.HasPrefix(version, "v") { return unknown } // Now work out the release channel switch { case strings.Contains(version, "-beta."): return Beta case strings.Contains(version, "-nightly."): return Nightly case strings.HasSuffix(version, "-develop") || strings.Contains(version, "-develop+"): return DevBuild default: return GA } } // Compare compares this version of Encore against another version // accounting for the release channel. // // If the releases are from the same channel, then it returns: // - 0 if the versions are the same // - a negative number if this version is older than the other // - a positive number if this version is newer than the other // // If the releases are from different channels, it always returns 1. func Compare(againstVersion string) int { againstChannel := ChannelFor(againstVersion) if Channel != againstChannel { // If the channels are different, this "version" is always newer return 1 } switch Channel { case GA, Beta, Nightly: return semver.Compare(Version, againstVersion) case DevBuild: return 0 // devel versions are always the same default: return 0 // never newer if we can't test } } ================================================ FILE: miniredis/.gitignore ================================================ /target /testdata/*.key /testdata/*.crt /testdata/*.srl ================================================ FILE: miniredis/Cargo.toml ================================================ [package] name = "miniredis-rs" version = "0.1.0" edition = "2024" description = "Pure Rust in-memory Redis test server for use in integration tests" license = "MIT" [features] default = [] tls = ["dep:tokio-rustls", "dep:rustls", "dep:rustls-pemfile"] [dependencies] # Async runtime tokio = { version = "1", features = ["full"] } # Byte buffers (zero-copy) bytes = "1" # Ordered floats for BTreeMap keys (sorted sets) ordered-float = "5" # Float formatting (Redis-compatible) ryu = "1" # SHA1 for EVAL script caching sha1_smol = "1" # Regex for KEYS/SCAN pattern matching regex = "1" # Random number generation rand = "0.9" # TLS (optional) tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring"] } rustls = { version = "0.23", optional = true, default-features = false, features = ["ring", "logging", "std", "tls12"] } rustls-pemfile = { version = "2", optional = true } # Lua scripting mlua = { version = "0.11", features = ["lua51", "vendored", "send"] } [[bin]] name = "miniredis-rs-server" path = "src/bin/miniredis-rs-server.rs" required-features = ["tls"] [dev-dependencies] redis = { version = "1.0", features = ["tokio-comp", "aio"] } tokio = { version = "1", features = ["full", "test-util"] } futures-lite = "2" rcgen = "0.14" miniredis-rs = { path = ".", features = ["tls"] } ================================================ FILE: miniredis/MINIREDIS_LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2014 Harmen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: miniredis/src/bin/miniredis-rs-server.rs ================================================ //! A thin CLI wrapper around miniredis-rs that speaks enough redis-server //! config to be used as a drop-in replacement in the miniredis Go integration //! test suite. //! //! Config lines are read from stdin (same as `redis-server -`). //! Recognised directives: //! port – ignored (always binds to 0, prints actual port) //! bind – bind address (default 127.0.0.1) //! requirepass – set default-user password //! user on +@all ~* > – add ACL user //! tls-port – enable TLS listener (port is ignored, uses 0) //! tls-cert-file

– server certificate path //! tls-key-file

– server private key path //! tls-ca-cert-file

– CA / client certificate path //! appendonly … – silently ignored //! cluster-enabled … – silently ignored //! cluster-config-file … – silently ignored //! //! Once ready, the actual listening port is printed to stdout as a single line: //! PORT= //! //! The process exits cleanly on SIGTERM or SIGINT. use std::io::{self, BufRead}; use std::sync::Arc; use miniredis_rs::Miniredis; use tokio::signal::unix::{SignalKind, signal}; #[cfg(feature = "tls")] use std::fs; #[cfg(feature = "tls")] fn load_tls_config( cert_path: &str, key_path: &str, ca_cert_path: &str, ) -> Arc { let cert_pem = fs::read(cert_path).expect("read cert file"); let key_pem = fs::read(key_path).expect("read key file"); let ca_pem = fs::read(ca_cert_path).expect("read CA cert file"); let certs: Vec<_> = rustls_pemfile::certs(&mut &cert_pem[..]) .collect::, _>>() .expect("parse certs"); let key = rustls_pemfile::private_key(&mut &key_pem[..]) .expect("parse key") .expect("no key found"); let mut root_store = rustls::RootCertStore::empty(); for cert in rustls_pemfile::certs(&mut &ca_pem[..]) { root_store .add(cert.expect("parse CA cert")) .expect("add CA cert"); } let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) .build() .expect("build client verifier"); let config = rustls::ServerConfig::builder() .with_client_cert_verifier(verifier) .with_single_cert(certs, key) .expect("build TLS config"); Arc::new(config) } #[tokio::main] async fn main() { let mut bind_addr = "127.0.0.1".to_string(); let mut password: Option = None; let mut users: Vec<(String, String)> = Vec::new(); let mut tls_enabled = false; let mut tls_cert = String::new(); let mut tls_key = String::new(); let mut tls_ca_cert = String::new(); // Read config from stdin let stdin = io::stdin(); for line in stdin.lock().lines() { let line = line.expect("read stdin"); let line = line.trim().to_string(); if line.is_empty() || line.starts_with('#') { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); if parts.is_empty() { continue; } match parts[0].to_lowercase().as_str() { "port" => { /* ignored – always use port 0 */ } "bind" => { if parts.len() > 1 { bind_addr = parts[1].to_string(); } } "requirepass" => { if parts.len() > 1 { password = Some(parts[1].to_string()); } } "user" => { // user on +@all ~* > // or: user default on -@all +hello if parts.len() >= 2 { let username = parts[1].to_string(); // Find the >password token let mut pw = None; for part in &parts[2..] { if let Some(p) = part.strip_prefix('>') { pw = Some(p.to_string()); } } if let Some(p) = pw { users.push((username, p)); } // "user default on -@all +hello" (no password) is ignored } } "tls-port" => { tls_enabled = true; } "tls-cert-file" => { if parts.len() > 1 { tls_cert = parts[1].to_string(); } } "tls-key-file" => { if parts.len() > 1 { tls_key = parts[1].to_string(); } } "tls-ca-cert-file" => { if parts.len() > 1 { tls_ca_cert = parts[1].to_string(); } } // Silently ignore everything else _ => {} } } let m = if tls_enabled { #[cfg(feature = "tls")] { let tls_config = load_tls_config(&tls_cert, &tls_key, &tls_ca_cert); Miniredis::run_tls_addr(&format!("{}:0", bind_addr), tls_config) .await .expect("start TLS server") } #[cfg(not(feature = "tls"))] { panic!("TLS requested but binary was compiled without tls feature"); } } else { Miniredis::run_addr(&format!("{}:0", bind_addr)) .await .expect("start server") }; // Set up authentication if let Some(pw) = &password { m.require_auth(pw); } for (user, pw) in &users { m.require_user_auth(user, pw); } // Print the port – the Go test harness reads this as readiness signal. println!("PORT={}", m.port()); // Wait for SIGTERM or SIGINT let mut sigterm = signal(SignalKind::terminate()).expect("signal handler"); let mut sigint = signal(SignalKind::interrupt()).expect("signal handler"); tokio::select! { _ = sigterm.recv() => {} _ = sigint.recv() => {} } m.close().await; } ================================================ FILE: miniredis/src/cmd/client.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::CommandTable; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("CLIENT", cmd_client, false, -2); } fn cmd_client(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "SETNAME" => { if args.len() != 2 { return Frame::error("ERR wrong number of arguments for 'client|setname' command"); } let name = String::from_utf8_lossy(&args[1]).to_string(); if name.contains(' ') || name.contains('\n') { return Frame::error( "ERR Client names cannot contain spaces, newlines or special characters.", ); } ctx.client_name = if name.is_empty() { None } else { Some(name) }; Frame::ok() } "GETNAME" => { if args.len() != 1 { return Frame::error("ERR wrong number of arguments for 'client|getname' command"); } match &ctx.client_name { Some(name) => Frame::Bulk(name.clone().into()), None => Frame::Null, } } _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try CLIENT HELP.", subcmd.to_lowercase() )), } } ================================================ FILE: miniredis/src/cmd/cluster.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::CommandTable; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("CLUSTER", cmd_cluster, true, -2); } fn cmd_cluster(_state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "SLOTS" => { // Single-node cluster: one slot range 0-16383 Frame::Array(vec![Frame::Array(vec![ Frame::Integer(0), Frame::Integer(16383), Frame::Array(vec![ Frame::Bulk("127.0.0.1".into()), Frame::Integer(6379), Frame::Bulk( "09dbe9720cda62f7865eabc5fd8857c5d2678366".into(), ), ]), ])]) } "KEYSLOT" => { if args.len() != 2 { return Frame::error( "ERR wrong number of arguments for 'cluster|keyslot' command", ); } // Simplified: always return 163 Frame::Integer(163) } "NODES" => { Frame::Bulk( "e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 127.0.0.1:6379@6379 myself,master - 0 0 1 connected 0-16383\n" .into(), ) } "SHARDS" => { // Simplified shard info as a flat array Frame::Array(vec![Frame::Array(vec![ Frame::Bulk("slots".into()), Frame::Array(vec![Frame::Integer(0), Frame::Integer(16383)]), Frame::Bulk("nodes".into()), Frame::Array(vec![Frame::Array(vec![ Frame::Bulk("id".into()), Frame::Bulk( "13f84e686106847b76671957dd348fde540a77bb".into(), ), Frame::Bulk("ip".into()), Frame::Bulk("127.0.0.1".into()), Frame::Bulk("port".into()), Frame::Integer(6379), Frame::Bulk("role".into()), Frame::Bulk("master".into()), Frame::Bulk("replication-offset".into()), Frame::Integer(0), Frame::Bulk("health".into()), Frame::Bulk("online".into()), ])]), ])]) } _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try CLUSTER HELP.", subcmd.to_lowercase() )), } } ================================================ FILE: miniredis/src/cmd/connection.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_DB_INDEX_OUT_OF_RANGE, MSG_SYNTAX_ERROR, err_wrong_number, }; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("PING", cmd_ping, true, -1); table.add("ECHO", cmd_echo, true, 2); table.add("QUIT", cmd_quit, true, 1); table.add("SELECT", cmd_select, true, 2); table.add("AUTH", cmd_auth, false, -2); table.add("HELLO", cmd_hello, false, -1); } /// PING [message] fn cmd_ping(_state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { match args.len() { 0 => Frame::Simple("PONG".into()), 1 => Frame::Bulk(args[0].clone().into()), _ => Frame::error(err_wrong_number("ping")), } } /// ECHO message fn cmd_echo(_state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { Frame::Bulk(args[0].clone().into()) } /// QUIT fn cmd_quit(_state: &Arc, _ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { Frame::ok() } /// SELECT db fn cmd_select(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let db_str = match std::str::from_utf8(&args[0]) { Ok(s) => s, Err(_) => return Frame::error(crate::dispatch::MSG_INVALID_INT), }; let db: i64 = match db_str.parse() { Ok(n) => n, Err(_) => return Frame::error(crate::dispatch::MSG_INVALID_INT), }; if !(0..16).contains(&db) { return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE); } ctx.selected_db = db as usize; Frame::ok() } /// AUTH [username] password fn cmd_auth(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 2 { return Frame::error(MSG_SYNTAX_ERROR); } let (username, password) = if args.len() == 2 { ( String::from_utf8_lossy(&args[0]).to_string(), String::from_utf8_lossy(&args[1]).to_string(), ) } else { ( "default".to_string(), String::from_utf8_lossy(&args[0]).to_string(), ) }; let inner = state.lock(); if inner.passwords.is_empty() && username == "default" { return Frame::error( "ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?", ); } match inner.passwords.get(&username) { Some(pw) if pw == &password => { ctx.authenticated = true; Frame::ok() } _ => Frame::error("WRONGPASS invalid username-password pair"), } } /// HELLO protover [AUTH username password] [SETNAME clientname] fn cmd_hello(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.is_empty() { return Frame::error(err_wrong_number("hello")); } // Parse protocol version let version: i64 = match std::str::from_utf8(&args[0]) .ok() .and_then(|s| s.parse().ok()) { Some(v) => v, None => { return Frame::error("ERR Protocol version is not an integer or out of range"); } }; if version != 2 && version != 3 { return Frame::error("NOPROTO unsupported protocol version"); } // Parse optional AUTH and SETNAME let mut check_auth = false; let mut username = "default".to_string(); let mut password = String::new(); let mut i = 1; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "AUTH" => { if i + 2 >= args.len() { return Frame::error(format!( "ERR Syntax error in HELLO option '{}'", String::from_utf8_lossy(&args[i]) )); } username = String::from_utf8_lossy(&args[i + 1]).to_string(); password = String::from_utf8_lossy(&args[i + 2]).to_string(); check_auth = true; i += 3; } "SETNAME" => { if i + 1 >= args.len() { return Frame::error(format!( "ERR Syntax error in HELLO option '{}'", String::from_utf8_lossy(&args[i]) )); } ctx.client_name = Some(String::from_utf8_lossy(&args[i + 1]).to_string()); i += 2; } _ => { return Frame::error(format!( "ERR Syntax error in HELLO option '{}'", String::from_utf8_lossy(&args[i]) )); } } } // Check authentication if AUTH was provided let inner = state.lock(); if inner.passwords.is_empty() && username == "default" { check_auth = false; } if check_auth { match inner.passwords.get(&username) { Some(pw) if pw == &password => { ctx.authenticated = true; } _ => { return Frame::error("WRONGPASS invalid username-password pair"); } } } // Set RESP3 mode if version is 3 ctx.resp3 = version == 3; // Return server info as a map Frame::Map(vec![ ( Frame::bulk_string("server"), Frame::bulk_string("miniredis"), ), (Frame::bulk_string("version"), Frame::bulk_string("8.4.0")), (Frame::bulk_string("proto"), Frame::Integer(version)), (Frame::bulk_string("id"), Frame::Integer(42)), (Frame::bulk_string("mode"), Frame::bulk_string("standalone")), (Frame::bulk_string("role"), Frame::bulk_string("master")), ( Frame::bulk_string("modules"), Frame::Array(vec![Frame::Map(vec![ (Frame::bulk_string("name"), Frame::bulk_string("vectorset")), (Frame::bulk_string("ver"), Frame::Integer(1)), (Frame::bulk_string("path"), Frame::bulk_string("")), (Frame::bulk_string("args"), Frame::Array(vec![])), ])]), ), ]) } ================================================ FILE: miniredis/src/cmd/generic.rs ================================================ use std::sync::Arc; use std::time::Duration; use rand::Rng; use super::parse_int; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_DB_INDEX_OUT_OF_RANGE, MSG_INVALID_CURSOR, MSG_INVALID_INT, MSG_KEY_NOT_FOUND, MSG_SYNTAX_ERROR, MSG_TIMEOUT_NEGATIVE, err_wrong_number, }; use crate::frame::Frame; use crate::types::KeyType; pub fn register(table: &mut CommandTable) { table.add("DEL", cmd_del, false, -2); table.add("UNLINK", cmd_del, false, -2); // alias table.add("EXISTS", cmd_exists, true, -2); table.add("TYPE", cmd_type, true, 2); table.add("RENAME", cmd_rename, false, 3); table.add("RENAMENX", cmd_renamenx, false, 3); table.add("EXPIRE", cmd_expire, false, -3); table.add("EXPIREAT", cmd_expireat, false, -3); table.add("PEXPIRE", cmd_pexpire, false, -3); table.add("PEXPIREAT", cmd_pexpireat, false, -3); table.add("PERSIST", cmd_persist, false, 2); table.add("TTL", cmd_ttl, true, 2); table.add("PTTL", cmd_pttl, true, 2); table.add("KEYS", cmd_keys, true, 2); table.add("SCAN", cmd_scan, true, -2); table.add("TOUCH", cmd_touch, true, -2); table.add("WAIT", cmd_wait, true, 3); table.add("RANDOMKEY", cmd_randomkey, true, 1); table.add("OBJECT", cmd_object, true, -2); table.add("EXPIRETIME", cmd_expiretime, true, 2); table.add("PEXPIRETIME", cmd_pexpiretime, true, 2); table.add("COPY", cmd_copy, false, -3); table.add("MOVE", cmd_move, false, 3); table.add("DUMP", cmd_dump, true, 2); table.add("RESTORE", cmd_restore, false, -4); } /// DEL key [key ...] fn cmd_del(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); let mut count = 0i64; for arg in args { let key = String::from_utf8_lossy(arg); db.check_ttl(&key); if db.del(&key) { count += 1; } } Frame::Integer(count) } /// EXISTS key [key ...] fn cmd_exists(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); let mut count = 0i64; for arg in args { let key = String::from_utf8_lossy(arg); db.check_ttl(&key); if db.exists(&key, now) { count += 1; } } Frame::Integer(count) } /// TYPE key fn cmd_type(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); let t = match db.key_type(&key) { Some(t) => t.as_str(), None => "none", }; Frame::Simple(t.to_owned()) } /// RENAME key newkey fn cmd_rename(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let from = String::from_utf8_lossy(&args[0]).into_owned(); let to = String::from_utf8_lossy(&args[1]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if !db.keys.contains_key(&from) { return Frame::error(MSG_KEY_NOT_FOUND); } db.rename(&from, &to, now); Frame::ok() } /// RENAMENX key newkey fn cmd_renamenx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let from = String::from_utf8_lossy(&args[0]).into_owned(); let to = String::from_utf8_lossy(&args[1]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if !db.keys.contains_key(&from) { return Frame::error(MSG_KEY_NOT_FOUND); } if db.keys.contains_key(&to) { return Frame::Integer(0); } db.rename(&from, &to, now); Frame::Integer(1) } /// EXPIRE key seconds [NX|XX|GT|LT] fn cmd_expire(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { expire_impl(state, ctx, args, |secs, _| { if secs <= 0 { Duration::ZERO } else { Duration::from_secs(secs as u64) } }) } /// EXPIREAT key timestamp [NX|XX|GT|LT] fn cmd_expireat(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { expire_impl(state, ctx, args, |ts, now| { if ts <= 0 { return Duration::ZERO; } let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64); target.duration_since(now).unwrap_or(Duration::ZERO) }) } /// PEXPIRE key milliseconds [NX|XX|GT|LT] fn cmd_pexpire(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { expire_impl(state, ctx, args, |ms, _| { if ms <= 0 { Duration::ZERO } else { Duration::from_millis(ms as u64) } }) } /// PEXPIREAT key timestamp-ms [NX|XX|GT|LT] fn cmd_pexpireat(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { expire_impl(state, ctx, args, |ts, now| { if ts <= 0 { return Duration::ZERO; } let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64); target.duration_since(now).unwrap_or(Duration::ZERO) }) } fn expire_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], to_duration: impl Fn(i64, std::time::SystemTime) -> Duration, ) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let value: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; // Parse optional flags (multiple can be combined, e.g. GT XX) let mut nx = false; let mut xx = false; let mut gt = false; let mut lt = false; for arg in &args[2..] { let flag = String::from_utf8_lossy(arg); match flag.to_uppercase().as_str() { "NX" => nx = true, "XX" => xx = true, "GT" => gt = true, "LT" => lt = true, _ => return Frame::error(format!("ERR Unsupported option {}", flag)), } } // NX is incompatible with GT/LT; GT and LT are mutually exclusive if nx && (gt || lt) { return Frame::error("ERR NX and XX, GT or LT options at the same time are not compatible"); } if gt && lt { return Frame::error("ERR GT and LT options at the same time are not compatible"); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Integer(0); } let new_ttl = to_duration(value, now); let has_ttl = db.ttl.contains_key(&key); // NX: only if no existing TTL if nx && has_ttl { return Frame::Integer(0); } // XX: only if has existing TTL if xx && !has_ttl { return Frame::Integer(0); } // GT: only if new TTL is greater (no TTL = infinite > any finite TTL) if gt { if let Some(&old_ttl) = db.ttl.get(&key) { if new_ttl <= old_ttl { return Frame::Integer(0); } } else { // No existing TTL means infinite lifetime, which is > any finite TTL return Frame::Integer(0); } } // LT: only if new TTL is less if lt { if let Some(&old_ttl) = db.ttl.get(&key) { if new_ttl >= old_ttl { return Frame::Integer(0); } } else { // No existing TTL, LT always applies } } db.ttl.insert(key.clone(), new_ttl); db.incr_version(&key, now); // Check if key already expired db.check_ttl(&key); Frame::Integer(1) } /// PERSIST key fn cmd_persist(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if !db.keys.contains_key(&key) { return Frame::Integer(0); } if db.ttl.remove(&key).is_some() { db.incr_version(&key, now); Frame::Integer(1) } else { Frame::Integer(0) } } /// TTL key fn cmd_ttl(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(-2); } match db.ttl.get(key.as_ref()) { Some(ttl) => Frame::Integer(ttl.as_secs() as i64), None => Frame::Integer(-1), } } /// PTTL key fn cmd_pttl(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(-2); } match db.ttl.get(key.as_ref()) { Some(ttl) => Frame::Integer(ttl.as_millis() as i64), None => Frame::Integer(-1), } } /// KEYS pattern fn cmd_keys(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let pattern = String::from_utf8_lossy(&args[0]); let inner = state.lock(); let db = inner.db(ctx.selected_db); let all_keys = db.all_keys(); let matched = match_keys(&all_keys, &pattern); Frame::Array(matched.into_iter().map(|k| Frame::Bulk(k.into())).collect()) } /// SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] fn cmd_scan(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let cursor: i64 = match parse_int(&args[0]) { Some(n) => n, None => return Frame::error(MSG_INVALID_CURSOR), }; let opts = match super::parse_scan_opts(&args[1..], true) { Ok(o) => o, Err(e) => return e, }; let inner = state.lock(); let db = inner.db(ctx.selected_db); let mut all_keys = db.all_keys(); // Filter by type if let Some(ref tf) = opts.type_filter { all_keys.retain(|k| { db.key_type(k) .map(|t| t.as_str() == tf.as_str()) .unwrap_or(false) }); } // Filter by pattern let matched = if let Some(ref pat) = opts.pattern { match_keys(&all_keys, pat) } else { all_keys }; // Simple implementation: return all results at once, no real cursor pagination if cursor != 0 { return Frame::Array(vec![Frame::Bulk("0".into()), Frame::Array(vec![])]); } Frame::Array(vec![ Frame::Bulk("0".into()), Frame::Array(matched.into_iter().map(|k| Frame::Bulk(k.into())).collect()), ]) } /// TOUCH key [key ...] fn cmd_touch(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let inner = state.lock(); let db = inner.db(ctx.selected_db); let mut count = 0i64; for arg in args { let key = String::from_utf8_lossy(arg); if db.keys.contains_key(key.as_ref()) { count += 1; } } Frame::Integer(count) } /// WAIT numreplicas timeout — always returns 0 (standalone) fn cmd_wait(_state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let _replicas: i64 = match parse_int(&args[0]) { Some(n) if n >= 0 => n, _ => return Frame::error(MSG_INVALID_INT), }; let timeout: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if timeout < 0 { return Frame::error(MSG_TIMEOUT_NEGATIVE); } Frame::Integer(0) } /// RANDOMKEY fn cmd_randomkey(state: &Arc, ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { let mut inner = state.lock(); let key_count = inner.db(ctx.selected_db).keys.len(); if key_count == 0 { return Frame::Null; } let idx = inner.rng.random_range(0..key_count); let key = inner .db(ctx.selected_db) .keys .keys() .nth(idx) .unwrap() .clone(); Frame::Bulk(key.into()) } /// OBJECT subcommand ... fn cmd_object(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let sub = String::from_utf8_lossy(&args[0]).to_uppercase(); match sub.as_str() { "HELP" => Frame::Array(vec![Frame::Bulk("OBJECT subcommand [arguments]".into())]), "ENCODING" => { if args.len() != 2 { return Frame::error(err_wrong_number("object|encoding")); } let key = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::error(MSG_KEY_NOT_FOUND); } // Stub: always return "raw" Frame::Bulk("raw".into()) } "IDLETIME" => { if args.len() != 2 { return Frame::error(err_wrong_number("object|idletime")); } let key = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::error(MSG_KEY_NOT_FOUND); } Frame::Integer(0) } "REFCOUNT" => { if args.len() != 2 { return Frame::error(err_wrong_number("object|refcount")); } let key = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::error(MSG_KEY_NOT_FOUND); } Frame::Integer(1) } "FREQ" => { if args.len() != 2 { return Frame::error(err_wrong_number("object|freq")); } let key = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::error(MSG_KEY_NOT_FOUND); } Frame::Integer(0) } _ => Frame::error(format!( "ERR unknown subcommand or wrong number of arguments for 'object|{}' command", sub.to_lowercase() )), } } /// EXPIRETIME key fn cmd_expiretime(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(-2); } match db.ttl.get(key.as_ref()) { Some(ttl) => { let expire_at = now + *ttl; let secs = expire_at .duration_since(std::time::UNIX_EPOCH) .unwrap_or(Duration::ZERO) .as_secs(); Frame::Integer(secs as i64) } None => Frame::Integer(-1), } } /// PEXPIRETIME key fn cmd_pexpiretime(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(-2); } match db.ttl.get(key.as_ref()) { Some(ttl) => { let expire_at = now + *ttl; let ms = expire_at .duration_since(std::time::UNIX_EPOCH) .unwrap_or(Duration::ZERO) .as_millis(); Frame::Integer(ms as i64) } None => Frame::Integer(-1), } } /// COPY source destination [DB db] [REPLACE] fn cmd_copy(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let mut dest_db = ctx.selected_db; let mut replace = false; let mut i = 2; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "DB" | "DESTINATION" => { i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } match parse_int(&args[i]) { Some(n) if (0..16).contains(&n) => dest_db = n as usize, Some(_) => return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE), None => return Frame::error(MSG_INVALID_INT), } } "REPLACE" => replace = true, _ => return Frame::error(MSG_SYNTAX_ERROR), } i += 1; } let mut inner = state.lock(); let now = inner.effective_now(); // COPY to self on same DB: error (checked before key existence) if ctx.selected_db == dest_db && src == dst { return Frame::error("ERR source and destination objects are the same"); } // Check source exists { let src_db = inner.db_mut(ctx.selected_db); src_db.check_ttl(&src); if !src_db.keys.contains_key(&src) { return Frame::Integer(0); } } // Check destination { let dst_db = inner.db_mut(dest_db); dst_db.check_ttl(&dst); if dst_db.keys.contains_key(&dst) && !replace { return Frame::Integer(0); } if replace { dst_db.del(&dst); } } if ctx.selected_db == dest_db { // Same DB: use copy_key let db = inner.db_mut(ctx.selected_db); db.copy_key(&src, &dst, now); } else { // Cross-DB copy: manually clone data let key_type = *inner.db(ctx.selected_db).keys.get(&src).unwrap(); let ttl = inner.db(ctx.selected_db).ttl.get(&src).copied(); match key_type { KeyType::String => { let val = inner.db(ctx.selected_db).string_keys.get(&src).cloned(); if let Some(v) = val { inner.db_mut(dest_db).string_set(&dst, v, now); } } KeyType::Hash => { let val = inner.db(ctx.selected_db).hash_keys.get(&src).cloned(); if let Some(v) = val { inner .db_mut(dest_db) .keys .insert(dst.clone(), KeyType::Hash); inner.db_mut(dest_db).hash_keys.insert(dst.clone(), v); inner.db_mut(dest_db).incr_version(&dst, now); } } KeyType::List => { let val = inner.db(ctx.selected_db).list_keys.get(&src).cloned(); if let Some(v) = val { inner .db_mut(dest_db) .keys .insert(dst.clone(), KeyType::List); inner.db_mut(dest_db).list_keys.insert(dst.clone(), v); inner.db_mut(dest_db).incr_version(&dst, now); } } KeyType::Set => { let val = inner.db(ctx.selected_db).set_keys.get(&src).cloned(); if let Some(v) = val { inner.db_mut(dest_db).set_set(&dst, v, now); } } KeyType::SortedSet => { let val = inner.db(ctx.selected_db).sorted_set_keys.get(&src).cloned(); if let Some(v) = val { inner.db_mut(dest_db).sset_set(&dst, v, now); } } KeyType::Stream => { let val = inner.db(ctx.selected_db).stream_keys.get(&src).cloned(); if let Some(v) = val { inner .db_mut(dest_db) .keys .insert(dst.clone(), KeyType::Stream); inner.db_mut(dest_db).stream_keys.insert(dst.clone(), v); inner.db_mut(dest_db).incr_version(&dst, now); } } KeyType::HyperLogLog => { let val = inner.db(ctx.selected_db).hll_keys.get(&src).cloned(); if let Some(v) = val { inner .db_mut(dest_db) .keys .insert(dst.clone(), KeyType::HyperLogLog); inner.db_mut(dest_db).hll_keys.insert(dst.clone(), v); inner.db_mut(dest_db).incr_version(&dst, now); } } } if let Some(ttl) = ttl { inner.db_mut(dest_db).ttl.insert(dst, ttl); } } Frame::Integer(1) } /// MOVE key db fn cmd_move(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let target_db = match parse_int(&args[1]) { Some(n) if (0..16).contains(&n) => n as usize, _ => return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE), }; if target_db == ctx.selected_db { return Frame::error("ERR source and destination objects are the same"); } let mut inner = state.lock(); let now = inner.effective_now(); // Check source exists { let src_db = inner.db_mut(ctx.selected_db); src_db.check_ttl(&key); if !src_db.keys.contains_key(&key) { return Frame::Integer(0); } } // Check target doesn't have the key { let dst_db = inner.db_mut(target_db); dst_db.check_ttl(&key); if dst_db.keys.contains_key(&key) { return Frame::Integer(0); } } // Copy to target, then delete from source let key_type = *inner.db(ctx.selected_db).keys.get(&key).unwrap(); let ttl = inner.db(ctx.selected_db).ttl.get(&key).copied(); match key_type { KeyType::String => { let val = inner.db(ctx.selected_db).string_keys.get(&key).cloned(); if let Some(v) = val { inner.db_mut(target_db).string_set(&key, v, now); } } KeyType::Hash => { let val = inner.db(ctx.selected_db).hash_keys.get(&key).cloned(); if let Some(v) = val { inner .db_mut(target_db) .keys .insert(key.clone(), KeyType::Hash); inner.db_mut(target_db).hash_keys.insert(key.clone(), v); inner.db_mut(target_db).incr_version(&key, now); } } KeyType::List => { let val = inner.db(ctx.selected_db).list_keys.get(&key).cloned(); if let Some(v) = val { inner .db_mut(target_db) .keys .insert(key.clone(), KeyType::List); inner.db_mut(target_db).list_keys.insert(key.clone(), v); inner.db_mut(target_db).incr_version(&key, now); } } KeyType::Set => { let val = inner.db(ctx.selected_db).set_keys.get(&key).cloned(); if let Some(v) = val { inner.db_mut(target_db).set_set(&key, v, now); } } KeyType::SortedSet => { let val = inner.db(ctx.selected_db).sorted_set_keys.get(&key).cloned(); if let Some(v) = val { inner.db_mut(target_db).sset_set(&key, v, now); } } KeyType::Stream => { let val = inner.db(ctx.selected_db).stream_keys.get(&key).cloned(); if let Some(v) = val { inner .db_mut(target_db) .keys .insert(key.clone(), KeyType::Stream); inner.db_mut(target_db).stream_keys.insert(key.clone(), v); inner.db_mut(target_db).incr_version(&key, now); } } KeyType::HyperLogLog => { let val = inner.db(ctx.selected_db).hll_keys.get(&key).cloned(); if let Some(v) = val { inner .db_mut(target_db) .keys .insert(key.clone(), KeyType::HyperLogLog); inner.db_mut(target_db).hll_keys.insert(key.clone(), v); inner.db_mut(target_db).incr_version(&key, now); } } } if let Some(ttl) = ttl { inner.db_mut(target_db).ttl.insert(key.clone(), ttl); } // Delete from source inner.db_mut(ctx.selected_db).del(&key); Frame::Integer(1) } /// DUMP key — stub: returns raw string value or null fn cmd_dump(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Null; } // Stub: only dump string values match db.key_type(&key) { Some(KeyType::String) => match db.string_get(&key) { Some(val) => Frame::Bulk(val.clone().into()), None => Frame::Null, }, _ => Frame::Null, } } /// RESTORE key ttl serialized-value [REPLACE] fn cmd_restore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let ttl_ms: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let value = args[2].clone(); let mut replace = false; for arg in &args[3..] { let opt = String::from_utf8_lossy(arg).to_uppercase(); if opt == "REPLACE" { replace = true; } } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if db.keys.contains_key(&key) { if !replace { return Frame::error("BUSYKEY Target key name already exists."); } db.del(&key); } // Stub: store as string value db.string_set(&key, value, now); if ttl_ms > 0 { db.ttl.insert(key, Duration::from_millis(ttl_ms as u64)); } Frame::ok() } // ── Pattern matching ───────────────────────────────────────────────── /// Match keys against a glob-style pattern (like Redis KEYS/SCAN). fn match_keys(keys: &[String], pattern: &str) -> Vec { if pattern == "*" { return keys.to_vec(); } keys.iter() .filter(|k| crate::keys::glob_match(pattern, k)) .cloned() .collect() } ================================================ FILE: miniredis/src/cmd/geo.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INVALID_INT, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, err_wrong_number, }; use crate::frame::Frame; use crate::geo::{from_geohash, haversine_distance, parse_unit, to_geohash}; use crate::types::{Direction, KeyType}; const MSG_UNSUPPORTED_UNIT: &str = "ERR unsupported unit provided. please use M, KM, FT, MI"; pub fn register(table: &mut CommandTable) { table.add("GEOADD", cmd_geoadd, false, -5); table.add("GEODIST", cmd_geodist, true, -4); table.add("GEOPOS", cmd_geopos, true, -2); table.add("GEORADIUS", cmd_georadius, false, -6); table.add("GEORADIUS_RO", cmd_georadius_ro, true, -6); table.add("GEORADIUSBYMEMBER", cmd_georadiusbymember, false, -5); table.add("GEORADIUSBYMEMBER_RO", cmd_georadiusbymember_ro, true, -5); } /// GEOADD key longitude latitude member [longitude latitude member ...] fn cmd_geoadd(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = to_str(&args[0]); let triplets = &args[1..]; if !triplets.len().is_multiple_of(3) { return Frame::error(err_wrong_number("geoadd")); } let mut entries = Vec::new(); let mut i = 0; while i + 2 < triplets.len() { let raw_long = to_str(&triplets[i]); let raw_lat = to_str(&triplets[i + 1]); let name = to_str(&triplets[i + 2]); i += 3; let longitude: f64 = match raw_long.parse() { Ok(v) => v, Err(_) => return Frame::error("ERR value is not a valid float"), }; let latitude: f64 = match raw_lat.parse() { Ok(v) => v, Err(_) => return Frame::error("ERR value is not a valid float"), }; if !(-85.05112878..=85.05112878).contains(&latitude) || !(-180.0..=180.0).contains(&longitude) { return Frame::error(format!( "ERR invalid longitude,latitude pair {:.6},{:.6}", longitude, latitude )); } let score = to_geohash(longitude, latitude) as f64; entries.push((name, score)); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let mut added = 0i64; for (name, score) in &entries { if db.sset_add(&key, *score, name, now) { added += 1; } } Frame::Integer(added) } /// GEODIST key member1 member2 [unit] fn cmd_geodist(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = to_str(&args[0]); let from = to_str(&args[1]); let to = to_str(&args[2]); let remaining = &args[3..]; let unit = if !remaining.is_empty() { to_str(&remaining[0]) } else { "m".to_string() }; if remaining.len() > 1 { return Frame::error(MSG_SYNTAX_ERROR); } let to_meter = match parse_unit(&unit) { Some(v) => v, None => return Frame::error(MSG_UNSUPPORTED_UNIT), }; let inner = state.lock(); let db = inner.db(ctx.selected_db); if !db.keys.contains_key(&key) { return Frame::Null; } if db.keys.get(&key) != Some(&KeyType::SortedSet) { return Frame::error(MSG_WRONG_TYPE); } let from_score = match db.sset_score(&key, &from) { Some(s) => s, None => return Frame::Null, }; let to_score = match db.sset_score(&key, &to) { Some(s) => s, None => return Frame::Null, }; let (from_lng, from_lat) = from_geohash(from_score as u64); let (to_lng, to_lat) = from_geohash(to_score as u64); let dist = haversine_distance(from_lat, from_lng, to_lat, to_lng) / to_meter; Frame::Bulk(format!("{:.4}", dist).into()) } /// GEOPOS key member [member ...] fn cmd_geopos(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = to_str(&args[0]); let members = &args[1..]; let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let mut results = Vec::with_capacity(members.len()); for member_arg in members { let member = to_str(member_arg); match db.sset_score(&key, &member) { Some(score) => { let (lng, lat) = from_geohash(score as u64); results.push(Frame::Array(vec![ Frame::Bulk(format!("{:.6}", lng).into()), Frame::Bulk(format!("{:.6}", lat).into()), ])); } None => { results.push(Frame::NullArray); } } } Frame::Array(results) } // ── Shared radius search types and helpers ────────────────────────── struct GeoMatch { name: String, score: f64, distance: f64, longitude: f64, latitude: f64, } #[derive(PartialEq)] enum SortDir { Unsorted, Asc, Desc, } struct RadiusOpts { with_dist: bool, with_coord: bool, direction: SortDir, count: usize, store_key: Option, storedist_key: Option, } fn within_radius( state: &Arc, db_idx: usize, key: &str, longitude: f64, latitude: f64, radius_meters: f64, ) -> Vec { let inner = state.lock(); let db = inner.db(db_idx); let ss = match db.sorted_set_keys.get(key) { Some(ss) => ss, None => return Vec::new(), }; let elems = ss.by_score(Direction::Asc); let mut matches = Vec::new(); for el in &elems { let (el_lng, el_lat) = from_geohash(el.score as u64); let d = haversine_distance(latitude, longitude, el_lat, el_lng); if d <= radius_meters { matches.push(GeoMatch { name: el.member.clone(), score: el.score, distance: d, longitude: el_lng, latitude: el_lat, }); } } matches } fn parse_radius_opts(args: &[Vec], read_only: bool) -> Result { let mut opts = RadiusOpts { with_dist: false, with_coord: false, direction: SortDir::Unsorted, count: 0, store_key: None, storedist_key: None, }; let mut i = 0; while i < args.len() { let arg = to_str(&args[i]).to_uppercase(); match arg.as_str() { "WITHCOORD" => opts.with_coord = true, "WITHDIST" => opts.with_dist = true, "ASC" => opts.direction = SortDir::Asc, "DESC" => opts.direction = SortDir::Desc, "COUNT" => { i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } let n: i64 = match to_str(&args[i]).parse() { Ok(v) => v, Err(_) => return Err(Frame::error(MSG_INVALID_INT)), }; if n <= 0 { return Err(Frame::error("ERR COUNT must be > 0")); } opts.count = n as usize; } "STORE" => { if read_only { return Err(Frame::error(MSG_SYNTAX_ERROR)); } i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } opts.store_key = Some(to_str(&args[i])); } "STOREDIST" => { if read_only { return Err(Frame::error(MSG_SYNTAX_ERROR)); } i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } opts.storedist_key = Some(to_str(&args[i])); } _ => return Err(Frame::error(MSG_SYNTAX_ERROR)), } i += 1; } Ok(opts) } fn format_radius_results(matches: &[GeoMatch], opts: &RadiusOpts, to_meter: f64) -> Frame { let mut frames = Vec::with_capacity(matches.len()); for m in matches { if !opts.with_dist && !opts.with_coord { frames.push(Frame::bulk_string(&m.name)); } else { let mut inner = Vec::new(); inner.push(Frame::bulk_string(&m.name)); if opts.with_dist { inner.push(Frame::Bulk(format!("{:.4}", m.distance / to_meter).into())); } if opts.with_coord { inner.push(Frame::Array(vec![ Frame::Bulk(format!("{:.6}", m.longitude).into()), Frame::Bulk(format!("{:.6}", m.latitude).into()), ])); } frames.push(Frame::Array(inner)); } } Frame::Array(frames) } fn apply_sort_and_count(matches: &mut Vec, opts: &RadiusOpts) { if opts.direction != SortDir::Unsorted { matches.sort_by(|a, b| { if opts.direction == SortDir::Desc { b.distance .partial_cmp(&a.distance) .unwrap_or(std::cmp::Ordering::Equal) } else { a.distance .partial_cmp(&b.distance) .unwrap_or(std::cmp::Ordering::Equal) } }); } if opts.count > 0 && matches.len() > opts.count { matches.truncate(opts.count); } } // ── GEORADIUS / GEORADIUS_RO ──────────────────────────────────────── fn cmd_georadius_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], read_only: bool, cmd_name: &str, ) -> Frame { let key = to_str(&args[0]); let longitude: f64 = match to_str(&args[1]).parse() { Ok(v) => v, Err(_) => return Frame::error(err_wrong_number(cmd_name)), }; let latitude: f64 = match to_str(&args[2]).parse() { Ok(v) => v, Err(_) => return Frame::error(err_wrong_number(cmd_name)), }; let radius: f64 = match to_str(&args[3]).parse() { Ok(v) if v >= 0.0 => v, _ => return Frame::error(err_wrong_number(cmd_name)), }; let to_meter = match parse_unit(&to_str(&args[4])) { Some(v) => v, None => return Frame::error(err_wrong_number(cmd_name)), }; let opts = match parse_radius_opts(&args[5..], read_only) { Ok(o) => o, Err(e) => return e, }; // Check STORE/STOREDIST incompatibility with WITHDIST/WITHCOORD if (opts.store_key.is_some() || opts.storedist_key.is_some()) && (opts.with_dist || opts.with_coord) { return Frame::error( "ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options", ); } let mut matches = within_radius( state, ctx.selected_db, &key, longitude, latitude, radius * to_meter, ); apply_sort_and_count(&mut matches, &opts); // Handle STORE if let Some(ref store_key) = opts.store_key { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(store_key); for m in &matches { db.sset_add(store_key, m.score, &m.name, now); } return Frame::Integer(matches.len() as i64); } // Handle STOREDIST if let Some(ref storedist_key) = opts.storedist_key { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(storedist_key); for m in &matches { db.sset_add(storedist_key, m.distance / to_meter, &m.name, now); } return Frame::Integer(matches.len() as i64); } format_radius_results(&matches, &opts, to_meter) } fn cmd_georadius(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_georadius_impl(state, ctx, args, false, "georadius") } fn cmd_georadius_ro(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_georadius_impl(state, ctx, args, true, "georadius_ro") } // ── GEORADIUSBYMEMBER / GEORADIUSBYMEMBER_RO ──────────────────────── fn cmd_georadiusbymember_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], read_only: bool, cmd_name: &str, ) -> Frame { let key = to_str(&args[0]); let member = to_str(&args[1]); let radius: f64 = match to_str(&args[2]).parse() { Ok(v) if v >= 0.0 => v, _ => return Frame::error(err_wrong_number(cmd_name)), }; let to_meter = match parse_unit(&to_str(&args[3])) { Some(v) => v, None => return Frame::error(err_wrong_number(cmd_name)), }; let opts = match parse_radius_opts(&args[4..], read_only) { Ok(o) => o, Err(e) => return e, }; // Check STORE/STOREDIST incompatibility if (opts.store_key.is_some() || opts.storedist_key.is_some()) && (opts.with_dist || opts.with_coord) { return Frame::error( "ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options", ); } // Look up the member's coordinates { let inner = state.lock(); let db = inner.db(ctx.selected_db); if !db.keys.contains_key(&key) { return Frame::Null; } if db.keys.get(&key) != Some(&KeyType::SortedSet) { return Frame::error(MSG_WRONG_TYPE); } match db.sset_score(&key, &member) { Some(score) => { let (longitude, latitude) = from_geohash(score as u64); drop(inner); let mut matches = within_radius( state, ctx.selected_db, &key, longitude, latitude, radius * to_meter, ); apply_sort_and_count(&mut matches, &opts); // Handle STORE if let Some(ref store_key) = opts.store_key { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(store_key); for m in &matches { db.sset_add(store_key, m.score, &m.name, now); } return Frame::Integer(matches.len() as i64); } // Handle STOREDIST if let Some(ref storedist_key) = opts.storedist_key { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(storedist_key); for m in &matches { db.sset_add(storedist_key, m.distance / to_meter, &m.name, now); } return Frame::Integer(matches.len() as i64); } format_radius_results(&matches, &opts, to_meter) } None => Frame::error("ERR could not decode requested zset member"), } } } fn cmd_georadiusbymember(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_georadiusbymember_impl(state, ctx, args, false, "georadiusbymember") } fn cmd_georadiusbymember_ro( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], ) -> Frame { cmd_georadiusbymember_impl(state, ctx, args, true, "georadiusbymember_ro") } // ── Helpers ───────────────────────────────────────────────────────── fn to_str(bytes: &[u8]) -> String { String::from_utf8_lossy(bytes).to_string() } ================================================ FILE: miniredis/src/cmd/hash.rs ================================================ use std::sync::Arc; use rand::Rng; use rand::seq::SliceRandom; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_GT_AND_LT, MSG_INT_OVERFLOW, MSG_INVALID_CURSOR, MSG_INVALID_FLOAT, MSG_INVALID_INT, MSG_NUM_FIELDS_INVALID, MSG_NUM_FIELDS_PARAMETER, MSG_NX_AND_XX_GT_LT, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, err_wrong_number, }; use crate::frame::Frame; use crate::types::KeyType; use super::parse_int; pub fn register(table: &mut CommandTable) { table.add("HSET", cmd_hset, false, -4); table.add("HSETNX", cmd_hsetnx, false, 4); table.add("HMSET", cmd_hmset, false, -4); table.add("HGET", cmd_hget, true, 3); table.add("HMGET", cmd_hmget, true, -3); table.add("HDEL", cmd_hdel, false, -3); table.add("HEXISTS", cmd_hexists, true, 3); table.add("HGETALL", cmd_hgetall, true, 2); table.add("HKEYS", cmd_hkeys, true, 2); table.add("HVALS", cmd_hvals, true, 2); table.add("HLEN", cmd_hlen, true, 2); table.add("HINCRBY", cmd_hincrby, false, 4); table.add("HINCRBYFLOAT", cmd_hincrbyfloat, false, 4); table.add("HSTRLEN", cmd_hstrlen, true, 3); table.add("HSCAN", cmd_hscan, true, -3); table.add("HRANDFIELD", cmd_hrandfield, true, -2); table.add("HEXPIRE", cmd_hexpire, false, -6); } /// HSET key field value [field value ...] fn cmd_hset(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len().is_multiple_of(2) { return Frame::error(err_wrong_number("hset")); } let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let pairs: Vec<(String, Vec)> = args[1..] .chunks_exact(2) .map(|c| (String::from_utf8_lossy(&c[0]).into_owned(), c[1].clone())) .collect(); let added = db.hash_set(&key, &pairs, now); Frame::Integer(added) } /// HSETNX key field value fn cmd_hsetnx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let field = String::from_utf8_lossy(&args[1]).into_owned(); let value = args[2].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } // Only set if field doesn't exist if let Some(hash) = db.hash_keys.get(&key) && hash.contains_key(&field) { return Frame::Integer(0); } db.hash_set(&key, &[(field, value)], now); Frame::Integer(1) } /// HMSET key field value [field value ...] fn cmd_hmset(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len().is_multiple_of(2) { return Frame::error(err_wrong_number("hmset")); } let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let pairs: Vec<(String, Vec)> = args[1..] .chunks_exact(2) .map(|c| (String::from_utf8_lossy(&c[0]).into_owned(), c[1].clone())) .collect(); db.hash_set(&key, &pairs, now); Frame::ok() } /// HGET key field fn cmd_hget(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let field = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } match db.hash_get(&key, &field) { Some(val) => Frame::Bulk(val.clone().into()), None => Frame::Null, } } /// HMGET key field [field ...] fn cmd_hmget(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let mut results = Vec::with_capacity(args.len() - 1); for arg in &args[1..] { let field = String::from_utf8_lossy(arg); match db.hash_get(&key, &field) { Some(val) => results.push(Frame::Bulk(val.clone().into())), None => results.push(Frame::Null), } } Frame::Array(results) } /// HDEL key field [field ...] fn cmd_hdel(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&key) { return Frame::Integer(0); } let fields: Vec = args[1..] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let count = db.hash_del(&key, &fields, now); Frame::Integer(count) } /// HEXISTS key field fn cmd_hexists(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let field = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } match db.hash_get(&key, &field) { Some(_) => Frame::Integer(1), None => Frame::Integer(0), } } /// HGETALL key fn cmd_hgetall(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let fields = db.hash_fields(&key); if ctx.resp3 { let mut pairs = Vec::with_capacity(fields.len()); for field in &fields { let val = db.hash_get(&key, field).cloned().unwrap_or_default(); pairs.push((Frame::Bulk(field.clone().into()), Frame::Bulk(val.into()))); } Frame::Map(pairs) } else { let mut result = Vec::with_capacity(fields.len() * 2); for field in &fields { result.push(Frame::Bulk(field.clone().into())); if let Some(val) = db.hash_get(&key, field) { result.push(Frame::Bulk(val.clone().into())); } } Frame::Array(result) } } /// HKEYS key fn cmd_hkeys(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let fields = db.hash_fields(&key); Frame::Array(fields.into_iter().map(|f| Frame::Bulk(f.into())).collect()) } /// HVALS key fn cmd_hvals(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let values = db.hash_values(&key); Frame::Array(values.into_iter().map(|v| Frame::Bulk(v.into())).collect()) } /// HLEN key fn cmd_hlen(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let len = db.hash_keys.get(key.as_ref()).map(|h| h.len()).unwrap_or(0); Frame::Integer(len as i64) } /// HINCRBY key field increment fn cmd_hincrby(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let field = String::from_utf8_lossy(&args[1]).into_owned(); let delta: i64 = match String::from_utf8_lossy(&args[2]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let current: i64 = match db.hash_get(&key, &field) { Some(v) => match String::from_utf8_lossy(v).parse::() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }, None => 0, }; let new_val = match current.checked_add(delta) { Some(n) => n, None => { return Frame::error(MSG_INT_OVERFLOW); } }; db.hash_set(&key, &[(field, new_val.to_string().into_bytes())], now); Frame::Integer(new_val) } /// HINCRBYFLOAT key field increment fn cmd_hincrbyfloat(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let field = String::from_utf8_lossy(&args[1]).into_owned(); let delta_str = String::from_utf8_lossy(&args[2]).into_owned(); // Validate by parsing as f64 let delta_f64: f64 = match delta_str.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_FLOAT), }; if delta_f64.is_nan() || delta_f64.is_infinite() { return Frame::error(MSG_INVALID_FLOAT); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let current_str = match db.hash_get(&key, &field) { Some(v) => { let s = String::from_utf8_lossy(v).into_owned(); if s.parse::().is_err() { return Frame::error(MSG_INVALID_FLOAT); } s } None => "0".to_string(), }; let formatted = crate::cmd::string::decimal_add_format(¤t_str, &delta_str); db.hash_set(&key, &[(field, formatted.as_bytes().to_vec())], now); Frame::Bulk(formatted.into_bytes().into()) } /// HSTRLEN key field fn cmd_hstrlen(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let field = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } match db.hash_get(&key, &field) { Some(val) => Frame::Integer(val.len() as i64), None => Frame::Integer(0), } } /// HSCAN key cursor [MATCH pattern] [COUNT count] fn cmd_hscan(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let _cursor: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_CURSOR), }; let opts = match super::parse_scan_opts(&args[2..], false) { Ok(o) => o, Err(e) => return e, }; let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let mut fields = db.hash_fields(&key); if let Some(ref pat) = opts.pattern { fields = crate::keys::match_keys_vec(&fields, pat); } let mut result = Vec::new(); for field in &fields { result.push(Frame::Bulk(field.clone().into())); let val = db.hash_get(&key, field).cloned().unwrap_or_default(); result.push(Frame::Bulk(val.into())); } Frame::Array(vec![Frame::Bulk("0".into()), Frame::Array(result)]) } /// HRANDFIELD key [count [WITHVALUES]] fn cmd_hrandfield(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 3 { return Frame::error(err_wrong_number("hrandfield")); } let key = String::from_utf8_lossy(&args[0]); let mut count: i64 = 0; let mut with_count = false; let mut with_values = false; if args.len() >= 2 { match parse_int(&args[1]) { Some(n) => { count = n; with_count = true; } None => return Frame::error(MSG_INVALID_INT), } } if args.len() == 3 { let opt = String::from_utf8_lossy(&args[2]).to_uppercase(); if opt == "WITHVALUES" { with_values = true; } else { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let mut fields = db.hash_fields(&key); if fields.is_empty() { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } // Collect values before shuffling (avoids borrow issues with inner.rng) let field_values: std::collections::HashMap> = fields .iter() .map(|f| (f.clone(), db.hash_get(&key, f).cloned().unwrap_or_default())) .collect(); if count < 0 { let abs_count = (-count) as usize; let mut result = Vec::new(); for _ in 0..abs_count { let idx = inner.rng.random_range(0..fields.len()); result.push(Frame::Bulk(fields[idx].clone().into())); if with_values { let val = field_values.get(&fields[idx]).cloned().unwrap_or_default(); result.push(Frame::Bulk(val.into())); } } return Frame::Array(result); } fields.shuffle(&mut inner.rng); let take = (count as usize).min(fields.len()); if !with_count { return Frame::Bulk(fields[0].clone().into()); } let mut result = Vec::new(); for f in &fields[..take] { result.push(Frame::Bulk(f.clone().into())); if with_values { let val = field_values.get(f).cloned().unwrap_or_default(); result.push(Frame::Bulk(val.into())); } } Frame::Array(result) } /// HEXPIRE key seconds [NX|XX|GT|LT] FIELDS numfields field [field ...] fn cmd_hexpire(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let ttl_secs: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut nx = false; let mut xx = false; let mut gt = false; let mut lt = false; let mut fields: Vec = Vec::new(); let mut i = 2; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "NX" => { nx = true; i += 1; } "XX" => { xx = true; i += 1; } "GT" => { gt = true; i += 1; } "LT" => { lt = true; i += 1; } "FIELDS" => { i += 1; if i >= args.len() { return Frame::error(MSG_NUM_FIELDS_INVALID); } let num_fields: i64 = match parse_int(&args[i]) { Some(n) => n, None => return Frame::error(MSG_NUM_FIELDS_INVALID), }; if num_fields <= 0 { return Frame::error(MSG_NUM_FIELDS_INVALID); } i += 1; let num_fields = num_fields as usize; if i + num_fields > args.len() { return Frame::error(MSG_NUM_FIELDS_PARAMETER); } for j in 0..num_fields { fields.push(String::from_utf8_lossy(&args[i + j]).into_owned()); } i += num_fields; } _ => { return Frame::error( "ERR Mandatory argument FIELDS is missing or not at the right position", ); } } } if gt && lt { return Frame::error(MSG_GT_AND_LT); } if nx && (xx || gt || lt) { return Frame::error(MSG_NX_AND_XX_GT_LT); } if fields.is_empty() { return Frame::error( "ERR Mandatory argument FIELDS is missing or not at the right position", ); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); // Key doesn't exist: return -2 for all fields if !db.keys.contains_key(&key) { return Frame::Array(fields.iter().map(|_| Frame::Integer(-2)).collect()); } if let Some(t) = db.key_type(&key) && t != KeyType::Hash { return Frame::error(MSG_WRONG_TYPE); } let new_ttl = std::time::Duration::from_secs(ttl_secs as u64); let field_ttls = db.hash_field_ttls.entry(key.clone()).or_default(); let mut results = Vec::with_capacity(fields.len()); for field in &fields { // Check field exists in hash let field_exists = db .hash_keys .get(&key) .is_some_and(|h| h.contains_key(field)); if !field_exists { results.push(Frame::Integer(-2)); continue; } let current_ttl = field_ttls.get(field).copied(); let has_ttl = current_ttl.is_some(); // NX: set only when field has no expiration if nx && has_ttl { results.push(Frame::Integer(0)); continue; } // XX: set only when field has existing expiration if xx && !has_ttl { results.push(Frame::Integer(0)); continue; } // GT: set only when new TTL > current TTL if gt && (!has_ttl || new_ttl <= current_ttl.unwrap()) { results.push(Frame::Integer(0)); continue; } // LT: set only when new TTL < current TTL (and field has expiration) if lt && has_ttl && new_ttl >= current_ttl.unwrap() { results.push(Frame::Integer(0)); continue; } field_ttls.insert(field.clone(), new_ttl); results.push(Frame::Integer(1)); } db.incr_version(&key, now); Frame::Array(results) } ================================================ FILE: miniredis/src/cmd/hll.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{CommandTable, MSG_NOT_VALID_HLL_VALUE, err_wrong_number}; use crate::frame::Frame; use crate::types::KeyType; pub fn register(table: &mut CommandTable) { table.add("PFADD", cmd_pfadd, false, -2); table.add("PFCOUNT", cmd_pfcount, true, -2); table.add("PFMERGE", cmd_pfmerge, false, -2); } /// PFADD key element [element ...] fn cmd_pfadd(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() < 2 { return Frame::error(err_wrong_number("pfadd")); } let key = String::from_utf8_lossy(&args[0]).to_string(); let items: Vec<&str> = args[1..] .iter() .map(|a| std::str::from_utf8(a).unwrap_or("")) .collect(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); // Check type if key already exists if let Some(kt) = db.keys.get(&key) && *kt != KeyType::HyperLogLog { return Frame::error(MSG_NOT_VALID_HLL_VALUE); } let altered = db.hll_add(&key, &items, now); Frame::Integer(altered) } /// PFCOUNT key [key ...] fn cmd_pfcount(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let keys: Vec<&str> = args .iter() .map(|a| std::str::from_utf8(a).unwrap_or("")) .collect(); let inner = state.lock(); let db = inner.db(ctx.selected_db); match db.hll_count(&keys) { Ok(count) => Frame::Integer(count), Err(msg) => Frame::error(msg), } } /// PFMERGE destkey [sourcekey ...] fn cmd_pfmerge(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let keys: Vec<&str> = args .iter() .map(|a| std::str::from_utf8(a).unwrap_or("")) .collect(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); match db.hll_merge(&keys, now) { Ok(()) => Frame::ok(), Err(msg) => Frame::error(msg), } } ================================================ FILE: miniredis/src/cmd/list.rs ================================================ use std::sync::Arc; use super::parse_int; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INVALID_INT, MSG_INVALID_TIMEOUT, MSG_KEY_NOT_FOUND, MSG_OUT_OF_RANGE, MSG_SYNTAX_ERROR, MSG_TIMEOUT_IS_OUT_OF_RANGE, MSG_TIMEOUT_NEGATIVE, MSG_WRONG_TYPE, err_wrong_number, }; use crate::frame::Frame; use crate::types::KeyType; pub fn register(table: &mut CommandTable) { table.add("LPUSH", cmd_lpush, false, -3); table.add("RPUSH", cmd_rpush, false, -3); table.add("LPUSHX", cmd_lpushx, false, -3); table.add("RPUSHX", cmd_rpushx, false, -3); table.add("LPOP", cmd_lpop, false, -2); table.add("RPOP", cmd_rpop, false, -2); table.add("LLEN", cmd_llen, true, 2); table.add("LINDEX", cmd_lindex, true, 3); table.add("LRANGE", cmd_lrange, true, 4); table.add("LSET", cmd_lset, false, 4); table.add("LINSERT", cmd_linsert, false, 5); table.add("LREM", cmd_lrem, false, 4); table.add("LTRIM", cmd_ltrim, false, 4); table.add("RPOPLPUSH", cmd_rpoplpush, false, 3); table.add("LMOVE", cmd_lmove, false, 5); table.add("LPOS", cmd_lpos, true, -3); // Blocking commands: registered for MULTI/EXEC queueing (non-blocking attempt) table.add("BLPOP", cmd_blpop, false, -3); table.add("BRPOP", cmd_brpop, false, -3); table.add("BRPOPLPUSH", cmd_brpoplpush, false, 4); table.add("BLMOVE", cmd_blmove, false, 6); } /// LPUSH key element [element ...] fn cmd_lpush(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpush(state, ctx, args, true, false) } /// RPUSH key element [element ...] fn cmd_rpush(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpush(state, ctx, args, false, false) } /// LPUSHX key element [element ...] fn cmd_lpushx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpush(state, ctx, args, true, true) } /// RPUSHX key element [element ...] fn cmd_rpushx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpush(state, ctx, args, false, true) } fn cmd_xpush( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], left: bool, only_existing: bool, ) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } // PUSHX: only push to existing keys if only_existing && !db.keys.contains_key(&key) { return Frame::Integer(0); } let values: Vec> = args[1..].to_vec(); let len = if left { db.list_lpush(&key, &values, now) } else { db.list_rpush(&key, &values, now) }; Frame::Integer(len) } /// LPOP key [count] fn cmd_lpop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpop(state, ctx, args, true) } /// RPOP key [count] fn cmd_rpop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xpop(state, ctx, args, false) } fn cmd_xpop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec], left: bool) -> Frame { let cmd_name = if left { "lpop" } else { "rpop" }; if args.len() > 2 { return Frame::error(err_wrong_number(cmd_name)); } let key = String::from_utf8_lossy(&args[0]).into_owned(); let count = if args.len() > 1 { match parse_int(&args[1]) { Some(n) if n < 0 => return Frame::error(MSG_OUT_OF_RANGE), Some(n) => Some(n as usize), None => return Frame::error(MSG_INVALID_INT), } } else { None }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&key) { return if count.is_some() { Frame::NullArray } else { Frame::Null }; } match count { Some(n) => { let mut results = Vec::new(); for _ in 0..n { let val = if left { db.list_lpop(&key, now) } else { db.list_rpop(&key, now) }; match val { Some(v) => results.push(Frame::Bulk(v.into())), None => break, } } Frame::Array(results) } None => { let val = if left { db.list_lpop(&key, now) } else { db.list_rpop(&key, now) }; match val { Some(v) => Frame::Bulk(v.into()), None => Frame::Null, } } } } /// LLEN key fn cmd_llen(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let len = db.list_keys.get(key.as_ref()).map(|l| l.len()).unwrap_or(0); Frame::Integer(len as i64) } /// LINDEX key index fn cmd_lindex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); // Reject "-0" (Go miniredis compat) if args[1] == b"-0" { return Frame::error(MSG_INVALID_INT); } let index: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let list = match db.list_keys.get(key.as_ref()) { Some(l) => l, None => return Frame::Null, }; let len = list.len() as i64; let mut idx = index; if idx < 0 { idx += len; } if idx < 0 || idx >= len { return Frame::Null; } Frame::Bulk(list[idx as usize].clone().into()) } /// LRANGE key start stop fn cmd_lrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let start: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let end: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let list = match db.list_keys.get(key.as_ref()) { Some(l) => l, None => return Frame::Array(vec![]), }; let len = list.len() as i64; let (rs, re) = redis_range(start, end, len); if rs > re || rs >= len { return Frame::Array(vec![]); } let results: Vec = (rs..=re) .map(|i| Frame::Bulk(list[i as usize].clone().into())) .collect(); Frame::Array(results) } /// LSET key index element fn cmd_lset(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let index: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let value = args[2].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::error(MSG_KEY_NOT_FOUND); } if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let list = match db.list_keys.get_mut(&key) { Some(l) => l, None => return Frame::error(MSG_KEY_NOT_FOUND), }; let len = list.len() as i64; let mut idx = index; if idx < 0 { idx += len; } if idx < 0 || idx >= len { return Frame::error(MSG_OUT_OF_RANGE); } list[idx as usize] = value; db.incr_version(&key, now); Frame::ok() } /// LINSERT key BEFORE|AFTER pivot element fn cmd_linsert(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let position = String::from_utf8_lossy(&args[1]).to_uppercase(); let before = match position.as_str() { "BEFORE" => true, "AFTER" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let pivot = &args[2]; let value = args[3].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&key) { return Frame::Integer(0); } let list = match db.list_keys.get_mut(&key) { Some(l) => l, None => return Frame::Integer(0), }; // Find pivot let pos = list.iter().position(|el| el == pivot); match pos { Some(i) => { let insert_at = if before { i } else { i + 1 }; list.insert(insert_at, value); let new_len = list.len() as i64; db.incr_version(&key, now); Frame::Integer(new_len) } None => Frame::Integer(-1), } } /// LREM key count element fn cmd_lrem(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let count: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let element = &args[2]; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let list = match db.list_keys.get_mut(&key) { Some(l) => l, None => return Frame::Integer(0), }; let mut removed = 0i64; let max_remove = if count == 0 { list.len() } else { count.unsigned_abs() as usize }; if count >= 0 { // Remove from head let mut i = 0; while i < list.len() && (removed as usize) < max_remove { if &list[i] == element { list.remove(i); removed += 1; } else { i += 1; } } } else { // Remove from tail let mut i = list.len(); while i > 0 && (removed as usize) < max_remove { i -= 1; if &list[i] == element { list.remove(i); removed += 1; } } } if list.is_empty() { db.del(&key); } else { db.incr_version(&key, now); } Frame::Integer(removed) } /// LTRIM key start stop fn cmd_ltrim(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let start: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let end: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&key) { return Frame::ok(); } let list = match db.list_keys.get(&key) { Some(l) => l, None => return Frame::ok(), }; let len = list.len() as i64; let (rs, re) = redis_range(start, end, len); if rs > re || rs >= len { db.del(&key); return Frame::ok(); } let trimmed: std::collections::VecDeque> = list .iter() .skip(rs as usize) .take((re - rs + 1) as usize) .cloned() .collect(); if trimmed.is_empty() { db.del(&key); } else { db.list_keys.insert(key.clone(), trimmed); db.incr_version(&key, now); } Frame::ok() } /// RPOPLPUSH source destination fn cmd_rpoplpush(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); // Type checks if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } // Save TTL when src == dst so we can restore it after pop+push cycle let saved_ttl = if src == dst { db.ttl.get(&src).cloned() } else { None }; let val = match db.list_rpop(&src, now) { Some(v) => v, None => return Frame::Null, }; db.list_lpush(&dst, std::slice::from_ref(&val), now); // Restore TTL if src == dst (pop may have deleted the key and its TTL) if let Some(ttl) = saved_ttl { db.ttl.insert(dst.clone(), ttl); } Frame::Bulk(val.into()) } /// LMOVE source destination LEFT|RIGHT LEFT|RIGHT fn cmd_lmove(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase(); let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase(); let pop_left = match src_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let push_left = match dst_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } // Save TTL when src == dst so we can restore it after pop+push cycle let saved_ttl = if src == dst { db.ttl.get(&src).cloned() } else { None }; let val = if pop_left { db.list_lpop(&src, now) } else { db.list_rpop(&src, now) }; match val { Some(v) => { if push_left { db.list_lpush(&dst, std::slice::from_ref(&v), now); } else { db.list_rpush(&dst, std::slice::from_ref(&v), now); } // Restore TTL if src == dst (pop may have deleted the key and its TTL) if let Some(ttl) = saved_ttl { db.ttl.insert(dst.clone(), ttl); } Frame::Bulk(v.into()) } None => Frame::Null, } } // ── Utility ────────────────────────────────────────────────────────── /// LPOS key element [RANK rank] [COUNT count] [MAXLEN maxlen] fn cmd_lpos(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let element = &args[1]; let mut rank: i64 = 1; let mut count: Option = None; let mut maxlen: i64 = 0; let mut i = 2; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "RANK" => { i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } match parse_int(&args[i]) { Some(n) => { if n == 0 { return Frame::error( "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative values meaning from the last match", ); } rank = n; } None => return Frame::error(MSG_INVALID_INT), } } "COUNT" => { i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } match parse_int(&args[i]) { Some(n) if n >= 0 => count = Some(n), _ => return Frame::error("ERR COUNT can't be negative"), } } "MAXLEN" => { i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } match parse_int(&args[i]) { Some(n) if n >= 0 => maxlen = n, _ => return Frame::error("ERR MAXLEN can't be negative"), } } _ => return Frame::error(MSG_SYNTAX_ERROR), } i += 1; } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let list = match db.list_keys.get(&key) { Some(l) => l, None => { return if count.is_some() { Frame::Array(vec![]) } else { Frame::Null }; } }; let len = list.len(); let max_count = count.unwrap_or(1); let scan_max = if maxlen > 0 { maxlen as usize } else { len }; let mut matches: Vec = Vec::new(); let mut match_count = 0i64; if rank > 0 { // Forward scan let mut skip = rank - 1; for (idx, item) in list.iter().enumerate().take(len.min(scan_max)) { if item == element { if skip > 0 { skip -= 1; continue; } matches.push(idx as i64); match_count += 1; if max_count > 0 && match_count >= max_count { break; } } } } else { // Reverse scan let mut skip = (-rank) - 1; let start = len.saturating_sub(scan_max); for idx in (start..len).rev() { if &list[idx] == element { if skip > 0 { skip -= 1; continue; } matches.push(idx as i64); match_count += 1; if max_count > 0 && match_count >= max_count { break; } } } } if count.is_some() { Frame::Array(matches.into_iter().map(Frame::Integer).collect()) } else { matches .first() .map(|&idx| Frame::Integer(idx)) .unwrap_or(Frame::Null) } } // ── Blocking command stubs (non-blocking for MULTI/EXEC) ───────────── /// BLPOP key [key ...] timeout — non-blocking attempt (for MULTI/EXEC) pub fn cmd_blpop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { // Last arg is timeout — validate it if let Some(err) = validate_timeout(&args[args.len() - 1]) { return err; } // Last arg is timeout, keys are all but last let keys = &args[..args.len() - 1]; let mut inner = state.lock(); let now = inner.effective_now(); for key_bytes in keys { let key = String::from_utf8_lossy(key_bytes).into_owned(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(val) = db.list_lpop(&key, now) { return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(val.into())]); } } Frame::NullArray } /// BRPOP key [key ...] timeout — non-blocking attempt (for MULTI/EXEC) pub fn cmd_brpop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { // Last arg is timeout — validate it if let Some(err) = validate_timeout(&args[args.len() - 1]) { return err; } let keys = &args[..args.len() - 1]; let mut inner = state.lock(); let now = inner.effective_now(); for key_bytes in keys { let key = String::from_utf8_lossy(key_bytes).into_owned(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(val) = db.list_rpop(&key, now) { return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(val.into())]); } } Frame::NullArray } /// BRPOPLPUSH source destination timeout — non-blocking attempt pub fn cmd_brpoplpush(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { // Last arg is timeout — validate it if let Some(err) = validate_timeout(&args[2]) { return err; } let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } match db.list_rpop(&src, now) { Some(val) => { db.list_lpush(&dst, std::slice::from_ref(&val), now); Frame::Bulk(val.into()) } None => Frame::Null, } } /// BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout — non-blocking attempt pub fn cmd_blmove(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase(); let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase(); let pop_left = match src_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let push_left = match dst_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; if let Some(err) = validate_timeout(&args[4]) { return err; } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } // Save TTL when src == dst so we can restore it after pop+push cycle let saved_ttl = if src == dst { db.ttl.get(&src).cloned() } else { None }; let val = if pop_left { db.list_lpop(&src, now) } else { db.list_rpop(&src, now) }; match val { Some(v) => { if push_left { db.list_lpush(&dst, std::slice::from_ref(&v), now); } else { db.list_rpush(&dst, std::slice::from_ref(&v), now); } // Restore TTL if src == dst if let Some(ttl) = saved_ttl { db.ttl.insert(dst.clone(), ttl); } Frame::Bulk(v.into()) } None => Frame::Null, } } // ── Utility ────────────────────────────────────────────────────────── /// Normalize Redis-style range indices for lists. Returns (start, end) inclusive. fn redis_range(start: i64, end: i64, len: i64) -> (i64, i64) { let mut s = start; let mut e = end; if s < 0 { s += len; } if e < 0 { e += len; } if s < 0 { s = 0; } if e >= len { e = len - 1; } (s, e) } /// Validate a blocking command timeout argument. /// Returns Some(Frame) with error if invalid, None if OK. fn validate_timeout(arg: &[u8]) -> Option { let s = String::from_utf8_lossy(arg); let s_lower = s.to_lowercase(); if s_lower == "inf" || s_lower == "+inf" || s_lower == "-inf" { return Some(Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE)); } match s.parse::() { Ok(t) if t < 0.0 => Some(Frame::error(MSG_TIMEOUT_NEGATIVE)), Ok(_) => None, Err(_) => Some(Frame::error(MSG_INVALID_TIMEOUT)), } } ================================================ FILE: miniredis/src/cmd/mod.rs ================================================ // Command handler modules. // // Each module implements a category of Redis commands. // Commands are registered in the dispatch table (src/dispatch.rs). pub mod client; // CLIENT SETNAME/GETNAME pub mod cluster; // CLUSTER SLOTS/KEYSLOT/NODES/SHARDS (mocked) pub mod connection; // PING, ECHO, QUIT, SELECT, AUTH, HELLO pub mod generic; // DEL, EXISTS, EXPIRE, TTL, KEYS, SCAN, etc. pub mod geo; // GEOADD, GEODIST, GEOPOS, GEORADIUS, etc. pub mod hash; // HSET, HGET, HDEL, HGETALL, etc. pub mod hll; // PFADD, PFCOUNT, PFMERGE pub mod list; // LPUSH, RPUSH, LPOP, RPOP, BLPOP, etc. pub mod object; pub mod pubsub; // SUBSCRIBE, PUBLISH, PSUBSCRIBE, etc. pub mod scripting; // EVAL, EVALSHA, SCRIPT pub mod server; // DBSIZE, FLUSHDB, INFO, TIME, etc. pub mod set; // SADD, SREM, SMEMBERS, SINTER, etc. pub mod sorted_set; // ZADD, ZRANGE, ZSCORE, ZRANK, etc. pub mod stream; // XADD, XREAD, XREADGROUP, XACK, etc. pub mod string; // GET, SET, MGET, MSET, INCR, etc. pub mod transactions; // MULTI, EXEC, WATCH, DISCARD // OBJECT IDLETIME pub(crate) fn parse_int(bytes: &[u8]) -> Option { String::from_utf8_lossy(bytes).parse::().ok() } pub(crate) fn parse_float(bytes: &[u8]) -> Option { let s = String::from_utf8_lossy(bytes); match s.to_lowercase().as_str() { "+inf" | "inf" => Some(f64::INFINITY), "-inf" => Some(f64::NEG_INFINITY), _ => s.parse::().ok(), } } use crate::dispatch::{MSG_INVALID_INT, MSG_SYNTAX_ERROR}; use crate::frame::Frame; pub(crate) struct ScanOpts { pub pattern: Option, pub count: Option, pub type_filter: Option, } /// Parse MATCH/COUNT/TYPE options common to SCAN, SSCAN, HSCAN, ZSCAN. /// `allow_type`: only SCAN supports the TYPE filter. pub(crate) fn parse_scan_opts(args: &[Vec], allow_type: bool) -> Result { let mut pattern = None; let mut count = None; let mut type_filter = None; let mut i = 0; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "MATCH" => { i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } pattern = Some(String::from_utf8_lossy(&args[i]).into_owned()); } "COUNT" => { i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } let n = String::from_utf8_lossy(&args[i]) .parse::() .map_err(|_| Frame::error(MSG_INVALID_INT))?; if n <= 0 { return Err(Frame::error(MSG_SYNTAX_ERROR)); } count = Some(n); } "TYPE" if allow_type => { i += 1; if i >= args.len() { return Err(Frame::error(MSG_SYNTAX_ERROR)); } type_filter = Some(String::from_utf8_lossy(&args[i]).to_lowercase()); } _ => return Err(Frame::error(MSG_SYNTAX_ERROR)), } i += 1; } Ok(ScanOpts { pattern, count, type_filter, }) } ================================================ FILE: miniredis/src/cmd/object.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::CommandTable; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("OBJECT", cmd_object, true, -2); } fn cmd_object(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "ENCODING" => { if args.len() != 2 { return Frame::error("ERR wrong number of arguments for 'object|encoding' command"); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); match db.keys.get(key.as_ref()) { None => Frame::Null, Some(kt) => { let encoding = match kt { crate::types::KeyType::String => "raw", crate::types::KeyType::Hash => "hashtable", crate::types::KeyType::List => "linkedlist", crate::types::KeyType::Set => "hashtable", crate::types::KeyType::SortedSet => "skiplist", crate::types::KeyType::Stream => "stream", crate::types::KeyType::HyperLogLog => "raw", }; Frame::Bulk(encoding.into()) } } } "REFCOUNT" => { if args.len() != 2 { return Frame::error("ERR wrong number of arguments for 'object|refcount' command"); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if !db.keys.contains_key(key.as_ref()) { return Frame::Null; } // Always return 1 (simplified) Frame::Integer(1) } "FREQ" => { if args.len() != 2 { return Frame::error("ERR wrong number of arguments for 'object|freq' command"); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if !db.keys.contains_key(key.as_ref()) { return Frame::Null; } // Always return 0 (simplified) Frame::Integer(0) } "IDLETIME" => { if args.len() != 2 { return Frame::error("ERR wrong number of arguments for 'object|idletime' command"); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if !db.keys.contains_key(key.as_ref()) { return Frame::Null; } match db.lru.get(key.as_ref()) { Some(last_access) => { let now = inner.effective_now(); let idle = now.duration_since(*last_access).unwrap_or_default(); Frame::Integer(idle.as_secs() as i64) } None => Frame::Integer(0), } } "HELP" => Frame::Array(vec![Frame::Bulk( "OBJECT IDLETIME - return idle time of key".into(), )]), _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try OBJECT HELP.", subcmd.to_lowercase() )), } } ================================================ FILE: miniredis/src/cmd/pubsub.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{CommandTable, err_wrong_number}; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("PUBLISH", cmd_publish, false, 3); table.add("PUBSUB", cmd_pubsub, true, -2); // SUBSCRIBE/PSUBSCRIBE/UNSUBSCRIBE/PUNSUBSCRIBE are normally handled in // server.rs (outside dispatch). These are registered so they can be queued // inside MULTI/EXEC. table.add("SUBSCRIBE", cmd_subscribe, false, -2); table.add("PSUBSCRIBE", cmd_psubscribe, false, -2); table.add("UNSUBSCRIBE", cmd_unsubscribe, false, -1); table.add("PUNSUBSCRIBE", cmd_punsubscribe, false, -1); } /// SUBSCRIBE channel [channel ...] — handler for MULTI/EXEC path. /// Normal (non-MULTI) SUBSCRIBE is handled directly in server.rs. fn cmd_subscribe(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut confirmations = Vec::new(); for arg in args { let channel = String::from_utf8_lossy(arg).to_string(); ctx.pending_subscribe.push(channel.clone()); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); confirmations.push(Frame::Array(vec![ Frame::Bulk("subscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ])); } if confirmations.len() == 1 { confirmations.pop().unwrap() } else { Frame::Array(confirmations) } } /// PSUBSCRIBE pattern [pattern ...] — handler for MULTI/EXEC path. fn cmd_psubscribe(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut confirmations = Vec::new(); for arg in args { let pattern = String::from_utf8_lossy(arg).to_string(); ctx.pending_psubscribe.push(pattern.clone()); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); confirmations.push(Frame::Array(vec![ Frame::Bulk("psubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ])); } if confirmations.len() == 1 { confirmations.pop().unwrap() } else { Frame::Array(confirmations) } } /// UNSUBSCRIBE [channel ...] — handler for MULTI/EXEC path. fn cmd_unsubscribe(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.is_empty() { // Unsubscribe from all pending channels ctx.pending_subscribe.clear(); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); return Frame::Array(vec![ Frame::Bulk("unsubscribe".into()), Frame::Null, Frame::Integer(count as i64), ]); } let mut confirmations = Vec::new(); for arg in args { let channel = String::from_utf8_lossy(arg).to_string(); ctx.pending_subscribe.retain(|ch| *ch != channel); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); confirmations.push(Frame::Array(vec![ Frame::Bulk("unsubscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ])); } if confirmations.len() == 1 { confirmations.pop().unwrap() } else { Frame::Array(confirmations) } } /// PUNSUBSCRIBE [pattern ...] — handler for MULTI/EXEC path. fn cmd_punsubscribe(_state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.is_empty() { // Unsubscribe from all pending patterns ctx.pending_psubscribe.clear(); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); return Frame::Array(vec![ Frame::Bulk("punsubscribe".into()), Frame::Null, Frame::Integer(count as i64), ]); } let mut confirmations = Vec::new(); for arg in args { let pattern = String::from_utf8_lossy(arg).to_string(); ctx.pending_psubscribe.retain(|p| *p != pattern); let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len(); confirmations.push(Frame::Array(vec![ Frame::Bulk("punsubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ])); } if confirmations.len() == 1 { confirmations.pop().unwrap() } else { Frame::Array(confirmations) } } /// PUBLISH channel message fn cmd_publish(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let channel = String::from_utf8_lossy(&args[0]).to_string(); let message = String::from_utf8_lossy(&args[1]).to_string(); let registry = state.pubsub.lock().unwrap(); let count = registry.publish(&channel, &message); Frame::Integer(count) } /// PUBSUB CHANNELS/NUMSUB/NUMPAT fn cmd_pubsub(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "CHANNELS" => { if args.len() > 2 { return Frame::error(err_wrong_number("pubsub|channels")); } let pattern = if args.len() > 1 { Some(String::from_utf8_lossy(&args[1]).to_string()) } else { None }; let registry = state.pubsub.lock().unwrap(); let channels = registry.active_channels(pattern.as_deref()); Frame::Array( channels .into_iter() .map(|ch| Frame::Bulk(ch.into())) .collect(), ) } "NUMSUB" => { let registry = state.pubsub.lock().unwrap(); let mut result = Vec::new(); for arg in &args[1..] { let channel = String::from_utf8_lossy(arg).to_string(); let count = registry.numsub(&channel); result.push(Frame::Bulk(channel.into())); result.push(Frame::Integer(count)); } Frame::Array(result) } "NUMPAT" => { if args.len() > 1 { return Frame::error(err_wrong_number("pubsub|numpat")); } let registry = state.pubsub.lock().unwrap(); Frame::Integer(registry.numpat()) } _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try PUBSUB HELP.", subcmd.to_lowercase() )), } } ================================================ FILE: miniredis/src/cmd/scripting.rs ================================================ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use mlua::prelude::*; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INVALID_INT, MSG_INVALID_KEYS_NUMBER, MSG_NEGATIVE_KEYS_NUMBER, MSG_NO_SCRIPT_FOUND, err_wrong_number, }; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("EVAL", cmd_eval, false, -3); table.add("EVAL_RO", cmd_eval_ro, true, -3); table.add("EVALSHA", cmd_evalsha, false, -3); table.add("EVALSHA_RO", cmd_evalsha_ro, true, -3); table.add("SCRIPT", cmd_script, false, -2); } fn sha1_hex(s: &str) -> String { use sha1_smol::Sha1; let mut hasher = Sha1::new(); hasher.update(s.as_bytes()); let digest = hasher.digest(); // Convert digest bytes to hex string digest .bytes() .iter() .map(|b| format!("{:02x}", b)) .collect() } fn msg_not_from_scripts(sha: &str) -> String { format!( "This Redis command is not allowed from script script: {}, &c", sha ) } fn err_lua_parse_error(err: &str) -> String { format!( "ERR Error compiling script (new function): {}", sanitize_lua_error(err) ) } /// Sanitize a Lua error message for RESP: /// 1. Strip everything after the first `\n` (stack traces) /// 2. Strip mlua "runtime error: " prefix /// 3. Strip Lua source location prefix like `[string "..."]:N: ` fn sanitize_lua_error(err: &str) -> String { // Take only the first line let first_line = err.split('\n').next().unwrap_or(err); // Strip mlua "runtime error: " prefix let stripped = first_line .strip_prefix("runtime error: ") .unwrap_or(first_line); // Strip Lua source location prefix like `[string "user_script"]:N: ` let stripped = if let Some(pos) = stripped.find("]: ") { let after = &stripped[pos + 3..]; // Strip the line number prefix "N: " that may remain if let Some(colon_pos) = after.find(": ") { if after[..colon_pos].chars().all(|c| c.is_ascii_digit()) { &after[colon_pos + 2..] } else { after } } else { after } } else { stripped }; stripped.to_string() } /// Commands that are not allowed inside Lua scripts. const DISALLOWED_IN_SCRIPTS: &[&str] = &[ "MULTI", "EXEC", "DISCARD", "EVAL", "EVAL_RO", "EVALSHA", "EVALSHA_RO", "SCRIPT", "AUTH", "WATCH", "UNWATCH", "SUBSCRIBE", "UNSUBSCRIBE", "PSUBSCRIBE", "PUNSUBSCRIBE", ]; // ── Frame <-> Lua value conversion ────────────────────────────────── /// Convert a Frame (Redis response) to a Lua value. fn frame_to_lua(lua: &Lua, frame: &Frame) -> LuaResult { match frame { Frame::Null | Frame::NullArray => Ok(LuaValue::Boolean(false)), Frame::Integer(n) => Ok(LuaValue::Integer(*n)), Frame::Simple(s) => { // Status reply -> table with "ok" field let tbl = lua.create_table()?; tbl.set("ok", s.as_str())?; Ok(LuaValue::Table(tbl)) } Frame::Bulk(b) => { let s = lua.create_string(b.as_ref())?; Ok(LuaValue::String(s)) } Frame::Error(msg) => { // Error -> table with "err" field let tbl = lua.create_table()?; tbl.set("err", msg.as_str())?; Ok(LuaValue::Table(tbl)) } Frame::Array(arr) | Frame::Set(arr) | Frame::Push(arr) => { let tbl = lua.create_table()?; for (i, item) in arr.iter().enumerate() { let val = frame_to_lua(lua, item)?; tbl.set(i + 1, val)?; } Ok(LuaValue::Table(tbl)) } Frame::Map(pairs) => { let tbl = lua.create_table()?; for (i, (k, v)) in pairs.iter().enumerate() { let kv_tbl = lua.create_table()?; kv_tbl.set(1, frame_to_lua(lua, k)?)?; kv_tbl.set(2, frame_to_lua(lua, v)?)?; tbl.set(i + 1, kv_tbl)?; } Ok(LuaValue::Table(tbl)) } Frame::Double(f) => Ok(LuaValue::Number(*f)), } } /// Convert a Lua value to a Frame (Redis response). fn lua_to_frame(value: LuaValue) -> Frame { match value { LuaValue::Nil => Frame::Null, LuaValue::Boolean(b) => { if b { Frame::Integer(1) } else { Frame::Null } } LuaValue::Integer(n) => Frame::Integer(n), LuaValue::Number(n) => Frame::Integer(n as i64), LuaValue::String(s) => Frame::Bulk(s.as_bytes().to_vec().into()), LuaValue::Table(tbl) => { // Check for special "err" field if let Ok(err_val) = tbl.get::("err") && let LuaValue::String(s) = err_val { let msg = String::from_utf8_lossy(&s.as_bytes()).to_string(); return Frame::Error(msg); } // Check for special "ok" field if let Ok(ok_val) = tbl.get::("ok") && let LuaValue::String(s) = ok_val { let msg = String::from_utf8_lossy(&s.as_bytes()).to_string(); return Frame::Simple(msg); } // Numeric array let mut result = Vec::new(); for i in 1.. { match tbl.get::(i) { Ok(LuaValue::Nil) => break, Ok(val) => result.push(lua_to_frame(val)), Err(_) => break, } } Frame::Array(result) } _ => Frame::Null, } } // ── cjson helpers ─────────────────────────────────────────────────── /// Convert a Lua value to a JSON string. fn lua_to_json_string(value: &LuaValue) -> Result { match value { LuaValue::Nil => Ok("null".to_string()), LuaValue::Boolean(b) => Ok(if *b { "true" } else { "false" }.to_string()), LuaValue::Integer(n) => Ok(n.to_string()), LuaValue::Number(n) => { if n.fract() == 0.0 && n.abs() < i64::MAX as f64 { Ok(format!("{}", *n as i64)) } else { Ok(n.to_string()) } } LuaValue::String(s) => { let raw = String::from_utf8_lossy(&s.as_bytes()).to_string(); Ok(json_escape_string(&raw)) } LuaValue::Table(tbl) => { // Check if it's an array (sequential integer keys starting at 1) let is_array = { let mut has_seq = false; if let Ok(v) = tbl.get::(1) && !matches!(v, LuaValue::Nil) { has_seq = true; } has_seq }; if is_array { let mut items = Vec::new(); for i in 1.. { match tbl.get::(i) { Ok(LuaValue::Nil) => break, Ok(val) => items.push(lua_to_json_string(&val)?), Err(_) => break, } } Ok(format!("[{}]", items.join(","))) } else { // Object - iterate pairs let mut pairs = Vec::new(); for (k, v) in tbl.clone().pairs::().flatten() { let key_str = match &k { LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(), LuaValue::Integer(n) => n.to_string(), LuaValue::Number(n) => n.to_string(), _ => continue, }; pairs.push(format!( "{}:{}", json_escape_string(&key_str), lua_to_json_string(&v)? )); } Ok(format!("{{{}}}", pairs.join(","))) } } _ => Err("Cannot encode non-supported Lua type".to_string()), } } /// JSON-escape a string. fn json_escape_string(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); out.push('"'); for ch in s.chars() { match ch { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), c if (c as u32) < 0x20 => { out.push_str(&format!("\\u{:04x}", c as u32)); } c => out.push(c), } } out.push('"'); out } /// Parse a JSON string into a Lua value. fn json_to_lua(lua: &Lua, json: &str) -> LuaResult { let json = json.trim(); if json.is_empty() { return Err(LuaError::RuntimeError( "Expected value but found EOF".to_string(), )); } let (val, _) = json_parse_value(lua, json, 0)?; Ok(val) } fn json_parse_value(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> { let pos = json_skip_whitespace(json, pos); if pos >= json.len() { return Err(LuaError::RuntimeError("Unexpected end of JSON".to_string())); } let ch = json.as_bytes()[pos]; match ch { b'"' => json_parse_string(lua, json, pos), b'{' => json_parse_object(lua, json, pos), b'[' => json_parse_array(lua, json, pos), b't' => { if json[pos..].starts_with("true") { Ok((LuaValue::Boolean(true), pos + 4)) } else { Err(LuaError::RuntimeError("Invalid JSON value".to_string())) } } b'f' => { if json[pos..].starts_with("false") { Ok((LuaValue::Boolean(false), pos + 5)) } else { Err(LuaError::RuntimeError("Invalid JSON value".to_string())) } } b'n' => { if json[pos..].starts_with("null") { Ok((LuaValue::Nil, pos + 4)) } else { Err(LuaError::RuntimeError("Invalid JSON value".to_string())) } } b'-' | b'0'..=b'9' => json_parse_number(lua, json, pos), _ => Err(LuaError::RuntimeError(format!( "Unexpected character '{}' at position {}", ch as char, pos ))), } } fn json_skip_whitespace(json: &str, mut pos: usize) -> usize { let bytes = json.as_bytes(); while pos < bytes.len() && matches!(bytes[pos], b' ' | b'\t' | b'\n' | b'\r') { pos += 1; } pos } fn json_parse_string(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> { // pos points to opening quote let mut i = pos + 1; let bytes = json.as_bytes(); let mut result = String::new(); while i < bytes.len() { match bytes[i] { b'"' => { let s = lua.create_string(result.as_bytes())?; return Ok((LuaValue::String(s), i + 1)); } b'\\' => { i += 1; if i >= bytes.len() { return Err(LuaError::RuntimeError( "Unterminated string escape".to_string(), )); } match bytes[i] { b'"' => result.push('"'), b'\\' => result.push('\\'), b'/' => result.push('/'), b'n' => result.push('\n'), b'r' => result.push('\r'), b't' => result.push('\t'), b'b' => result.push('\u{0008}'), b'f' => result.push('\u{000C}'), b'u' => { if i + 4 >= bytes.len() { return Err(LuaError::RuntimeError( "Invalid unicode escape".to_string(), )); } let hex = &json[i + 1..i + 5]; let code = u32::from_str_radix(hex, 16).map_err(|_| { LuaError::RuntimeError("Invalid unicode escape".to_string()) })?; if let Some(c) = char::from_u32(code) { result.push(c); } i += 4; } _ => { result.push('\\'); result.push(bytes[i] as char); } } } _ => result.push(bytes[i] as char), } i += 1; } Err(LuaError::RuntimeError("Unterminated string".to_string())) } fn json_parse_number(_lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> { let bytes = json.as_bytes(); let mut i = pos; let mut is_float = false; if i < bytes.len() && bytes[i] == b'-' { i += 1; } while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if i < bytes.len() && bytes[i] == b'.' { is_float = true; i += 1; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } } if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') { is_float = true; i += 1; if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') { i += 1; } while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } } let num_str = &json[pos..i]; if is_float { let n: f64 = num_str .parse() .map_err(|_| LuaError::RuntimeError("Invalid number".to_string()))?; Ok((LuaValue::Number(n), i)) } else { match num_str.parse::() { Ok(n) => Ok((LuaValue::Integer(n), i)), Err(_) => { let n: f64 = num_str .parse() .map_err(|_| LuaError::RuntimeError("Invalid number".to_string()))?; Ok((LuaValue::Number(n), i)) } } } } fn json_parse_array(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> { let tbl = lua.create_table()?; let mut i = pos + 1; // skip '[' let mut idx = 1; i = json_skip_whitespace(json, i); if i < json.len() && json.as_bytes()[i] == b']' { return Ok((LuaValue::Table(tbl), i + 1)); } loop { let (val, new_pos) = json_parse_value(lua, json, i)?; tbl.set(idx, val)?; idx += 1; i = json_skip_whitespace(json, new_pos); if i >= json.len() { return Err(LuaError::RuntimeError("Unterminated array".to_string())); } if json.as_bytes()[i] == b']' { return Ok((LuaValue::Table(tbl), i + 1)); } if json.as_bytes()[i] != b',' { return Err(LuaError::RuntimeError("Expected ',' in array".to_string())); } i += 1; } } fn json_parse_object(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> { let tbl = lua.create_table()?; let mut i = pos + 1; // skip '{' i = json_skip_whitespace(json, i); if i < json.len() && json.as_bytes()[i] == b'}' { return Ok((LuaValue::Table(tbl), i + 1)); } loop { i = json_skip_whitespace(json, i); // Parse key (must be a string) let (key_val, new_pos) = json_parse_string(lua, json, i)?; i = json_skip_whitespace(json, new_pos); if i >= json.len() || json.as_bytes()[i] != b':' { return Err(LuaError::RuntimeError("Expected ':' in object".to_string())); } i += 1; // Parse value let (val, new_pos) = json_parse_value(lua, json, i)?; tbl.set(key_val, val)?; i = json_skip_whitespace(json, new_pos); if i >= json.len() { return Err(LuaError::RuntimeError("Unterminated object".to_string())); } if json.as_bytes()[i] == b'}' { return Ok((LuaValue::Table(tbl), i + 1)); } if json.as_bytes()[i] != b',' { return Err(LuaError::RuntimeError("Expected ',' in object".to_string())); } i += 1; } } // ── Lua script execution ──────────────────────────────────────────── /// Run a Lua script. Returns the response Frame. fn run_lua_script( state: &Arc, ctx: &mut ConnCtx, sha: &str, script: &str, read_only: bool, args: &[Vec], ) -> Result { // Parse numkeys and split args into KEYS/ARGV if args.is_empty() { return Err(MSG_INVALID_INT.to_string()); } let numkeys_str = String::from_utf8_lossy(&args[0]); let numkeys: i64 = numkeys_str .parse() .map_err(|_| MSG_INVALID_INT.to_string())?; if numkeys < 0 { return Err(MSG_NEGATIVE_KEYS_NUMBER.to_string()); } let numkeys = numkeys as usize; let remaining = &args[1..]; if numkeys > remaining.len() { return Err(MSG_INVALID_KEYS_NUMBER.to_string()); } let keys = &remaining[..numkeys]; let argv = &remaining[numkeys..]; // Create Lua state let lua = Lua::new(); // Set up global protection metatable to catch accesses to nonexistent globals. // This mimics Redis's behavior of erroring on undefined global variables. lua.load(r#" -- Sandbox the Lua environment like Redis does. -- Remove dangerous os functions, keep only os.clock. if os then local clock = os.clock os = { clock = clock } end -- Remove other dangerous functions loadfile = nil dofile = nil local _orig_globals = {} for k, v in pairs(_G) do _orig_globals[k] = true end -- Also allow KEYS, ARGV, redis, cjson which will be set after this _orig_globals["KEYS"] = true _orig_globals["ARGV"] = true _orig_globals["redis"] = true _orig_globals["cjson"] = true setmetatable(_G, { __index = function(t, name) if _orig_globals[name] then return rawget(t, name) end error("Script attempted to access nonexistent global variable '" .. tostring(name) .. "'") end, __newindex = function(t, name, value) rawset(t, name, value) end }) "#) .exec() .map_err(|e| e.to_string())?; // Set KEYS global let keys_table = lua.create_table().map_err(|e| e.to_string())?; for (i, k) in keys.iter().enumerate() { let s = lua.create_string(k.as_slice()).map_err(|e| e.to_string())?; keys_table.set(i + 1, s).map_err(|e| e.to_string())?; } lua.globals() .set("KEYS", keys_table) .map_err(|e| e.to_string())?; // Set ARGV global let argv_table = lua.create_table().map_err(|e| e.to_string())?; for (i, a) in argv.iter().enumerate() { let s = lua.create_string(a.as_slice()).map_err(|e| e.to_string())?; argv_table.set(i + 1, s).map_err(|e| e.to_string())?; } lua.globals() .set("ARGV", argv_table) .map_err(|e| e.to_string())?; // Create cjson module let cjson_table = lua.create_table().map_err(|e| e.to_string())?; // cjson.decode() let decode_fn = lua .create_function(|lua_ctx, args: LuaMultiValue| { if args.len() != 1 { return Err(LuaError::RuntimeError( "bad argument #1 to 'decode' (string expected, got no value)".to_string(), )); } let arg = args.into_iter().next().unwrap(); let json_str = match arg { LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(), _ => { return Err(LuaError::RuntimeError( "bad argument #1 to 'decode' (string expected)".to_string(), )); } }; json_to_lua(lua_ctx, &json_str) }) .map_err(|e| e.to_string())?; cjson_table .set("decode", decode_fn) .map_err(|e| e.to_string())?; // cjson.encode() let encode_fn = lua .create_function(|_, args: LuaMultiValue| { if args.len() != 1 { return Err(LuaError::RuntimeError( "bad argument #1 to 'encode' (value expected)".to_string(), )); } let arg = args.into_iter().next().unwrap(); lua_to_json_string(&arg).map_err(LuaError::RuntimeError) }) .map_err(|e| e.to_string())?; cjson_table .set("encode", encode_fn) .map_err(|e| e.to_string())?; lua.globals() .set("cjson", cjson_table) .map_err(|e| e.to_string())?; // Create redis module let redis_table = lua.create_table().map_err(|e| e.to_string())?; // Use an AtomicUsize to share selected_db between closures so that SELECT inside // a script persists for subsequent redis.call() invocations within the same script. let shared_selected_db = Arc::new(AtomicUsize::new(ctx.selected_db)); // redis.call() and redis.pcall() let authenticated = ctx.authenticated; { let state_call = Arc::clone(state); let db_cell_call = Arc::clone(&shared_selected_db); let sha_str = sha.to_string(); let call_fn = lua .create_function(move |lua_ctx, args: LuaMultiValue| { redis_call_impl( lua_ctx, &state_call, &db_cell_call, authenticated, &sha_str, true, read_only, args, ) }) .map_err(|e| e.to_string())?; redis_table .set("call", call_fn) .map_err(|e| e.to_string())?; } { let state_pcall = Arc::clone(state); let db_cell_pcall = Arc::clone(&shared_selected_db); let sha_str2 = sha.to_string(); let pcall_fn = lua .create_function(move |lua_ctx, args: LuaMultiValue| { redis_call_impl( lua_ctx, &state_pcall, &db_cell_pcall, authenticated, &sha_str2, false, read_only, args, ) }) .map_err(|e| e.to_string())?; redis_table .set("pcall", pcall_fn) .map_err(|e| e.to_string())?; } // redis.error_reply() - must receive exactly one string argument let error_reply_fn = lua .create_function(|lua_ctx, args: LuaMultiValue| { if args.len() != 1 { return Err(LuaError::RuntimeError( "wrong number or type of arguments".to_string(), )); } let arg = args.into_iter().next().unwrap(); let s = match arg { LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(), _ => { return Err(LuaError::RuntimeError( "wrong number or type of arguments".to_string(), )); } }; let parts: Vec<&str> = s.splitn(2, ' ').collect(); let final_msg = if parts.len() == 2 { let prefix = parts[0].strip_prefix('-').unwrap_or(parts[0]); format!("{} {}", prefix, parts[1]) } else { let prefix = parts[0].strip_prefix('-').unwrap_or(parts[0]); format!("ERR {}", prefix) }; let tbl = lua_ctx.create_table()?; tbl.set("err", final_msg)?; Ok(LuaValue::Table(tbl)) }) .map_err(|e| e.to_string())?; redis_table .set("error_reply", error_reply_fn) .map_err(|e| e.to_string())?; // redis.status_reply() - must receive exactly one string argument let status_reply_fn = lua .create_function(|lua_ctx, args: LuaMultiValue| { if args.len() != 1 { return Err(LuaError::RuntimeError( "wrong number or type of arguments".to_string(), )); } let arg = args.into_iter().next().unwrap(); let msg = match arg { LuaValue::String(s) => s, _ => { return Err(LuaError::RuntimeError( "wrong number or type of arguments".to_string(), )); } }; let tbl = lua_ctx.create_table()?; tbl.set("ok", msg)?; Ok(LuaValue::Table(tbl)) }) .map_err(|e| e.to_string())?; redis_table .set("status_reply", status_reply_fn) .map_err(|e| e.to_string())?; // redis.log() - no-op let log_fn = lua .create_function(|_, (_level, _msg): (i32, LuaString)| Ok(())) .map_err(|e| e.to_string())?; redis_table.set("log", log_fn).map_err(|e| e.to_string())?; // redis.sha1hex() - handle nil/non-string args (treat as empty string) let sha1hex_fn = lua .create_function(|lua_ctx, args: LuaMultiValue| { if args.len() != 1 { return Err(LuaError::RuntimeError( "wrong number of arguments".to_string(), )); } let arg = args.into_iter().next().unwrap(); let s = match arg { LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(), LuaValue::Integer(n) => n.to_string(), LuaValue::Number(n) => n.to_string(), LuaValue::Nil => String::new(), _ => String::new(), // tables, booleans etc treated as empty string }; let hash = sha1_hex(&s); let result = lua_ctx.create_string(hash.as_bytes())?; Ok(LuaValue::String(result)) }) .map_err(|e| e.to_string())?; redis_table .set("sha1hex", sha1hex_fn) .map_err(|e| e.to_string())?; // redis.replicate_commands() - no-op, returns true (always succeeds since Redis 7.0) let replicate_fn = lua .create_function(|_, ()| Ok(LuaValue::Boolean(true))) .map_err(|e| e.to_string())?; redis_table .set("replicate_commands", replicate_fn) .map_err(|e| e.to_string())?; // redis.set_repl() - no-op, accepts any value let set_repl_fn = lua .create_function(|_, _: LuaMultiValue| Ok(())) .map_err(|e| e.to_string())?; redis_table .set("set_repl", set_repl_fn) .map_err(|e| e.to_string())?; // redis.setresp() - validate arg (must be 2 or 3) let setresp_fn = lua .create_function(|_, version: i32| { if version != 2 && version != 3 { return Err(LuaError::RuntimeError( "RESP version must be 2 or 3.".to_string(), )); } Ok(LuaValue::Nil) }) .map_err(|e| e.to_string())?; redis_table .set("setresp", setresp_fn) .map_err(|e| e.to_string())?; // Redis constants redis_table.set("LOG_DEBUG", 0).map_err(|e| e.to_string())?; redis_table .set("LOG_VERBOSE", 1) .map_err(|e| e.to_string())?; redis_table .set("LOG_NOTICE", 2) .map_err(|e| e.to_string())?; redis_table .set("LOG_WARNING", 3) .map_err(|e| e.to_string())?; // Replication constants (used with set_repl) redis_table.set("REPL_NONE", 0).map_err(|e| e.to_string())?; redis_table .set("REPL_SLAVE", 1) .map_err(|e| e.to_string())?; redis_table .set("REPL_REPLICA", 1) .map_err(|e| e.to_string())?; redis_table.set("REPL_AOF", 2).map_err(|e| e.to_string())?; redis_table.set("REPL_ALL", 3).map_err(|e| e.to_string())?; lua.globals() .set("redis", redis_table) .map_err(|e| e.to_string())?; // Execute the script: compile to a function, then call it. // This ensures that only explicit `return` statements produce return values // (unlike eval() which may try to prepend `return` to the code). let func = lua .load(script) .into_function() .map_err(|e| err_lua_parse_error(&e.to_string()))?; // Cache the script after successful compilation (Redis caches on EVAL even if // execution fails at runtime, but not on compilation or argument errors). { let mut inner = state.lock(); inner.scripts.insert(sha.to_string(), script.to_string()); } let result: LuaValue = match func.call(()) { Ok(v) => v, Err(e) => { let msg = sanitize_lua_error(&e.to_string()); // Check if it looks like a Redis error (starts with ERR, WRONGTYPE, etc.) if msg.starts_with("ERR ") || msg.starts_with("WRONGTYPE ") || msg.starts_with("NOSCRIPT ") || msg.starts_with("NOGROUP ") || msg.starts_with("BUSYKEY ") || msg.contains("@user_script") { return Err(msg); } return Err(format!("ERR @user_script:0: {}", msg)); } }; Ok(lua_to_frame(result)) } /// Implementation of redis.call() / redis.pcall() within Lua. #[allow(clippy::too_many_arguments)] fn redis_call_impl( lua: &Lua, state: &Arc, selected_db_cell: &Arc, authenticated: bool, sha: &str, fail_fast: bool, read_only: bool, args: LuaMultiValue, ) -> LuaResult { if args.is_empty() { return Err(LuaError::RuntimeError(format!( "Please specify at least one argument for this redis lib call script: {}, &c.", sha ))); } // Convert Lua args to string args let mut cmd_args: Vec> = Vec::new(); for arg in args { match arg { LuaValue::String(s) => cmd_args.push(s.as_bytes().to_vec()), LuaValue::Integer(n) => cmd_args.push(n.to_string().into_bytes()), LuaValue::Number(n) => cmd_args.push((n as i64).to_string().into_bytes()), _ => { return Err(LuaError::RuntimeError(format!( "Lua redis lib command arguments must be strings or integers script: {}, &c.", sha ))); } } } if cmd_args.is_empty() { return Err(LuaError::RuntimeError(msg_not_from_scripts(sha))); } let cmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase(); let cmd_args_rest = &cmd_args[1..]; // Check if the command is disallowed in scripts if DISALLOWED_IN_SCRIPTS.contains(&cmd.as_str()) { let msg = msg_not_from_scripts(sha); if fail_fast { return Err(LuaError::RuntimeError(msg)); } let tbl = lua.create_table()?; tbl.set("err", msg)?; return Ok(LuaValue::Table(tbl)); } // Get the command table let table = state .command_table .get() .expect("command table not initialized"); // Look up the command let meta = match table.get(&cmd) { Some(m) => m, None => { let msg = if fail_fast { format!( "Unknown Redis command called from script script: {}, &c.", sha ) } else { "ERR Unknown Redis command called from script".to_string() }; if fail_fast { return Err(LuaError::RuntimeError(msg)); } let tbl = lua.create_table()?; tbl.set("err", msg)?; return Ok(LuaValue::Table(tbl)); } }; // Check read-only mode if read_only && !meta.read_only { let msg = "Write commands are not allowed in read-only scripts"; if fail_fast { return Err(LuaError::RuntimeError(msg.to_string())); } let tbl = lua.create_table()?; tbl.set("err", msg)?; return Ok(LuaValue::Table(tbl)); } // Create a nested ConnCtx for this call let mut nested_ctx = ConnCtx::new(); nested_ctx.selected_db = selected_db_cell.load(Ordering::Relaxed); nested_ctx.authenticated = authenticated; nested_ctx.nested = true; nested_ctx.nested_sha = Some(sha.to_string()); // Check arity before executing if meta.arity != 0 { let n = cmd_args.len() as i32; // cmd_args already includes command name let bad = if meta.arity > 0 { n != meta.arity } else { n < -meta.arity }; if bad { let msg = format!( "ERR wrong number of arguments for '{}' command", cmd.to_lowercase() ); if fail_fast { return Err(LuaError::RuntimeError(msg)); } let tbl = lua.create_table()?; tbl.set("err", msg)?; return Ok(LuaValue::Table(tbl)); } } // Execute the command let frame = (meta.handler)(state, &mut nested_ctx, cmd_args_rest); // If this was a SELECT command, update the shared selected_db if cmd == "SELECT" && !matches!(&frame, Frame::Error(_)) { selected_db_cell.store(nested_ctx.selected_db, Ordering::Relaxed); } // Convert result to Lua match &frame { Frame::Error(msg) => { if fail_fast { return Err(LuaError::RuntimeError(msg.clone())); } let tbl = lua.create_table()?; tbl.set("err", msg.as_str())?; Ok(LuaValue::Table(tbl)) } _ => frame_to_lua(lua, &frame), } } // ── Command handlers ──────────────────────────────────────────────── fn cmd_eval(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_eval_shared(state, ctx, args, false) } fn cmd_eval_ro(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_eval_shared(state, ctx, args, true) } fn cmd_eval_shared( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], read_only: bool, ) -> Frame { if ctx.nested { return Frame::error(msg_not_from_scripts( ctx.nested_sha.as_deref().unwrap_or(""), )); } let script = String::from_utf8_lossy(&args[0]).to_string(); let sha = sha1_hex(&script); let remaining = &args[1..]; match run_lua_script(state, ctx, &sha, &script, read_only, remaining) { Ok(frame) => frame, Err(msg) => Frame::error(msg), } } fn cmd_evalsha(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_evalsha_shared(state, ctx, args, false) } fn cmd_evalsha_ro(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_evalsha_shared(state, ctx, args, true) } fn cmd_evalsha_shared( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], read_only: bool, ) -> Frame { if ctx.nested { return Frame::error(msg_not_from_scripts( ctx.nested_sha.as_deref().unwrap_or(""), )); } let sha = String::from_utf8_lossy(&args[0]).to_string(); let remaining = &args[1..]; // Look up the script let script = { let inner = state.lock(); inner.scripts.get(&sha).cloned() }; match script { Some(script) => match run_lua_script(state, ctx, &sha, &script, read_only, remaining) { Ok(frame) => frame, Err(msg) => Frame::error(msg), }, None => Frame::error(MSG_NO_SCRIPT_FOUND), } } fn cmd_script(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if ctx.nested { return Frame::error(msg_not_from_scripts( ctx.nested_sha.as_deref().unwrap_or(""), )); } let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); let sub_args = &args[1..]; match subcmd.as_str() { "LOAD" => { if sub_args.len() != 1 { return Frame::error(format!( "ERR unknown subcommand or wrong number of arguments for '{}'. Try SCRIPT HELP.", "LOAD" )); } let script = String::from_utf8_lossy(&sub_args[0]).to_string(); // Validate syntax by attempting to load in a Lua state let lua = Lua::new(); if let Err(e) = lua.load(&script).into_function() { return Frame::error(err_lua_parse_error(&e.to_string())); } let sha = sha1_hex(&script); let mut inner = state.lock(); inner.scripts.insert(sha.clone(), script); Frame::Bulk(sha.into()) } "EXISTS" => { if sub_args.is_empty() { return Frame::error(err_wrong_number("script|exists")); } let inner = state.lock(); let mut results = Vec::with_capacity(sub_args.len()); for arg in sub_args { let sha = String::from_utf8_lossy(arg); if inner.scripts.contains_key(sha.as_ref()) { results.push(Frame::Integer(1)); } else { results.push(Frame::Integer(0)); } } Frame::Array(results) } "FLUSH" => { // Accept optional SYNC/ASYNC arg if sub_args.len() > 1 { return Frame::error("ERR SCRIPT FLUSH only support SYNC|ASYNC option"); } if sub_args.len() == 1 { let opt = String::from_utf8_lossy(&sub_args[0]).to_uppercase(); if opt != "SYNC" && opt != "ASYNC" { return Frame::error("ERR SCRIPT FLUSH only support SYNC|ASYNC option"); } } let mut inner = state.lock(); inner.scripts.clear(); Frame::ok() } _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try SCRIPT HELP.", subcmd )), } } ================================================ FILE: miniredis/src/cmd/server.rs ================================================ use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::Duration; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{CommandTable, MSG_INVALID_INT, MSG_SYNTAX_ERROR, err_wrong_number}; use crate::frame::Frame; use crate::types::KeyType; /// Raw RESP blob for COMMAND response, captured from Redis 5.0.7. static COMMAND_RESP: &[u8] = include_bytes!("command_resp.bin"); pub fn register(table: &mut CommandTable) { table.add("DBSIZE", cmd_dbsize, true, 1); table.add("FLUSHDB", cmd_flushdb, false, -1); table.add("FLUSHALL", cmd_flushall, false, -1); table.add("COMMAND", cmd_command, true, -1); table.add("TIME", cmd_time, true, 1); table.add("INFO", cmd_info, true, -1); table.add("SWAPDB", cmd_swapdb, false, 3); table.add("MEMORY", cmd_memory, true, -2); table.add("MINIREDIS.FASTFORWARD", cmd_fastforward, false, 2); } /// DBSIZE fn cmd_dbsize(state: &Arc, ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { let inner = state.lock(); let db = inner.db(ctx.selected_db); Frame::Integer(db.keys.len() as i64) } /// FLUSHDB [ASYNC|SYNC] fn cmd_flushdb(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 1 { return Frame::error(MSG_SYNTAX_ERROR); } if args.len() == 1 { let opt = String::from_utf8_lossy(&args[0]).to_uppercase(); if opt != "ASYNC" && opt != "SYNC" { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); inner.db_mut(ctx.selected_db).flush(); Frame::ok() } /// FLUSHALL [ASYNC|SYNC] fn cmd_flushall(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 1 { return Frame::error(MSG_SYNTAX_ERROR); } if args.len() == 1 { let opt = String::from_utf8_lossy(&args[0]).to_uppercase(); if opt != "ASYNC" && opt != "SYNC" { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); for i in 0..16 { inner.db_mut(i).flush(); } Frame::ok() } /// COMMAND — returns static command metadata (captured from Redis 5.0.7). fn cmd_command(_state: &Arc, _ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { use std::sync::OnceLock; static CACHED: OnceLock = OnceLock::new(); CACHED .get_or_init(|| { let mut pos = 0; parse_resp(COMMAND_RESP, &mut pos) }) .clone() } // ── Minimal RESP parser (for the static COMMAND blob) ──────────────── fn parse_resp(data: &[u8], pos: &mut usize) -> Frame { match data[*pos] { b'*' => { *pos += 1; let n = parse_resp_int(data, pos); let mut items = Vec::with_capacity(n as usize); for _ in 0..n { items.push(parse_resp(data, pos)); } Frame::Array(items) } b'$' => { *pos += 1; let n = parse_resp_int(data, pos) as usize; let val = data[*pos..*pos + n].to_vec(); *pos += n + 2; // skip data + \r\n Frame::Bulk(val.into()) } b':' => { *pos += 1; Frame::Integer(parse_resp_int(data, pos)) } b'+' => { *pos += 1; let start = *pos; while data[*pos] != b'\r' { *pos += 1; } let val = String::from_utf8_lossy(&data[start..*pos]).to_string(); *pos += 2; // skip \r\n Frame::Simple(val) } _ => { *pos += 1; Frame::Null } } } fn parse_resp_int(data: &[u8], pos: &mut usize) -> i64 { let neg = data[*pos] == b'-'; if neg { *pos += 1; } let mut val: i64 = 0; while data[*pos] != b'\r' { val = val * 10 + (data[*pos] - b'0') as i64; *pos += 1; } *pos += 2; // skip \r\n if neg { -val } else { val } } /// TIME — returns [seconds, microseconds] of server time. fn cmd_time(state: &Arc, _ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { let inner = state.lock(); let now = inner.effective_now(); let since_epoch = now .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); let secs = since_epoch.as_secs(); let micros = since_epoch.subsec_micros(); Frame::Array(vec![ Frame::Bulk(secs.to_string().into()), Frame::Bulk(micros.to_string().into()), ]) } /// INFO [section] fn cmd_info(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 1 { return Frame::error(err_wrong_number("info")); } let connected = state.connected_clients.load(Ordering::Relaxed); let total_conn = state.total_connections_received.load(Ordering::Relaxed); let total_cmds = state.total_commands_processed.load(Ordering::Relaxed); let section = if args.len() == 1 { String::from_utf8_lossy(&args[0]).to_lowercase() } else { String::new() }; let want_all = section.is_empty(); if !want_all && section != "clients" && section != "stats" { return Frame::error(format!("ERR section ({}) is not supported", section)); } let mut result = String::new(); if want_all || section == "clients" { result.push_str(&format!("# Clients\r\nconnected_clients:{}\r\n", connected)); } if want_all || section == "stats" { result.push_str(&format!( "# Stats\r\ntotal_connections_received:{}\r\ntotal_commands_processed:{}\r\n", total_conn, total_cmds )); } Frame::Bulk(result.into()) } /// SWAPDB db1 db2 fn cmd_swapdb(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let db1 = match String::from_utf8_lossy(&args[0]).parse::() { Ok(n) => n, Err(_) => return Frame::error("ERR invalid first DB index"), }; let db2 = match String::from_utf8_lossy(&args[1]).parse::() { Ok(n) => n, Err(_) => return Frame::error("ERR invalid second DB index"), }; if !(0..16).contains(&db1) { return Frame::error("ERR DB index is out of range"); } if !(0..16).contains(&db2) { return Frame::error("ERR DB index is out of range"); } let db1 = db1 as usize; let db2 = db2 as usize; if db1 != db2 { let mut inner = state.lock(); inner.dbs.swap(db1, db2); } Frame::ok() } /// MEMORY USAGE key fn cmd_memory(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "USAGE" => { if args.len() < 2 { return Frame::error("ERR wrong number of arguments for 'memory|usage' command"); } if args.len() > 2 { return Frame::error(crate::dispatch::MSG_SYNTAX_ERROR); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); match db.keys.get(key.as_ref()) { None => Frame::Null, Some(kt) => { let size = estimate_key_size(db, &key, *kt); Frame::Integer(size as i64) } } } "HELP" => Frame::Array(vec![Frame::Bulk( "MEMORY USAGE - estimate memory usage of key".into(), )]), _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try MEMORY HELP.", subcmd.to_lowercase() )), } } /// Estimate the memory usage of a key in bytes (simplified). fn estimate_key_size(db: &crate::db::RedisDB, key: &str, kt: KeyType) -> usize { let key_overhead = 16 + key.len(); // pointer + key string let value_size = match kt { KeyType::String => db.string_keys.get(key).map(|v| v.len() + 3).unwrap_or(0), KeyType::Hash => db .hash_keys .get(key) .map(|h| h.iter().map(|(f, v)| f.len() + v.len() + 16).sum::() + 16) .unwrap_or(0), KeyType::List => db .list_keys .get(key) .map(|l| l.iter().map(|v| v.len() + 16).sum::() + 16) .unwrap_or(0), KeyType::Set => db .set_keys .get(key) .map(|s| s.iter().map(|m| m.len() + 16).sum::() + 16) .unwrap_or(0), KeyType::SortedSet => db .sorted_set_keys .get(key) .map(|ss| ss.card() * 32 + 16) .unwrap_or(0), KeyType::Stream => 64, KeyType::HyperLogLog => { // 16384 registers + overhead 16384 + 24 } }; key_overhead + value_size } /// MINIREDIS.FASTFORWARD /// /// Advance mock time by the given number of milliseconds, expiring any keys /// whose TTL falls to zero. Used by the integration test suite. fn cmd_fastforward(state: &Arc, _ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let ms: u64 = match String::from_utf8_lossy(&args[0]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); inner.fast_forward(Duration::from_millis(ms)); Frame::ok() } ================================================ FILE: miniredis/src/cmd/set.rs ================================================ use std::collections::HashSet; use std::sync::Arc; use rand::Rng; use rand::seq::SliceRandom; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INVALID_CURSOR, MSG_INVALID_INT, MSG_INVALID_KEYS_NUMBER, MSG_OUT_OF_RANGE, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, }; use crate::frame::Frame; use crate::types::KeyType; use super::parse_int; pub fn register(table: &mut CommandTable) { table.add("SADD", cmd_sadd, false, -3); table.add("SREM", cmd_srem, false, -3); table.add("SCARD", cmd_scard, true, 2); table.add("SMEMBERS", cmd_smembers, true, 2); table.add("SISMEMBER", cmd_sismember, true, 3); table.add("SMISMEMBER", cmd_smismember, true, -3); table.add("SDIFF", cmd_sdiff, true, -2); table.add("SDIFFSTORE", cmd_sdiffstore, false, -3); table.add("SINTER", cmd_sinter, true, -2); table.add("SINTERSTORE", cmd_sinterstore, false, -3); table.add("SINTERCARD", cmd_sintercard, true, -3); table.add("SUNION", cmd_sunion, true, -2); table.add("SUNIONSTORE", cmd_sunionstore, false, -3); table.add("SMOVE", cmd_smove, false, 4); table.add("SPOP", cmd_spop, false, -2); table.add("SRANDMEMBER", cmd_srandmember, true, -2); table.add("SSCAN", cmd_sscan, true, -3); } /// SADD key member [member ...] fn cmd_sadd(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let members: Vec = args[1..] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let added = db.set_add(&key, &members, now); Frame::Integer(added) } /// SREM key member [member ...] fn cmd_srem(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&key) { return Frame::Integer(0); } let members: Vec = args[1..] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let removed = db.set_rem(&key, &members, now); Frame::Integer(removed) } /// SCARD key fn cmd_scard(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let count = db.set_keys.get(key.as_ref()).map(|s| s.len()).unwrap_or(0); Frame::Integer(count as i64) } /// SMEMBERS key fn cmd_smembers(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let members = db.set_members(&key); let items: Vec = members.into_iter().map(|m| Frame::Bulk(m.into())).collect(); if ctx.resp3 { Frame::Set(items) } else { Frame::Array(items) } } /// SISMEMBER key member fn cmd_sismember(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let member = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } if db.set_is_member(&key, &member) { Frame::Integer(1) } else { Frame::Integer(0) } } /// SMISMEMBER key member [member ...] fn cmd_smismember(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let results: Vec = args[1..] .iter() .map(|a| { let member = String::from_utf8_lossy(a); if db.set_is_member(&key, &member) { Frame::Integer(1) } else { Frame::Integer(0) } }) .collect(); Frame::Array(results) } // ── Set operations (diff, inter, union) ────────────────────────────── enum SetOp { Diff, Inter, Union, } fn set_op( state: &Arc, ctx: &mut ConnCtx, keys: &[String], op: SetOp, ) -> Result, Frame> { let inner = state.lock(); let db = inner.db(ctx.selected_db); for key in keys { if let Some(t) = db.key_type(key) && t != KeyType::Set { return Err(Frame::error(MSG_WRONG_TYPE)); } } if keys.is_empty() { return Ok(HashSet::new()); } match op { SetOp::Diff => { let first = db.set_keys.get(&keys[0]).cloned().unwrap_or_default(); let mut result: HashSet = first; for key in &keys[1..] { if let Some(other) = db.set_keys.get(key) { result = result.difference(other).cloned().collect(); } } Ok(result) } SetOp::Inter => { for key in keys { if !db.keys.contains_key(key) { return Ok(HashSet::new()); } } let first = db.set_keys.get(&keys[0]).cloned().unwrap_or_default(); let mut result: HashSet = first; for key in &keys[1..] { if let Some(other) = db.set_keys.get(key) { result = result.intersection(other).cloned().collect(); } else { return Ok(HashSet::new()); } } Ok(result) } SetOp::Union => { let mut result = HashSet::new(); for key in keys { if let Some(set) = db.set_keys.get(key) { result = result.union(set).cloned().collect(); } } Ok(result) } } } fn set_to_frame(set: &HashSet, resp3: bool) -> Frame { let mut members: Vec = set.iter().cloned().collect(); members.sort(); let items: Vec = members.into_iter().map(|m| Frame::Bulk(m.into())).collect(); if resp3 { Frame::Set(items) } else { Frame::Array(items) } } fn cmd_set_op(state: &Arc, ctx: &mut ConnCtx, args: &[Vec], op: SetOp) -> Frame { let keys: Vec = args .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); match set_op(state, ctx, &keys, op) { Ok(set) => set_to_frame(&set, ctx.resp3), Err(e) => e, } } fn cmd_set_store( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], op: SetOp, ) -> Frame { let dest = String::from_utf8_lossy(&args[0]).into_owned(); let keys: Vec = args[1..] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let result = match set_op(state, ctx, &keys, op) { Ok(set) => set, Err(e) => return e, }; let count = result.len() as i64; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(&dest); if !result.is_empty() { db.set_set(&dest, result, now); } Frame::Integer(count) } /// SDIFF key [key ...] fn cmd_sdiff(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_op(state, ctx, args, SetOp::Diff) } /// SDIFFSTORE destination key [key ...] fn cmd_sdiffstore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_store(state, ctx, args, SetOp::Diff) } /// SINTER key [key ...] fn cmd_sinter(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_op(state, ctx, args, SetOp::Inter) } /// SINTERSTORE destination key [key ...] fn cmd_sinterstore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_store(state, ctx, args, SetOp::Inter) } /// SUNION key [key ...] fn cmd_sunion(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_op(state, ctx, args, SetOp::Union) } /// SUNIONSTORE destination key [key ...] fn cmd_sunionstore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_set_store(state, ctx, args, SetOp::Union) } /// SMOVE source destination member fn cmd_smove(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let member = String::from_utf8_lossy(&args[2]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); // Type checks if let Some(t) = db.key_type(&src) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } if !db.keys.contains_key(&src) { return Frame::Integer(0); } if !db.set_is_member(&src, &member) { return Frame::Integer(0); } db.set_rem(&src, std::slice::from_ref(&member), now); db.set_add(&dst, std::slice::from_ref(&member), now); Frame::Integer(1) } /// SPOP key [count] fn cmd_spop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut with_count = false; let mut count: usize = 1; if args.len() > 1 { match parse_int(&args[1]) { Some(n) if n < 0 => return Frame::error(MSG_OUT_OF_RANGE), Some(n) => { count = n as usize; with_count = true; } None => return Frame::error(MSG_INVALID_INT), } } if args.len() > 2 { return Frame::error(MSG_INVALID_INT); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let mut members = db.set_members(&key); let mut deleted = Vec::new(); for _ in 0..count { if members.is_empty() { break; } let idx = inner.rng.random_range(0..members.len()); let member = members.remove(idx); let db = inner.db_mut(ctx.selected_db); db.set_rem(&key, std::slice::from_ref(&member), now); deleted.push(member); } if !with_count { if deleted.is_empty() { Frame::Null } else { Frame::Bulk(deleted[0].clone().into()) } } else { Frame::Array(deleted.into_iter().map(|m| Frame::Bulk(m.into())).collect()) } } /// SRANDMEMBER key [count] fn cmd_srandmember(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 2 { return Frame::error(MSG_SYNTAX_ERROR); } let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut count: i64 = 0; let mut with_count = false; if args.len() == 2 { match parse_int(&args[1]) { Some(n) => { count = n; with_count = true; } None => return Frame::error(MSG_INVALID_INT), } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } if let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let mut members = db.set_members(&key); if count < 0 { // Negative count: allow duplicates let abs_count = (-count) as usize; let mut result = Vec::with_capacity(abs_count); for _ in 0..abs_count { let idx = inner.rng.random_range(0..members.len()); result.push(Frame::Bulk(members[idx].clone().into())); } return Frame::Array(result); } // Positive count: unique members, shuffle members.shuffle(&mut inner.rng); let take = (count as usize).min(members.len()); if !with_count { return Frame::Bulk(members[0].clone().into()); } Frame::Array( members[..take] .iter() .map(|m| Frame::Bulk(m.clone().into())) .collect(), ) } /// SSCAN key cursor [MATCH pattern] [COUNT count] fn cmd_sscan(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let cursor: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_CURSOR), }; let opts = match super::parse_scan_opts(&args[2..], false) { Ok(o) => o, Err(e) => return e, }; // SSCAN validates COUNT more strictly let scan_count: usize = match opts.count { Some(n) if n < 0 => return Frame::error(MSG_INVALID_INT), Some(0) => return Frame::error(MSG_SYNTAX_ERROR), Some(n) => n as usize, None => 0, }; let inner = state.lock(); let db = inner.db(ctx.selected_db); if db.keys.contains_key(key.as_ref()) && let Some(t) = db.key_type(&key) && t != KeyType::Set { return Frame::error(MSG_WRONG_TYPE); } let mut members = db.set_members(&key); members.sort(); // Apply MATCH filter if let Some(ref pat) = opts.pattern { members = crate::keys::match_keys_vec(&members, pat); } let low = cursor as usize; let high = if scan_count > 0 { (low + scan_count).min(members.len()) } else { members.len() }; if low >= members.len() { return Frame::Array(vec![Frame::Bulk("0".into()), Frame::Array(vec![])]); } let cursor_value = if high >= members.len() { 0 } else { high }; let selected = &members[low..high]; Frame::Array(vec![ Frame::Bulk(cursor_value.to_string().into()), Frame::Array( selected .iter() .map(|m| Frame::Bulk(m.clone().into())) .collect(), ), ]) } /// SINTERCARD numkeys key [key ...] [LIMIT limit] fn cmd_sintercard(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let num_keys = match parse_int(&args[0]) { Some(n) if n < 1 => { return Frame::error("ERR numkeys should be greater than 0"); } Some(n) => n as usize, None => { return Frame::error("ERR numkeys should be greater than 0"); } }; if args.len() < 1 + num_keys { return Frame::error(MSG_INVALID_KEYS_NUMBER); } let keys: Vec = args[1..1 + num_keys] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let mut limit: usize = 0; let rest = &args[1 + num_keys..]; if rest.len() == 2 { let opt = String::from_utf8_lossy(&rest[0]).to_uppercase(); if opt == "LIMIT" { match parse_int(&rest[1]) { Some(n) if n < 0 => { return Frame::error("ERR LIMIT can't be negative"); } Some(n) => limit = n as usize, None => return Frame::error(MSG_INVALID_INT), } } else { return Frame::error(MSG_SYNTAX_ERROR); } } else if !rest.is_empty() { return Frame::error(MSG_SYNTAX_ERROR); } let result = match set_op(state, ctx, &keys, SetOp::Inter) { Ok(set) => set, Err(e) => return e, }; let count = result.len(); if limit > 0 && count > limit { Frame::Integer(limit as i64) } else { Frame::Integer(count as i64) } } ================================================ FILE: miniredis/src/cmd/sorted_set.rs ================================================ use std::sync::Arc; use rand::Rng; use rand::seq::SliceRandom; use super::{parse_float, parse_int}; use crate::cmd::string::format_float; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INVALID_CURSOR, MSG_INVALID_FLOAT, MSG_INVALID_INT, MSG_INVALID_MIN_MAX, MSG_INVALID_RANGE_ITEM, MSG_SINGLE_ELEMENT_PAIR, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, MSG_XX_AND_NX, err_wrong_number, }; use crate::frame::Frame; use crate::types::{Direction, SSElem, SortedSet}; pub fn register(table: &mut CommandTable) { table.add("ZADD", cmd_zadd, false, -4); table.add("ZCARD", cmd_zcard, true, 2); table.add("ZCOUNT", cmd_zcount, true, 4); table.add("ZINCRBY", cmd_zincrby, false, 4); table.add("ZSCORE", cmd_zscore, true, 3); table.add("ZMSCORE", cmd_zmscore, true, -3); table.add("ZRANK", cmd_zrank, true, -3); table.add("ZREVRANK", cmd_zrevrank, true, -3); table.add("ZREM", cmd_zrem, false, -3); table.add("ZRANGE", cmd_zrange, true, -4); table.add("ZREVRANGE", cmd_zrevrange, true, -4); table.add("ZRANGEBYSCORE", cmd_zrangebyscore, true, -4); table.add("ZREVRANGEBYSCORE", cmd_zrevrangebyscore, true, -4); table.add("ZRANGEBYLEX", cmd_zrangebylex, true, -4); table.add("ZREVRANGEBYLEX", cmd_zrevrangebylex, true, -4); table.add("ZLEXCOUNT", cmd_zlexcount, true, 4); table.add("ZREMRANGEBYRANK", cmd_zremrangebyrank, false, 4); table.add("ZREMRANGEBYSCORE", cmd_zremrangebyscore, false, 4); table.add("ZREMRANGEBYLEX", cmd_zremrangebylex, false, 4); table.add("ZUNIONSTORE", cmd_zunionstore, false, -4); table.add("ZINTERSTORE", cmd_zinterstore, false, -4); table.add("ZPOPMIN", cmd_zpopmin, false, -2); table.add("ZPOPMAX", cmd_zpopmax, false, -2); table.add("ZSCAN", cmd_zscan, true, -3); table.add("ZINTER", cmd_zinter, true, -3); table.add("ZUNION", cmd_zunion, true, -3); table.add("ZRANDMEMBER", cmd_zrandmember, true, -2); } /// Format a float for Redis output (scores). fn write_float(f: f64) -> String { if f == f64::INFINITY { "inf".to_string() } else if f == f64::NEG_INFINITY { "-inf".to_string() } else { format_float(f) } } /// Parse a score range like "1.5", "(1.5", "+inf", "-inf". fn parse_float_range(s: &str) -> Result<(f64, bool), ()> { if s.is_empty() { return Err(()); } let (s, inclusive) = if let Some(rest) = s.strip_prefix('(') { (rest, false) } else { (s, true) }; match s.to_lowercase().as_str() { "+inf" | "inf" => Ok((f64::INFINITY, true)), "-inf" => Ok((f64::NEG_INFINITY, true)), _ => s.parse::().map(|f| (f, inclusive)).map_err(|_| ()), } } /// Parse a lex range like "[a", "(a", "+", "-". fn parse_lex_range(s: &str) -> Result<(String, bool), ()> { if s.is_empty() { return Err(()); } if s == "+" || s == "-" { return Ok((s.to_string(), false)); } match s.as_bytes()[0] { b'(' => Ok((s[1..].to_string(), false)), b'[' => Ok((s[1..].to_string(), true)), _ => Err(()), } } /// Filter elements by score range. fn with_ss_range( members: Vec, min: f64, min_incl: bool, max: f64, max_incl: bool, ) -> Vec { members .into_iter() .filter(|e| { let above_min = if min_incl { e.score >= min } else { e.score > min }; let below_max = if max_incl { e.score <= max } else { e.score < max }; above_min && below_max }) .collect() } /// Filter member names by lex range. fn with_lex_range( members: Vec, min: &str, min_incl: bool, max: &str, max_incl: bool, ) -> Vec { if max == "-" || min == "+" { return Vec::new(); } members .into_iter() .filter(|m| { let above_min = if min == "-" { true } else if min_incl { m.as_str() >= min } else { m.as_str() > min }; let below_max = if max == "+" { true } else if max_incl { m.as_str() <= max } else { m.as_str() < max }; above_min && below_max }) .collect() } /// Normalize Redis-style range indices for sorted sets. fn redis_range(len: usize, start: i64, stop: i64) -> (usize, usize) { let len = len as i64; let mut s = if start < 0 { len + start } else { start }; let mut e = if stop < 0 { len + stop } else { stop }; if s < 0 { s = 0; } if e >= len { e = len - 1; } if s > e || s >= len { return (0, 0); } (s as usize, (e + 1) as usize) } // ── Commands ───────────────────────────────────────────────────────── /// ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...] fn cmd_zadd(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut i = 1; let mut nx = false; let mut xx = false; let mut gt = false; let mut lt = false; let mut ch = false; let mut incr = false; // Parse flags while i < args.len() { let flag = String::from_utf8_lossy(&args[i]).to_uppercase(); match flag.as_str() { "NX" => { nx = true; i += 1; } "XX" => { xx = true; i += 1; } "GT" => { gt = true; i += 1; } "LT" => { lt = true; i += 1; } "CH" => { ch = true; i += 1; } "INCR" => { incr = true; i += 1; } _ => break, } } // Remaining args should be score-member pairs let remaining = &args[i..]; if remaining.is_empty() || !remaining.len().is_multiple_of(2) { return Frame::error(MSG_SYNTAX_ERROR); } // Parse score-member pairs let mut elems: Vec<(String, f64)> = Vec::new(); let mut j = 0; while j < remaining.len() { let score = match parse_float(&remaining[j]) { Some(f) => f, None => return Frame::error(MSG_INVALID_FLOAT), }; let member = String::from_utf8_lossy(&remaining[j + 1]).into_owned(); elems.push((member, score)); j += 2; } // Validation if xx && nx { return Frame::error(MSG_XX_AND_NX); } if (gt || lt) && (gt && lt || nx) { return Frame::error("ERR GT, LT, and/or NX options at the same time are not compatible"); } if incr && elems.len() > 1 { return Frame::error(MSG_SINGLE_ELEMENT_PAIR); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } // INCR mode if incr { let (member, delta) = &elems[0]; if nx && db.sset_exists(&key, member) { return Frame::Null; } if xx && !db.sset_exists(&key, member) { return Frame::Null; } let new_score = db.sset_incrby(&key, member, *delta, now); return if ctx.resp3 { Frame::Double(new_score) } else { Frame::Bulk(write_float(new_score).into()) }; } let mut count = 0i64; for (member, score) in &elems { let exists = db.sset_exists(&key, member); if nx && exists { continue; } if xx && !exists { continue; } if gt && exists && let Some(old) = db.sset_score(&key, member) && *score <= old { continue; } if lt && exists && let Some(old) = db.sset_score(&key, member) && *score >= old { continue; } let old_score = db.sset_score(&key, member); let is_new = db.sset_add(&key, *score, member, now); if is_new || (ch && old_score != Some(*score)) { count += 1; } } Frame::Integer(count) } /// ZCARD key fn cmd_zcard(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } Frame::Integer(db.sset_card(&key) as i64) } /// ZCOUNT key min max fn cmd_zcount(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let min_s = String::from_utf8_lossy(&args[1]); let max_s = String::from_utf8_lossy(&args[2]); let (min, min_incl) = match parse_float_range(&min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; let (max, max_incl) = match parse_float_range(&max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key.as_ref()) { Some(ss) => ss, None => return Frame::Integer(0), }; let elems = ss.by_score(Direction::Asc); let filtered = with_ss_range(elems, min, min_incl, max, max_incl); Frame::Integer(filtered.len() as i64) } /// ZINCRBY key increment member fn cmd_zincrby(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let delta = match parse_float(&args[1]) { Some(f) => f, None => return Frame::error(MSG_INVALID_FLOAT), }; let member = String::from_utf8_lossy(&args[2]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let new_score = db.sset_incrby(&key, &member, delta, now); Frame::Bulk(write_float(new_score).into()) } /// ZSCORE key member fn cmd_zscore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let member = String::from_utf8_lossy(&args[1]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Null; } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } match db.sset_score(&key, &member) { Some(score) => { if ctx.resp3 { Frame::Double(score) } else { Frame::Bulk(write_float(score).into()) } } None => Frame::Null, } } /// ZMSCORE key member [member ...] fn cmd_zmscore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let results: Vec = args[1..] .iter() .map(|a| { let member = String::from_utf8_lossy(a); match db.sset_score(&key, &member) { Some(score) => Frame::Bulk(write_float(score).into()), None => Frame::Null, } }) .collect(); Frame::Array(results) } /// ZRANK key member fn cmd_zrank(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrank_impl(state, ctx, args, Direction::Asc, "zrank") } /// ZREVRANK key member fn cmd_zrevrank(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrank_impl(state, ctx, args, Direction::Desc, "zrevrank") } fn zrank_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], dir: Direction, cmd: &str, ) -> Frame { if args.len() > 3 { return Frame::error(err_wrong_number(cmd)); } let key = String::from_utf8_lossy(&args[0]); let member = String::from_utf8_lossy(&args[1]); let with_score = args.len() == 3 && String::from_utf8_lossy(&args[2]).to_uppercase() == "WITHSCORE"; if args.len() == 3 && !with_score { return Frame::error(MSG_SYNTAX_ERROR); } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return if with_score { Frame::NullArray } else { Frame::Null }; } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key.as_ref()) { Some(ss) => ss, None => { return if with_score { Frame::NullArray } else { Frame::Null }; } }; match ss.rank(&member, dir) { Some(rank) => { if with_score { let score = ss.get(&member).unwrap_or(0.0); Frame::Array(vec![ Frame::Integer(rank as i64), Frame::Bulk(write_float(score).into()), ]) } else { Frame::Integer(rank as i64) } } None => { if with_score { Frame::NullArray } else { Frame::Null } } } } /// ZREM key member [member ...] fn cmd_zrem(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let mut deleted = 0i64; for a in &args[1..] { let member = String::from_utf8_lossy(a); if db.sset_rem(&key, &member, now) { deleted += 1; } } Frame::Integer(deleted) } // ── Range commands ─────────────────────────────────────────────────── /// ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES] fn cmd_zrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]).into_owned(); let max_s = String::from_utf8_lossy(&args[2]).into_owned(); let mut with_scores = false; let mut by_score = false; let mut by_lex = false; let mut reverse = false; let mut with_limit = false; let mut offset_s = String::new(); let mut count_s = String::new(); let mut i = 3; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "BYSCORE" => { by_score = true; i += 1; } "BYLEX" => { by_lex = true; i += 1; } "REV" => { reverse = true; i += 1; } "LIMIT" => { with_limit = true; i += 1; if i + 1 >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } offset_s = String::from_utf8_lossy(&args[i]).into_owned(); count_s = String::from_utf8_lossy(&args[i + 1]).into_owned(); i += 2; } "WITHSCORES" => { with_scores = true; i += 1; } _ => return Frame::error(MSG_SYNTAX_ERROR), } } if by_score && by_lex { return Frame::error(MSG_SYNTAX_ERROR); } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if by_score { run_range_by_score( db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s, with_scores, ) } else if by_lex { run_range_by_lex( db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s, ) } else { if with_limit { return Frame::error( "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX", ); } run_range_by_rank(db, &key, &min_s, &max_s, reverse, with_scores) } } /// ZREVRANGE key start stop [WITHSCORES] fn cmd_zrevrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]).into_owned(); let max_s = String::from_utf8_lossy(&args[2]).into_owned(); let mut with_scores = false; if args.len() > 3 { let opt = String::from_utf8_lossy(&args[3]).to_uppercase(); if opt == "WITHSCORES" { with_scores = true; } else { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); run_range_by_rank(db, &key, &min_s, &max_s, true, with_scores) } /// ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] fn cmd_zrangebyscore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrangebyscore_impl(state, ctx, args, false) } /// ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] fn cmd_zrevrangebyscore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrangebyscore_impl(state, ctx, args, true) } fn zrangebyscore_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], reverse: bool, ) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]).into_owned(); let max_s = String::from_utf8_lossy(&args[2]).into_owned(); let mut with_scores = false; let mut with_limit = false; let mut offset_s = String::new(); let mut count_s = String::new(); let mut i = 3; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "WITHSCORES" => { with_scores = true; i += 1; } "LIMIT" => { with_limit = true; i += 1; if i + 1 >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } offset_s = String::from_utf8_lossy(&args[i]).into_owned(); count_s = String::from_utf8_lossy(&args[i + 1]).into_owned(); i += 2; } _ => return Frame::error(MSG_SYNTAX_ERROR), } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); run_range_by_score( db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s, with_scores, ) } /// ZRANGEBYLEX key min max [LIMIT offset count] fn cmd_zrangebylex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrangebylex_impl(state, ctx, args, false) } /// ZREVRANGEBYLEX key max min [LIMIT offset count] fn cmd_zrevrangebylex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zrangebylex_impl(state, ctx, args, true) } fn zrangebylex_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], reverse: bool, ) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]).into_owned(); let max_s = String::from_utf8_lossy(&args[2]).into_owned(); let mut with_limit = false; let mut offset_s = String::new(); let mut count_s = String::new(); let mut i = 3; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); if opt == "LIMIT" { with_limit = true; i += 1; if i + 1 >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } offset_s = String::from_utf8_lossy(&args[i]).into_owned(); count_s = String::from_utf8_lossy(&args[i + 1]).into_owned(); i += 2; } else { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); run_range_by_lex( db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s, ) } /// ZLEXCOUNT key min max fn cmd_zlexcount(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let min_s = String::from_utf8_lossy(&args[1]); let max_s = String::from_utf8_lossy(&args[2]); let (min, min_incl) = match parse_lex_range(&min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; let (max, max_incl) = match parse_lex_range(&max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key.as_ref()) { Some(ss) => ss, None => return Frame::Integer(0), }; let mut members = ss.members_sorted(); members.sort(); let filtered = with_lex_range(members, &min, min_incl, &max, max_incl); Frame::Integer(filtered.len() as i64) } // ── Remove range commands ──────────────────────────────────────────── /// ZREMRANGEBYRANK key start stop fn cmd_zremrangebyrank(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let start = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let stop = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(&key) { Some(ss) => ss, None => return Frame::Integer(0), }; let members = ss.members_sorted(); let (rs, re) = redis_range(members.len(), start, stop); let to_remove: Vec = members[rs..re].to_vec(); for m in &to_remove { db.sset_rem(&key, m, now); } Frame::Integer(to_remove.len() as i64) } /// ZREMRANGEBYSCORE key min max fn cmd_zremrangebyscore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]); let max_s = String::from_utf8_lossy(&args[2]); let (min, min_incl) = match parse_float_range(&min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; let (max, max_incl) = match parse_float_range(&max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(&key) { Some(ss) => ss, None => return Frame::Integer(0), }; let elems = ss.by_score(Direction::Asc); let filtered = with_ss_range(elems, min, min_incl, max, max_incl); let to_remove: Vec = filtered.into_iter().map(|e| e.member).collect(); for m in &to_remove { db.sset_rem(&key, m, now); } Frame::Integer(to_remove.len() as i64) } /// ZREMRANGEBYLEX key min max fn cmd_zremrangebylex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let min_s = String::from_utf8_lossy(&args[1]); let max_s = String::from_utf8_lossy(&args[2]); let (min, min_incl) = match parse_lex_range(&min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; let (max, max_incl) = match parse_lex_range(&max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Integer(0); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(&key) { Some(ss) => ss, None => return Frame::Integer(0), }; let mut members = ss.members_sorted(); members.sort(); let filtered = with_lex_range(members, &min, min_incl, &max, max_incl); for m in &filtered { db.sset_rem(&key, m, now); } Frame::Integer(filtered.len() as i64) } // ── Set operations (ZUNIONSTORE, ZINTERSTORE) ──────────────────────── /// ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS w...] [AGGREGATE SUM|MIN|MAX] fn cmd_zunionstore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zstore_impl(state, ctx, args, false) } /// ZINTERSTORE destination numkeys key [key ...] [WEIGHTS w...] [AGGREGATE SUM|MIN|MAX] fn cmd_zinterstore(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zstore_impl(state, ctx, args, true) } fn zstore_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], intersect: bool, ) -> Frame { let dest = String::from_utf8_lossy(&args[0]).into_owned(); let num_keys = match parse_int(&args[1]) { Some(n) if n > 0 => n as usize, Some(_) => { return Frame::error("ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE"); } _ => return Frame::error(MSG_INVALID_INT), }; if args.len() < 2 + num_keys { return Frame::error(MSG_SYNTAX_ERROR); } let keys: Vec = args[2..2 + num_keys] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let mut rest = &args[2 + num_keys..]; let mut weights: Vec = Vec::new(); let mut with_weights = false; let mut aggregate = "sum".to_string(); while !rest.is_empty() { let opt = String::from_utf8_lossy(&rest[0]).to_lowercase(); match opt.as_str() { "weights" => { if rest.len() < num_keys + 1 { return Frame::error(MSG_SYNTAX_ERROR); } for i in 0..num_keys { match parse_float(&rest[i + 1]) { Some(f) => weights.push(f), None => return Frame::error("ERR weight value is not a float"), } } with_weights = true; rest = &rest[num_keys + 1..]; } "aggregate" => { if rest.len() < 2 { return Frame::error(MSG_SYNTAX_ERROR); } aggregate = String::from_utf8_lossy(&rest[1]).to_lowercase(); match aggregate.as_str() { "sum" | "min" | "max" => {} _ => return Frame::error(MSG_SYNTAX_ERROR), } rest = &rest[2..]; } _ => return Frame::error(MSG_SYNTAX_ERROR), } } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); // Collect all scores let mut sset: std::collections::HashMap = std::collections::HashMap::new(); let mut counts: std::collections::HashMap = std::collections::HashMap::new(); for (i, key) in keys.iter().enumerate() { if !db.keys.contains_key(key) { continue; } let key_type = db.key_type(key); let set: std::collections::HashMap = match key_type { Some(crate::types::KeyType::Set) => db .set_keys .get(key) .map(|s| s.iter().map(|m| (m.clone(), 1.0)).collect()) .unwrap_or_default(), Some(crate::types::KeyType::SortedSet) => db .sorted_set_keys .get(key) .map(|ss| ss.scores.clone()) .unwrap_or_default(), _ => return Frame::error(MSG_WRONG_TYPE), }; for (member, mut score) in set { if with_weights { score *= weights[i]; } *counts.entry(member.clone()).or_insert(0) += 1; let entry = sset.entry(member); match entry { std::collections::hash_map::Entry::Vacant(e) => { e.insert(score); } std::collections::hash_map::Entry::Occupied(mut e) => { let old = *e.get(); match aggregate.as_str() { "sum" => *e.get_mut() += score, "min" => { if score < old { *e.get_mut() = score; } } "max" => { if score > old { *e.get_mut() = score; } } _ => {} } } } } } // For ZINTERSTORE: only keep members present in ALL keys if intersect { sset.retain(|member, _| counts.get(member).copied().unwrap_or(0) == keys.len()); } // Store result db.del(&dest); if !sset.is_empty() { let mut new_ss = SortedSet::new(); for (member, score) in &sset { new_ss.set(*score, member); } db.sset_set(&dest, new_ss, now); } Frame::Integer(sset.len() as i64) } // ── ZPOPMIN/ZPOPMAX ───────────────────────────────────────────────── /// ZPOPMIN key [count] fn cmd_zpopmin(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zpop_impl(state, ctx, args, false) } /// ZPOPMAX key [count] fn cmd_zpopmax(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zpop_impl(state, ctx, args, true) } fn zpop_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], reverse: bool, ) -> Frame { if args.len() > 2 { return Frame::error(MSG_SYNTAX_ERROR); } let key = String::from_utf8_lossy(&args[0]).into_owned(); let count = if args.len() > 1 { match parse_int(&args[1]) { Some(n) if n >= 0 => n as usize, _ => return Frame::error(MSG_INVALID_INT), } } else { 1 }; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Array(vec![]); } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(&key) { Some(ss) => ss, None => return Frame::Array(vec![]), }; let dir = if reverse { Direction::Desc } else { Direction::Asc }; let elems = ss.by_score(dir); let take = count.min(elems.len()); let to_pop: Vec = elems[..take].to_vec(); let mut result = Vec::new(); for e in &to_pop { result.push(Frame::Bulk(e.member.clone().into())); result.push(Frame::Bulk(write_float(e.score).into())); db.sset_rem(&key, &e.member, now); } Frame::Array(result) } // ── ZSCAN ──────────────────────────────────────────────────────────── /// ZSCAN key cursor [MATCH pattern] [COUNT count] fn cmd_zscan(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let _cursor = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_CURSOR), }; let opts = match super::parse_scan_opts(&args[2..], false) { Ok(o) => o, Err(e) => return e, }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key.as_ref()) { Some(ss) => ss, None => { return Frame::Array(vec![Frame::Bulk("0".into()), Frame::Array(vec![])]); } }; let mut members = ss.members_sorted(); members.sort(); // Apply MATCH filter if let Some(ref pat) = opts.pattern { members = crate::keys::match_keys_vec(&members, pat); } // Return all members with cursor=0 (no real cursor pagination) let mut result = Vec::new(); for m in &members { result.push(Frame::Bulk(m.clone().into())); let score = ss.get(m).unwrap_or(0.0); result.push(Frame::Bulk(write_float(score).into())); } Frame::Array(vec![Frame::Bulk("0".into()), Frame::Array(result)]) } // ── Range helper functions ─────────────────────────────────────────── fn run_range_by_rank( db: &crate::db::RedisDB, key: &str, min_s: &str, max_s: &str, reverse: bool, with_scores: bool, ) -> Frame { let min: i64 = match min_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; let max: i64 = match max_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; if !db.keys.contains_key(key) { return Frame::Array(vec![]); } if let Some(t) = db.key_type(key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key) { Some(ss) => ss, None => return Frame::Array(vec![]), }; let dir = if reverse { Direction::Desc } else { Direction::Asc }; let elems = ss.by_score(dir); let (rs, re) = redis_range(elems.len(), min, max); let mut result = Vec::new(); for e in &elems[rs..re] { result.push(Frame::Bulk(e.member.clone().into())); if with_scores { result.push(Frame::Bulk(write_float(e.score).into())); } } Frame::Array(result) } #[allow(clippy::too_many_arguments)] fn run_range_by_score( db: &crate::db::RedisDB, key: &str, min_s: &str, max_s: &str, reverse: bool, with_limit: bool, offset_s: &str, count_s: &str, with_scores: bool, ) -> Frame { let mut limit_offset = 0i64; let mut limit_count = -1i64; if with_limit { limit_offset = match offset_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; limit_count = match count_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; } let (min, min_incl) = match parse_float_range(min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; let (max, max_incl) = match parse_float_range(max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_MIN_MAX), }; if !db.keys.contains_key(key) { return Frame::Array(vec![]); } if let Some(t) = db.key_type(key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key) { Some(ss) => ss, None => return Frame::Array(vec![]), }; let elems = ss.by_score(Direction::Asc); // For reverse, swap min/max and their inclusiveness let (fmin, fmin_incl, fmax, fmax_incl) = if reverse { (max, max_incl, min, min_incl) } else { (min, min_incl, max, max_incl) }; let mut filtered = with_ss_range(elems, fmin, fmin_incl, fmax, fmax_incl); if reverse { filtered.reverse(); } // Apply LIMIT if with_limit { if limit_offset < 0 { filtered = Vec::new(); } else { let offset = limit_offset as usize; if offset < filtered.len() { filtered = filtered[offset..].to_vec(); } else { filtered = Vec::new(); } if limit_count >= 0 { let count = limit_count as usize; if filtered.len() > count { filtered.truncate(count); } } } } let mut result = Vec::new(); for e in &filtered { result.push(Frame::Bulk(e.member.clone().into())); if with_scores { result.push(Frame::Bulk(write_float(e.score).into())); } } Frame::Array(result) } #[allow(clippy::too_many_arguments)] fn run_range_by_lex( db: &crate::db::RedisDB, key: &str, min_s: &str, max_s: &str, reverse: bool, with_limit: bool, offset_s: &str, count_s: &str, ) -> Frame { let mut limit_offset = 0i64; let mut limit_count = -1i64; if with_limit { limit_offset = match offset_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; limit_count = match count_s.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; } let (min, min_incl) = match parse_lex_range(min_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; let (max, max_incl) = match parse_lex_range(max_s) { Ok(v) => v, Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM), }; if !db.keys.contains_key(key) { return Frame::Array(vec![]); } if let Some(t) = db.key_type(key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key) { Some(ss) => ss, None => return Frame::Array(vec![]), }; let mut members = ss.members_sorted(); members.sort(); // For reverse, swap min/max let (fmin, fmin_incl, fmax, fmax_incl) = if reverse { (max.clone(), max_incl, min.clone(), min_incl) } else { (min, min_incl, max, max_incl) }; let mut filtered = with_lex_range(members, &fmin, fmin_incl, &fmax, fmax_incl); if reverse { filtered.reverse(); } // Apply LIMIT if with_limit { if limit_offset < 0 { filtered = Vec::new(); } else { let offset = limit_offset as usize; if offset < filtered.len() { filtered = filtered[offset..].to_vec(); } else { filtered = Vec::new(); } if limit_count >= 0 { let count = limit_count as usize; if filtered.len() > count { filtered.truncate(count); } } } } let result: Vec = filtered .into_iter() .map(|m| Frame::Bulk(m.into())) .collect(); Frame::Array(result) } // ── ZINTER / ZUNION (without STORE) ────────────────────────────────── /// Parse args for ZINTER/ZUNION: numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES] fn zop_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], intersect: bool, ) -> Frame { let num_keys = match parse_int(&args[0]) { Some(n) if n > 0 => n as usize, Some(_) => { return Frame::error( "ERR at least 1 input key is needed for 'zinter'/'zunion' command", ); } _ => return Frame::error(MSG_INVALID_INT), }; if args.len() < 1 + num_keys { return Frame::error(MSG_SYNTAX_ERROR); } let keys: Vec = args[1..1 + num_keys] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); let mut rest = &args[1 + num_keys..]; let mut weights: Vec = Vec::new(); let mut with_weights = false; let mut aggregate = "sum".to_string(); let mut with_scores = false; while !rest.is_empty() { let opt = String::from_utf8_lossy(&rest[0]).to_lowercase(); match opt.as_str() { "weights" => { if rest.len() < num_keys + 1 { return Frame::error(MSG_SYNTAX_ERROR); } for i in 0..num_keys { match parse_float(&rest[i + 1]) { Some(f) => weights.push(f), None => return Frame::error("ERR weight value is not a float"), } } with_weights = true; rest = &rest[num_keys + 1..]; } "aggregate" => { if rest.len() < 2 { return Frame::error(MSG_SYNTAX_ERROR); } aggregate = String::from_utf8_lossy(&rest[1]).to_lowercase(); match aggregate.as_str() { "sum" | "min" | "max" => {} _ => return Frame::error(MSG_SYNTAX_ERROR), } rest = &rest[2..]; } "withscores" => { with_scores = true; rest = &rest[1..]; } _ => return Frame::error(MSG_SYNTAX_ERROR), } } let inner = state.lock(); let db = inner.db(ctx.selected_db); let mut sset: std::collections::HashMap = std::collections::HashMap::new(); let mut counts: std::collections::HashMap = std::collections::HashMap::new(); for (i, key) in keys.iter().enumerate() { if !db.keys.contains_key(key) { continue; } let key_type = db.key_type(key); let set: std::collections::HashMap = match key_type { Some(crate::types::KeyType::Set) => db .set_keys .get(key) .map(|s| s.iter().map(|m| (m.clone(), 1.0)).collect()) .unwrap_or_default(), Some(crate::types::KeyType::SortedSet) => db .sorted_set_keys .get(key) .map(|ss| ss.scores.clone()) .unwrap_or_default(), _ => return Frame::error(MSG_WRONG_TYPE), }; for (member, mut score) in set { if with_weights { score *= weights[i]; } *counts.entry(member.clone()).or_insert(0) += 1; let entry = sset.entry(member); match entry { std::collections::hash_map::Entry::Vacant(e) => { e.insert(score); } std::collections::hash_map::Entry::Occupied(mut e) => { let old = *e.get(); match aggregate.as_str() { "sum" => *e.get_mut() += score, "min" => { if score < old { *e.get_mut() = score; } } "max" => { if score > old { *e.get_mut() = score; } } _ => {} } } } } } if intersect { sset.retain(|member, _| counts.get(member).copied().unwrap_or(0) == keys.len()); } // Sort by score, then by member let mut elems: Vec<(String, f64)> = sset.into_iter().collect(); elems.sort_by(|a, b| { a.1.partial_cmp(&b.1) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| a.0.cmp(&b.0)) }); let mut result = Vec::new(); for (member, score) in &elems { result.push(Frame::Bulk(member.clone().into())); if with_scores { result.push(Frame::Bulk(write_float(*score).into())); } } Frame::Array(result) } /// ZINTER numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES] fn cmd_zinter(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zop_impl(state, ctx, args, true) } /// ZUNION numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES] fn cmd_zunion(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { zop_impl(state, ctx, args, false) } /// ZRANDMEMBER key [count [WITHSCORES]] fn cmd_zrandmember(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if args.len() > 3 { return Frame::error(err_wrong_number("zrandmember")); } let key = String::from_utf8_lossy(&args[0]); let mut count: i64 = 0; let mut with_count = false; let mut with_scores = false; if args.len() >= 2 { match parse_int(&args[1]) { Some(n) => { count = n; with_count = true; } None => return Frame::error(MSG_INVALID_INT), } } if args.len() == 3 { let opt = String::from_utf8_lossy(&args[2]).to_uppercase(); if opt == "WITHSCORES" { with_scores = true; } else { return Frame::error(MSG_SYNTAX_ERROR); } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(key.as_ref()) { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } if let Some(t) = db.key_type(&key) && t != crate::types::KeyType::SortedSet { return Frame::error(MSG_WRONG_TYPE); } let ss = match db.sorted_set_keys.get(key.as_ref()) { Some(ss) => ss, None => { return if with_count { Frame::Array(vec![]) } else { Frame::Null }; } }; let mut members = ss.members_sorted(); // Collect scores before shuffling (avoids borrow issues with inner.rng) let scores: std::collections::HashMap = members .iter() .map(|m| (m.clone(), ss.get(m).unwrap_or(0.0))) .collect(); if count < 0 { // Negative count: allow duplicates let abs_count = (-count) as usize; let mut result = Vec::new(); for _ in 0..abs_count { let idx = inner.rng.random_range(0..members.len()); result.push(Frame::Bulk(members[idx].clone().into())); if with_scores { let score = scores.get(&members[idx]).copied().unwrap_or(0.0); result.push(Frame::Bulk(write_float(score).into())); } } return Frame::Array(result); } // Positive count: unique, shuffle members.shuffle(&mut inner.rng); let take = (count as usize).min(members.len()); if !with_count { return Frame::Bulk(members[0].clone().into()); } let mut result = Vec::new(); for m in &members[..take] { result.push(Frame::Bulk(m.clone().into())); if with_scores { let score = scores.get(m).copied().unwrap_or(0.0); result.push(Frame::Bulk(write_float(score).into())); } } Frame::Array(result) } ================================================ FILE: miniredis/src/cmd/stream.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{CommandTable, MSG_WRONG_TYPE, err_wrong_number}; use crate::frame::Frame; use crate::types::{KeyType, Stream, format_stream_range_bound}; pub fn register(table: &mut CommandTable) { table.add("XADD", cmd_xadd, false, -5); table.add("XLEN", cmd_xlen, true, 2); table.add("XRANGE", cmd_xrange, true, -4); table.add("XREVRANGE", cmd_xrevrange, true, -4); table.add("XREAD", cmd_xread, true, -4); table.add("XINFO", cmd_xinfo, true, -2); table.add("XDEL", cmd_xdel, false, -3); table.add("XTRIM", cmd_xtrim, false, -4); table.add("XGROUP", cmd_xgroup, false, -2); table.add("XREADGROUP", cmd_xreadgroup, false, -7); table.add("XACK", cmd_xack, false, -4); table.add("XPENDING", cmd_xpending, true, -3); table.add("XCLAIM", cmd_xclaim, false, -6); table.add("XAUTOCLAIM", cmd_xautoclaim, false, -6); } /// XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold] id field value [field value ...] fn cmd_xadd(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let mut i = 1; let mut nomkstream = false; let mut maxlen: Option = None; let mut minid: Option = None; // Parse options while i < args.len() { let arg = String::from_utf8_lossy(&args[i]).to_uppercase(); match arg.as_str() { "NOMKSTREAM" => { nomkstream = true; i += 1; } "MAXLEN" => { i += 1; if i < args.len() { let next = String::from_utf8_lossy(&args[i]).to_string(); if next == "~" || next == "=" { i += 1; } } if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n >= 0 => { maxlen = Some(n as usize); i += 1; } Ok(_) => { return Frame::error("ERR The MAXLEN argument must be >= 0."); } Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } } "MINID" => { i += 1; if i < args.len() { let next = String::from_utf8_lossy(&args[i]).to_string(); if next == "~" || next == "=" { i += 1; } } if i >= args.len() { return Frame::error("ERR syntax error"); } minid = Some(String::from_utf8_lossy(&args[i]).to_string()); i += 1; } _ => break, } } if i >= args.len() { return Frame::error(err_wrong_number("xadd")); } let id = String::from_utf8_lossy(&args[i]).to_string(); i += 1; // Remaining args are field-value pairs let remaining = &args[i..]; if !remaining.len().is_multiple_of(2) { return Frame::error(err_wrong_number("xadd")); } let values: Vec = remaining .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); let mut inner = state.lock(); let now = inner.effective_now(); let ms = now .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; let db = inner.db_mut(ctx.selected_db); // Type check if let Some(kt) = db.keys.get(&key) { if *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } } else if nomkstream { return Frame::Null; } db.keys.entry(key.clone()).or_insert(KeyType::Stream); let stream = db.stream_keys.entry(key.clone()).or_default(); match stream.add(&id, values, ms) { Ok(final_id) => { if let Some(ml) = maxlen { stream.trim_maxlen(ml); } if let Some(mi) = minid { let normalized = Stream::normalize_id(&mi); stream.trim_minid(&normalized); } db.incr_version(&key, now); Frame::Bulk(final_id.into()) } Err(e) => Frame::error(e), } } /// XLEN key fn cmd_xlen(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(key.as_ref()) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } match db.stream_keys.get(key.as_ref()) { Some(stream) => Frame::Integer(stream.entries.len() as i64), None => Frame::Integer(0), } } /// XRANGE key start end [COUNT count] fn cmd_xrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xrange_impl(state, ctx, args, false) } /// XREVRANGE key end start [COUNT count] fn cmd_xrevrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { cmd_xrange_impl(state, ctx, args, true) } fn cmd_xrange_impl( state: &Arc, ctx: &mut ConnCtx, args: &[Vec], reverse: bool, ) -> Frame { let key = String::from_utf8_lossy(&args[0]); let arg_start = String::from_utf8_lossy(&args[1]).to_string(); let arg_end = String::from_utf8_lossy(&args[2]).to_string(); let mut count: Option = None; if args.len() > 3 { if args.len() != 5 { return Frame::error("ERR syntax error"); } let opt = String::from_utf8_lossy(&args[3]).to_uppercase(); if opt != "COUNT" { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[4]).parse::() { Ok(n) => count = Some(n), Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } } let (start, end) = if reverse { let s = match format_stream_range_bound(&arg_end, true) { Ok(s) => s, Err(e) => return Frame::error(e), }; let e = match format_stream_range_bound(&arg_start, false) { Ok(e) => e, Err(e) => return Frame::error(e), }; (s, e) } else { let s = match format_stream_range_bound(&arg_start, true) { Ok(s) => s, Err(e) => return Frame::error(e), }; let e = match format_stream_range_bound(&arg_end, false) { Ok(e) => e, Err(e) => return Frame::error(e), }; (s, e) }; let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(key.as_ref()) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get(key.as_ref()) { Some(s) => s, None => return Frame::Array(vec![]), }; let entries = if reverse { stream.rev_range(&start, &end, count) } else { stream.range(&start, &end, count) }; Frame::Array( entries .into_iter() .map(|e| { let vals: Vec = e .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)]) }) .collect(), ) } /// XREAD [COUNT count] [BLOCK ms] STREAMS key [key ...] id [id ...] fn cmd_xread(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut i = 0; let mut count: Option = None; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "COUNT" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) => count = Some(n), Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } "BLOCK" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n < 0 => { return Frame::error("ERR timeout is negative"); } Ok(_) => {} // Accept but don't actually block Err(_) => { return Frame::error("ERR timeout is not an integer or out of range"); } } i += 1; } "STREAMS" => { i += 1; break; } _ => { return Frame::error("ERR syntax error"); } } } let remaining = &args[i..]; if remaining.is_empty() || !remaining.len().is_multiple_of(2) { return Frame::error( "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified.", ); } let half = remaining.len() / 2; let keys: Vec = remaining[..half] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); let inner = state.lock(); let mut ids = Vec::with_capacity(half); for (idx, a) in remaining[half..].iter().enumerate() { let s = String::from_utf8_lossy(a).to_string(); if s == "$" { // Get current last ID for this stream let db = inner.db(ctx.selected_db); ids.push( db.stream_keys .get(&keys[idx]) .map(|stream| stream.last_id().to_string()) .unwrap_or_else(|| "0-0".to_string()), ); } else { let normalized = Stream::normalize_id(&s); if Stream::parse_id(&normalized).is_err() { return Frame::error("ERR Invalid stream ID specified as stream command argument"); } ids.push(normalized); } } let db = inner.db(ctx.selected_db); let mut results = Vec::new(); let mut has_data = false; for (idx, key) in keys.iter().enumerate() { if let Some(kt) = db.keys.get(key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let entries = match db.stream_keys.get(key) { Some(stream) => { let mut entries = stream.after(&ids[idx]); if let Some(c) = count { entries.truncate(c); } entries } None => vec![], }; if entries.is_empty() { continue; } has_data = true; let entry_frames: Vec = entries .into_iter() .map(|e| { let vals: Vec = e .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)]) }) .collect(); results.push(Frame::Array(vec![ Frame::Bulk(key.clone().into()), Frame::Array(entry_frames), ])); } if !has_data { return Frame::NullArray; } Frame::Array(results) } /// XDEL key id [id ...] fn cmd_xdel(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let ids: Vec = args[1..] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } match db.stream_keys.get_mut(&key) { Some(stream) => { // Validate all IDs before deleting for id in &id_refs { let normalized = Stream::normalize_id(id); if Stream::parse_id(&normalized).is_err() { return Frame::error( "ERR Invalid stream ID specified as stream command argument", ); } } let count = stream.del(&id_refs); db.incr_version(&key, now); Frame::Integer(count) } None => { // Non-existing key: return 0 even for invalid IDs Frame::Integer(0) } } } /// XTRIM key MAXLEN|MINID [=|~] threshold [LIMIT count] fn cmd_xtrim(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let strategy = String::from_utf8_lossy(&args[1]).to_uppercase(); if strategy != "MAXLEN" && strategy != "MINID" { return Frame::error(err_wrong_number("xtrim")); } let mut i = 2; let mut approx = false; if i < args.len() { let next = String::from_utf8_lossy(&args[i]).to_string(); if next == "~" { approx = true; i += 1; } else if next == "=" { i += 1; } } if i >= args.len() { return Frame::error(err_wrong_number("xtrim")); } let threshold = String::from_utf8_lossy(&args[i]).to_string(); i += 1; // Parse optional LIMIT if i < args.len() { let next = String::from_utf8_lossy(&args[i]).to_uppercase(); if next == "LIMIT" { if !approx { return Frame::error( "ERR syntax error, LIMIT cannot be used without the special ~ flag", ); } i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } // Parse the limit value (we accept it but don't use it for exact behavior) match String::from_utf8_lossy(&args[i]).parse::() { Ok(_) => { i += 1; } Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } } } if i < args.len() { return Frame::error("ERR syntax error"); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => return Frame::Integer(0), }; let count = match strategy.as_str() { "MAXLEN" => match threshold.parse::() { Ok(n) if n >= 0 => stream.trim_maxlen(n as usize), _ => { return Frame::error("ERR value is not an integer or out of range"); } }, "MINID" => { let normalized = Stream::normalize_id(&threshold); stream.trim_minid(&normalized) } _ => { return Frame::error("ERR syntax error"); } }; db.incr_version(&key, now); Frame::Integer(count) } /// XGROUP CREATE/DESTROY/CREATECONSUMER/DELCONSUMER fn cmd_xgroup(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "CREATE" => { if args.len() < 3 { return Frame::error(err_wrong_number("xgroup|create")); } let key = String::from_utf8_lossy(&args[1]).to_string(); let group = String::from_utf8_lossy(&args[2]).to_string(); let id = if args.len() > 3 { String::from_utf8_lossy(&args[3]).to_string() } else { "$".to_string() }; let mkstream = args.len() > 4 && String::from_utf8_lossy(&args[4]).to_uppercase() == "MKSTREAM"; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) { if *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } } else if !mkstream { return Frame::error( "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", ); } else { db.keys.insert(key.clone(), KeyType::Stream); db.stream_keys.insert(key.clone(), Stream::new()); } let stream = db.stream_keys.get_mut(&key).unwrap(); match stream.create_group(&group, &id) { Ok(()) => { db.incr_version(&key, now); Frame::ok() } Err(e) => Frame::error(e), } } "DESTROY" => { if args.len() < 3 { return Frame::error(err_wrong_number("xgroup|destroy")); } let key = String::from_utf8_lossy(&args[1]).to_string(); let group = String::from_utf8_lossy(&args[2]).to_string(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => { return Frame::error( "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.", ); } }; if stream.groups.remove(&group).is_some() { db.incr_version(&key, now); Frame::Integer(1) } else { Frame::Integer(0) } } "CREATECONSUMER" => { if args.len() < 4 { return Frame::error(err_wrong_number("xgroup|createconsumer")); } let key = String::from_utf8_lossy(&args[1]).to_string(); let group_name = String::from_utf8_lossy(&args[2]).to_string(); let consumer_name = String::from_utf8_lossy(&args[3]).to_string(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => { return Frame::error("ERR The XGROUP subcommand requires the key to exist."); } }; let group = match stream.groups.get_mut(&group_name) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; if let std::collections::hash_map::Entry::Vacant(e) = group.consumers.entry(consumer_name) { e.insert(crate::types::StreamConsumer { num_pending: 0, last_seen: now, last_success: now, }); Frame::Integer(1) } else { Frame::Integer(0) } } "DELCONSUMER" => { if args.len() < 4 { return Frame::error(err_wrong_number("xgroup|delconsumer")); } let key = String::from_utf8_lossy(&args[1]).to_string(); let group_name = String::from_utf8_lossy(&args[2]).to_string(); let consumer_name = String::from_utf8_lossy(&args[3]).to_string(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => { return Frame::error("ERR The XGROUP subcommand requires the key to exist."); } }; let group = match stream.groups.get_mut(&group_name) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; let pending_count = group .pending .iter() .filter(|pe| pe.consumer == consumer_name) .count() as i64; group.pending.retain(|pe| pe.consumer != consumer_name); group.consumers.remove(&consumer_name); db.incr_version(&key, now); Frame::Integer(pending_count) } _ => Frame::error(format!( "ERR unknown subcommand '{}'. Try XGROUP HELP.", String::from_utf8_lossy(&args[0]) )), } } /// XREADGROUP GROUP group consumer [COUNT count] [BLOCK ms] [NOACK] STREAMS key [key ...] id [id ...] fn cmd_xreadgroup(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut i = 0; let group_kw = String::from_utf8_lossy(&args[i]).to_uppercase(); if group_kw != "GROUP" { return Frame::error("ERR syntax error"); } i += 1; let group_name = String::from_utf8_lossy(&args[i]).to_string(); i += 1; let consumer_name = String::from_utf8_lossy(&args[i]).to_string(); i += 1; let mut count: Option = None; let mut noack = false; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "COUNT" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n > 0 => count = Some(n as usize), Ok(_) => { // Negative or zero COUNT: treat as unlimited (no count limit) count = None; } Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } "BLOCK" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n < 0 => { return Frame::error("ERR timeout is negative"); } Ok(_) => {} // Accept but don't actually block Err(_) => { return Frame::error("ERR timeout is not an integer or out of range"); } } i += 1; } "NOACK" => { noack = true; i += 1; } "STREAMS" => { i += 1; break; } _ => { return Frame::error("ERR syntax error"); } } } let remaining = &args[i..]; if remaining.is_empty() || !remaining.len().is_multiple_of(2) { return Frame::error( "ERR Unbalanced XREADGROUP list of streams: for each stream key an ID or '$' must be specified.", ); } let half = remaining.len() / 2; let keys: Vec = remaining[..half] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); // Collect IDs (validation deferred to per-stream loop, after group check) let mut ids = Vec::with_capacity(half); for a in &remaining[half..] { ids.push(String::from_utf8_lossy(a).to_string()); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); let mut results = Vec::new(); let mut has_data = false; for (idx, key) in keys.iter().enumerate() { if let Some(kt) = db.keys.get(key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(key) { Some(s) => s, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; // Check group exists before ID validation if !stream.groups.contains_key(&group_name) { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } // Validate non-">" IDs after confirming the group exists if ids[idx] != ">" && ids[idx] != "$" { let normalized = Stream::normalize_id(&ids[idx]); if Stream::parse_id(&normalized).is_err() { return Frame::error("ERR Invalid stream ID specified as stream command argument"); } } let entries = match stream.read_group(&group_name, &consumer_name, &ids[idx], count, noack, now) { Ok(entries) => entries, Err(e) => return Frame::error(e), }; // For ">" IDs, omit streams with no new entries from results if entries.is_empty() && ids[idx] == ">" { continue; } if !entries.is_empty() { has_data = true; } let entry_frames: Vec = entries .into_iter() .map(|e| { let vals: Vec = e .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); Frame::Array(vec![Frame::Bulk(e.id.into()), Frame::Array(vals)]) }) .collect(); results.push(Frame::Array(vec![ Frame::Bulk(key.clone().into()), Frame::Array(entry_frames), ])); } if !has_data && ids.iter().all(|id| id == ">") { return Frame::NullArray; } Frame::Array(results) } /// XACK key group id [id ...] fn cmd_xack(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let group_name = String::from_utf8_lossy(&args[1]).to_string(); let ids: Vec = args[2..] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); // Validate all IDs for id in &ids { let normalized = Stream::normalize_id(id); if Stream::parse_id(&normalized).is_err() { return Frame::error("ERR Invalid stream ID specified as stream command argument"); } } let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect(); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => return Frame::Integer(0), }; match stream.ack(&group_name, &id_refs) { Ok(count) => Frame::Integer(count), Err(e) => Frame::error(e), } } /// XPENDING key group [[IDLE ms] start end count [consumer]] fn cmd_xpending(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let group_name = String::from_utf8_lossy(&args[1]).to_string(); let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get(&key) { Some(s) => s, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; let group = match stream.groups.get(&group_name) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; if args.len() == 2 { // Summary mode let active: Vec<&crate::types::PendingEntry> = group .pending .iter() .filter(|pe| stream.entries.iter().any(|e| e.id == pe.id)) .collect(); if active.is_empty() { return Frame::Array(vec![ Frame::Integer(0), Frame::Null, Frame::Null, Frame::NullArray, ]); } let min_id = active .iter() .map(|pe| &pe.id) .min_by(|a, b| Stream::cmp_ids(a, b)) .unwrap(); let max_id = active .iter() .map(|pe| &pe.id) .max_by(|a, b| Stream::cmp_ids(a, b)) .unwrap(); // Count per consumer let mut consumer_counts: std::collections::HashMap<&str, i64> = std::collections::HashMap::new(); for pe in &active { *consumer_counts.entry(&pe.consumer).or_insert(0) += 1; } let mut consumers: Vec = consumer_counts .iter() .map(|(name, count)| { Frame::Array(vec![ Frame::Bulk(name.to_string().into()), Frame::Bulk(count.to_string().into()), ]) }) .collect(); consumers.sort_by(|a, b| { if let (Frame::Array(a), Frame::Array(b)) = (a, b) && let (Frame::Bulk(a), Frame::Bulk(b)) = (&a[0], &b[0]) { return a.cmp(b); } std::cmp::Ordering::Equal }); return Frame::Array(vec![ Frame::Integer(active.len() as i64), Frame::Bulk(min_id.clone().into()), Frame::Bulk(max_id.clone().into()), Frame::Array(consumers), ]); } // Detail mode: XPENDING key group [IDLE ms] start end count [consumer] let mut i = 2; let mut idle_filter: Option = None; if i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); if opt == "IDLE" { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) => idle_filter = Some(n), Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } } if i + 3 > args.len() { return Frame::error("ERR syntax error"); } let start = match format_stream_range_bound(&String::from_utf8_lossy(&args[i]), true) { Ok(s) => s, Err(e) => return Frame::error(e), }; let end = match format_stream_range_bound(&String::from_utf8_lossy(&args[i + 1]), false) { Ok(e) => e, Err(e) => return Frame::error(e), }; let count_val = match String::from_utf8_lossy(&args[i + 2]).parse::() { Ok(n) => n, Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } }; let consumer_filter = if i + 3 < args.len() { Some(String::from_utf8_lossy(&args[i + 3]).to_string()) } else { None }; if count_val <= 0 { return Frame::Array(vec![]); } let now = inner.effective_now(); let mut result = Vec::new(); for pe in &group.pending { if !stream.entries.iter().any(|e| e.id == pe.id) { continue; } if Stream::cmp_ids(&pe.id, &start) == std::cmp::Ordering::Less { continue; } if Stream::cmp_ids(&pe.id, &end) == std::cmp::Ordering::Greater { continue; } if let Some(consumer) = &consumer_filter && pe.consumer != *consumer { continue; } let idle_ms = now .duration_since(pe.last_delivery) .unwrap_or_default() .as_millis() as u64; if let Some(min_idle) = idle_filter && idle_ms < min_idle { continue; } result.push(Frame::Array(vec![ Frame::Bulk(pe.id.clone().into()), Frame::Bulk(pe.consumer.clone().into()), Frame::Integer(idle_ms as i64), Frame::Integer(pe.delivery_count), ])); if result.len() >= count_val as usize { break; } } Frame::Array(result) } /// XCLAIM key group consumer min-idle-ms id [id ...] [IDLE ms] [TIME ms] [RETRYCOUNT count] [FORCE] [JUSTID] fn cmd_xclaim(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let group_name = String::from_utf8_lossy(&args[1]).to_string(); let consumer_name = String::from_utf8_lossy(&args[2]).to_string(); let _min_idle_ms = match String::from_utf8_lossy(&args[3]).parse::() { Ok(n) => n, Err(_) => { return Frame::error("ERR Invalid min-idle-time argument for XCLAIM"); } }; let mut ids = Vec::new(); let mut justid = false; let mut force = false; let mut in_options = false; let mut i = 4; while i < args.len() { let arg = String::from_utf8_lossy(&args[i]).to_uppercase(); match arg.as_str() { "JUSTID" => { in_options = true; justid = true; i += 1; } "FORCE" => { in_options = true; force = true; i += 1; } "IDLE" => { in_options = true; i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(_) => {} Err(_) => { return Frame::error("ERR Invalid IDLE option argument for XCLAIM"); } } i += 1; } "TIME" => { in_options = true; i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(_) => {} Err(_) => { return Frame::error("ERR Invalid TIME option argument for XCLAIM"); } } i += 1; } "RETRYCOUNT" => { in_options = true; i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(_) => {} Err(_) => { return Frame::error("ERR Invalid RETRYCOUNT option argument for XCLAIM"); } } i += 1; } _ => { if in_options { return Frame::error(format!( "ERR Unrecognized XCLAIM option '{}'", String::from_utf8_lossy(&args[i]) )); } ids.push(String::from_utf8_lossy(&args[i]).to_string()); i += 1; } } } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => { return Frame::error(format!( "NOGROUP No such key '{}' or consumer group '{}' in XCLAIM for key name '{}'", key, group_name, key )); } }; let group = match stream.groups.get_mut(&group_name) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such key '{}' or consumer group '{}' in XCLAIM for key name '{}'", key, group_name, key )); } }; // Ensure consumer exists group .consumers .entry(consumer_name.clone()) .or_insert(crate::types::StreamConsumer { num_pending: 0, last_seen: now, last_success: now, }); let mut claimed = Vec::new(); for id in &ids { let entry_exists = stream.entries.iter().any(|e| e.id == *id); let in_pel = group.pending.iter().any(|pe| pe.id == *id); if !entry_exists && !force && !in_pel { // Entry doesn't exist, not forced, and not in PEL: skip continue; } if !entry_exists && in_pel && !force { // Entry was deleted but is still in PEL: remove from PEL let consumer_name_of_pe = group .pending .iter() .find(|pe| pe.id == *id) .map(|pe| pe.consumer.clone()); group.pending.retain(|pe| pe.id != *id); if let Some(cname) = consumer_name_of_pe && let Some(c) = group.consumers.get_mut(&cname) { c.num_pending -= 1; } continue; } if !entry_exists && !force { continue; } // Find in pending or create if force let found = group.pending.iter_mut().find(|pe| pe.id == *id); match found { Some(pe) => { // Transfer to new consumer let old_consumer = pe.consumer.clone(); pe.consumer = consumer_name.clone(); pe.delivery_count += 1; pe.last_delivery = now; // Update consumer pending counts if let Some(c) = group.consumers.get_mut(&old_consumer) { c.num_pending -= 1; } if let Some(c) = group.consumers.get_mut(&consumer_name) { c.num_pending += 1; } } None => { if force { group.pending.push(crate::types::PendingEntry { id: id.clone(), consumer: consumer_name.clone(), delivery_count: 1, last_delivery: now, }); if let Some(c) = group.consumers.get_mut(&consumer_name) { c.num_pending += 1; } } else { continue; } } } if justid { claimed.push(Frame::Bulk(id.clone().into())); } else if let Some(entry) = stream.entries.iter().find(|e| e.id == *id) { let vals: Vec = entry .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); claimed.push(Frame::Array(vec![ Frame::Bulk(entry.id.clone().into()), Frame::Array(vals), ])); } } Frame::Array(claimed) } /// XAUTOCLAIM key group consumer min-idle-ms start [COUNT count] [JUSTID] fn cmd_xautoclaim(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).to_string(); let group_name = String::from_utf8_lossy(&args[1]).to_string(); let consumer_name = String::from_utf8_lossy(&args[2]).to_string(); let min_idle_ms = match String::from_utf8_lossy(&args[3]).parse::() { Ok(n) => n, Err(_) => { return Frame::error("ERR Invalid min-idle-time argument for XAUTOCLAIM"); } }; let start = String::from_utf8_lossy(&args[4]).to_string(); let start_id = Stream::normalize_id(&start); // Validate the start ID if Stream::parse_id(&start_id).is_err() { return Frame::error("ERR Invalid stream ID specified as stream command argument"); } let mut count: usize = 100; let mut justid = false; let mut i = 5; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "COUNT" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) => count = n, Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } "JUSTID" => { justid = true; i += 1; } _ => { return Frame::error("ERR syntax error"); } } } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); if let Some(kt) = db.keys.get(&key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get_mut(&key) { Some(s) => s, None => { return Frame::error(format!( "NOGROUP No such key '{}' or consumer group '{}' in XAUTOCLAIM for key name '{}'", key, group_name, key )); } }; let group = match stream.groups.get_mut(&group_name) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such key '{}' or consumer group '{}' in XAUTOCLAIM for key name '{}'", key, group_name, key )); } }; // Ensure consumer exists group .consumers .entry(consumer_name.clone()) .or_insert(crate::types::StreamConsumer { num_pending: 0, last_seen: now, last_success: now, }); let mut claimed = Vec::new(); let mut last_claimed_id: Option = None; let mut hit_count_limit = false; for pe in group.pending.iter_mut() { if Stream::cmp_ids(&pe.id, &start_id) == std::cmp::Ordering::Less { continue; } let idle_ms = now .duration_since(pe.last_delivery) .unwrap_or_default() .as_millis() as u64; if idle_ms < min_idle_ms { continue; } if !stream.entries.iter().any(|e| e.id == pe.id) { continue; } // Claim this entry let old_consumer = pe.consumer.clone(); pe.consumer = consumer_name.clone(); pe.delivery_count += 1; pe.last_delivery = now; if let Some(c) = group.consumers.get_mut(&old_consumer) { c.num_pending -= 1; } if let Some(c) = group.consumers.get_mut(&consumer_name) { c.num_pending += 1; } if justid { claimed.push(Frame::Bulk(pe.id.clone().into())); } else if let Some(entry) = stream.entries.iter().find(|e| e.id == pe.id) { let vals: Vec = entry .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); claimed.push(Frame::Array(vec![ Frame::Bulk(entry.id.clone().into()), Frame::Array(vals), ])); } last_claimed_id = Some(pe.id.clone()); if claimed.len() >= count { hit_count_limit = true; break; } } // Compute next_id: only return a non-zero cursor if we stopped early due to COUNT limit. // If we scanned all eligible entries, return "0-0". let next_id = if hit_count_limit { match last_claimed_id { Some(id) => { if let Ok((ms, seq)) = Stream::parse_id(&id) { Stream::format_id(ms, seq + 1) } else { "0-0".to_string() } } None => "0-0".to_string(), } } else { "0-0".to_string() }; Frame::Array(vec![ Frame::Bulk(next_id.into()), Frame::Array(claimed), Frame::Array(vec![]), // deleted entries (not implemented) ]) } /// XINFO STREAM/GROUPS/CONSUMERS fn cmd_xinfo(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase(); match subcmd.as_str() { "STREAM" => { if args.len() < 2 { return Frame::error(err_wrong_number("xinfo|stream")); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(key.as_ref()) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get(key.as_ref()) { Some(s) => s, None => return Frame::error("ERR no such key"), }; Frame::Array(vec![ Frame::Bulk("length".into()), Frame::Integer(stream.entries.len() as i64), Frame::Bulk("groups".into()), Frame::Integer(stream.groups.len() as i64), Frame::Bulk("last-generated-id".into()), Frame::Bulk(stream.last_id().to_string().into()), ]) } "GROUPS" => { if args.len() < 2 { return Frame::error(err_wrong_number("xinfo|groups")); } let key = String::from_utf8_lossy(&args[1]); let inner = state.lock(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(key.as_ref()) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get(key.as_ref()) { Some(s) => s, None => return Frame::error("ERR no such key"), }; let mut groups: Vec = stream .groups .iter() .map(|(name, group)| { // Compute entries-read and lag let (entries_read, lag) = compute_entries_read_lag(stream, group); Frame::Array(vec![ Frame::Bulk("name".into()), Frame::Bulk(name.clone().into()), Frame::Bulk("consumers".into()), Frame::Integer(group.consumers.len() as i64), Frame::Bulk("pending".into()), Frame::Integer(group.pending.len() as i64), Frame::Bulk("last-delivered-id".into()), Frame::Bulk(group.last_id.clone().into()), Frame::Bulk("entries-read".into()), entries_read, Frame::Bulk("lag".into()), lag, ]) }) .collect(); groups.sort_by(|a, b| { if let (Frame::Array(a), Frame::Array(b)) = (a, b) && let (Frame::Bulk(a), Frame::Bulk(b)) = (&a[1], &b[1]) { return a.cmp(b); } std::cmp::Ordering::Equal }); Frame::Array(groups) } "CONSUMERS" => { if args.len() < 3 { return Frame::error(err_wrong_number("xinfo|consumers")); } let key = String::from_utf8_lossy(&args[1]); let group_name = String::from_utf8_lossy(&args[2]); let inner = state.lock(); let now = inner.effective_now(); let db = inner.db(ctx.selected_db); if let Some(kt) = db.keys.get(key.as_ref()) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } let stream = match db.stream_keys.get(key.as_ref()) { Some(s) => s, None => return Frame::error("ERR no such key"), }; let group = match stream.groups.get(group_name.as_ref()) { Some(g) => g, None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } }; let consumers: Vec = group .consumers .iter() .map(|(name, consumer)| { let idle = now .duration_since(consumer.last_seen) .unwrap_or_default() .as_millis() as i64; let inactive = now .duration_since(consumer.last_success) .unwrap_or_default() .as_millis() as i64; Frame::Array(vec![ Frame::Bulk("name".into()), Frame::Bulk(name.clone().into()), Frame::Bulk("pending".into()), Frame::Integer(consumer.num_pending), Frame::Bulk("idle".into()), Frame::Integer(idle), Frame::Bulk("inactive".into()), Frame::Integer(inactive), ]) }) .collect(); Frame::Array(consumers) } _ => Frame::error( "ERR unknown subcommand or wrong number of arguments for 'XINFO' command".to_string(), ), } } /// Compute `entries-read` and `lag` for XINFO GROUPS output. fn compute_entries_read_lag(stream: &Stream, group: &crate::types::StreamGroup) -> (Frame, Frame) { // If last_id is "0-0", the group has never delivered anything. if group.last_id == "0-0" { return (Frame::Null, Frame::Integer(stream.entries.len() as i64)); } // If entries_read_known is false (group was created with $ or a specific ID // but never actually delivered entries), return nil for entries-read. if !group.entries_read_known { // We still know the lag: number of entries after the group's last_id. let entries_after = stream .entries .iter() .filter(|e| Stream::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater) .count() as i64; return (Frame::Null, Frame::Integer(entries_after)); } // entries-read: number of entries with id <= group.last_id. // Find the position of the first entry after last_id. let pos = stream .entries .iter() .position(|e| Stream::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater); let entries_read = match pos { Some(p) => p as i64, None => stream.entries.len() as i64, // last_id >= all entries }; let lag = stream.entries.len() as i64 - entries_read; (Frame::Integer(entries_read), Frame::Integer(lag)) } ================================================ FILE: miniredis/src/cmd/string.rs ================================================ use std::sync::Arc; use std::time::Duration; use super::parse_int; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::{ CommandTable, MSG_INT_OVERFLOW, MSG_INVALID_FLOAT, MSG_INVALID_INT, MSG_INVALID_PSETEX_TIME, MSG_INVALID_SE_TIME, MSG_INVALID_SETEX_TIME, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, MSG_XX_AND_NX, err_wrong_number, }; use crate::frame::Frame; use crate::types::KeyType; pub fn register(table: &mut CommandTable) { table.add("GET", cmd_get, true, 2); table.add("SET", cmd_set, false, -3); table.add("SETNX", cmd_setnx, false, 3); table.add("GETSET", cmd_getset, false, 3); table.add("SETEX", cmd_setex, false, 4); table.add("PSETEX", cmd_psetex, false, 4); table.add("MGET", cmd_mget, true, -2); table.add("MSET", cmd_mset, false, -3); table.add("MSETNX", cmd_msetnx, false, -3); table.add("INCR", cmd_incr, false, 2); table.add("INCRBY", cmd_incrby, false, 3); table.add("INCRBYFLOAT", cmd_incrbyfloat, false, 3); table.add("DECR", cmd_decr, false, 2); table.add("DECRBY", cmd_decrby, false, 3); table.add("STRLEN", cmd_strlen, true, 2); table.add("APPEND", cmd_append, false, 3); table.add("GETRANGE", cmd_getrange, true, 4); table.add("SUBSTR", cmd_getrange, true, 4); // alias table.add("SETRANGE", cmd_setrange, false, 4); table.add("GETDEL", cmd_getdel, false, 2); table.add("GETEX", cmd_getex, false, -2); table.add("GETBIT", cmd_getbit, true, 3); table.add("SETBIT", cmd_setbit, false, 4); table.add("BITCOUNT", cmd_bitcount, true, -2); table.add("BITOP", cmd_bitop, false, -4); table.add("BITPOS", cmd_bitpos, true, -3); } // ── Helpers ────────────────────────────────────────────────────────── fn string_incr( state: &Arc, ctx: &mut ConnCtx, key: &str, delta: i64, ) -> Result { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(key); // Check type if let Some(t) = db.key_type(key) && t != KeyType::String { return Err(Frame::error(MSG_WRONG_TYPE)); } let current: i64 = match db.string_get(key) { Some(v) => match String::from_utf8_lossy(v).parse::() { Ok(n) => n, Err(_) => return Err(Frame::error(MSG_INVALID_INT)), }, None => 0, }; let new_val = match current.checked_add(delta) { Some(n) => n, None => return Err(Frame::error(MSG_INT_OVERFLOW)), }; db.string_set(key, new_val.to_string().into_bytes(), now); Ok(new_val) } // ── Commands ───────────────────────────────────────────────────────── /// GET key fn cmd_get(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } match db.string_get(&key) { Some(val) => Frame::Bulk(val.clone().into()), None => Frame::Null, } } /// SET key value [EX seconds] [PX milliseconds] [NX|XX] [KEEPTTL] [GET] fn cmd_set(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let value = args[1].clone(); let mut ex: Option = None; let mut nx = false; let mut xx = false; let mut keepttl = false; let mut get = false; let mut expire_opt_set = false; // Track if any EX/PX/EXAT/PXAT was already set let mut i = 2; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "EX" => { if expire_opt_set { return Frame::error(MSG_SYNTAX_ERROR); } expire_opt_set = true; i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } let secs: i64 = match String::from_utf8_lossy(&args[i]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; if secs <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } ex = Some(Duration::from_secs(secs as u64)); } "PX" => { if expire_opt_set { return Frame::error(MSG_SYNTAX_ERROR); } expire_opt_set = true; i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } let ms: i64 = match String::from_utf8_lossy(&args[i]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; if ms <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } ex = Some(Duration::from_millis(ms as u64)); } "EXAT" => { if expire_opt_set { return Frame::error(MSG_SYNTAX_ERROR); } expire_opt_set = true; i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } let ts: i64 = match String::from_utf8_lossy(&args[i]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; if ts <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } let inner = state.lock(); let now = inner.effective_now(); let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64); match target.duration_since(now) { Ok(d) => ex = Some(d), Err(_) => ex = Some(Duration::ZERO), } drop(inner); } "PXAT" => { if expire_opt_set { return Frame::error(MSG_SYNTAX_ERROR); } expire_opt_set = true; i += 1; if i >= args.len() { return Frame::error(MSG_SYNTAX_ERROR); } let ts: i64 = match String::from_utf8_lossy(&args[i]).parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_INT), }; if ts <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } let inner = state.lock(); let now = inner.effective_now(); let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64); match target.duration_since(now) { Ok(d) => ex = Some(d), Err(_) => ex = Some(Duration::ZERO), } drop(inner); } "NX" => nx = true, "XX" => xx = true, "KEEPTTL" => keepttl = true, "GET" => get = true, _ => return Frame::error(MSG_SYNTAX_ERROR), } i += 1; } if nx && xx { return Frame::error(MSG_XX_AND_NX); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); let old_value = if get { match db.key_type(&key) { Some(KeyType::String) => db.string_get(&key).map(|v| Frame::Bulk(v.clone().into())), Some(_) => return Frame::error(MSG_WRONG_TYPE), None => Some(Frame::Null), } } else { None }; let key_exists = db.keys.contains_key(&key); if nx && key_exists { return old_value.unwrap_or(Frame::Null); } if xx && !key_exists { return old_value.unwrap_or(Frame::Null); } let old_ttl = if keepttl { db.ttl.get(&key).copied() } else { None }; db.string_set(&key, value, now); if let Some(ttl) = ex { db.ttl.insert(key.clone(), ttl); } else if let Some(old_ttl) = old_ttl { db.ttl.insert(key.clone(), old_ttl); } else { db.ttl.remove(&key); } old_value.unwrap_or(Frame::ok()) } /// SETNX key value fn cmd_setnx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let value = args[1].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if db.keys.contains_key(&key) { return Frame::Integer(0); } db.string_set(&key, value, now); db.ttl.remove(&key); Frame::Integer(1) } /// SETEX key seconds value fn cmd_setex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let secs: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if secs <= 0 { return Frame::error(MSG_INVALID_SETEX_TIME); } let value = args[2].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(&key); db.string_set(&key, value, now); db.ttl.insert(key, Duration::from_secs(secs as u64)); Frame::ok() } /// PSETEX key milliseconds value fn cmd_psetex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let ms: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if ms <= 0 { return Frame::error(MSG_INVALID_PSETEX_TIME); } let value = args[2].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.del(&key); db.string_set(&key, value, now); db.ttl.insert(key, Duration::from_millis(ms as u64)); Frame::ok() } /// GETSET key value fn cmd_getset(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let value = args[1].clone(); let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let old = db .string_get(&key) .map(|v| Frame::Bulk(v.clone().into())) .unwrap_or(Frame::Null); db.string_set(&key, value, now); db.ttl.remove(&key); old } /// MGET key [key ...] fn cmd_mget(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); let mut results = Vec::with_capacity(args.len()); for arg in args { let key = String::from_utf8_lossy(arg); db.check_ttl(&key); match db.key_type(&key) { Some(KeyType::String) => { if let Some(val) = db.string_get(&key) { results.push(Frame::Bulk(val.clone().into())); } else { results.push(Frame::Null); } } _ => results.push(Frame::Null), } } Frame::Array(results) } /// MSET key value [key value ...] fn cmd_mset(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if !args.len().is_multiple_of(2) { return Frame::error(err_wrong_number("mset")); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); for pair in args.chunks_exact(2) { let key = String::from_utf8_lossy(&pair[0]).into_owned(); let value = pair[1].clone(); db.del(&key); db.string_set(&key, value, now); } Frame::ok() } /// MSETNX key value [key value ...] fn cmd_msetnx(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if !args.len().is_multiple_of(2) { return Frame::error(err_wrong_number("msetnx")); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); // Check if ANY key already exists for pair in args.chunks_exact(2) { let key = String::from_utf8_lossy(&pair[0]); if db.keys.contains_key(key.as_ref()) { return Frame::Integer(0); } } // Set all for pair in args.chunks_exact(2) { let key = String::from_utf8_lossy(&pair[0]).into_owned(); let value = pair[1].clone(); db.string_set(&key, value, now); } Frame::Integer(1) } /// INCR key fn cmd_incr(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); match string_incr(state, ctx, &key, 1) { Ok(n) => Frame::Integer(n), Err(f) => f, } } /// INCRBY key increment fn cmd_incrby(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let delta: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; match string_incr(state, ctx, &key, delta) { Ok(n) => Frame::Integer(n), Err(f) => f, } } /// INCRBYFLOAT key increment fn cmd_incrbyfloat(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let delta_str = String::from_utf8_lossy(&args[1]).into_owned(); // Validate by parsing as f64 first let delta_f64: f64 = match delta_str.parse() { Ok(n) => n, Err(_) => return Frame::error(MSG_INVALID_FLOAT), }; if delta_f64.is_nan() || delta_f64.is_infinite() { return Frame::error(MSG_INVALID_FLOAT); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let current_str = match db.string_get(&key) { Some(v) => { let s = String::from_utf8_lossy(v).into_owned(); // Validate it's a float if s.parse::().is_err() { return Frame::error(MSG_INVALID_FLOAT); } s } None => "0".to_string(), }; let formatted = decimal_add_format(¤t_str, &delta_str); // Validate result is not infinite if let Ok(v) = formatted.parse::() && v.is_infinite() { return Frame::error(MSG_INT_OVERFLOW); } db.string_set(&key, formatted.as_bytes().to_vec(), now); Frame::Bulk(formatted.into_bytes().into()) } /// DECR key fn cmd_decr(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); match string_incr(state, ctx, &key, -1) { Ok(n) => Frame::Integer(n), Err(f) => f, } } /// DECRBY key decrement fn cmd_decrby(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let delta: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; match string_incr(state, ctx, &key, -delta) { Ok(n) => Frame::Integer(n), Err(f) => f, } } /// STRLEN key fn cmd_strlen(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } match db.string_get(&key) { Some(val) => Frame::Integer(val.len() as i64), None => Frame::Integer(0), } } /// APPEND key value fn cmd_append(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let value = &args[1]; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let mut current = db.string_get(&key).cloned().unwrap_or_default(); current.extend_from_slice(value); let new_len = current.len() as i64; db.string_set(&key, current, now); Frame::Integer(new_len) } /// GETRANGE key start end (also aliased as SUBSTR) fn cmd_getrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let start: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let end: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let val = match db.string_get(&key) { Some(v) => v.clone(), None => return Frame::Bulk(bytes::Bytes::new()), }; let len = val.len() as i64; let (rs, re) = redis_range(start, end, len, true); if rs > re || rs >= len { return Frame::Bulk(bytes::Bytes::new()); } Frame::Bulk(val[rs as usize..=re as usize].to_vec().into()) } /// SETRANGE key offset value fn cmd_setrange(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let offset: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if offset < 0 { return Frame::error("ERR offset is out of range"); } let offset = offset as usize; let replacement = &args[2]; let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let mut val = db.string_get(&key).cloned().unwrap_or_default(); // Extend with zeros if needed let needed = offset + replacement.len(); if val.len() < needed { val.resize(needed, 0); } // Copy replacement bytes val[offset..offset + replacement.len()].copy_from_slice(replacement); let new_len = val.len() as i64; db.string_set(&key, val, now); Frame::Integer(new_len) } /// GETDEL key fn cmd_getdel(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Null; } if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let val = db .string_get(&key) .map(|v| Frame::Bulk(v.clone().into())) .unwrap_or(Frame::Null); db.del(&key); val } /// GETEX key [PERSIST | EX seconds | PX ms | EXAT ts | PXAT ts] fn cmd_getex(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); // Parse options let mut persist = false; let mut ex: Option = None; if args.len() > 1 { let opt = String::from_utf8_lossy(&args[1]).to_uppercase(); match opt.as_str() { "PERSIST" => { if args.len() != 2 { return Frame::error(MSG_SYNTAX_ERROR); } persist = true; } "EX" => { if args.len() != 3 { return Frame::error(MSG_SYNTAX_ERROR); } let secs: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if secs <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } ex = Some(Duration::from_secs(secs as u64)); } "PX" => { if args.len() != 3 { return Frame::error(MSG_SYNTAX_ERROR); } let ms: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if ms <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } ex = Some(Duration::from_millis(ms as u64)); } "EXAT" => { if args.len() != 3 { return Frame::error(MSG_SYNTAX_ERROR); } let ts: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if ts <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } let inner = state.lock(); let now = inner.effective_now(); let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64); ex = Some(target.duration_since(now).unwrap_or(Duration::ZERO)); drop(inner); } "PXAT" => { if args.len() != 3 { return Frame::error(MSG_SYNTAX_ERROR); } let ts: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if ts <= 0 { return Frame::error(MSG_INVALID_SE_TIME); } let inner = state.lock(); let now = inner.effective_now(); let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64); ex = Some(target.duration_since(now).unwrap_or(Duration::ZERO)); drop(inner); } _ => return Frame::error(MSG_SYNTAX_ERROR), } } let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if !db.keys.contains_key(&key) { return Frame::Null; } if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } // Apply TTL changes if persist { db.ttl.remove(&key); } else if let Some(ttl) = ex { db.ttl.insert(key.clone(), ttl); } match db.string_get(&key) { Some(val) => Frame::Bulk(val.clone().into()), None => Frame::Null, } } // ── Utility functions ──────────────────────────────────────────────── /// Normalize Redis-style range indices. Returns (start, end) inclusive. /// `string_mode`: for GETRANGE, the range is inclusive and never returns negative spans. fn redis_range(start: i64, end: i64, len: i64, string_mode: bool) -> (i64, i64) { let mut s = start; let mut e = end; if s < 0 { s += len; } if e < 0 { e += len; } if s < 0 { s = 0; } if e < 0 { e = 0; } if string_mode && e >= len { e = len - 1; } (s, e) } /// Format a float value the way Redis does. pub fn format_float(v: f64) -> String { if v == 0.0 && v.is_sign_negative() { return "0".to_string(); } // Use ryu for fast formatting, then strip trailing zeros after decimal point let mut buf = ryu::Buffer::new(); let s = buf.format(v); // ryu uses 'e' notation for very large/small numbers; check for that if s.contains('e') || s.contains('E') { // Fall back to standard formatting return format!("{}", v); } if s.contains('.') { let trimmed = s.trim_end_matches('0'); if trimmed.ends_with('.') { // Keep at least one decimal place (like Redis does for INCRBYFLOAT) // Actually Redis removes trailing zeros completely: "3.0" -> "3" // But "3.14000" -> "3.14" // And "3.0" -> "3" not "3.0" return trimmed.trim_end_matches('.').to_string(); } return trimmed.to_string(); } s.to_string() } /// Add two decimal numbers (as strings) and format the result like Redis does. /// Uses fixed-point i128 arithmetic with 17 decimal places to match /// Go miniredis's big.Float(128-bit) + fmt.Sprintf("%.17f") behavior. pub fn decimal_add_format(a: &str, b: &str) -> String { const PREC: u32 = 17; let scale: i128 = 10i128.pow(PREC); let a_fixed = match parse_decimal_fixed(a, PREC) { Some(v) => v, None => { // Fall back to f64 for values we can't parse (e.g., very large scientific notation) let af: f64 = a.parse().unwrap_or(0.0); let bf: f64 = b.parse().unwrap_or(0.0); return format_float(af + bf); } }; let b_fixed = match parse_decimal_fixed(b, PREC) { Some(v) => v, None => { let af: f64 = a.parse().unwrap_or(0.0); let bf: f64 = b.parse().unwrap_or(0.0); return format_float(af + bf); } }; let sum = a_fixed + b_fixed; // Format as decimal with PREC decimal places, then strip trailing zeros let negative = sum < 0; let abs = sum.unsigned_abs(); let int_part = abs / scale as u128; let frac_part = abs % scale as u128; let mut s = if frac_part == 0 { format!("{}", int_part) } else { let frac_str = format!("{:017}", frac_part); let trimmed = frac_str.trim_end_matches('0'); format!("{}.{}", int_part, trimmed) }; if negative && s != "0" { s.insert(0, '-'); } s } /// Parse a decimal string (possibly with scientific notation) into fixed-point i128. /// Returns value * 10^prec. Returns None if the value would overflow i128. fn parse_decimal_fixed(s: &str, prec: u32) -> Option { let s = s.trim(); let (negative, s) = if let Some(s) = s.strip_prefix('-') { (true, s) } else if let Some(s) = s.strip_prefix('+') { (false, s) } else { (false, s) }; // Handle scientific notation: split on 'e' or 'E' let (mantissa, exp) = if let Some(pos) = s.find(['e', 'E']) { let exp: i32 = s[pos + 1..].parse().ok()?; (&s[..pos], exp) } else { (s, 0) }; // Split mantissa into integer and fractional parts let (int_str, frac_str) = if let Some(dot) = mantissa.find('.') { (&mantissa[..dot], &mantissa[dot + 1..]) } else { (mantissa, "") }; // Build the full digit string: integer + fractional digits let mut digits = String::with_capacity(int_str.len() + frac_str.len()); digits.push_str(int_str); digits.push_str(frac_str); // The implicit decimal point is after int_str.len() digits. // The exponent shifts it by exp positions to the right. // We need prec digits after the decimal point. // Position of decimal point from the left: int_str.len() + exp // We need total_digits = decimal_point_pos + prec digits total (with zero-padding) let decimal_point = int_str.len() as i32 + exp; let total_needed = decimal_point + prec as i32; if total_needed < 0 { // Result is too small, rounds to 0 return Some(0); } // Pad or truncate digits to total_needed length let total_needed = total_needed as usize; while digits.len() < total_needed { digits.push('0'); } // If we have more digits than needed, truncate (rounding towards zero) digits.truncate(total_needed); if digits.is_empty() { return Some(0); } let value: i128 = digits.parse().ok()?; Some(if negative { -value } else { value }) } // ── Bit operations ─────────────────────────────────────────────────── /// GETBIT key offset fn cmd_getbit(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let offset: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error("ERR bit offset is not an integer or out of range"), }; if offset < 0 { return Frame::error("ERR bit offset is not an integer or out of range"); } let offset = offset as usize; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let val = db.string_get(&key).cloned().unwrap_or_default(); let byte_idx = offset / 8; let bit_idx = 7 - (offset % 8); if byte_idx >= val.len() { return Frame::Integer(0); } let bit = (val[byte_idx] >> bit_idx) & 1; Frame::Integer(bit as i64) } /// SETBIT key offset value fn cmd_setbit(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]).into_owned(); let offset: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error("ERR bit offset is not an integer or out of range"), }; if offset < 0 { return Frame::error("ERR bit offset is not an integer or out of range"); } let offset = offset as usize; let bit_val: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error("ERR bit is not an integer or out of range"), }; if bit_val != 0 && bit_val != 1 { return Frame::error("ERR bit is not an integer or out of range"); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let mut val = db.string_get(&key).cloned().unwrap_or_default(); let byte_idx = offset / 8; let bit_idx = 7 - (offset % 8); // Expand if needed if byte_idx >= val.len() { val.resize(byte_idx + 1, 0); } let old_bit = (val[byte_idx] >> bit_idx) & 1; if bit_val == 1 { val[byte_idx] |= 1 << bit_idx; } else { val[byte_idx] &= !(1 << bit_idx); } db.string_set(&key, val, now); Frame::Integer(old_bit as i64) } /// BITCOUNT key [start end [BYTE|BIT]] fn cmd_bitcount(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let val = db.string_get(&key).cloned().unwrap_or_default(); if args.len() == 1 { // Count all bits let count: u32 = val.iter().map(|b| b.count_ones()).sum(); return Frame::Integer(count as i64); } if args.len() < 3 { return Frame::error(MSG_SYNTAX_ERROR); } let start: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let end: i64 = match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; let bit_mode = if args.len() > 3 { let mode = String::from_utf8_lossy(&args[3]).to_uppercase(); match mode.as_str() { "BYTE" => false, "BIT" => true, _ => return Frame::error(MSG_SYNTAX_ERROR), } } else { false }; if bit_mode { let bit_len = val.len() as i64 * 8; let (rs, re) = bitcount_range(start, end, bit_len); if rs > re { return Frame::Integer(0); } let mut count = 0u32; for i in rs..=re { let byte_idx = (i / 8) as usize; let bit_idx = 7 - (i % 8) as usize; if byte_idx < val.len() && (val[byte_idx] >> bit_idx) & 1 == 1 { count += 1; } } Frame::Integer(count as i64) } else { let byte_len = val.len() as i64; let (rs, re) = bitcount_range(start, end, byte_len); if rs > re { return Frame::Integer(0); } let count: u32 = val[rs as usize..=re as usize] .iter() .map(|b| b.count_ones()) .sum(); Frame::Integer(count as i64) } } fn bitcount_range(start: i64, end: i64, len: i64) -> (i64, i64) { let mut s = start; let mut e = end; if s < 0 { s += len; } if e < 0 { e += len; } if s < 0 { s = 0; } if e < 0 { e = 0; } if e >= len { e = len - 1; } (s, e) } /// BITOP operation destkey key [key ...] fn cmd_bitop(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let op = String::from_utf8_lossy(&args[0]).to_uppercase(); let dest = String::from_utf8_lossy(&args[1]).into_owned(); let src_keys: Vec = args[2..] .iter() .map(|a| String::from_utf8_lossy(a).into_owned()) .collect(); if op == "NOT" && src_keys.len() != 1 { return Frame::error("ERR BITOP NOT must be called with a single source key."); } let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); // Collect all source values let mut values: Vec> = Vec::new(); let mut max_len = 0; for key in &src_keys { db.check_ttl(key); if let Some(t) = db.key_type(key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let val = db.string_get(key).cloned().unwrap_or_default(); if val.len() > max_len { max_len = val.len(); } values.push(val); } let mut result = vec![0u8; max_len]; match op.as_str() { "AND" => { if !values.is_empty() { result = vec![0xFF; max_len]; for val in &values { for i in 0..max_len { let b = if i < val.len() { val[i] } else { 0 }; result[i] &= b; } } } } "OR" => { for val in &values { for i in 0..max_len { let b = if i < val.len() { val[i] } else { 0 }; result[i] |= b; } } } "XOR" => { for val in &values { for i in 0..max_len { let b = if i < val.len() { val[i] } else { 0 }; result[i] ^= b; } } } "NOT" => { for i in 0..max_len { let b = if i < values[0].len() { values[0][i] } else { 0 }; result[i] = !b; } } _ => return Frame::error(MSG_SYNTAX_ERROR), } let len = result.len() as i64; if result.is_empty() { // No source data → delete destination key (like Redis) db.del(&dest); } else { db.string_set(&dest, result, now); } Frame::Integer(len) } /// BITPOS key bit [start [end [BYTE|BIT]]] fn cmd_bitpos(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { let key = String::from_utf8_lossy(&args[0]); let target_bit: i64 = match parse_int(&args[1]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), }; if target_bit != 0 && target_bit != 1 { return Frame::error("ERR The bit argument must be 1 or 0."); } let target = target_bit as u8; let mut inner = state.lock(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::String { return Frame::error(MSG_WRONG_TYPE); } let key_exists = db.keys.contains_key(key.as_ref()); let val = db.string_get(&key).cloned().unwrap_or_default(); if val.is_empty() { if !key_exists && target == 0 { // Non-existent key: virtual infinite zeros, first 0-bit at position 0 return Frame::Integer(0); } return Frame::Integer(-1); } let has_range = args.len() > 2; let has_end = args.len() > 3; let bit_mode = if args.len() > 4 { let mode = String::from_utf8_lossy(&args[4]).to_uppercase(); match mode.as_str() { "BYTE" => false, "BIT" => true, _ => return Frame::error(MSG_SYNTAX_ERROR), } } else { false }; let byte_len = val.len() as i64; let bit_len = byte_len * 8; if bit_mode { let start = if args.len() > 2 { match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), } } else { 0 }; let end = if args.len() > 3 { match parse_int(&args[3]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), } } else { bit_len - 1 }; let (rs, re) = bitcount_range(start, end, bit_len); if rs > re { return Frame::Integer(-1); } for i in rs..=re { let byte_idx = (i / 8) as usize; let bit_idx = 7 - (i % 8) as usize; let bit = if byte_idx < val.len() { (val[byte_idx] >> bit_idx) & 1 } else { 0 }; if bit == target { return Frame::Integer(i); } } Frame::Integer(-1) } else { // BYTE mode let start = if args.len() > 2 { match parse_int(&args[2]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), } } else { 0 }; let end = if args.len() > 3 { match parse_int(&args[3]) { Some(n) => n, None => return Frame::error(MSG_INVALID_INT), } } else { byte_len - 1 }; let (rs, re) = bitcount_range(start, end, byte_len); if rs > re { return Frame::Integer(-1); } for byte_idx in rs..=re { let b = val[byte_idx as usize]; for bit_idx in 0..8 { let bit = (b >> (7 - bit_idx)) & 1; if bit == target { return Frame::Integer(byte_idx * 8 + bit_idx); } } } // If looking for 0 and no end was specified, the 0 bit is at end+1 if target == 0 && !has_end && !has_range { return Frame::Integer(bit_len); } if target == 0 && has_range && !has_end { return Frame::Integer(bit_len); } Frame::Integer(-1) } } ================================================ FILE: miniredis/src/cmd/transactions.rs ================================================ use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::dispatch::CommandTable; use crate::frame::Frame; pub fn register(table: &mut CommandTable) { table.add("MULTI", cmd_multi, false, 1); table.add("DISCARD", cmd_discard, false, 1); table.add("WATCH", cmd_watch, true, -2); table.add("UNWATCH", cmd_unwatch, false, 1); // EXEC is handled directly in dispatch.rs (needs command table access). } /// MULTI fn cmd_multi(_state: &Arc, ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { if ctx.in_tx() { return Frame::error("ERR MULTI calls can not be nested"); } ctx.transaction = Some(Vec::new()); ctx.dirty_transaction = false; Frame::ok() } /// DISCARD fn cmd_discard(_state: &Arc, ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { if !ctx.in_tx() { return Frame::error("ERR DISCARD without MULTI"); } ctx.transaction = None; ctx.watch.clear(); Frame::ok() } /// WATCH key [key ...] fn cmd_watch(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame { if ctx.in_tx() { return Frame::error("ERR WATCH inside MULTI is not allowed"); } let inner = state.lock(); let db = inner.db(ctx.selected_db); for arg in args { let key = String::from_utf8_lossy(arg).to_string(); let version = db.key_version.get(&key).copied().unwrap_or(0); ctx.watch.insert((ctx.selected_db, key), version); } Frame::ok() } /// UNWATCH fn cmd_unwatch(_state: &Arc, ctx: &mut ConnCtx, _args: &[Vec]) -> Frame { ctx.watch.clear(); Frame::ok() } ================================================ FILE: miniredis/src/connection.rs ================================================ use bytes::BytesMut; use std::collections::HashMap; use std::io::Cursor; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufWriter}; use tokio::net::TcpStream; use crate::frame::{Frame, FrameError}; /// Trait alias for an async stream that supports both read and write. pub trait IoStream: AsyncRead + AsyncWrite + Unpin + Send {} impl IoStream for T {} /// A connection wraps a stream with buffered read/write and RESP /// frame parsing. Supports both plain TCP and TLS connections. pub struct Connection { stream: BufWriter>, buffer: BytesMut, /// Whether this connection uses RESP3 encoding (set via HELLO 3). pub resp3: bool, } impl Connection { /// Create a new `Connection` backed by a plain TCP socket. pub fn new(socket: TcpStream) -> Connection { Connection { stream: BufWriter::new(Box::new(socket)), buffer: BytesMut::with_capacity(4096), resp3: false, } } /// Create a new `Connection` backed by any async read/write stream (e.g. TLS). pub fn new_stream(stream: impl IoStream + 'static) -> Connection { Connection { stream: BufWriter::new(Box::new(stream)), buffer: BytesMut::with_capacity(4096), resp3: false, } } /// Read a single RESP frame from the connection. /// /// Returns `None` if the remote half closed the connection cleanly. pub async fn read_frame(&mut self) -> crate::Result> { loop { // Try to parse a frame from the buffered data. if let Some(frame) = self.parse_frame()? { return Ok(Some(frame)); } // Not enough data for a frame — read more from the socket. let n = self.stream.read_buf(&mut self.buffer).await?; if n == 0 { // Connection closed if self.buffer.is_empty() { return Ok(None); } else { return Err("connection reset by peer".into()); } } } } /// Try to parse a frame from the current buffer contents. fn parse_frame(&mut self) -> crate::Result> { use bytes::Buf; let mut cursor = Cursor::new(&self.buffer[..]); match Frame::check(&mut cursor) { Ok(()) => { // We know a complete frame is in the buffer. let len = cursor.position() as usize; // Reset cursor and parse. cursor.set_position(0); let frame = Frame::parse(&mut cursor) .map_err(|e| -> crate::Error { e.to_string().into() })?; // Advance the buffer past the consumed bytes. self.buffer.advance(len); Ok(Some(frame)) } Err(FrameError::Incomplete) => Ok(None), Err(e) => Err(e.to_string().into()), } } /// Write a frame to the connection, using RESP3 encoding if negotiated. pub async fn write_frame(&mut self, frame: &Frame) -> crate::Result<()> { let bytes = frame.serialize_resp(self.resp3); self.stream.write_all(&bytes).await?; self.stream.flush().await?; Ok(()) } /// Write raw bytes (used for inline protocol or multi-frame writes). pub async fn write_all(&mut self, data: &[u8]) -> crate::Result<()> { self.stream.write_all(data).await?; self.stream.flush().await?; Ok(()) } } // ── Per-Connection State ───────────────────────────────────────────── /// Per-connection context, carrying state that persists across commands /// within a single client session. pub struct ConnCtx { /// Currently selected database index (0-15). pub selected_db: usize, /// True once the client has sent a valid AUTH command (when passwords are configured). pub authenticated: bool, /// If Some, we're inside a MULTI block; the vec holds queued command args. pub transaction: Option>, /// Set to true if any error occurs while queuing commands in a MULTI. pub dirty_transaction: bool, /// WATCH map: (db_index, key) -> version at WATCH time. pub watch: HashMap<(usize, String), u64>, /// True if the client negotiated RESP3 via HELLO. pub resp3: bool, /// CLIENT SETNAME value. pub client_name: Option, /// True when executing inside a Lua script (nested call). pub nested: bool, /// SHA of the currently executing Lua script (if nested). pub nested_sha: Option, /// Channels to subscribe to after EXEC completes (for SUBSCRIBE inside MULTI). pub pending_subscribe: Vec, /// Patterns to subscribe to after EXEC completes (for PSUBSCRIBE inside MULTI). pub pending_psubscribe: Vec, } /// A command queued inside a MULTI transaction. pub struct QueuedCommand { /// The raw arguments (command name + args). pub args: Vec>, } impl Default for ConnCtx { fn default() -> Self { Self::new() } } impl ConnCtx { pub fn new() -> Self { ConnCtx { selected_db: 0, authenticated: false, transaction: None, dirty_transaction: false, watch: HashMap::new(), resp3: false, client_name: None, nested: false, nested_sha: None, pending_subscribe: Vec::new(), pending_psubscribe: Vec::new(), } } /// Are we inside a MULTI transaction? pub fn in_tx(&self) -> bool { self.transaction.is_some() } } ================================================ FILE: miniredis/src/db.rs ================================================ use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::time::{Duration, SystemTime}; use rand::SeedableRng; use rand::rngs::StdRng; use tokio::sync::{Notify, broadcast}; use crate::hll::HyperLogLog; use crate::types::{KeyType, SortedSet, Stream}; /// A single numbered Redis database (0-15). #[derive(Debug)] pub struct RedisDB { /// Master map: key name -> type tag. pub keys: HashMap, /// String values. pub string_keys: HashMap>, /// Hash values: key -> (field -> value). pub hash_keys: HashMap>>, /// List values. pub list_keys: HashMap>>, /// Set values. pub set_keys: HashMap>, /// Sorted set values. pub sorted_set_keys: HashMap, /// Stream values. pub stream_keys: HashMap, /// HyperLogLog values. pub hll_keys: HashMap, /// Key TTLs (remaining duration). pub ttl: HashMap, /// Hash field TTLs: key -> (field -> remaining duration). pub hash_field_ttls: HashMap>, /// Key versions (bumped on every mutation, used by WATCH). pub key_version: HashMap, /// Last-recently-used timestamps. pub lru: HashMap, } impl Default for RedisDB { fn default() -> Self { Self::new() } } impl RedisDB { pub fn new() -> Self { RedisDB { keys: HashMap::new(), string_keys: HashMap::new(), hash_keys: HashMap::new(), list_keys: HashMap::new(), set_keys: HashMap::new(), sorted_set_keys: HashMap::new(), stream_keys: HashMap::new(), hll_keys: HashMap::new(), ttl: HashMap::new(), hash_field_ttls: HashMap::new(), key_version: HashMap::new(), lru: HashMap::new(), } } /// Check if a key exists (also updates LRU). pub fn exists(&mut self, key: &str, now: SystemTime) -> bool { if self.keys.contains_key(key) { self.lru.insert(key.to_owned(), now); true } else { false } } /// Get the type of a key, or None. pub fn key_type(&self, key: &str) -> Option { self.keys.get(key).copied() } /// Increment the key version and update LRU. pub fn incr_version(&mut self, key: &str, now: SystemTime) { self.lru.insert(key.to_owned(), now); let v = self.key_version.entry(key.to_owned()).or_insert(0); *v += 1; } /// Delete a key and its data. Returns true if the key existed. pub fn del(&mut self, key: &str) -> bool { let key_type = match self.keys.remove(key) { Some(t) => t, None => return false, }; self.lru.remove(key); self.ttl.remove(key); self.hash_field_ttls.remove(key); let v = self.key_version.entry(key.to_owned()).or_insert(0); *v += 1; match key_type { KeyType::String => { self.string_keys.remove(key); } KeyType::Hash => { self.hash_keys.remove(key); } KeyType::List => { self.list_keys.remove(key); } KeyType::Set => { self.set_keys.remove(key); } KeyType::SortedSet => { self.sorted_set_keys.remove(key); } KeyType::Stream => { self.stream_keys.remove(key); } KeyType::HyperLogLog => { self.hll_keys.remove(key); } } true } /// Delete a key without removing its TTL (used by string_set etc.). pub fn del_keep_ttl(&mut self, key: &str) { let key_type = match self.keys.remove(key) { Some(t) => t, None => return, }; match key_type { KeyType::String => { self.string_keys.remove(key); } KeyType::Hash => { self.hash_keys.remove(key); } KeyType::List => { self.list_keys.remove(key); } KeyType::Set => { self.set_keys.remove(key); } KeyType::SortedSet => { self.sorted_set_keys.remove(key); } KeyType::Stream => { self.stream_keys.remove(key); } KeyType::HyperLogLog => { self.hll_keys.remove(key); } } } /// GET: returns the value of a string key, or None. pub fn string_get(&self, key: &str) -> Option<&Vec> { if self.keys.get(key) != Some(&KeyType::String) { return None; } self.string_keys.get(key) } /// SET: force-set a string key. Does NOT remove TTL. pub fn string_set(&mut self, key: &str, value: Vec, now: SystemTime) { self.del_keep_ttl(key); self.keys.insert(key.to_owned(), KeyType::String); self.string_keys.insert(key.to_owned(), value); self.incr_version(key, now); } // ── Hash helpers ────────────────────────────────────────────────── /// Set hash fields. Returns the number of NEW fields added. pub fn hash_set(&mut self, key: &str, pairs: &[(String, Vec)], now: SystemTime) -> i64 { self.keys.entry(key.to_owned()).or_insert(KeyType::Hash); let hash = self.hash_keys.entry(key.to_owned()).or_default(); let mut new_count = 0i64; for (field, value) in pairs { if !hash.contains_key(field) { new_count += 1; } hash.insert(field.clone(), value.clone()); } self.incr_version(key, now); new_count } /// Get a hash field value. pub fn hash_get(&self, key: &str, field: &str) -> Option<&Vec> { self.hash_keys.get(key)?.get(field) } /// Delete hash fields. Returns the number deleted. Removes key if hash becomes empty. pub fn hash_del(&mut self, key: &str, fields: &[String], now: SystemTime) -> i64 { let hash = match self.hash_keys.get_mut(key) { Some(h) => h, None => return 0, }; let mut count = 0i64; for field in fields { if hash.remove(field).is_some() { count += 1; } } if hash.is_empty() { self.del(key); } else { self.incr_version(key, now); } count } /// Get all hash field names, sorted. pub fn hash_fields(&self, key: &str) -> Vec { match self.hash_keys.get(key) { Some(h) => { let mut fields: Vec = h.keys().cloned().collect(); fields.sort(); fields } None => Vec::new(), } } /// Get all hash values in field-sorted order. pub fn hash_values(&self, key: &str) -> Vec> { let fields = self.hash_fields(key); let hash = match self.hash_keys.get(key) { Some(h) => h, None => return Vec::new(), }; fields.iter().filter_map(|f| hash.get(f).cloned()).collect() } // ── List helpers ───────────────────────────────────────────────── /// LPUSH: prepend value(s) to a list. Returns new length. pub fn list_lpush(&mut self, key: &str, values: &[Vec], now: SystemTime) -> i64 { self.keys.entry(key.to_owned()).or_insert(KeyType::List); let list = self.list_keys.entry(key.to_owned()).or_default(); for v in values { list.push_front(v.clone()); } let len = list.len() as i64; self.incr_version(key, now); len } /// RPUSH: append value(s) to a list. Returns new length. pub fn list_rpush(&mut self, key: &str, values: &[Vec], now: SystemTime) -> i64 { self.keys.entry(key.to_owned()).or_insert(KeyType::List); let list = self.list_keys.entry(key.to_owned()).or_default(); for v in values { list.push_back(v.clone()); } let len = list.len() as i64; self.incr_version(key, now); len } /// LPOP: remove and return the first element. pub fn list_lpop(&mut self, key: &str, now: SystemTime) -> Option> { let list = self.list_keys.get_mut(key)?; let val = list.pop_front()?; if list.is_empty() { self.del(key); } else { self.incr_version(key, now); } Some(val) } /// RPOP: remove and return the last element. pub fn list_rpop(&mut self, key: &str, now: SystemTime) -> Option> { let list = self.list_keys.get_mut(key)?; let val = list.pop_back()?; if list.is_empty() { self.del(key); } else { self.incr_version(key, now); } Some(val) } // ── Set helpers ────────────────────────────────────────────────── /// SADD: add members to a set. Returns count of new members added. pub fn set_add(&mut self, key: &str, members: &[String], now: SystemTime) -> i64 { self.keys.entry(key.to_owned()).or_insert(KeyType::Set); let set = self.set_keys.entry(key.to_owned()).or_default(); let mut added = 0i64; for m in members { if set.insert(m.clone()) { added += 1; } } self.incr_version(key, now); added } /// SREM: remove members from a set. Returns count removed. pub fn set_rem(&mut self, key: &str, members: &[String], now: SystemTime) -> i64 { let set = match self.set_keys.get_mut(key) { Some(s) => s, None => return 0, }; let mut removed = 0i64; for m in members { if set.remove(m) { removed += 1; } } if set.is_empty() { self.del(key); } else { self.incr_version(key, now); } removed } /// Get all members of a set, sorted. pub fn set_members(&self, key: &str) -> Vec { match self.set_keys.get(key) { Some(s) => { let mut members: Vec = s.iter().cloned().collect(); members.sort(); members } None => Vec::new(), } } /// Check if a member is in a set. pub fn set_is_member(&self, key: &str, member: &str) -> bool { self.set_keys .get(key) .map(|s| s.contains(member)) .unwrap_or(false) } /// Replace a set entirely (used by set operations like SDIFFSTORE). pub fn set_set(&mut self, key: &str, members: HashSet, now: SystemTime) { if members.is_empty() { return; } self.del(key); self.keys.insert(key.to_owned(), KeyType::Set); self.set_keys.insert(key.to_owned(), members); self.incr_version(key, now); } // ── Sorted set helpers ─────────────────────────────────────────── /// ZADD: add a member with score. Returns true if the member was new. pub fn sset_add(&mut self, key: &str, score: f64, member: &str, now: SystemTime) -> bool { self.keys .entry(key.to_owned()) .or_insert(KeyType::SortedSet); let ss = self.sorted_set_keys.entry(key.to_owned()).or_default(); let is_new = ss.set(score, member); self.incr_version(key, now); is_new } /// Check if a member exists in a sorted set. pub fn sset_exists(&self, key: &str, member: &str) -> bool { self.sorted_set_keys .get(key) .map(|ss| ss.exists(member)) .unwrap_or(false) } /// Get a member's score. pub fn sset_score(&self, key: &str, member: &str) -> Option { self.sorted_set_keys.get(key)?.get(member) } /// Get cardinality of sorted set. pub fn sset_card(&self, key: &str) -> usize { self.sorted_set_keys .get(key) .map(|ss| ss.card()) .unwrap_or(0) } /// ZINCRBY: increment member's score. Returns new score. pub fn sset_incrby(&mut self, key: &str, member: &str, delta: f64, now: SystemTime) -> f64 { self.keys .entry(key.to_owned()) .or_insert(KeyType::SortedSet); let ss = self.sorted_set_keys.entry(key.to_owned()).or_default(); let new_score = ss.incrby(member, delta); self.incr_version(key, now); new_score } /// Remove a member from a sorted set. Returns true if it existed. pub fn sset_rem(&mut self, key: &str, member: &str, now: SystemTime) -> bool { let ss = match self.sorted_set_keys.get_mut(key) { Some(ss) => ss, None => return false, }; let removed = ss.remove(member); if ss.card() == 0 { self.del(key); } else { self.incr_version(key, now); } removed } /// Replace a sorted set entirely. pub fn sset_set(&mut self, key: &str, ss: SortedSet, now: SystemTime) { if ss.card() == 0 { self.del(key); return; } self.del(key); self.keys.insert(key.to_owned(), KeyType::SortedSet); self.sorted_set_keys.insert(key.to_owned(), ss); self.incr_version(key, now); } // ── HyperLogLog helpers ──────────────────────────────────────── /// PFADD: add items to a HyperLogLog. Returns 1 if any register changed, 0 otherwise. pub fn hll_add(&mut self, key: &str, items: &[&str], now: SystemTime) -> i64 { self.keys .entry(key.to_owned()) .or_insert(KeyType::HyperLogLog); let hll = self.hll_keys.entry(key.to_owned()).or_default(); let mut changed = false; for item in items { if hll.add(item.as_bytes()) { changed = true; } } self.incr_version(key, now); if changed { 1 } else { 0 } } /// PFCOUNT: count across one or more HLL keys. Returns error if any key is wrong type. pub fn hll_count(&self, keys: &[&str]) -> Result { if keys.len() == 1 { let key = keys[0]; if let Some(kt) = self.keys.get(key) && *kt != KeyType::HyperLogLog { return Err("WRONGTYPE Key is not a valid HyperLogLog string value."); } match self.hll_keys.get(key) { Some(hll) => Ok(hll.count() as i64), None => Ok(0), } } else { // Multiple keys: merge into temporary HLL and count let mut merged = HyperLogLog::new(); for &key in keys { if let Some(kt) = self.keys.get(key) && *kt != KeyType::HyperLogLog { return Err("WRONGTYPE Key is not a valid HyperLogLog string value."); } if let Some(hll) = self.hll_keys.get(key) { merged.merge(hll); } } Ok(merged.count() as i64) } } /// PFMERGE: merge source HLLs into dest. keys[0] is dest, rest are sources. /// Returns error if any key is wrong type. pub fn hll_merge(&mut self, keys: &[&str], now: SystemTime) -> Result<(), &'static str> { // Validate all keys first for &key in keys { if let Some(kt) = self.keys.get(key) && *kt != KeyType::HyperLogLog { return Err("WRONGTYPE Key is not a valid HyperLogLog string value."); } } let dest = keys[0]; // Collect source HLLs into a merged result let mut merged = self.hll_keys.get(dest).cloned().unwrap_or_default(); for &key in &keys[1..] { if let Some(hll) = self.hll_keys.get(key) { merged.merge(hll); } } // Store the result self.keys.insert(dest.to_owned(), KeyType::HyperLogLog); self.hll_keys.insert(dest.to_owned(), merged); self.incr_version(dest, now); Ok(()) } // ── Key rename helper ──────────────────────────────────────────── /// Rename a key. Returns false if source doesn't exist. pub fn rename(&mut self, from: &str, to: &str, now: SystemTime) -> bool { let key_type = match self.keys.remove(from) { Some(t) => t, None => return false, }; // Remove destination if it exists self.del(to); // Move the type tag self.keys.insert(to.to_owned(), key_type); // Move the actual data match key_type { KeyType::String => { if let Some(v) = self.string_keys.remove(from) { self.string_keys.insert(to.to_owned(), v); } } KeyType::Hash => { if let Some(v) = self.hash_keys.remove(from) { self.hash_keys.insert(to.to_owned(), v); } } KeyType::List => { if let Some(v) = self.list_keys.remove(from) { self.list_keys.insert(to.to_owned(), v); } } KeyType::Set => { if let Some(v) = self.set_keys.remove(from) { self.set_keys.insert(to.to_owned(), v); } } KeyType::SortedSet => { if let Some(v) = self.sorted_set_keys.remove(from) { self.sorted_set_keys.insert(to.to_owned(), v); } } KeyType::Stream => { if let Some(v) = self.stream_keys.remove(from) { self.stream_keys.insert(to.to_owned(), v); } } KeyType::HyperLogLog => { if let Some(v) = self.hll_keys.remove(from) { self.hll_keys.insert(to.to_owned(), v); } } } // Move TTL if let Some(ttl) = self.ttl.remove(from) { self.ttl.insert(to.to_owned(), ttl); } // Move hash field TTLs if let Some(field_ttls) = self.hash_field_ttls.remove(from) { self.hash_field_ttls.insert(to.to_owned(), field_ttls); } // Move LRU if let Some(lru) = self.lru.remove(from) { self.lru.insert(to.to_owned(), lru); } // Update versions self.incr_version(from, now); self.incr_version(to, now); true } /// Check and delete a key if its TTL has expired. pub fn check_ttl(&mut self, key: &str) -> bool { if let Some(&ttl) = self.ttl.get(key) && ttl <= Duration::ZERO { self.del(key); return true; // key was expired } false // key still alive or has no TTL } /// Remove all keys and values. pub fn flush(&mut self) { self.keys.clear(); self.string_keys.clear(); self.hash_keys.clear(); self.list_keys.clear(); self.set_keys.clear(); self.sorted_set_keys.clear(); self.stream_keys.clear(); self.hll_keys.clear(); self.ttl.clear(); self.hash_field_ttls.clear(); self.key_version.clear(); self.lru.clear(); } /// Deep-copy a key's data (type, value, TTL) within the same DB. Returns true on success. pub fn copy_key(&mut self, from: &str, to: &str, now: SystemTime) -> bool { let key_type = match self.keys.get(from) { Some(t) => *t, None => return false, }; self.keys.insert(to.to_owned(), key_type); match key_type { KeyType::String => { if let Some(v) = self.string_keys.get(from).cloned() { self.string_keys.insert(to.to_owned(), v); } } KeyType::Hash => { if let Some(v) = self.hash_keys.get(from).cloned() { self.hash_keys.insert(to.to_owned(), v); } } KeyType::List => { if let Some(v) = self.list_keys.get(from).cloned() { self.list_keys.insert(to.to_owned(), v); } } KeyType::Set => { if let Some(v) = self.set_keys.get(from).cloned() { self.set_keys.insert(to.to_owned(), v); } } KeyType::SortedSet => { if let Some(v) = self.sorted_set_keys.get(from).cloned() { self.sorted_set_keys.insert(to.to_owned(), v); } } KeyType::Stream => { if let Some(v) = self.stream_keys.get(from).cloned() { self.stream_keys.insert(to.to_owned(), v); } } KeyType::HyperLogLog => { if let Some(v) = self.hll_keys.get(from).cloned() { self.hll_keys.insert(to.to_owned(), v); } } } // Copy TTL if let Some(ttl) = self.ttl.get(from).copied() { self.ttl.insert(to.to_owned(), ttl); } // Copy hash field TTLs if let Some(field_ttls) = self.hash_field_ttls.get(from).cloned() { self.hash_field_ttls.insert(to.to_owned(), field_ttls); } self.incr_version(to, now); true } /// Return all keys, sorted. pub fn all_keys(&self) -> Vec { let mut keys: Vec = self.keys.keys().cloned().collect(); keys.sort(); keys } /// Decrease all TTLs by `duration`, deleting expired keys. pub fn fast_forward(&mut self, duration: Duration) { let keys: Vec = self.ttl.keys().cloned().collect(); for key in keys { if let Some(ttl) = self.ttl.get_mut(&key) { *ttl = ttl.saturating_sub(duration); } self.check_ttl(&key); } // Handle hash field TTLs let hash_keys: Vec = self.hash_field_ttls.keys().cloned().collect(); for key in hash_keys { self.check_hash_field_ttls(&key, duration); } } /// Check and expire hash field TTLs. Removes expired fields, and if /// the hash becomes empty, deletes the key entirely. pub fn check_hash_field_ttls(&mut self, key: &str, duration: Duration) { let field_ttls = match self.hash_field_ttls.get_mut(key) { Some(t) => t, None => return, }; let mut expired_fields = Vec::new(); for (field, ttl) in field_ttls.iter_mut() { *ttl = ttl.saturating_sub(duration); if *ttl <= Duration::ZERO { expired_fields.push(field.clone()); } } for field in &expired_fields { field_ttls.remove(field); if let Some(hash) = self.hash_keys.get_mut(key) { hash.remove(field); } } if field_ttls.is_empty() { self.hash_field_ttls.remove(key); } // If hash is now empty, delete the key entirely if let Some(hash) = self.hash_keys.get(key) && hash.is_empty() { self.del(key); } } } /// The inner state shared across all connections. /// Protected by a `std::sync::Mutex` (never held across .await). #[derive(Debug)] pub struct Inner { /// 16 databases (0-15). pub dbs: Vec, /// Cached Lua scripts: SHA1 hex -> source. pub scripts: HashMap, /// AUTH passwords: username -> password. pub passwords: HashMap, /// Mock time. If None, use real time. pub now: Option, /// Seeded RNG for deterministic tests. pub rng: StdRng, } impl Default for Inner { fn default() -> Self { Self::new() } } impl Inner { pub fn new() -> Self { let mut dbs = Vec::with_capacity(16); for _ in 0..16 { dbs.push(RedisDB::new()); } Inner { dbs, scripts: HashMap::new(), passwords: HashMap::new(), now: None, rng: StdRng::from_os_rng(), } } /// Get the effective "now" time (mock or real). pub fn effective_now(&self) -> SystemTime { self.now.unwrap_or_else(SystemTime::now) } /// Advance mock time and expire keys in all databases. pub fn fast_forward(&mut self, duration: Duration) { if let Some(ref mut now) = self.now { *now += duration; } for db in &mut self.dbs { db.fast_forward(duration); } } /// Get a reference to a database. pub fn db(&self, idx: usize) -> &RedisDB { &self.dbs[idx] } /// Get a mutable reference to a database. pub fn db_mut(&mut self, idx: usize) -> &mut RedisDB { &mut self.dbs[idx] } } /// The shared state wrapper used across all connections. /// `inner` is a std::sync::Mutex (not tokio::sync::Mutex) because we never /// hold the lock across an .await point. pub struct SharedState { /// The inner database state. pub inner: std::sync::Mutex, /// Notifies blocking commands (BLPOP, XREAD BLOCK, etc.) when data changes. pub notify: Notify, /// Shutdown signal broadcaster. pub shutdown_tx: broadcast::Sender<()>, /// Total connections received (cumulative). pub total_connections_received: AtomicU64, /// Currently connected clients. pub connected_clients: AtomicU64, /// Total commands processed (cumulative). pub total_commands_processed: AtomicU64, /// Pub/Sub subscriber registry. pub pubsub: std::sync::Mutex, /// Command dispatch table (set once at server startup, used by Lua scripting). pub command_table: std::sync::OnceLock>, } impl SharedState { pub fn new() -> Arc { let (shutdown_tx, _) = broadcast::channel(1); Arc::new(SharedState { inner: std::sync::Mutex::new(Inner::new()), notify: Notify::new(), shutdown_tx, total_connections_received: AtomicU64::new(0), connected_clients: AtomicU64::new(0), total_commands_processed: AtomicU64::new(0), pubsub: std::sync::Mutex::new(crate::pubsub::PubsubRegistry::new()), command_table: std::sync::OnceLock::new(), }) } /// Lock the inner state. pub fn lock(&self) -> std::sync::MutexGuard<'_, Inner> { self.inner.lock().unwrap() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_redis_db_string_set_get() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("hello", b"world".to_vec(), now); assert_eq!(db.string_get("hello"), Some(&b"world".to_vec())); assert_eq!(db.key_type("hello"), Some(KeyType::String)); } #[test] fn test_redis_db_del() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("key", b"val".to_vec(), now); assert!(db.exists("key", now)); assert!(db.del("key")); assert!(!db.exists("key", now)); assert_eq!(db.string_get("key"), None); } #[test] fn test_redis_db_del_nonexistent() { let mut db = RedisDB::new(); assert!(!db.del("nope")); } #[test] fn test_redis_db_type_check() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("str", b"val".to_vec(), now); assert_eq!(db.key_type("str"), Some(KeyType::String)); assert_eq!(db.key_type("nonexistent"), None); } #[test] fn test_redis_db_ttl_expiration() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("ephemeral", b"data".to_vec(), now); db.ttl .insert("ephemeral".to_owned(), Duration::from_secs(10)); // Fast forward 5s -- key should still be alive db.fast_forward(Duration::from_secs(5)); assert!(db.keys.contains_key("ephemeral")); // Fast forward another 6s -- key should be gone db.fast_forward(Duration::from_secs(6)); assert!(!db.keys.contains_key("ephemeral")); assert_eq!(db.string_get("ephemeral"), None); } #[test] fn test_redis_db_flush() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("a", b"1".to_vec(), now); db.string_set("b", b"2".to_vec(), now); assert_eq!(db.keys.len(), 2); db.flush(); assert!(db.keys.is_empty()); assert!(db.string_keys.is_empty()); } #[test] fn test_redis_db_all_keys_sorted() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("charlie", b"3".to_vec(), now); db.string_set("alpha", b"1".to_vec(), now); db.string_set("bravo", b"2".to_vec(), now); assert_eq!(db.all_keys(), vec!["alpha", "bravo", "charlie"]); } #[test] fn test_redis_db_key_version() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("k", b"v1".to_vec(), now); let v1 = db.key_version["k"]; db.string_set("k", b"v2".to_vec(), now); let v2 = db.key_version["k"]; assert!(v2 > v1); } #[test] fn test_redis_db_overwrite_type() { let mut db = RedisDB::new(); let now = SystemTime::now(); db.string_set("key", b"val".to_vec(), now); assert_eq!(db.key_type("key"), Some(KeyType::String)); db.string_set("key", b"new_val".to_vec(), now); assert_eq!(db.string_get("key"), Some(&b"new_val".to_vec())); } #[test] fn test_inner_16_dbs() { let inner = Inner::new(); assert_eq!(inner.dbs.len(), 16); } #[test] fn test_inner_effective_now_real() { let inner = Inner::new(); let now = inner.effective_now(); let real_now = SystemTime::now(); let diff = real_now.duration_since(now).unwrap_or(Duration::ZERO); assert!(diff < Duration::from_secs(1)); } #[test] fn test_inner_effective_now_mock() { let mut inner = Inner::new(); let mock_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000); inner.now = Some(mock_time); assert_eq!(inner.effective_now(), mock_time); } #[test] fn test_shared_state_lock() { let state = SharedState::new(); { let mut inner = state.lock(); inner .db_mut(0) .string_set("test", b"value".to_vec(), SystemTime::now()); } { let inner = state.lock(); assert_eq!(inner.db(0).string_get("test"), Some(&b"value".to_vec())); } } } ================================================ FILE: miniredis/src/dispatch.rs ================================================ use std::collections::HashMap; use std::fmt; use std::sync::Arc; use crate::connection::ConnCtx; use crate::db::SharedState; use crate::frame::Frame; // ── Error message constants ────────────────────────────────────────── pub const MSG_WRONG_TYPE: &str = "WRONGTYPE Operation against a key holding the wrong kind of value"; pub const MSG_INVALID_INT: &str = "ERR value is not an integer or out of range"; pub const MSG_INT_OVERFLOW: &str = "ERR increment or decrement would overflow"; pub const MSG_INVALID_FLOAT: &str = "ERR value is not a valid float"; pub const MSG_INVALID_MIN_MAX: &str = "ERR min or max is not a float"; pub const MSG_INVALID_RANGE_ITEM: &str = "ERR min or max not valid string range item"; pub const MSG_INVALID_TIMEOUT: &str = "ERR timeout is not a float or out of range"; pub const MSG_SYNTAX_ERROR: &str = "ERR syntax error"; pub const MSG_KEY_NOT_FOUND: &str = "ERR no such key"; pub const MSG_OUT_OF_RANGE: &str = "ERR index out of range"; pub const MSG_INVALID_CURSOR: &str = "ERR invalid cursor"; pub const MSG_XX_AND_NX: &str = "ERR XX and NX options at the same time are not compatible"; pub const MSG_TIMEOUT_NEGATIVE: &str = "ERR timeout is negative"; pub const MSG_INVALID_SE_TIME: &str = "ERR invalid expire time in set"; pub const MSG_INVALID_SETEX_TIME: &str = "ERR invalid expire time in setex"; pub const MSG_INVALID_PSETEX_TIME: &str = "ERR invalid expire time in psetex"; pub const MSG_INVALID_KEYS_NUMBER: &str = "ERR Number of keys can't be greater than number of args"; pub const MSG_NEGATIVE_KEYS_NUMBER: &str = "ERR Number of keys can't be negative"; pub const MSG_NO_SCRIPT_FOUND: &str = "NOSCRIPT No matching script. Please use EVAL."; pub const MSG_DB_INDEX_OUT_OF_RANGE: &str = "ERR DB index is out of range"; pub const MSG_SINGLE_ELEMENT_PAIR: &str = "ERR INCR option supports a single increment-element pair"; pub const MSG_NOT_VALID_HLL_VALUE: &str = "WRONGTYPE Key is not a valid HyperLogLog string value."; pub const MSG_INVALID_RANGE: &str = "ERR value is out of range, must be positive"; pub const MSG_TIMEOUT_IS_OUT_OF_RANGE: &str = "ERR timeout is out of range"; pub const MSG_GT_LT_AND_NX: &str = "ERR GT, LT, and/or NX options at the same time are not compatible"; pub const MSG_INVALID_STREAM_ID: &str = "ERR Invalid stream ID specified as stream command argument"; pub const MSG_STREAM_ID_TOO_SMALL: &str = "ERR The ID specified in XADD is equal or smaller than the target stream top item"; pub const MSG_STREAM_ID_ZERO: &str = "ERR The ID specified in XADD must be greater than 0-0"; pub const MSG_UNSUPPORTED_UNIT: &str = "ERR unsupported unit provided. please use M, KM, FT, MI"; pub const MSG_XGROUP_KEY_NOT_FOUND: &str = "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically."; pub const MSG_LIMIT_COMBINATION: &str = "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX"; pub const MSG_GT_AND_LT: &str = "ERR GT and LT options at the same time are not compatible"; pub const MSG_NX_AND_XX_GT_LT: &str = "ERR NX and XX, GT or LT options at the same time are not compatible"; pub const MSG_NUM_FIELDS_INVALID: &str = "ERR Parameter `numFields` should be greater than 0"; pub const MSG_NUM_FIELDS_PARAMETER: &str = "ERR The `numfields` parameter must match the number of arguments"; /// Generate the "wrong number of arguments" error for a command. pub fn err_wrong_number(cmd: &str) -> String { format!( "ERR wrong number of arguments for '{}' command", cmd.to_lowercase() ) } /// Generate the "unknown command" error. pub fn err_unknown_command(cmd: &str, args: &[Vec]) -> String { let mut s = format!("ERR unknown command `{}`, with args beginning with: ", cmd); for (i, a) in args.iter().enumerate() { if i >= 20 { break; } let a_str = String::from_utf8_lossy(a); s.push_str(&format!("`{}`, ", a_str)); } s } // ── Command handler types ──────────────────────────────────────────── /// The signature for a command handler function. /// Takes shared state, per-connection context, and the raw arguments. /// Returns a Frame to send as the response. pub type CommandHandler = fn(state: &Arc, ctx: &mut ConnCtx, args: &[Vec]) -> Frame; /// Metadata about a registered command. pub struct CommandMeta { pub handler: CommandHandler, pub read_only: bool, /// Redis arity. Positive = exact arg count (including cmd name). /// Negative = minimum arg count. Zero = skip check. pub arity: i32, } /// The command dispatch table. pub struct CommandTable { commands: HashMap<&'static str, CommandMeta>, } impl Default for CommandTable { fn default() -> Self { Self::new() } } impl CommandTable { pub fn new() -> Self { let mut table = CommandTable { commands: HashMap::new(), }; // Register all implemented commands. crate::cmd::connection::register(&mut table); crate::cmd::string::register(&mut table); crate::cmd::generic::register(&mut table); crate::cmd::server::register(&mut table); crate::cmd::hash::register(&mut table); crate::cmd::list::register(&mut table); crate::cmd::set::register(&mut table); crate::cmd::sorted_set::register(&mut table); crate::cmd::transactions::register(&mut table); crate::cmd::hll::register(&mut table); crate::cmd::geo::register(&mut table); crate::cmd::pubsub::register(&mut table); crate::cmd::client::register(&mut table); crate::cmd::cluster::register(&mut table); crate::cmd::object::register(&mut table); crate::cmd::stream::register(&mut table); crate::cmd::scripting::register(&mut table); table } /// Register a command with arity. /// Arity follows Redis convention: positive = exact arg count (incl. cmd name), /// negative = minimum arg count, zero = skip check. pub fn add( &mut self, name: &'static str, handler: CommandHandler, read_only: bool, arity: i32, ) { self.commands.insert( name, CommandMeta { handler, read_only, arity, }, ); } /// Look up a command by name (uppercase). pub fn get(&self, name: &str) -> Option<&CommandMeta> { self.commands.get(name) } } impl fmt::Debug for CommandTable { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CommandTable") .field("commands", &self.commands.keys().collect::>()) .finish() } } // ── Dispatch logic ─────────────────────────────────────────────────── /// Dispatch a single command. Returns the response frame, and whether the /// connection should be closed (QUIT). pub fn dispatch( table: &CommandTable, state: &Arc, ctx: &mut ConnCtx, args: &[Vec], ) -> (Frame, bool) { if args.is_empty() { return (Frame::error("ERR empty command"), false); } let cmd = String::from_utf8_lossy(&args[0]).to_uppercase(); let cmd_args = &args[1..]; // Handle MULTI/EXEC/DISCARD specially — they're not queued. // Note: the MULTI path only runs when authenticated (you can't enter MULTI // without being authenticated), so no auth check is needed here. if ctx.in_tx() && cmd != "EXEC" && cmd != "DISCARD" && cmd != "MULTI" && cmd != "WATCH" { // Validate the command exists before queueing let meta = match table.get(&cmd) { Some(m) => m, None => { ctx.dirty_transaction = true; return (Frame::error(err_unknown_command(&cmd, cmd_args)), false); } }; // Validate arity before queueing (Redis checks this before QUEUED) if meta.arity != 0 { let n = args.len() as i32; let bad = if meta.arity > 0 { n != meta.arity } else { n < -meta.arity }; if bad { ctx.dirty_transaction = true; return (Frame::error(err_wrong_number(&cmd.to_lowercase())), false); } } // Special case: validate SCRIPT subcommands before queueing. // Real Redis (and Go miniredis) rejects unknown subcommands immediately. if cmd == "SCRIPT" && !cmd_args.is_empty() { let subcmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase(); if !["LOAD", "EXISTS", "FLUSH"].contains(&subcmd.as_str()) { ctx.dirty_transaction = true; return ( Frame::error(format!( "ERR unknown subcommand '{}'. Try SCRIPT HELP.", subcmd )), false, ); } } // Special case: validate OBJECT subcommand arity before queueing. if cmd == "OBJECT" && !cmd_args.is_empty() { let subcmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase(); match subcmd.as_str() { "ENCODING" | "IDLETIME" | "REFCOUNT" | "FREQ" => { // These subcommands require exactly 1 additional arg (the key) if cmd_args.len() != 2 { ctx.dirty_transaction = true; return ( Frame::error(err_wrong_number(&format!( "object|{}", subcmd.to_lowercase() ))), false, ); } } "HELP" => {} _ => { ctx.dirty_transaction = true; return ( Frame::error(format!( "ERR unknown subcommand or wrong number of arguments for 'object|{}' command", subcmd.to_lowercase() )), false, ); } } } // Queue the command if let Some(ref mut tx) = ctx.transaction { tx.push(crate::connection::QueuedCommand { args: args.to_vec(), }); } return (Frame::Simple("QUEUED".into()), false); } // Handle EXEC specially — it needs the command table to replay queued commands. if cmd == "EXEC" { return (cmd_exec(table, state, ctx, cmd_args), false); } // Look up the command let meta = match table.get(&cmd) { Some(m) => m, None => { // Unknown commands: check auth before returning unknown error if !ctx.authenticated { let inner = state.lock(); if !inner.passwords.is_empty() { return (Frame::error("NOAUTH Authentication required."), false); } } return (Frame::error(err_unknown_command(&cmd, cmd_args)), false); } }; // Validate arity before auth (Redis checks arity first) if meta.arity != 0 { let n = args.len() as i32; let bad = if meta.arity > 0 { n != meta.arity } else { n < -meta.arity }; if bad { return (Frame::error(err_wrong_number(&cmd.to_lowercase())), false); } } // Check auth (after arity validation) if !ctx.authenticated { let inner = state.lock(); if !inner.passwords.is_empty() && cmd != "AUTH" && cmd != "HELLO" && cmd != "QUIT" { return (Frame::error("NOAUTH Authentication required."), false); } } // Execute the command under the lock let response = with_lock(state, ctx, meta.handler, cmd_args); let should_close = cmd == "QUIT"; (response, should_close) } /// Execute a command handler under the database lock. /// This is the normal (non-MULTI) path: lock → execute → notify → unlock. fn with_lock( state: &Arc, ctx: &mut ConnCtx, handler: CommandHandler, args: &[Vec], ) -> Frame { let response = handler(state, ctx, args); // Notify any blocking commands that data may have changed. state.notify.notify_waiters(); response } /// EXEC — execute a queued transaction. /// Handled in dispatch because it needs access to the command table. fn cmd_exec( table: &CommandTable, state: &Arc, ctx: &mut ConnCtx, args: &[Vec], ) -> Frame { if !args.is_empty() { return Frame::error(err_wrong_number("exec")); } if !ctx.in_tx() { return Frame::error("ERR EXEC without MULTI"); } // Dirty transaction (e.g. unknown command was queued) — abort. if ctx.dirty_transaction { ctx.transaction = None; ctx.watch.clear(); return Frame::error("EXECABORT Transaction discarded because of previous errors."); } // Check WATCHed keys. { let inner = state.lock(); for ((db_idx, key), version) in &ctx.watch { let current = inner.db(*db_idx).key_version.get(key).copied().unwrap_or(0); if current > *version { // WATCH detected a change — abort. ctx.transaction = None; ctx.watch.clear(); return Frame::NullArray; } } } // Take the queued commands and clear transaction state. let commands = ctx.transaction.take().unwrap_or_default(); ctx.watch.clear(); // Execute each queued command and collect results. let mut results = Vec::with_capacity(commands.len()); for queued in &commands { let cmd_name = String::from_utf8_lossy(&queued.args[0]).to_uppercase(); let cmd_args = &queued.args[1..]; let meta = match table.get(&cmd_name) { Some(m) => m, None => { results.push(Frame::error(err_unknown_command(&cmd_name, cmd_args))); continue; } }; let result = (meta.handler)(state, ctx, cmd_args); results.push(result); } // Notify any blocking commands that data may have changed. state.notify.notify_waiters(); Frame::Array(results) } ================================================ FILE: miniredis/src/error.rs ================================================ /// Error type for miniredis-rs. pub type Error = Box; /// Result type for miniredis-rs. pub type Result = std::result::Result; ================================================ FILE: miniredis/src/frame.rs ================================================ use bytes::{Buf, Bytes}; use std::fmt; use std::io::Cursor; /// A RESP2/RESP3 protocol frame. #[derive(Clone, Debug, PartialEq)] pub enum Frame { /// Simple string: `+OK\r\n` Simple(String), /// Error: `-ERR message\r\n` Error(String), /// Integer: `:42\r\n` Integer(i64), /// Bulk string: `$5\r\nhello\r\n` Bulk(Bytes), /// Null: `$-1\r\n` (RESP2) or `_\r\n` (RESP3) Null, /// Null array: `*-1\r\n` (RESP2) or `_\r\n` (RESP3) NullArray, /// Array: `*2\r\n...` Array(Vec), // ── RESP3 types ────────────────────────────────────────────────── /// Map: `%N\r\n...` (RESP3) or flat array `*2N\r\n...` (RESP2 fallback) Map(Vec<(Frame, Frame)>), /// Set: `~N\r\n...` (RESP3) or array `*N\r\n...` (RESP2 fallback) Set(Vec), /// Push: `>N\r\n...` (RESP3) or array `*N\r\n...` (RESP2 fallback) Push(Vec), /// Double: `,3.14\r\n` (RESP3) or bulk string (RESP2 fallback) Double(f64), } /// Errors that can occur during frame parsing. #[derive(Debug)] pub enum FrameError { /// Not enough data is available to parse a full frame. Incomplete, /// Invalid frame data. Protocol(String), } impl Frame { // ── Response builder helpers ────────────────────────────────────── /// `+OK\r\n` pub fn ok() -> Frame { Frame::Simple("OK".into()) } /// `-{msg}\r\n` pub fn error(msg: impl Into) -> Frame { Frame::Error(msg.into()) } /// `:{n}\r\n` pub fn integer(n: i64) -> Frame { Frame::Integer(n) } /// Bulk string from bytes. pub fn bulk(data: impl Into) -> Frame { Frame::Bulk(data.into()) } /// Bulk string from a str. pub fn bulk_string(s: &str) -> Frame { Frame::Bulk(Bytes::from(s.to_owned())) } /// `$-1\r\n` pub fn null() -> Frame { Frame::Null } /// `*-1\r\n` pub fn null_array() -> Frame { Frame::NullArray } /// Build an Array of bulk strings. pub fn strings(strs: &[&str]) -> Frame { Frame::Array( strs.iter() .map(|s| Frame::Bulk(Bytes::from(s.to_string()))) .collect(), ) } // ── Validation (no allocation) ─────────────────────────────────── /// Check whether a complete frame can be read from `src` without /// allocating. Advances the cursor past the frame on success. pub fn check(src: &mut Cursor<&[u8]>) -> Result<(), FrameError> { match get_u8(src)? { b'+' | b'-' | b':' => { skip_line(src)?; Ok(()) } b'$' => { let len = get_line_as_int(src)?; if len < 0 { // Null bulk string `$-1\r\n` Ok(()) } else { // Skip `len` bytes + `\r\n` let len = len as usize; skip(src, len + 2)?; Ok(()) } } b'*' => { let count = get_line_as_int(src)?; if count < 0 { // Null array Ok(()) } else { for _ in 0..count { Frame::check(src)?; } Ok(()) } } // RESP3: Map b'%' => { let count = get_line_as_int(src)?; for _ in 0..count { Frame::check(src)?; // key Frame::check(src)?; // value } Ok(()) } // RESP3: Set or Push b'~' | b'>' => { let count = get_line_as_int(src)?; for _ in 0..count { Frame::check(src)?; } Ok(()) } // RESP3: Double b',' => { skip_line(src)?; Ok(()) } // RESP3: Null b'_' => { skip_line(src)?; Ok(()) } b => Err(FrameError::Protocol(format!( "invalid frame type byte: `{}`", b as char ))), } } // ── Parsing (allocates) ────────────────────────────────────────── /// Parse a single frame from `src`. pub fn parse(src: &mut Cursor<&[u8]>) -> Result { match get_u8(src)? { b'+' => { let line = get_line(src)?; let s = String::from_utf8(line.to_vec()) .map_err(|e| FrameError::Protocol(e.to_string()))?; Ok(Frame::Simple(s)) } b'-' => { let line = get_line(src)?; let s = String::from_utf8(line.to_vec()) .map_err(|e| FrameError::Protocol(e.to_string()))?; Ok(Frame::Error(s)) } b':' => { let n = get_line_as_int(src)?; Ok(Frame::Integer(n)) } b'$' => { let len = get_line_as_int(src)?; if len < 0 { Ok(Frame::Null) } else { let len = len as usize; if src.remaining() < len + 2 { return Err(FrameError::Incomplete); } let data = Bytes::copy_from_slice(&src.get_ref()[src.position() as usize..][..len]); skip(src, len + 2)?; Ok(Frame::Bulk(data)) } } b'*' => { let count = get_line_as_int(src)?; if count < 0 { Ok(Frame::Null) } else { let mut frames = Vec::with_capacity(count as usize); for _ in 0..count { frames.push(Frame::parse(src)?); } Ok(Frame::Array(frames)) } } // RESP3: Map b'%' => { let count = get_line_as_int(src)?; let mut pairs = Vec::with_capacity(count as usize); for _ in 0..count { let key = Frame::parse(src)?; let value = Frame::parse(src)?; pairs.push((key, value)); } Ok(Frame::Map(pairs)) } // RESP3: Set b'~' => { let count = get_line_as_int(src)?; let mut items = Vec::with_capacity(count as usize); for _ in 0..count { items.push(Frame::parse(src)?); } Ok(Frame::Set(items)) } // RESP3: Push b'>' => { let count = get_line_as_int(src)?; let mut items = Vec::with_capacity(count as usize); for _ in 0..count { items.push(Frame::parse(src)?); } Ok(Frame::Push(items)) } // RESP3: Double b',' => { let line = get_line(src)?; let s = std::str::from_utf8(line).map_err(|e| FrameError::Protocol(e.to_string()))?; let f: f64 = match s { "inf" => f64::INFINITY, "-inf" => f64::NEG_INFINITY, "nan" => f64::NAN, _ => s.parse().map_err(|e: std::num::ParseFloatError| { FrameError::Protocol(e.to_string()) })?, }; Ok(Frame::Double(f)) } // RESP3: Null b'_' => { skip_line(src)?; Ok(Frame::Null) } b => Err(FrameError::Protocol(format!( "invalid frame type byte: `{}`", b as char ))), } } // ── Serialization ──────────────────────────────────────────────── /// Serialize this frame as RESP2 into a byte vector. pub fn serialize(&self) -> Vec { let mut buf = Vec::new(); self.write_to_buf(&mut buf, false); buf } /// Serialize this frame into a byte vector, using RESP3 encoding if `resp3` is true. pub fn serialize_resp(&self, resp3: bool) -> Vec { let mut buf = Vec::new(); self.write_to_buf(&mut buf, resp3); buf } /// Write this frame into a byte buffer. /// When `resp3` is true, RESP3-specific types use their native wire format. /// When false, they degrade to RESP2 equivalents. pub fn write_to_buf(&self, buf: &mut Vec, resp3: bool) { match self { Frame::Simple(s) => { buf.push(b'+'); buf.extend_from_slice(s.as_bytes()); buf.extend_from_slice(b"\r\n"); } Frame::Error(s) => { buf.push(b'-'); buf.extend_from_slice(s.as_bytes()); buf.extend_from_slice(b"\r\n"); } Frame::Integer(n) => { buf.push(b':'); buf.extend_from_slice(n.to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); } Frame::Bulk(data) => { buf.push(b'$'); buf.extend_from_slice(data.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); buf.extend_from_slice(data); buf.extend_from_slice(b"\r\n"); } Frame::Null => { if resp3 { buf.extend_from_slice(b"_\r\n"); } else { buf.extend_from_slice(b"$-1\r\n"); } } Frame::NullArray => { if resp3 { buf.extend_from_slice(b"_\r\n"); } else { buf.extend_from_slice(b"*-1\r\n"); } } Frame::Array(frames) => { buf.push(b'*'); buf.extend_from_slice(frames.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); for frame in frames { frame.write_to_buf(buf, resp3); } } Frame::Map(pairs) => { if resp3 { buf.push(b'%'); buf.extend_from_slice(pairs.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); } else { // RESP2 fallback: flat array with 2*N elements buf.push(b'*'); buf.extend_from_slice((pairs.len() * 2).to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); } for (k, v) in pairs { k.write_to_buf(buf, resp3); v.write_to_buf(buf, resp3); } } Frame::Set(items) => { if resp3 { buf.push(b'~'); } else { buf.push(b'*'); } buf.extend_from_slice(items.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); for item in items { item.write_to_buf(buf, resp3); } } Frame::Push(items) => { if resp3 { buf.push(b'>'); } else { buf.push(b'*'); } buf.extend_from_slice(items.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); for item in items { item.write_to_buf(buf, resp3); } } Frame::Double(f) => { let s = format_double(*f); if resp3 { buf.push(b','); buf.extend_from_slice(s.as_bytes()); buf.extend_from_slice(b"\r\n"); } else { // RESP2 fallback: bulk string buf.push(b'$'); buf.extend_from_slice(s.len().to_string().as_bytes()); buf.extend_from_slice(b"\r\n"); buf.extend_from_slice(s.as_bytes()); buf.extend_from_slice(b"\r\n"); } } } } } impl fmt::Display for Frame { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Frame::Simple(s) => write!(f, "+{}", s), Frame::Error(s) => write!(f, "-{}", s), Frame::Integer(n) => write!(f, ":{}", n), Frame::Bulk(data) => match std::str::from_utf8(data) { Ok(s) => write!(f, "${}", s), Err(_) => write!(f, "${:?}", data), }, Frame::Null => write!(f, "(nil)"), Frame::NullArray => write!(f, "(nil)"), Frame::Array(frames) => { write!(f, "[")?; for (i, frame) in frames.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{}", frame)?; } write!(f, "]") } Frame::Map(pairs) => { write!(f, "{{")?; for (i, (k, v)) in pairs.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{}: {}", k, v)?; } write!(f, "}}") } Frame::Set(items) => { write!(f, "~[")?; for (i, item) in items.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{}", item)?; } write!(f, "]") } Frame::Push(items) => { write!(f, ">[")?; for (i, item) in items.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{}", item)?; } write!(f, "]") } Frame::Double(v) => write!(f, ",{}", format_double(*v)), } } } /// Format a f64 for RESP3 Double wire encoding. fn format_double(f: f64) -> String { if f.is_infinite() { if f.is_sign_positive() { "inf".to_string() } else { "-inf".to_string() } } else if f.is_nan() { "nan".to_string() } else if f.fract() == 0.0 && f.abs() < 1e15 { // Integer doubles: format without decimal point (Redis compat) format!("{:.0}", f) } else { // Use ryu for shortest representation let mut buf = ryu::Buffer::new(); let s = buf.format(f); s.to_string() } } impl std::error::Error for FrameError {} impl fmt::Display for FrameError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FrameError::Incomplete => write!(f, "incomplete frame"), FrameError::Protocol(msg) => write!(f, "protocol error: {}", msg), } } } // ── Private helpers ────────────────────────────────────────────────── /// Peek at and consume a single byte. fn get_u8(src: &mut Cursor<&[u8]>) -> Result { if !src.has_remaining() { return Err(FrameError::Incomplete); } Ok(src.get_u8()) } /// Skip `n` bytes in the cursor. fn skip(src: &mut Cursor<&[u8]>, n: usize) -> Result<(), FrameError> { if src.remaining() < n { return Err(FrameError::Incomplete); } src.advance(n); Ok(()) } /// Read until `\r\n`, return the bytes before the delimiter. /// Advances the cursor past the `\r\n`. fn get_line<'a>(src: &mut Cursor<&'a [u8]>) -> Result<&'a [u8], FrameError> { let start = src.position() as usize; let end = src.get_ref().len(); for i in start..end.saturating_sub(1) { if src.get_ref()[i] == b'\r' && src.get_ref()[i + 1] == b'\n' { let line = &src.get_ref()[start..i]; src.set_position((i + 2) as u64); return Ok(line); } } Err(FrameError::Incomplete) } /// Skip until after `\r\n`. fn skip_line(src: &mut Cursor<&[u8]>) -> Result<(), FrameError> { get_line(src)?; Ok(()) } /// Read a line and parse it as an i64. fn get_line_as_int(src: &mut Cursor<&[u8]>) -> Result { let line = get_line(src)?; let s = std::str::from_utf8(line).map_err(|e| FrameError::Protocol(e.to_string()))?; s.parse::() .map_err(|e| FrameError::Protocol(e.to_string())) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_simple_string() { let data = b"+OK\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Simple("OK".into())); } #[test] fn parse_error() { let data = b"-ERR unknown command\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Error("ERR unknown command".into())); } #[test] fn parse_integer() { let data = b":42\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Integer(42)); } #[test] fn parse_negative_integer() { let data = b":-1\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Integer(-1)); } #[test] fn parse_bulk_string() { let data = b"$5\r\nhello\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Bulk(Bytes::from("hello"))); } #[test] fn parse_empty_bulk_string() { let data = b"$0\r\n\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Bulk(Bytes::from(""))); } #[test] fn parse_null() { let data = b"$-1\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Null); } #[test] fn parse_array() { let data = b"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!( frame, Frame::Array(vec![ Frame::Bulk(Bytes::from("foo")), Frame::Bulk(Bytes::from("bar")), ]) ); } #[test] fn parse_empty_array() { let data = b"*0\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Array(vec![])); } #[test] fn parse_nested_array() { let data = b"*2\r\n*2\r\n:1\r\n:2\r\n*1\r\n+OK\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!( frame, Frame::Array(vec![ Frame::Array(vec![Frame::Integer(1), Frame::Integer(2)]), Frame::Array(vec![Frame::Simple("OK".into())]), ]) ); } #[test] fn check_incomplete() { let data = b"$5\r\nhel"; let mut cursor = Cursor::new(&data[..]); assert!(matches!( Frame::check(&mut cursor), Err(FrameError::Incomplete) )); } #[test] fn check_complete() { let data = b"+OK\r\n"; let mut cursor = Cursor::new(&data[..]); assert!(Frame::check(&mut cursor).is_ok()); } #[test] fn round_trip_simple() { let frame = Frame::Simple("OK".into()); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_error() { let frame = Frame::Error("ERR something went wrong".into()); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_integer() { let frame = Frame::Integer(-999); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_bulk() { let frame = Frame::Bulk(Bytes::from("hello world")); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_null() { let frame = Frame::Null; let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_array() { let frame = Frame::Array(vec![ Frame::Bulk(Bytes::from("SET")), Frame::Bulk(Bytes::from("key")), Frame::Bulk(Bytes::from("value")), ]); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn round_trip_nested_array() { let frame = Frame::Array(vec![ Frame::Integer(1), Frame::Array(vec![Frame::Simple("inner".into()), Frame::Null]), Frame::Bulk(Bytes::from("end")), ]); let bytes = frame.serialize(); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn helper_ok() { assert_eq!(Frame::ok(), Frame::Simple("OK".into())); } #[test] fn helper_strings() { let frame = Frame::strings(&["a", "b", "c"]); assert_eq!( frame, Frame::Array(vec![ Frame::Bulk(Bytes::from("a")), Frame::Bulk(Bytes::from("b")), Frame::Bulk(Bytes::from("c")), ]) ); } #[test] fn serialize_null() { assert_eq!(Frame::Null.serialize(), b"$-1\r\n"); } #[test] fn parse_protocol_error() { let data = b"!invalid\r\n"; let mut cursor = Cursor::new(&data[..]); assert!(matches!( Frame::parse(&mut cursor), Err(FrameError::Protocol(_)) )); } #[test] fn parse_multiple_frames_sequentially() { let data = b"+OK\r\n:42\r\n$3\r\nfoo\r\n"; let mut cursor = Cursor::new(&data[..]); let f1 = Frame::parse(&mut cursor).unwrap(); assert_eq!(f1, Frame::Simple("OK".into())); let f2 = Frame::parse(&mut cursor).unwrap(); assert_eq!(f2, Frame::Integer(42)); let f3 = Frame::parse(&mut cursor).unwrap(); assert_eq!(f3, Frame::Bulk(Bytes::from("foo"))); } #[test] fn parse_binary_bulk_string() { let mut data = Vec::new(); data.extend_from_slice(b"$6\r\n"); data.extend_from_slice(b"he\r\nlo"); data.extend_from_slice(b"\r\n"); let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Bulk(Bytes::from(&b"he\r\nlo"[..]))); } // ── RESP3 tests ───────────────────────────────────────────────── #[test] fn resp3_null_serialization() { // RESP2 null assert_eq!(Frame::Null.serialize(), b"$-1\r\n"); // RESP3 null assert_eq!(Frame::Null.serialize_resp(true), b"_\r\n"); } #[test] fn resp3_parse_null() { let data = b"_\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Null); } #[test] fn resp3_double_serialization() { // RESP3 double let bytes = Frame::Double(1.23).serialize_resp(true); assert_eq!(bytes, b",1.23\r\n"); // RESP2 fallback: bulk string let bytes = Frame::Double(1.23).serialize_resp(false); assert_eq!(bytes, b"$4\r\n1.23\r\n"); } #[test] fn resp3_double_inf() { let bytes = Frame::Double(f64::INFINITY).serialize_resp(true); assert_eq!(bytes, b",inf\r\n"); let bytes = Frame::Double(f64::NEG_INFINITY).serialize_resp(true); assert_eq!(bytes, b",-inf\r\n"); } #[test] fn resp3_parse_double() { let data = b",1.23\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Double(1.23)); } #[test] fn resp3_parse_double_inf() { let data = b",inf\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Double(f64::INFINITY)); let data = b",-inf\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, Frame::Double(f64::NEG_INFINITY)); } #[test] fn resp3_map_serialization() { let frame = Frame::Map(vec![ (Frame::bulk_string("key1"), Frame::bulk_string("val1")), (Frame::bulk_string("key2"), Frame::Integer(42)), ]); // RESP3: %2\r\n... let bytes = frame.serialize_resp(true); let expected = b"%2\r\n$4\r\nkey1\r\n$4\r\nval1\r\n$4\r\nkey2\r\n:42\r\n"; assert_eq!(bytes, expected); // RESP2 fallback: *4\r\n... (flat array) let bytes = frame.serialize_resp(false); let expected = b"*4\r\n$4\r\nkey1\r\n$4\r\nval1\r\n$4\r\nkey2\r\n:42\r\n"; assert_eq!(bytes, expected); } #[test] fn resp3_parse_map() { let data = b"%2\r\n$4\r\nkey1\r\n$4\r\nval1\r\n$4\r\nkey2\r\n:42\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!( frame, Frame::Map(vec![ ( Frame::Bulk(Bytes::from("key1")), Frame::Bulk(Bytes::from("val1")) ), (Frame::Bulk(Bytes::from("key2")), Frame::Integer(42)), ]) ); } #[test] fn resp3_set_serialization() { let frame = Frame::Set(vec![Frame::bulk_string("a"), Frame::bulk_string("b")]); // RESP3: ~2\r\n... let bytes = frame.serialize_resp(true); assert_eq!(bytes, b"~2\r\n$1\r\na\r\n$1\r\nb\r\n"); // RESP2 fallback: *2\r\n... let bytes = frame.serialize_resp(false); assert_eq!(bytes, b"*2\r\n$1\r\na\r\n$1\r\nb\r\n"); } #[test] fn resp3_parse_set() { let data = b"~2\r\n$1\r\na\r\n$1\r\nb\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!( frame, Frame::Set(vec![ Frame::Bulk(Bytes::from("a")), Frame::Bulk(Bytes::from("b")), ]) ); } #[test] fn resp3_push_serialization() { let frame = Frame::Push(vec![ Frame::bulk_string("message"), Frame::bulk_string("chan"), Frame::bulk_string("data"), ]); // RESP3: >3\r\n... let bytes = frame.serialize_resp(true); assert_eq!( bytes, b">3\r\n$7\r\nmessage\r\n$4\r\nchan\r\n$4\r\ndata\r\n" ); // RESP2 fallback: *3\r\n... let bytes = frame.serialize_resp(false); assert_eq!( bytes, b"*3\r\n$7\r\nmessage\r\n$4\r\nchan\r\n$4\r\ndata\r\n" ); } #[test] fn resp3_parse_push() { let data = b">3\r\n$7\r\nmessage\r\n$4\r\nchan\r\n$4\r\ndata\r\n"; let mut cursor = Cursor::new(&data[..]); let frame = Frame::parse(&mut cursor).unwrap(); assert_eq!( frame, Frame::Push(vec![ Frame::Bulk(Bytes::from("message")), Frame::Bulk(Bytes::from("chan")), Frame::Bulk(Bytes::from("data")), ]) ); } #[test] fn resp3_round_trip_map() { let frame = Frame::Map(vec![ (Frame::bulk_string("a"), Frame::Integer(1)), (Frame::bulk_string("b"), Frame::Integer(2)), ]); let bytes = frame.serialize_resp(true); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } #[test] fn resp3_round_trip_double() { let frame = Frame::Double(42.5); let bytes = frame.serialize_resp(true); let mut cursor = Cursor::new(&bytes[..]); let parsed = Frame::parse(&mut cursor).unwrap(); assert_eq!(frame, parsed); } } ================================================ FILE: miniredis/src/geo.rs ================================================ /// Geospatial encoding/decoding and distance calculations. /// /// Implements 52-bit integer geohash encoding (matching Redis's geohash_helper.c) /// and Haversine distance formula. use std::f64::consts::PI; const ENC_LAT: f64 = 85.05112878; const ENC_LONG: f64 = 180.0; const EXP2_32: f64 = 4294967296.0; // 2^32 /// Earth radius in meters (matching Redis src/geohash_helper.c). const EARTH_RADIUS: f64 = 6372797.560856; // ── Range encoding ────────────────────────────────────────────────── /// Encode the position of x within the range [-r, r] as a 32-bit integer. fn encode_range(x: f64, r: f64) -> u32 { let p = (x + r) / (2.0 * r); (p * EXP2_32) as u32 } /// Decode the 32-bit range encoding back to a value in [-r, r]. fn decode_range(x: u32, r: f64) -> f64 { let p = x as f64 / EXP2_32; 2.0 * r * p - r } // ── Bit interleaving ──────────────────────────────────────────────── /// Spread 32 bits into the even bit positions of a 64-bit word. fn spread(x: u32) -> u64 { let mut v = x as u64; v = (v | (v << 16)) & 0x0000ffff0000ffff; v = (v | (v << 8)) & 0x00ff00ff00ff00ff; v = (v | (v << 4)) & 0x0f0f0f0f0f0f0f0f; v = (v | (v << 2)) & 0x3333333333333333; v = (v | (v << 1)) & 0x5555555555555555; v } /// Squash the even bit positions of a 64-bit word into 32 bits. fn squash(x: u64) -> u32 { let mut v = x & 0x5555555555555555; v = (v | (v >> 1)) & 0x3333333333333333; v = (v | (v >> 2)) & 0x0f0f0f0f0f0f0f0f; v = (v | (v >> 4)) & 0x00ff00ff00ff00ff; v = (v | (v >> 8)) & 0x0000ffff0000ffff; v = (v | (v >> 16)) & 0x00000000ffffffff; v as u32 } /// Interleave the bits of x (lat) and y (lng). x occupies even bit positions, /// y occupies odd bit positions. fn interleave(x: u32, y: u32) -> u64 { spread(x) | (spread(y) << 1) } /// Deinterleave: extract even and odd bit positions into two 32-bit words. fn deinterleave(v: u64) -> (u32, u32) { (squash(v), squash(v >> 1)) } // ── Geohash encode/decode ─────────────────────────────────────────── /// Encode latitude and longitude into a full 64-bit integer geohash. fn encode_int(lat: f64, lng: f64) -> u64 { let lat_int = encode_range(lat, ENC_LAT); let lng_int = encode_range(lng, ENC_LONG); interleave(lat_int, lng_int) } /// Encode coordinates as a 52-bit geohash (stored as the upper 52 bits of /// a 64-bit value, i.e. right-shifted by 12). pub fn to_geohash(longitude: f64, latitude: f64) -> u64 { encode_int(latitude, longitude) >> (64 - 52) } /// Decode a 52-bit geohash back to (longitude, latitude). pub fn from_geohash(hash: u64) -> (f64, f64) { let full_hash = hash << (64 - 52); let (lat_int, lng_int) = deinterleave(full_hash); let lat = decode_range(lat_int, ENC_LAT); let lng = decode_range(lng_int, ENC_LONG); // Bounding box center: add half the error let lat_bits = 52 / 2; // 26 let lng_bits = 52 - lat_bits; // 26 let lat_err = 180.0 * 2.0f64.powi(-lat_bits); let lng_err = 360.0 * 2.0f64.powi(-lng_bits); (lng + lng_err / 2.0, lat + lat_err / 2.0) } // ── Haversine distance ────────────────────────────────────────────── /// Haversine helper: sin²(θ/2). fn hsin(theta: f64) -> f64 { let s = (theta / 2.0).sin(); s * s } /// Calculate the great-circle distance in meters between two points given /// as (latitude, longitude) in degrees, using the Haversine formula. pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 { let la1 = lat1 * PI / 180.0; let lo1 = lon1 * PI / 180.0; let la2 = lat2 * PI / 180.0; let lo2 = lon2 * PI / 180.0; let h = hsin(la2 - la1) + la1.cos() * la2.cos() * hsin(lo2 - lo1); 2.0 * EARTH_RADIUS * h.sqrt().asin() } // ── Unit conversion ───────────────────────────────────────────────── /// Parse a distance unit string and return the conversion factor to meters. /// Returns None for unrecognized units. pub fn parse_unit(unit: &str) -> Option { match unit.to_lowercase().as_str() { "m" => Some(1.0), "km" => Some(1000.0), "mi" => Some(1609.34), "ft" => Some(0.3048), _ => None, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_geohash_roundtrip() { let lng = 13.361_389_338_970_184; let lat = 38.115_556_395_496_3; let hash = to_geohash(lng, lat); assert_eq!(hash, 3479099956230698); let (lng_back, lat_back) = from_geohash(hash); assert!( (lng - lng_back).abs() < 0.000001, "longitude: {} vs {}", lng, lng_back ); assert!( (lat - lat_back).abs() < 0.000001, "latitude: {} vs {}", lat, lat_back ); } #[test] fn test_haversine_palermo_catania() { // Palermo: 13.361389, 38.115556 // Catania: 15.087269, 37.502669 let d = haversine_distance(38.115556, 13.361389, 37.502669, 15.087269); // Expected ~166274 meters assert!((d - 166274.0).abs() < 100.0, "distance: {}", d); } #[test] fn test_parse_unit() { assert_eq!(parse_unit("m"), Some(1.0)); assert_eq!(parse_unit("km"), Some(1000.0)); assert_eq!(parse_unit("mi"), Some(1609.34)); assert_eq!(parse_unit("ft"), Some(0.3048)); assert_eq!(parse_unit("mm"), None); assert_eq!(parse_unit("M"), Some(1.0)); assert_eq!(parse_unit("KM"), Some(1000.0)); } } ================================================ FILE: miniredis/src/hll.rs ================================================ /// HyperLogLog probabilistic cardinality estimator. /// /// Dense-only implementation with p=14 (16,384 registers). const P: u8 = 14; const M: usize = 1 << P; // 16384 registers /// Alpha constant for bias correction. fn alpha_m(m: f64) -> f64 { match m as u64 { 16 => 0.673, 32 => 0.697, 64 => 0.709, _ => 0.7213 / (1.0 + 1.079 / m), } } /// Bias correction polynomial for p=14. fn beta14(ez: f64) -> f64 { let zl = (ez + 1.0).ln(); -0.370393911 * ez + 0.070471823 * zl + 0.17393686 * zl.powi(2) + 0.16339839 * zl.powi(3) + -0.09237745 * zl.powi(4) + 0.03738027 * zl.powi(5) + -0.005384159 * zl.powi(6) + 0.00042419 * zl.powi(7) } /// 64-bit hash function (FNV-1a with avalanche mixing). fn hash64(data: &[u8]) -> u64 { let mut hash: u64 = 0xcbf29ce484222325; for &byte in data { hash ^= byte as u64; hash = hash.wrapping_mul(0x100000001b3); } // Avalanche: ensure all output bits depend on all input bits hash ^= hash >> 33; hash = hash.wrapping_mul(0xff51afd7ed558ccd); hash ^= hash >> 33; hash = hash.wrapping_mul(0xc4ceb9fe1a85ec53); hash ^= hash >> 33; hash } /// Extract register index and rho (leading zeros + 1) from a hash. fn get_pos_val(hash: u64) -> (usize, u8) { // Top P bits → register index let idx = (hash >> (64 - P)) as usize; // Remaining bits: count leading zeros + 1 // Set bit (P-1) to guarantee at least one 1-bit let w = (hash << P) | (1u64 << (P - 1)); let rho = w.leading_zeros() as u8 + 1; (idx, rho) } /// HyperLogLog probabilistic set cardinality estimator. #[derive(Clone, Debug)] pub struct HyperLogLog { registers: Vec, } impl Default for HyperLogLog { fn default() -> Self { Self::new() } } impl HyperLogLog { pub fn new() -> Self { HyperLogLog { registers: vec![0; M], } } /// Add an element. Returns true if the internal state changed /// (i.e., the approximated cardinality changed). pub fn add(&mut self, element: &[u8]) -> bool { let hash = hash64(element); let (idx, rho) = get_pos_val(hash); if rho > self.registers[idx] { self.registers[idx] = rho; true } else { false } } /// Estimate the cardinality (number of unique elements). pub fn count(&self) -> u64 { let m = M as f64; let alpha = alpha_m(m); let mut sum = 0.0f64; let mut zeros = 0.0f64; for ® in &self.registers { sum += 2.0f64.powi(-(reg as i32)); if reg == 0 { zeros += 1.0; } } let est = alpha * m * (m - zeros) / (sum + beta14(zeros)); (est + 0.5) as u64 } /// Merge another HLL into this one (element-wise max of registers). pub fn merge(&mut self, other: &HyperLogLog) { for i in 0..M { if other.registers[i] > self.registers[i] { self.registers[i] = other.registers[i]; } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_hll_empty() { let hll = HyperLogLog::new(); assert_eq!(hll.count(), 0); } #[test] fn test_hll_add_single() { let mut hll = HyperLogLog::new(); assert!(hll.add(b"hello")); assert!(!hll.add(b"hello")); // duplicate assert_eq!(hll.count(), 1); } #[test] fn test_hll_add_multiple() { let mut hll = HyperLogLog::new(); for i in 0..100 { hll.add(format!("item-{}", i).as_bytes()); } let count = hll.count(); // Should be approximately 100 (within 10% for p=14) assert!((90..=110).contains(&count), "count was {}", count); } #[test] fn test_hll_duplicate_returns_false() { let mut hll = HyperLogLog::new(); assert!(hll.add(b"a")); assert!(hll.add(b"b")); assert!(!hll.add(b"a")); // already added assert!(!hll.add(b"b")); // already added } #[test] fn test_hll_merge() { let mut hll1 = HyperLogLog::new(); let mut hll2 = HyperLogLog::new(); for i in 0..50 { hll1.add(format!("item-{}", i).as_bytes()); } for i in 50..100 { hll2.add(format!("item-{}", i).as_bytes()); } hll1.merge(&hll2); let count = hll1.count(); // Should be approximately 100 assert!((90..=110).contains(&count), "count was {}", count); } #[test] fn test_hll_merge_overlap() { let mut hll1 = HyperLogLog::new(); let mut hll2 = HyperLogLog::new(); // Add same elements to both for i in 0..50 { hll1.add(format!("item-{}", i).as_bytes()); hll2.add(format!("item-{}", i).as_bytes()); } hll1.merge(&hll2); let count = hll1.count(); // Should still be approximately 50 assert!((45..=55).contains(&count), "count was {}", count); } #[test] fn test_hll_large_cardinality() { let mut hll = HyperLogLog::new(); for i in 0..10000 { hll.add(format!("element-{}", i).as_bytes()); } let count = hll.count(); // p=14 gives ~0.8% standard error, allow 5% margin assert!((9500..=10500).contains(&count), "count was {}", count); } } ================================================ FILE: miniredis/src/keys.rs ================================================ /// Glob pattern matching for Redis KEYS, SCAN, PSUBSCRIBE etc. /// /// Supports *, ?, [abc], [a-z], [^a], \ escape. /// /// Simple glob matching: supports *, ?, [abc], [a-z], [^a]. pub fn glob_match(pattern: &str, text: &str) -> bool { let pat = pattern.as_bytes(); let txt = text.as_bytes(); glob_match_inner(pat, txt) } fn glob_match_inner(pat: &[u8], txt: &[u8]) -> bool { let mut pi = 0; let mut ti = 0; let mut star_pi = usize::MAX; let mut star_ti = 0; while ti < txt.len() { if pi < pat.len() && (pat[pi] == b'?' || (pat[pi] == txt[ti] && pat[pi] != b'[' && pat[pi] != b'\\')) { pi += 1; ti += 1; } else if pi < pat.len() && pat[pi] == b'*' { star_pi = pi; star_ti = ti; pi += 1; } else if pi < pat.len() && pat[pi] == b'[' { let (matched, end) = match_char_class(&pat[pi..], txt[ti]); if matched { pi += end; ti += 1; } else if star_pi != usize::MAX { pi = star_pi + 1; star_ti += 1; ti = star_ti; } else { return false; } } else if pi < pat.len() && pat[pi] == b'\\' && pi + 1 < pat.len() { pi += 1; if pat[pi] == txt[ti] { pi += 1; ti += 1; } else if star_pi != usize::MAX { pi = star_pi + 1; star_ti += 1; ti = star_ti; } else { return false; } } else if star_pi != usize::MAX { pi = star_pi + 1; star_ti += 1; ti = star_ti; } else { return false; } } while pi < pat.len() && pat[pi] == b'*' { pi += 1; } pi == pat.len() } /// Match a character class like [abc] or [a-z] or [^a]. Returns (matched, bytes consumed). fn match_char_class(pat: &[u8], ch: u8) -> (bool, usize) { if pat.is_empty() || pat[0] != b'[' { return (false, 0); } let mut i = 1; let negate = if i < pat.len() && pat[i] == b'^' { i += 1; true } else { false }; let mut matched = false; while i < pat.len() && pat[i] != b']' { // Handle backslash escape inside char class let c = if pat[i] == b'\\' && i + 1 < pat.len() { i += 1; pat[i] } else { pat[i] }; if i + 2 < pat.len() && pat[i + 1] == b'-' { let end = if pat[i + 2] == b'\\' && i + 3 < pat.len() { i += 1; pat[i + 2] } else { pat[i + 2] }; if ch >= c && ch <= end { matched = true; } i += 3; } else { if c == ch { matched = true; } i += 1; } } if i < pat.len() && pat[i] == b']' { i += 1; } if negate { matched = !matched; } (matched, i) } /// Filter a list of strings by a glob pattern. Used by SSCAN, HSCAN etc. pub fn match_keys_vec(keys: &[String], pattern: &str) -> Vec { if pattern == "*" { return keys.to_vec(); } keys.iter() .filter(|k| glob_match(pattern, k)) .cloned() .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_glob_match_basic() { assert!(glob_match("*", "anything")); assert!(glob_match("hello", "hello")); assert!(!glob_match("hello", "world")); assert!(glob_match("h?llo", "hello")); assert!(glob_match("h*o", "hello")); assert!(glob_match("h[ae]llo", "hello")); assert!(!glob_match("h[ae]llo", "hillo")); } #[test] fn test_glob_match_patterns() { assert!(glob_match("event*", "event123")); assert!(glob_match("event*", "event")); assert!(!glob_match("event*", "other")); assert!(glob_match("*news*", "the-news-today")); assert!(glob_match("h[a-z]llo", "hello")); } #[test] fn test_glob_match_escape_in_char_class() { // Backslash inside [] should escape the next character assert!(glob_match("[\\[o]*", "[one]")); assert!(!glob_match("[\\[o]*", "two")); assert!(glob_match("[\\[o]*", "other")); } } ================================================ FILE: miniredis/src/lib.rs ================================================ // This code is a Rust port of miniredis by Harmen (https://github.com/alicebob/miniredis), // originally licensed under the MIT License. See MINIREDIS_LICENSE.txt for the original license. //! # miniredis-rs //! //! Pure Rust in-memory Redis test server, for use in Rust integration tests. //! //! Start a server with `Miniredis::run().await`, it will listen on a random port. //! Point your Redis client to `m.redis_url()` or `m.addr()`. //! //! ```rust //! # async fn example() { //! let m = miniredis_rs::Miniredis::run().await.unwrap(); //! m.set("foo", "bar"); //! // Use m.redis_url() with any Redis client //! m.close().await; //! # } //! ``` pub mod cmd; pub mod connection; pub mod db; pub mod dispatch; pub mod frame; pub mod geo; pub mod hll; pub mod keys; pub mod pubsub; pub mod server; pub mod types; mod error; pub use error::{Error, Result}; use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, SystemTime}; use tokio::net::TcpListener; use crate::db::SharedState; /// A running miniredis instance for use in tests. /// /// Create one with [`Miniredis::run()`], use [`addr()`](Miniredis::addr) or /// [`redis_url()`](Miniredis::redis_url) to connect a client, and call /// [`close()`](Miniredis::close) when done. #[derive(Clone)] pub struct Miniredis { state: Arc, addr: SocketAddr, selected_db: usize, } impl Miniredis { /// Start a new miniredis server on a random available port (127.0.0.1:0). pub async fn run() -> Result { Self::run_addr("127.0.0.1:0").await } /// Start a new miniredis server on the given address. pub async fn run_addr(addr: &str) -> Result { let listener = TcpListener::bind(addr).await?; let local_addr = listener.local_addr()?; let state = SharedState::new(); let state_clone = Arc::clone(&state); let shutdown_rx = state.shutdown_tx.subscribe(); tokio::spawn(async move { server::run(listener, state_clone, shutdown_rx, None).await; }); Ok(Miniredis { state, addr: local_addr, selected_db: 0, }) } /// Start a new miniredis server with TLS on a random port. /// /// Connections must use TLS (rediss:// scheme). Use `tls_url()` for the URL. #[cfg(feature = "tls")] pub async fn run_tls(tls_config: Arc) -> Result { Self::run_tls_addr("127.0.0.1:0", tls_config).await } /// Start a new miniredis server with TLS on the given address. #[cfg(feature = "tls")] pub async fn run_tls_addr(addr: &str, tls_config: Arc) -> Result { let listener = TcpListener::bind(addr).await?; let local_addr = listener.local_addr()?; let state = SharedState::new(); let state_clone = Arc::clone(&state); let shutdown_rx = state.shutdown_tx.subscribe(); let acceptor = tokio_rustls::TlsAcceptor::from(tls_config); tokio::spawn(async move { server::run(listener, state_clone, shutdown_rx, Some(acceptor)).await; }); Ok(Miniredis { state, addr: local_addr, selected_db: 0, }) } /// Shut down the server. pub async fn close(&self) { let _ = self.state.shutdown_tx.send(()); // Give tasks a moment to clean up tokio::task::yield_now().await; } // ── Address helpers ────────────────────────────────────────────── /// The bound address as a `SocketAddr`. pub fn addr(&self) -> SocketAddr { self.addr } /// Just the host (e.g. "127.0.0.1"). pub fn host(&self) -> String { self.addr.ip().to_string() } /// Just the port number. pub fn port(&self) -> u16 { self.addr.port() } /// A `redis://host:port` URL suitable for most Redis clients. pub fn redis_url(&self) -> String { format!("redis://{}:{}", self.addr.ip(), self.addr.port()) } /// A `rediss://host:port` URL for TLS Redis clients. #[cfg(feature = "tls")] pub fn tls_url(&self) -> String { format!("rediss://{}:{}", self.addr.ip(), self.addr.port()) } // ── Database selection ─────────────────────────────────────────── /// Select the database used by the direct-access methods. pub fn select(&mut self, db: usize) { assert!(db < 16, "database index must be 0-15"); self.selected_db = db; } // ── Authentication ─────────────────────────────────────────────── /// Require AUTH with a password (default user). pub fn require_auth(&self, password: &str) { let mut inner = self.state.lock(); inner .passwords .insert("default".to_owned(), password.to_owned()); } /// Require AUTH with a username and password. pub fn require_user_auth(&self, username: &str, password: &str) { let mut inner = self.state.lock(); inner .passwords .insert(username.to_owned(), password.to_owned()); } // ── Time & determinism ─────────────────────────────────────────── /// Set a fixed mock time. Affects EXPIREAT, stream IDs, etc. pub fn set_time(&self, t: SystemTime) { let mut inner = self.state.lock(); inner.now = Some(t); } /// Decrease all TTLs by `duration`, expiring any that drop to zero. pub fn fast_forward(&self, duration: Duration) { let mut inner = self.state.lock(); inner.fast_forward(duration); } /// Seed the random number generator for deterministic tests. pub fn seed(&self, seed: u64) { use rand::SeedableRng; let mut inner = self.state.lock(); inner.rng = rand::rngs::StdRng::seed_from_u64(seed); } // ── Key management ─────────────────────────────────────────────── /// Delete a key. Returns true if it existed. pub fn del(&self, key: &str) -> bool { let mut inner = self.state.lock(); inner.db_mut(self.selected_db).del(key) } /// Check if a key exists. pub fn exists(&self, key: &str) -> bool { let mut inner = self.state.lock(); let now = inner.effective_now(); inner.db_mut(self.selected_db).exists(key, now) } /// Return the type of a key ("string", "list", "set", "hash", "zset", /// "stream", "none"). pub fn key_type(&self, key: &str) -> &'static str { let inner = self.state.lock(); match inner.db(self.selected_db).key_type(key) { Some(t) => t.as_str(), None => "none", } } /// Return all keys from the selected database, sorted. pub fn keys(&self) -> Vec { let inner = self.state.lock(); inner.db(self.selected_db).all_keys() } /// Get the TTL of a key. Returns None if the key has no TTL. pub fn ttl(&self, key: &str) -> Option { let inner = self.state.lock(); inner.db(self.selected_db).ttl.get(key).copied() } /// Set the TTL for a key. pub fn set_ttl(&self, key: &str, ttl: Duration) { let mut inner = self.state.lock(); inner .db_mut(self.selected_db) .ttl .insert(key.to_owned(), ttl); } // ── String operations ──────────────────────────────────────────── /// Get a string key value. pub fn get(&self, key: &str) -> Option { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.string_get(key) .map(|v| String::from_utf8_lossy(v).into_owned()) } /// Set a string key. Removes any existing TTL. pub fn set(&self, key: &str, value: &str) { let mut inner = self.state.lock(); let now = inner.effective_now(); let db = inner.db_mut(self.selected_db); db.string_set(key, value.as_bytes().to_vec(), now); db.ttl.remove(key); } /// Increment a string key by delta. Creates the key if it doesn't exist. pub fn incr(&self, key: &str, delta: i64) -> i64 { let mut inner = self.state.lock(); let now = inner.effective_now(); let db = inner.db_mut(self.selected_db); let current = db .string_get(key) .and_then(|v| String::from_utf8_lossy(v).parse::().ok()) .unwrap_or(0); let new_val = current + delta; db.string_set(key, new_val.to_string().into_bytes(), now); new_val } // ── List operations ────────────────────────────────────────────── /// Push values to the end (right) of a list. Creates the key if needed. /// Returns the new list length. pub fn push(&self, key: &str, values: &[&str]) -> usize { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::List); let list = db.list_keys.entry(key.to_owned()).or_default(); for v in values { list.push_back(v.as_bytes().to_vec()); } list.len() } /// Push a value to the beginning (left) of a list. pub fn lpush(&self, key: &str, value: &str) -> usize { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::List); let list = db.list_keys.entry(key.to_owned()).or_default(); list.push_front(value.as_bytes().to_vec()); list.len() } /// Pop from the end (right) of a list. pub fn pop(&self, key: &str) -> Option { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); let list = db.list_keys.get_mut(key)?; let val = list.pop_back()?; if list.is_empty() { db.list_keys.remove(key); db.del(key); } Some(String::from_utf8_lossy(&val).into_owned()) } /// Pop from the beginning (left) of a list. pub fn lpop(&self, key: &str) -> Option { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); let list = db.list_keys.get_mut(key)?; let val = list.pop_front()?; if list.is_empty() { db.list_keys.remove(key); db.del(key); } Some(String::from_utf8_lossy(&val).into_owned()) } /// Get all values in a list. pub fn list(&self, key: &str) -> Option> { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.list_keys.get(key).map(|list| { list.iter() .map(|v| String::from_utf8_lossy(v).into_owned()) .collect() }) } // ── Set operations ─────────────────────────────────────────────── /// Add members to a set. Returns the number of new members added. pub fn set_add(&self, key: &str, members: &[&str]) -> usize { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::Set); let set = db.set_keys.entry(key.to_owned()).or_default(); let mut added = 0; for m in members { if set.insert(m.to_string()) { added += 1; } } added } /// Get all members of a set, sorted. pub fn members(&self, key: &str) -> Option> { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.set_keys.get(key).map(|set| { let mut v: Vec = set.iter().cloned().collect(); v.sort(); v }) } /// Check if a value is a member of a set. pub fn is_member(&self, key: &str, member: &str) -> bool { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.set_keys .get(key) .map(|set| set.contains(member)) .unwrap_or(false) } // ── Hash operations ────────────────────────────────────────────── /// Set a hash field. pub fn hset(&self, key: &str, field: &str, value: &str) { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::Hash); let hash = db.hash_keys.entry(key.to_owned()).or_default(); hash.insert(field.to_owned(), value.as_bytes().to_vec()); } /// Get a hash field value. pub fn hget(&self, key: &str, field: &str) -> Option { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.hash_keys .get(key) .and_then(|h| h.get(field)) .map(|v| String::from_utf8_lossy(v).into_owned()) } /// Get all field names in a hash, sorted. pub fn hkeys(&self, key: &str) -> Option> { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.hash_keys.get(key).map(|h| { let mut keys: Vec = h.keys().cloned().collect(); keys.sort(); keys }) } /// Delete a hash field. Returns true if the field existed. pub fn hdel(&self, key: &str, field: &str) -> bool { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); if let Some(hash) = db.hash_keys.get_mut(key) { let removed = hash.remove(field).is_some(); if hash.is_empty() { db.hash_keys.remove(key); db.del(key); } removed } else { false } } // ── Sorted set operations ──────────────────────────────────────── /// Add a member to a sorted set with the given score. /// Returns true if the member was new. pub fn zadd(&self, key: &str, score: f64, member: &str) -> bool { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::SortedSet); let ss = db.sorted_set_keys.entry(key.to_owned()).or_default(); ss.set(score, member) } /// Get the score of a member in a sorted set. pub fn zscore(&self, key: &str, member: &str) -> Option { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.sorted_set_keys.get(key).and_then(|ss| ss.get(member)) } /// Get all members of a sorted set, sorted by score then member name. pub fn zmembers(&self, key: &str) -> Option> { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.sorted_set_keys.get(key).map(|ss| ss.members_sorted()) } // ── Stream operations ──────────────────────────────────────────── /// Add an entry to a stream. Returns the assigned ID. pub fn xadd(&self, key: &str, id: &str, values: &[(&str, &str)]) -> String { let mut inner = self.state.lock(); let now = inner.effective_now(); let now_ms = now .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::Stream); let stream = db.stream_keys.entry(key.to_owned()).or_default(); let field_values: Vec = values .iter() .flat_map(|(k, v)| vec![k.to_string(), v.to_string()]) .collect(); stream.add(id, field_values, now_ms).unwrap_or_default() } // ── HyperLogLog operations ─────────────────────────────────────── /// Add elements to a HyperLogLog. Returns true if the cardinality estimate changed. pub fn pfadd(&self, key: &str, elements: &[&str]) -> bool { let mut inner = self.state.lock(); let db = inner.db_mut(self.selected_db); db.keys.insert(key.to_owned(), types::KeyType::HyperLogLog); let hll = db.hll_keys.entry(key.to_owned()).or_default(); let mut changed = false; for elem in elements { if hll.add(elem.as_bytes()) { changed = true; } } changed } /// Get the cardinality estimate of a HyperLogLog. pub fn pfcount(&self, key: &str) -> i64 { let inner = self.state.lock(); let db = inner.db(self.selected_db); db.hll_keys .get(key) .map(|hll| hll.count() as i64) .unwrap_or(0) } // ── Flush ──────────────────────────────────────────────────────── /// Remove all keys from the selected database. pub fn flush_db(&self) { let mut inner = self.state.lock(); inner.db_mut(self.selected_db).flush(); } /// Remove all keys from all databases. pub fn flush_all(&self) { let mut inner = self.state.lock(); for db in &mut inner.dbs { db.flush(); } } // ── Testing assertions ─────────────────────────────────────────── /// Assert that a string key has the expected value. Panics on mismatch. pub fn check_get(&self, key: &str, expected: &str) { let val = self .get(key) .unwrap_or_else(|| panic!("key {:?} not found", key)); assert_eq!( val, expected, "key {:?}: expected {:?}, got {:?}", key, expected, val ); } // ── Pub/Sub ────────────────────────────────────────────────────── /// Publish a message on a channel. Returns the number of subscribers that received it. pub fn publish(&self, channel: &str, message: &str) -> i64 { let registry = self.state.pubsub.lock().unwrap(); registry.publish(channel, message) } /// Return active pub/sub channels, optionally filtered by glob pattern. pub fn pubsub_channels(&self, pattern: Option<&str>) -> Vec { let registry = self.state.pubsub.lock().unwrap(); registry.active_channels(pattern) } /// Return the number of subscribers for specific channels. pub fn pubsub_numsub(&self, channels: &[&str]) -> Vec<(String, i64)> { let registry = self.state.pubsub.lock().unwrap(); channels .iter() .map(|ch| (ch.to_string(), registry.numsub(ch))) .collect() } /// Return the total number of pattern subscriptions. pub fn pubsub_numpat(&self) -> i64 { let registry = self.state.pubsub.lock().unwrap(); registry.numpat() } // ── Testing assertions ─────────────────────────────────────────── /// Assert that a list key has the expected values. Panics on mismatch. pub fn check_list(&self, key: &str, expected: &[&str]) { let val = self .list(key) .unwrap_or_else(|| panic!("key {:?} not found or not a list", key)); let expected_strs: Vec = expected.iter().map(|s| s.to_string()).collect(); assert_eq!( val, expected_strs, "key {:?}: expected {:?}, got {:?}", key, expected, val ); } /// Assert that a set key has the expected members (order-independent). Panics on mismatch. pub fn check_set(&self, key: &str, expected: &[&str]) { let val = self .members(key) .unwrap_or_else(|| panic!("key {:?} not found or not a set", key)); let mut expected_sorted: Vec = expected.iter().map(|s| s.to_string()).collect(); expected_sorted.sort(); assert_eq!( val, expected_sorted, "key {:?}: expected {:?}, got {:?}", key, expected_sorted, val ); } // ── Server introspection ───────────────────────────────────────── /// Number of currently connected clients. pub fn current_connection_count(&self) -> u64 { self.state .connected_clients .load(std::sync::atomic::Ordering::Relaxed) } /// Total number of connections received since startup. pub fn total_connection_count(&self) -> u64 { self.state .total_connections_received .load(std::sync::atomic::Ordering::Relaxed) } // ── Direct DB access ────────────────────────────────────────────── /// Access a specific database by index (0-15) without changing the /// selected database. /// /// The returned [`DbRef`] borrows `self` and provides the same /// direct-access methods (get, set, keys, etc.) scoped to the given DB. pub fn db(&self, id: usize) -> DbRef<'_> { assert!(id < 16, "database index must be 0-15"); DbRef { state: &self.state, db_id: id, } } // ── Restart ───────────────────────────────────────────────────── /// Restart a closed server on a new port. All data is preserved. /// The previous server must have been closed with [`close()`](Self::close). pub async fn restart(&mut self) -> Result<()> { let listener = TcpListener::bind("127.0.0.1:0").await?; self.addr = listener.local_addr()?; let state_clone = Arc::clone(&self.state); let shutdown_rx = self.state.shutdown_tx.subscribe(); tokio::spawn(async move { server::run(listener, state_clone, shutdown_rx, None).await; }); Ok(()) } // ── Dump ──────────────────────────────────────────────────────── /// Return a text representation of the selected database, useful for /// debugging. pub fn dump(&self) -> String { let inner = self.state.lock(); let db = inner.db(self.selected_db); dump_db(db) } // ── Internals (for advanced usage) ─────────────────────────────── /// Get a reference to the shared state (for custom commands, etc.). pub fn shared_state(&self) -> &Arc { &self.state } /// Number of keys in the selected database. pub fn db_size(&self) -> usize { let inner = self.state.lock(); inner.db(self.selected_db).keys.len() } } /// A handle to a specific database, returned by [`Miniredis::db()`]. /// Provides the same direct-access methods scoped to the given DB index. pub struct DbRef<'a> { state: &'a Arc, db_id: usize, } impl DbRef<'_> { /// Return all keys, sorted. pub fn keys(&self) -> Vec { let inner = self.state.lock(); inner.db(self.db_id).all_keys() } /// Get a string key value. pub fn get(&self, key: &str) -> Option { let inner = self.state.lock(); inner .db(self.db_id) .string_get(key) .map(|v| String::from_utf8_lossy(v).into_owned()) } /// Set a string key. Removes any existing TTL. pub fn set(&self, key: &str, value: &str) { let mut inner = self.state.lock(); let now = inner.effective_now(); let db = inner.db_mut(self.db_id); db.string_set(key, value.as_bytes().to_vec(), now); db.ttl.remove(key); } /// Check if a key exists. pub fn exists(&self, key: &str) -> bool { let mut inner = self.state.lock(); let now = inner.effective_now(); inner.db_mut(self.db_id).exists(key, now) } /// Return the type of a key. pub fn key_type(&self, key: &str) -> &'static str { let inner = self.state.lock(); match inner.db(self.db_id).key_type(key) { Some(t) => t.as_str(), None => "none", } } /// Number of keys in this database. pub fn db_size(&self) -> usize { let inner = self.state.lock(); inner.db(self.db_id).keys.len() } /// Return a text representation of this database. pub fn dump(&self) -> String { let inner = self.state.lock(); dump_db(inner.db(self.db_id)) } } /// Maximum number of characters to show per value in [`Miniredis::dump()`]. const DUMP_MAX_LINE_LEN: usize = 200; fn dump_db(db: &db::RedisDB) -> String { use std::fmt::Write; use types::Direction; let indent = " "; let mut r = String::new(); let truncate = |s: &str| -> String { if s.len() > DUMP_MAX_LINE_LEN { let suffix = format!("...({})", s.len()); let end = DUMP_MAX_LINE_LEN - suffix.len(); format!("{:?}{}", &s[..end], suffix) } else { format!("{:?}", s) } }; for k in db.all_keys() { let _ = writeln!(r, "- {}", k); match db.key_type(&k) { Some(types::KeyType::String) => { if let Some(v) = db.string_get(&k) { let _ = writeln!(r, "{}{}", indent, truncate(&String::from_utf8_lossy(v))); } } Some(types::KeyType::Hash) => { if let Some(hash) = db.hash_keys.get(&k) { let mut fields: Vec<&String> = hash.keys().collect(); fields.sort(); for f in fields { let v = String::from_utf8_lossy(&hash[f]); let _ = writeln!(r, "{}{}: {}", indent, f, truncate(&v)); } } } Some(types::KeyType::List) => { if let Some(list) = db.list_keys.get(&k) { for item in list { let _ = writeln!(r, "{}{}", indent, truncate(&String::from_utf8_lossy(item))); } } } Some(types::KeyType::Set) => { if let Some(set) = db.set_keys.get(&k) { let mut members: Vec<&String> = set.iter().collect(); members.sort(); for m in members { let _ = writeln!(r, "{}{}", indent, truncate(m)); } } } Some(types::KeyType::SortedSet) => { if let Some(ss) = db.sorted_set_keys.get(&k) { for el in ss.by_score(Direction::Asc) { let _ = writeln!(r, "{}{}: {}", indent, el.score, truncate(&el.member)); } } } Some(types::KeyType::Stream) => { if let Some(stream) = db.stream_keys.get(&k) { for entry in &stream.entries { let _ = writeln!(r, "{}{}", indent, entry.id); let ev = &entry.values; let mut i = 0; while i + 1 < ev.len() { let _ = writeln!( r, "{}{}{}: {}", indent, indent, truncate(&ev[i]), truncate(&ev[i + 1]) ); i += 2; } } } } Some(types::KeyType::HyperLogLog) => { let _ = writeln!(r, "{}(HyperLogLog)", indent); } None => {} } } r } ================================================ FILE: miniredis/src/pubsub.rs ================================================ /// Pub/Sub message delivery infrastructure. use std::collections::HashSet; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; /// A message delivered to a pub/sub subscriber. pub struct PubsubMessage { /// "message" or "pmessage" pub kind: &'static str, /// The pattern that matched (for pmessage only) pub pattern: Option, /// The channel the message was published to pub channel: String, /// The message payload pub data: String, } /// A pattern matcher: (pattern_string, compiled_matcher). pub type PatternMatcher = (String, Box bool + Send + Sync>); /// Per-subscriber state: channels, patterns, and message sender. pub struct SubscriberInner { pub channels: HashSet, pub patterns: Vec, pub tx: mpsc::UnboundedSender, } /// Handle to a subscriber, stored in the global registry. pub type SubscriberHandle = Arc>; /// Global pub/sub subscriber registry. pub struct PubsubRegistry { subscribers: Vec, } impl Default for PubsubRegistry { fn default() -> Self { Self::new() } } impl PubsubRegistry { pub fn new() -> Self { PubsubRegistry { subscribers: Vec::new(), } } pub fn add(&mut self, handle: SubscriberHandle) { self.subscribers.push(handle); } pub fn remove(&mut self, handle: &SubscriberHandle) { self.subscribers.retain(|s| !Arc::ptr_eq(s, handle)); } /// Publish a message to all matching subscribers. Returns total delivery count. pub fn publish(&self, channel: &str, message: &str) -> i64 { let mut count = 0i64; for sub in &self.subscribers { let inner = sub.lock().unwrap(); // Check direct channel subscription if inner.channels.contains(channel) { let _ = inner.tx.send(PubsubMessage { kind: "message", pattern: None, channel: channel.to_string(), data: message.to_string(), }); count += 1; } // Check pattern subscriptions for (pat_str, matcher) in &inner.patterns { if matcher(channel) { let _ = inner.tx.send(PubsubMessage { kind: "pmessage", pattern: Some(pat_str.clone()), channel: channel.to_string(), data: message.to_string(), }); count += 1; break; // only one match per subscriber } } } count } /// Return all unique channels that have at least one subscriber, optionally filtered by pattern. pub fn active_channels(&self, pattern: Option<&str>) -> Vec { let mut channels = HashSet::new(); for sub in &self.subscribers { let inner = sub.lock().unwrap(); for ch in &inner.channels { channels.insert(ch.clone()); } } let mut result: Vec = if let Some(pat) = pattern { channels .into_iter() .filter(|ch| crate::keys::glob_match(pat, ch)) .collect() } else { channels.into_iter().collect() }; result.sort(); result } /// Count subscribers for a specific channel (direct subscriptions only). pub fn numsub(&self, channel: &str) -> i64 { let mut count = 0i64; for sub in &self.subscribers { let inner = sub.lock().unwrap(); if inner.channels.contains(channel) { count += 1; } } count } /// Total number of pattern subscriptions across all subscribers. pub fn numpat(&self) -> i64 { let mut count = 0i64; for sub in &self.subscribers { let inner = sub.lock().unwrap(); count += inner.patterns.len() as i64; } count } } /// Per-connection pub/sub context. pub struct PubsubCtx { /// Shared handle registered in the global registry. pub handle: SubscriberHandle, /// Receiver for pub/sub messages. pub rx: mpsc::UnboundedReceiver, } impl PubsubCtx { /// Create a new pub/sub context. Returns the context and registers it in the registry. pub fn new(registry: &mut PubsubRegistry) -> Self { let (tx, rx) = mpsc::unbounded_channel(); let inner = SubscriberInner { channels: HashSet::new(), patterns: Vec::new(), tx, }; let handle = Arc::new(Mutex::new(inner)); registry.add(handle.clone()); PubsubCtx { handle, rx } } /// Subscribe to a channel. Returns the total subscription count. pub fn subscribe(&self, channel: &str) -> usize { let mut inner = self.handle.lock().unwrap(); inner.channels.insert(channel.to_string()); inner.channels.len() + inner.patterns.len() } /// Unsubscribe from a channel. Returns the total subscription count. pub fn unsubscribe(&self, channel: &str) -> usize { let mut inner = self.handle.lock().unwrap(); inner.channels.remove(channel); inner.channels.len() + inner.patterns.len() } /// Subscribe to a pattern. Returns the total subscription count. pub fn psubscribe(&self, pattern: &str) -> usize { let pat = pattern.to_string(); let pat_clone = pat.clone(); let matcher: Box bool + Send + Sync> = Box::new(move |text: &str| crate::keys::glob_match(&pat_clone, text)); let mut inner = self.handle.lock().unwrap(); inner.patterns.push((pat, matcher)); inner.channels.len() + inner.patterns.len() } /// Unsubscribe from a pattern. Returns the total subscription count. pub fn punsubscribe(&self, pattern: &str) -> usize { let mut inner = self.handle.lock().unwrap(); inner.patterns.retain(|(p, _)| p != pattern); inner.channels.len() + inner.patterns.len() } /// Get all subscribed channel names. pub fn channels(&self) -> Vec { let inner = self.handle.lock().unwrap(); let mut channels: Vec = inner.channels.iter().cloned().collect(); channels.sort(); channels } /// Get all subscribed pattern strings. pub fn patterns(&self) -> Vec { let inner = self.handle.lock().unwrap(); inner.patterns.iter().map(|(p, _)| p.clone()).collect() } /// Total subscription count (channels + patterns). pub fn total_count(&self) -> usize { let inner = self.handle.lock().unwrap(); inner.channels.len() + inner.patterns.len() } } ================================================ FILE: miniredis/src/server.rs ================================================ use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::broadcast; use crate::connection::{ConnCtx, Connection}; use crate::db::SharedState; use crate::dispatch::{CommandTable, dispatch, err_wrong_number}; use crate::frame::Frame; use crate::pubsub::PubsubCtx; /// Start the server: bind to the given address, accept connections, and /// dispatch commands. /// /// Returns the bound address (useful when binding to port 0). /// The server runs until a shutdown signal is received via `shutdown_rx`. /// If `tls_acceptor` is Some, connections are wrapped with TLS. pub async fn run( listener: TcpListener, state: Arc, mut shutdown_rx: broadcast::Receiver<()>, #[cfg(feature = "tls")] tls_acceptor: Option, #[cfg(not(feature = "tls"))] _tls_acceptor: Option<()>, ) { let table = Arc::new(CommandTable::new()); let _ = state.command_table.set(Arc::clone(&table)); loop { tokio::select! { result = listener.accept() => { let (socket, _addr) = match result { Ok(s) => s, Err(_) => continue, }; let state = Arc::clone(&state); let table = Arc::clone(&table); let shutdown_rx = state.shutdown_tx.subscribe(); #[cfg(feature = "tls")] let tls = tls_acceptor.clone(); tokio::spawn(async move { #[cfg(feature = "tls")] if let Some(acceptor) = tls { if let Ok(tls_stream) = acceptor.accept(socket).await { handle_connection_stream( Connection::new_stream(tls_stream), state, table, shutdown_rx, ).await; return; } return; } handle_connection_stream( Connection::new(socket), state, table, shutdown_rx, ).await; }); } _ = shutdown_rx.recv() => { // Shutdown signal received return; } } } } /// Handle a single client connection (plain or TLS). async fn handle_connection_stream( mut conn: Connection, state: Arc, table: Arc, mut shutdown_rx: broadcast::Receiver<()>, ) { use std::sync::atomic::Ordering; state .total_connections_received .fetch_add(1, Ordering::Relaxed); state.connected_clients.fetch_add(1, Ordering::Relaxed); let mut ctx = ConnCtx::new(); let mut pubsub: Option = None; handle_connection_inner( &mut conn, &mut ctx, &mut pubsub, &state, &table, &mut shutdown_rx, ) .await; // Cleanup: remove subscriber from registry if in pub/sub mode if let Some(ps) = pubsub.take() { let mut registry = state.pubsub.lock().unwrap(); registry.remove(&ps.handle); } state.connected_clients.fetch_sub(1, Ordering::Relaxed); } async fn handle_connection_inner( conn: &mut Connection, ctx: &mut ConnCtx, pubsub: &mut Option, state: &Arc, table: &Arc, shutdown_rx: &mut broadcast::Receiver<()>, ) { loop { if let Some(ps) = pubsub.as_mut() { // ── Pub/Sub mode event loop ──────────────────────────── tokio::select! { result = conn.read_frame() => { let frame = match result { Ok(Some(frame)) => frame, Ok(None) => return, Err(_) => return, }; let args = match frame_to_args(frame) { Some(args) => args, None => { let _ = conn.write_frame(&Frame::error("ERR invalid command format")).await; continue; } }; if args.is_empty() { let _ = conn.write_frame(&Frame::error("ERR empty command")).await; continue; } let cmd = String::from_utf8_lossy(&args[0]).to_uppercase(); let cmd_args = &args[1..]; match cmd.as_str() { "SUBSCRIBE" => { if cmd_args.is_empty() { let _ = conn.write_frame(&Frame::error(err_wrong_number("subscribe"))).await; continue; } for arg in cmd_args { let channel = String::from_utf8_lossy(arg).to_string(); let count = ps.subscribe(&channel); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("subscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } "UNSUBSCRIBE" => { if cmd_args.is_empty() { // Unsubscribe from all channels let channels = ps.channels(); if channels.is_empty() { let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("unsubscribe".into()), Frame::Null, Frame::Integer(ps.total_count() as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } else { for channel in channels { let count = ps.unsubscribe(&channel); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("unsubscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } } else { for arg in cmd_args { let channel = String::from_utf8_lossy(arg).to_string(); let count = ps.unsubscribe(&channel); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("unsubscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } // Exit pub/sub mode if no subscriptions left if ps.total_count() == 0 { let handle = &ps.handle; let mut registry = state.pubsub.lock().unwrap(); registry.remove(handle); drop(registry); *pubsub = None; } } "PSUBSCRIBE" => { if cmd_args.is_empty() { let _ = conn.write_frame(&Frame::error(err_wrong_number("psubscribe"))).await; continue; } for arg in cmd_args { let pattern = String::from_utf8_lossy(arg).to_string(); let count = ps.psubscribe(&pattern); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("psubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } "PUNSUBSCRIBE" => { if cmd_args.is_empty() { let patterns = ps.patterns(); if patterns.is_empty() { let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("punsubscribe".into()), Frame::Null, Frame::Integer(ps.total_count() as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } else { for pattern in patterns { let count = ps.punsubscribe(&pattern); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("punsubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } } else { for arg in cmd_args { let pattern = String::from_utf8_lossy(arg).to_string(); let count = ps.punsubscribe(&pattern); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("punsubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } // Exit pub/sub mode if no subscriptions left if ps.total_count() == 0 { let handle = &ps.handle; let mut registry = state.pubsub.lock().unwrap(); registry.remove(handle); drop(registry); *pubsub = None; } } "PING" => { if cmd_args.is_empty() { let pong = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("pong".into()), Frame::Bulk("".into()), ]); if conn.write_frame(&pong).await.is_err() { return; } } else { let msg = String::from_utf8_lossy(&cmd_args[0]).to_string(); let pong = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("pong".into()), Frame::Bulk(msg.into()), ]); if conn.write_frame(&pong).await.is_err() { return; } } } "QUIT" => { let _ = conn.write_frame(&Frame::ok()).await; return; } _ => { let err = format!( "ERR Can't execute '{}': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context", cmd.to_lowercase() ); if conn.write_frame(&Frame::error(err)).await.is_err() { return; } } } } msg = ps.rx.recv() => { match msg { Some(m) => { let frame = match m.kind { "message" => pubsub_msg(ctx.resp3, vec![ Frame::Bulk("message".into()), Frame::Bulk(m.channel.into()), Frame::Bulk(m.data.into()), ]), "pmessage" => pubsub_msg(ctx.resp3, vec![ Frame::Bulk("pmessage".into()), Frame::Bulk(m.pattern.unwrap_or_default().into()), Frame::Bulk(m.channel.into()), Frame::Bulk(m.data.into()), ]), _ => continue, }; if conn.write_frame(&frame).await.is_err() { return; } } None => return, // channel closed } } _ = shutdown_rx.recv() => { return; } } } else { // ── Normal command loop ──────────────────────────────── tokio::select! { result = conn.read_frame() => { let frame = match result { Ok(Some(frame)) => frame, Ok(None) => return, Err(_) => return, }; let args = match frame_to_args(frame) { Some(args) => args, None => { let _ = conn.write_frame(&Frame::error("ERR invalid command format")).await; continue; } }; if args.is_empty() { let _ = conn.write_frame(&Frame::error("ERR empty command")).await; continue; } let cmd = String::from_utf8_lossy(&args[0]).to_uppercase(); // Handle SUBSCRIBE/PSUBSCRIBE — enter pub/sub mode // (but not inside MULTI — let dispatch queue it) if (cmd == "SUBSCRIBE" || cmd == "PSUBSCRIBE") && !ctx.in_tx() { let cmd_args = &args[1..]; if cmd_args.is_empty() { let _ = conn.write_frame(&Frame::error(err_wrong_number(&cmd.to_lowercase()))).await; continue; } // Create pub/sub context let ps = { let mut registry = state.pubsub.lock().unwrap(); PubsubCtx::new(&mut registry) }; *pubsub = Some(ps); let ps = pubsub.as_mut().unwrap(); if cmd == "SUBSCRIBE" { for arg in cmd_args { let channel = String::from_utf8_lossy(arg).to_string(); let count = ps.subscribe(&channel); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("subscribe".into()), Frame::Bulk(channel.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } else { for arg in cmd_args { let pattern = String::from_utf8_lossy(arg).to_string(); let count = ps.psubscribe(&pattern); let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("psubscribe".into()), Frame::Bulk(pattern.into()), Frame::Integer(count as i64), ]); if conn.write_frame(&confirm).await.is_err() { return; } } } continue; } // Handle UNSUBSCRIBE/PUNSUBSCRIBE outside pub/sub mode (no-op) // But not inside MULTI — let dispatch queue it. if cmd == "UNSUBSCRIBE" && !ctx.in_tx() { let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("unsubscribe".into()), Frame::Null, Frame::Integer(0), ]); let _ = conn.write_frame(&confirm).await; continue; } if cmd == "PUNSUBSCRIBE" && !ctx.in_tx() { let confirm = pubsub_msg(ctx.resp3, vec![ Frame::Bulk("punsubscribe".into()), Frame::Null, Frame::Integer(0), ]); let _ = conn.write_frame(&confirm).await; continue; } state.total_commands_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // Intercept blocking commands (outside MULTI/EXEC) if !ctx.in_tx() && matches!(cmd.as_str(), "BLPOP" | "BRPOP" | "BRPOPLPUSH" | "BLMOVE") { let response = handle_blocking_command( &cmd, &args[1..], state, ctx, shutdown_rx ).await; conn.resp3 = ctx.resp3; if conn.write_frame(&response).await.is_err() { return; } continue; } // Intercept XREAD/XREADGROUP with BLOCK (outside MULTI/EXEC) if !ctx.in_tx() && matches!(cmd.as_str(), "XREAD" | "XREADGROUP") && has_block_arg(&args[1..]) { let response = handle_blocking_stream_command( &cmd, &args[1..], state, ctx, shutdown_rx ).await; conn.resp3 = ctx.resp3; if conn.write_frame(&response).await.is_err() { return; } continue; } let (response, should_close) = dispatch(table, state, ctx, &args); // Sync RESP3 flag (set by HELLO command) conn.resp3 = ctx.resp3; if conn.write_frame(&response).await.is_err() { return; } if should_close { return; } // Check if SUBSCRIBE/PSUBSCRIBE was executed inside EXEC. // If so, enter pub/sub mode with the pending channels/patterns. if !ctx.pending_subscribe.is_empty() || !ctx.pending_psubscribe.is_empty() { let channels = std::mem::take(&mut ctx.pending_subscribe); let patterns = std::mem::take(&mut ctx.pending_psubscribe); let ps = { let mut registry = state.pubsub.lock().unwrap(); PubsubCtx::new(&mut registry) }; *pubsub = Some(ps); let ps = pubsub.as_mut().unwrap(); for channel in channels { ps.subscribe(&channel); } for pattern in patterns { ps.psubscribe(&pattern); } } } _ = shutdown_rx.recv() => { return; } } } } } /// Wrap pub/sub confirmation/message frames: use Push in RESP3, Array in RESP2. fn pubsub_msg(resp3: bool, elements: Vec) -> Frame { if resp3 { Frame::Push(elements) } else { Frame::Array(elements) } } /// Handle blocking list commands (BLPOP, BRPOP, BRPOPLPUSH, BLMOVE). /// These block until data is available or timeout expires. async fn handle_blocking_command( cmd: &str, args: &[Vec], state: &Arc, ctx: &mut ConnCtx, shutdown_rx: &mut broadcast::Receiver<()>, ) -> Frame { use crate::dispatch::{ MSG_INVALID_TIMEOUT, MSG_SYNTAX_ERROR, MSG_TIMEOUT_IS_OUT_OF_RANGE, MSG_TIMEOUT_NEGATIVE, MSG_WRONG_TYPE, }; use crate::types::KeyType; match cmd { "BLPOP" | "BRPOP" => { if args.len() < 2 { return Frame::error(crate::dispatch::err_wrong_number(&cmd.to_lowercase())); } let keys = &args[..args.len() - 1]; let timeout_str = String::from_utf8_lossy(&args[args.len() - 1]); let timeout_lower = timeout_str.to_lowercase(); if timeout_lower == "inf" || timeout_lower == "+inf" || timeout_lower == "-inf" { return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE); } let timeout_s: f64 = match timeout_str.parse() { Ok(t) => t, Err(_) => return Frame::error(MSG_INVALID_TIMEOUT), }; if timeout_s < 0.0 { return Frame::error(MSG_TIMEOUT_NEGATIVE); } let is_left = cmd == "BLPOP"; // Try immediate pop { let mut inner = state.lock(); let now = inner.effective_now(); for key_bytes in keys { let key = String::from_utf8_lossy(key_bytes).into_owned(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); if let Some(t) = db.key_type(&key) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } let val = if is_left { db.list_lpop(&key, now) } else { db.list_rpop(&key, now) }; if let Some(v) = val { state.notify.notify_waiters(); return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(v.into())]); } } } // Block until data or timeout let timeout_dur = if timeout_s == 0.0 { std::time::Duration::from_secs(300) // max wait } else { std::time::Duration::from_secs_f64(timeout_s) }; let deadline = tokio::time::Instant::now() + timeout_dur; loop { tokio::select! { _ = state.notify.notified() => { let mut inner = state.lock(); let now = inner.effective_now(); for key_bytes in keys { let key = String::from_utf8_lossy(key_bytes).into_owned(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&key); let val = if is_left { db.list_lpop(&key, now) } else { db.list_rpop(&key, now) }; if let Some(v) = val { state.notify.notify_waiters(); return Frame::Array(vec![ Frame::Bulk(key.into()), Frame::Bulk(v.into()), ]); } } } _ = tokio::time::sleep_until(deadline) => { return Frame::NullArray; } _ = shutdown_rx.recv() => { return Frame::NullArray; } } } } "BRPOPLPUSH" => { if args.len() != 3 { return Frame::error(crate::dispatch::err_wrong_number("brpoplpush")); } let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let timeout_str = String::from_utf8_lossy(&args[2]); let timeout_lower = timeout_str.to_lowercase(); if timeout_lower == "inf" || timeout_lower == "+inf" || timeout_lower == "-inf" { return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE); } let timeout_s: f64 = match timeout_str.parse() { Ok(t) => t, Err(_) => return Frame::error(MSG_INVALID_TIMEOUT), }; if timeout_s < 0.0 { return Frame::error(MSG_TIMEOUT_NEGATIVE); } // Try immediate { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(val) = db.list_rpop(&src, now) { db.list_lpush(&dst, std::slice::from_ref(&val), now); state.notify.notify_waiters(); return Frame::Bulk(val.into()); } } let timeout_dur = if timeout_s == 0.0 { std::time::Duration::from_secs(300) } else { std::time::Duration::from_secs_f64(timeout_s) }; let deadline = tokio::time::Instant::now() + timeout_dur; loop { tokio::select! { _ = state.notify.notified() => { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); if let Some(val) = db.list_rpop(&src, now) { db.list_lpush(&dst, std::slice::from_ref(&val), now); state.notify.notify_waiters(); return Frame::Bulk(val.into()); } } _ = tokio::time::sleep_until(deadline) => { return Frame::NullArray; } _ = shutdown_rx.recv() => { return Frame::NullArray; } } } } "BLMOVE" => { if args.len() != 5 { return Frame::error(crate::dispatch::err_wrong_number("blmove")); } let src = String::from_utf8_lossy(&args[0]).into_owned(); let dst = String::from_utf8_lossy(&args[1]).into_owned(); let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase(); let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase(); let pop_left = match src_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let push_left = match dst_dir.as_str() { "LEFT" => true, "RIGHT" => false, _ => return Frame::error(MSG_SYNTAX_ERROR), }; let timeout_str = String::from_utf8_lossy(&args[4]); let timeout_lower = timeout_str.to_lowercase(); if timeout_lower == "inf" || timeout_lower == "+inf" || timeout_lower == "-inf" { return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE); } let timeout_s: f64 = match timeout_str.parse() { Ok(t) => t, Err(_) => return Frame::error(MSG_INVALID_TIMEOUT), }; if timeout_s < 0.0 { return Frame::error(MSG_TIMEOUT_NEGATIVE); } // Try immediate { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); db.check_ttl(&dst); if let Some(t) = db.key_type(&src) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } if let Some(t) = db.key_type(&dst) && t != KeyType::List { return Frame::error(MSG_WRONG_TYPE); } // Save TTL when src == dst let saved_ttl = if src == dst { db.ttl.get(&src).cloned() } else { None }; let val = if pop_left { db.list_lpop(&src, now) } else { db.list_rpop(&src, now) }; if let Some(v) = val { if push_left { db.list_lpush(&dst, std::slice::from_ref(&v), now); } else { db.list_rpush(&dst, std::slice::from_ref(&v), now); } if let Some(ttl) = saved_ttl { db.ttl.insert(dst.clone(), ttl); } state.notify.notify_waiters(); return Frame::Bulk(v.into()); } } let timeout_dur = if timeout_s == 0.0 { std::time::Duration::from_secs(300) } else { std::time::Duration::from_secs_f64(timeout_s) }; let deadline = tokio::time::Instant::now() + timeout_dur; loop { tokio::select! { _ = state.notify.notified() => { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); db.check_ttl(&src); // Save TTL when src == dst let saved_ttl = if src == dst { db.ttl.get(&src).cloned() } else { None }; let val = if pop_left { db.list_lpop(&src, now) } else { db.list_rpop(&src, now) }; if let Some(v) = val { if push_left { db.list_lpush(&dst, std::slice::from_ref(&v), now); } else { db.list_rpush(&dst, std::slice::from_ref(&v), now); } if let Some(ttl) = saved_ttl { db.ttl.insert(dst.clone(), ttl); } state.notify.notify_waiters(); return Frame::Bulk(v.into()); } } _ = tokio::time::sleep_until(deadline) => { return Frame::NullArray; } _ = shutdown_rx.recv() => { return Frame::NullArray; } } } } _ => Frame::error("ERR unsupported blocking command"), } } /// Check if BLOCK argument is present in the command args. fn has_block_arg(args: &[Vec]) -> bool { args.iter() .any(|a| String::from_utf8_lossy(a).to_uppercase() == "BLOCK") } /// Handle blocking XREAD/XREADGROUP commands. async fn handle_blocking_stream_command( cmd: &str, args: &[Vec], state: &Arc, ctx: &mut ConnCtx, shutdown_rx: &mut broadcast::Receiver<()>, ) -> Frame { use crate::dispatch::MSG_WRONG_TYPE; use crate::types::{KeyType, Stream}; match cmd { "XREAD" => { if args.len() < 3 { return Frame::error(crate::dispatch::err_wrong_number("xread")); } let mut i = 0; let mut count: Option = None; let mut block_ms: Option = None; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "COUNT" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) => count = Some(n), Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } "BLOCK" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n < 0 => { return Frame::error("ERR timeout is negative"); } Ok(n) => block_ms = Some(n), Err(_) => { return Frame::error( "ERR timeout is not an integer or out of range", ); } } i += 1; } "STREAMS" => { i += 1; break; } _ => { return Frame::error("ERR syntax error"); } } } let remaining = &args[i..]; if remaining.is_empty() || !remaining.len().is_multiple_of(2) { return Frame::error( "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified.", ); } let half = remaining.len() / 2; let keys: Vec = remaining[..half] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); // Resolve $ IDs to current last IDs let mut ids = Vec::with_capacity(half); { let inner = state.lock(); let db = inner.db(ctx.selected_db); for (idx, a) in remaining[half..].iter().enumerate() { let s = String::from_utf8_lossy(a).to_string(); if s == "$" { ids.push( db.stream_keys .get(&keys[idx]) .map(|stream| stream.last_id().to_string()) .unwrap_or_else(|| "0-0".to_string()), ); } else { let normalized = Stream::normalize_id(&s); if Stream::parse_id(&normalized).is_err() { return Frame::error( "ERR Invalid stream ID specified as stream command argument", ); } ids.push(normalized); } } } // Helper closure to try reading from streams let try_read = |state: &SharedState, keys: &[String], ids: &[String], count: Option| -> Option { let inner = state.lock(); let db = inner.db(ctx.selected_db); let mut results = Vec::new(); for (idx, key) in keys.iter().enumerate() { if let Some(kt) = db.keys.get(key) && *kt != KeyType::Stream { return Some(Frame::error(MSG_WRONG_TYPE)); } let entries = match db.stream_keys.get(key) { Some(stream) => { let mut entries = stream.after(&ids[idx]); if let Some(c) = count { entries.truncate(c); } entries } None => vec![], }; if entries.is_empty() { continue; } let entry_frames: Vec = entries .into_iter() .map(|e| { let vals: Vec = e .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)]) }) .collect(); results.push(Frame::Array(vec![ Frame::Bulk(key.clone().into()), Frame::Array(entry_frames), ])); } if results.is_empty() { None // No data yet } else { Some(Frame::Array(results)) } }; // Try immediate read if let Some(result) = try_read(state, &keys, &ids, count) { return result; } // Block until data or timeout let timeout_ms = block_ms.unwrap_or(0); let timeout_dur = if timeout_ms == 0 { std::time::Duration::from_secs(300) // max wait } else { std::time::Duration::from_millis(timeout_ms as u64) }; let deadline = tokio::time::Instant::now() + timeout_dur; loop { tokio::select! { _ = state.notify.notified() => { if let Some(result) = try_read(state, &keys, &ids, count) { return result; } } _ = tokio::time::sleep_until(deadline) => { return Frame::NullArray; } _ = shutdown_rx.recv() => { return Frame::NullArray; } } } } "XREADGROUP" => { if args.len() < 6 { return Frame::error(crate::dispatch::err_wrong_number("xreadgroup")); } let mut i = 0; let group_kw = String::from_utf8_lossy(&args[i]).to_uppercase(); if group_kw != "GROUP" { return Frame::error("ERR syntax error"); } i += 1; let group_name = String::from_utf8_lossy(&args[i]).to_string(); i += 1; let consumer_name = String::from_utf8_lossy(&args[i]).to_string(); i += 1; let mut count: Option = None; let mut block_ms: Option = None; let mut noack = false; while i < args.len() { let opt = String::from_utf8_lossy(&args[i]).to_uppercase(); match opt.as_str() { "COUNT" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n > 0 => count = Some(n as usize), Ok(_) => count = None, Err(_) => { return Frame::error("ERR value is not an integer or out of range"); } } i += 1; } "BLOCK" => { i += 1; if i >= args.len() { return Frame::error("ERR syntax error"); } match String::from_utf8_lossy(&args[i]).parse::() { Ok(n) if n < 0 => { return Frame::error("ERR timeout is negative"); } Ok(n) => block_ms = Some(n), Err(_) => { return Frame::error( "ERR timeout is not an integer or out of range", ); } } i += 1; } "NOACK" => { noack = true; i += 1; } "STREAMS" => { i += 1; break; } _ => { return Frame::error("ERR syntax error"); } } } let remaining = &args[i..]; if remaining.is_empty() || !remaining.len().is_multiple_of(2) { return Frame::error( "ERR Unbalanced XREADGROUP list of streams: for each stream key an ID or '$' must be specified.", ); } let half = remaining.len() / 2; let keys: Vec = remaining[..half] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); let ids: Vec = remaining[half..] .iter() .map(|a| String::from_utf8_lossy(a).to_string()) .collect(); // If any ID is not ">", BLOCK is ignored — run synchronously let all_gt = ids.iter().all(|id| id == ">"); if !all_gt { // Fall through to non-blocking execution via dispatch // Run non-blocking via dispatch let mut full_cmd = Vec::with_capacity(args.len() + 1); full_cmd.push(b"XREADGROUP".to_vec()); full_cmd.extend(args.iter().cloned()); let (response, _) = crate::dispatch::dispatch( state.command_table.get().unwrap(), state, ctx, &full_cmd, ); return response; } // Validate group existence for all streams before blocking { let inner = state.lock(); let db = inner.db(ctx.selected_db); for key in &keys { if let Some(kt) = db.keys.get(key) && *kt != KeyType::Stream { return Frame::error(MSG_WRONG_TYPE); } match db.stream_keys.get(key) { Some(stream) => { if !stream.groups.contains_key(&group_name) { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } } None => { return Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key )); } } } } // Helper closure to try reading from groups let try_read_group = |state: &SharedState, ctx: &ConnCtx, keys: &[String], group_name: &str, consumer_name: &str, count: Option, noack: bool| -> Result, Frame> { let mut inner = state.lock(); let now = inner.effective_now(); let db = inner.db_mut(ctx.selected_db); let mut results = Vec::new(); for key in keys { let stream = match db.stream_keys.get_mut(key) { Some(s) => s, None => { return Err(Frame::error(format!( "NOGROUP No such consumer group '{}' for key name '{}'", group_name, key ))); } }; let entries = match stream.read_group( group_name, consumer_name, ">", count, noack, now, ) { Ok(entries) => entries, Err(e) => return Err(Frame::error(e)), }; if entries.is_empty() { continue; } let entry_frames: Vec = entries .into_iter() .map(|e| { let vals: Vec = e .values .iter() .map(|v| Frame::Bulk(v.clone().into())) .collect(); Frame::Array(vec![Frame::Bulk(e.id.into()), Frame::Array(vals)]) }) .collect(); results.push(Frame::Array(vec![ Frame::Bulk(key.clone().into()), Frame::Array(entry_frames), ])); } if results.is_empty() { Ok(None) } else { Ok(Some(Frame::Array(results))) } }; // Try immediate read match try_read_group(state, ctx, &keys, &group_name, &consumer_name, count, noack) { Err(e) => return e, Ok(Some(result)) => return result, Ok(None) => {} // No data, proceed to blocking } // Block until data or timeout let timeout_ms = block_ms.unwrap_or(0); let timeout_dur = if timeout_ms == 0 { std::time::Duration::from_secs(300) // max wait } else { std::time::Duration::from_millis(timeout_ms as u64) }; let deadline = tokio::time::Instant::now() + timeout_dur; loop { tokio::select! { _ = state.notify.notified() => { match try_read_group(state, ctx, &keys, &group_name, &consumer_name, count, noack) { Err(e) => return e, Ok(Some(result)) => return result, Ok(None) => {} // Continue waiting } } _ = tokio::time::sleep_until(deadline) => { return Frame::NullArray; } _ = shutdown_rx.recv() => { return Frame::NullArray; } } } } _ => Frame::error("ERR unsupported blocking stream command"), } } /// Convert a Frame (expected to be an Array of Bulk strings) into a /// vector of byte vectors (the command arguments). fn frame_to_args(frame: Frame) -> Option>> { match frame { Frame::Array(frames) => { let mut args = Vec::with_capacity(frames.len()); for f in frames { match f { Frame::Bulk(data) => args.push(data.to_vec()), Frame::Simple(s) => args.push(s.into_bytes()), Frame::Integer(n) => args.push(n.to_string().into_bytes()), _ => return None, } } Some(args) } // Some clients send inline commands (single bulk string) Frame::Bulk(data) => { let s = String::from_utf8_lossy(&data); let args: Vec> = s .split_whitespace() .map(|w| w.as_bytes().to_vec()) .collect(); if args.is_empty() { None } else { Some(args) } } _ => None, } } ================================================ FILE: miniredis/src/types.rs ================================================ use std::collections::HashMap; /// The type tag for a Redis key. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum KeyType { String, Hash, List, Set, SortedSet, Stream, HyperLogLog, } impl KeyType { /// Return the Redis TYPE command string for this key type. pub fn as_str(&self) -> &'static str { match self { KeyType::String => "string", KeyType::Hash => "hash", KeyType::List => "list", KeyType::Set => "set", KeyType::SortedSet => "zset", KeyType::Stream => "stream", KeyType::HyperLogLog => "hll", // not "string" — miniredis uses a distinct type } } } /// A sorted set element with score and member. #[derive(Clone, Debug)] pub struct SSElem { pub score: f64, pub member: String, } /// Ascending or descending direction for sorted set operations. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Direction { Asc, Desc, } /// Redis sorted set — uses a HashMap for O(1) score lookups, sorts on demand for range queries. #[derive(Clone, Debug, Default)] pub struct SortedSet { pub scores: HashMap, } impl SortedSet { pub fn new() -> Self { Self::default() } pub fn card(&self) -> usize { self.scores.len() } /// Add or update a member. Returns true if the member was new. pub fn set(&mut self, score: f64, member: &str) -> bool { let is_new = !self.scores.contains_key(member); self.scores.insert(member.to_owned(), score); is_new } /// Get a member's score. pub fn get(&self, member: &str) -> Option { self.scores.get(member).copied() } /// Check if a member exists. pub fn exists(&self, member: &str) -> bool { self.scores.contains_key(member) } /// Remove a member. Returns true if it existed. pub fn remove(&mut self, member: &str) -> bool { self.scores.remove(member).is_some() } /// Return all elements sorted by (score, member). pub fn by_score(&self, dir: Direction) -> Vec { let mut elems: Vec = self .scores .iter() .map(|(m, &s)| SSElem { score: s, member: m.clone(), }) .collect(); elems.sort_by(|a, b| { a.score .partial_cmp(&b.score) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| a.member.cmp(&b.member)) }); if dir == Direction::Desc { elems.reverse(); } elems } /// Return sorted member names (all same score → lex order). pub fn members_sorted(&self) -> Vec { self.by_score(Direction::Asc) .into_iter() .map(|e| e.member) .collect() } /// Get the rank (0-based index) of a member in sorted order. pub fn rank(&self, member: &str, dir: Direction) -> Option { if !self.scores.contains_key(member) { return None; } let elems = self.by_score(dir); elems.iter().position(|e| e.member == member) } /// Increment a member's score by delta. Creates the member if it doesn't exist. /// Returns the new score. pub fn incrby(&mut self, member: &str, delta: f64) -> f64 { let score = self.scores.entry(member.to_owned()).or_insert(0.0); *score += delta; *score } } /// A single stream entry. #[derive(Clone, Debug)] pub struct StreamEntry { pub id: String, /// Alternating key-value pairs. pub values: Vec, } /// A pending entry in a consumer group's PEL. #[derive(Clone, Debug)] pub struct PendingEntry { pub id: String, pub consumer: String, pub delivery_count: i64, pub last_delivery: std::time::SystemTime, } /// A consumer within a group. #[derive(Clone, Debug)] pub struct StreamConsumer { pub num_pending: i64, pub last_seen: std::time::SystemTime, pub last_success: std::time::SystemTime, } /// A consumer group on a stream. #[derive(Clone, Debug)] pub struct StreamGroup { pub last_id: String, pub pending: Vec, pub consumers: HashMap, /// Whether entries-read is known (set to false initially, true once entries are delivered). pub entries_read_known: bool, } /// Redis stream. #[derive(Clone, Debug, Default)] pub struct Stream { pub entries: Vec, pub groups: HashMap, pub last_allocated_id: String, } impl Stream { pub fn new() -> Self { Self::default() } /// Parse a stream ID string "ms-seq" into (ms, seq). pub fn parse_id(id: &str) -> Result<(u64, u64), &'static str> { let parts: Vec<&str> = id.splitn(2, '-').collect(); let ms = parts[0] .parse::() .map_err(|_| "ERR Invalid stream ID specified as stream command argument")?; let seq = if parts.len() > 1 { parts[1] .parse::() .map_err(|_| "ERR Invalid stream ID specified as stream command argument")? } else { 0 }; Ok((ms, seq)) } /// Compare two stream IDs. Returns Ordering. pub fn cmp_ids(a: &str, b: &str) -> std::cmp::Ordering { let a_parsed = Self::parse_id(a).unwrap_or((0, 0)); let b_parsed = Self::parse_id(b).unwrap_or((0, 0)); a_parsed.cmp(&b_parsed) } /// Format a stream ID from parts. pub fn format_id(ms: u64, seq: u64) -> String { format!("{}-{}", ms, seq) } /// Normalize a partial ID to full "ms-seq" format. pub fn normalize_id(id: &str) -> String { if id.contains('-') { id.to_string() } else { format!("{}-0", id) } } /// Get the last entry's ID, or "0-0" if empty. pub fn last_id(&self) -> &str { self.entries.last().map(|e| e.id.as_str()).unwrap_or("0-0") } /// Generate a new ID based on timestamp. pub fn generate_id(&mut self, ms: u64) -> String { let mut new_ms = ms; let mut new_seq = 0u64; // Check against lastAllocatedID if !self.last_allocated_id.is_empty() && let Ok((alloc_ms, alloc_seq)) = Self::parse_id(&self.last_allocated_id) && new_ms <= alloc_ms { new_ms = alloc_ms; new_seq = alloc_seq + 1; } // Check against last entry if let Some(last) = self.entries.last() && let Ok((last_ms, last_seq)) = Self::parse_id(&last.id) && (new_ms < last_ms || (new_ms == last_ms && new_seq <= last_seq)) { new_ms = last_ms; new_seq = last_seq + 1; } let id = Self::format_id(new_ms, new_seq); self.last_allocated_id = id.clone(); id } /// Generate ID with a specific timestamp and auto-sequence. pub fn generate_id_seq(&mut self, ms: u64) -> String { self.generate_id(ms) } /// Add an entry. Returns the assigned ID or error. pub fn add( &mut self, id: &str, values: Vec, now_ms: u64, ) -> Result { let final_id = if id.is_empty() || id == "*" { self.generate_id(now_ms) } else if let Some(ms_str) = id.strip_suffix("-*") { let ms = ms_str .parse::() .map_err(|_| "ERR Invalid stream ID specified as stream command argument")?; self.generate_id_seq(ms) } else { let normalized = Self::normalize_id(id); // Validate the ID let (ms, seq) = Self::parse_id(&normalized)?; if ms == 0 && seq == 0 { return Err("ERR The ID specified in XADD must be greater than 0-0"); } // Must be greater than the last entry if let Some(last) = self.entries.last() && Self::cmp_ids(&normalized, &last.id) != std::cmp::Ordering::Greater { return Err( "ERR The ID specified in XADD is equal or smaller than the target stream top item", ); } self.last_allocated_id = normalized.clone(); normalized }; self.entries.push(StreamEntry { id: final_id.clone(), values, }); Ok(final_id) } /// Trim to at most n entries (MAXLEN). pub fn trim_maxlen(&mut self, n: usize) -> i64 { if self.entries.len() <= n { return 0; } let remove = self.entries.len() - n; self.entries.drain(..remove); remove as i64 } /// Remove all entries with ID < threshold (MINID). pub fn trim_minid(&mut self, threshold: &str) -> i64 { let before = self.entries.len(); self.entries .retain(|e| Self::cmp_ids(&e.id, threshold) != std::cmp::Ordering::Less); (before - self.entries.len()) as i64 } /// Get entries after the given ID. pub fn after(&self, id: &str) -> Vec<&StreamEntry> { self.entries .iter() .filter(|e| Self::cmp_ids(&e.id, id) == std::cmp::Ordering::Greater) .collect() } /// Get entries in range [start, end]. pub fn range(&self, start: &str, end: &str, count: Option) -> Vec<&StreamEntry> { let mut result: Vec<&StreamEntry> = self .entries .iter() .filter(|e| { Self::cmp_ids(&e.id, start) != std::cmp::Ordering::Less && Self::cmp_ids(&e.id, end) != std::cmp::Ordering::Greater }) .collect(); if let Some(c) = count { result.truncate(c); } result } /// Get entries in reverse range [end, start] (for XREVRANGE). pub fn rev_range(&self, start: &str, end: &str, count: Option) -> Vec<&StreamEntry> { let mut result: Vec<&StreamEntry> = self .entries .iter() .filter(|e| { Self::cmp_ids(&e.id, start) != std::cmp::Ordering::Less && Self::cmp_ids(&e.id, end) != std::cmp::Ordering::Greater }) .rev() .collect(); if let Some(c) = count { result.truncate(c); } result } /// Delete entries by ID. Returns count deleted. pub fn del(&mut self, ids: &[&str]) -> i64 { let mut count = 0i64; for id in ids { let before = self.entries.len(); self.entries.retain(|e| e.id != **id); if self.entries.len() < before { count += 1; } } count } /// Get an entry by ID. pub fn get(&self, id: &str) -> Option<&StreamEntry> { self.entries.iter().find(|e| e.id == id) } /// Check if an entry exists. pub fn entry_exists(&self, id: &str) -> bool { self.entries.iter().any(|e| e.id == id) } /// Create a consumer group. Returns error if already exists. pub fn create_group(&mut self, name: &str, id: &str) -> Result<(), String> { if self.groups.contains_key(name) { return Err("BUSYGROUP Consumer Group name already exists".to_string()); } let last_id = if id == "$" { self.last_id().to_string() } else { Self::normalize_id(id) }; // entries_read_known is true only when the group starts at "0-0" // (meaning we know it has read 0 entries). For "$" or specific IDs, // we don't know the true count. let entries_read_known = last_id == "0-0"; self.groups.insert( name.to_string(), StreamGroup { last_id, pending: Vec::new(), consumers: HashMap::new(), entries_read_known, }, ); Ok(()) } /// Read from a consumer group. Returns entries and updates PEL. pub fn read_group( &mut self, group_name: &str, consumer_name: &str, id: &str, count: Option, noack: bool, now: std::time::SystemTime, ) -> Result, String> { let group = self.groups.get_mut(group_name).ok_or_else(|| { format!( "NOGROUP No such consumer group '{}' for key name", group_name ) })?; // Ensure consumer exists group .consumers .entry(consumer_name.to_string()) .or_insert(StreamConsumer { num_pending: 0, last_seen: now, last_success: now, }); if id == ">" { // New undelivered messages let entries: Vec = self .entries .iter() .filter(|e| Self::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater) .cloned() .collect(); let entries = if let Some(c) = count { entries.into_iter().take(c).collect::>() } else { entries }; if let Some(last) = entries.last() { group.last_id = last.id.clone(); group.entries_read_known = true; } if !noack { for entry in &entries { // Check if already in PEL if let Some(pe) = group.pending.iter_mut().find(|pe| pe.id == entry.id) { // Already in PEL - update consumer ownership let old_consumer = pe.consumer.clone(); if old_consumer != consumer_name { pe.consumer = consumer_name.to_string(); if let Some(c) = group.consumers.get_mut(&old_consumer) { c.num_pending -= 1; } if let Some(c) = group.consumers.get_mut(consumer_name) { c.num_pending += 1; } } pe.delivery_count += 1; pe.last_delivery = now; } else { group.pending.push(PendingEntry { id: entry.id.clone(), consumer: consumer_name.to_string(), delivery_count: 1, last_delivery: now, }); if let Some(c) = group.consumers.get_mut(consumer_name) { c.num_pending += 1; c.last_success = now; } } } } Ok(entries) } else { // Re-deliver from PEL let normalized = Self::normalize_id(id); let mut result = Vec::new(); for pe in &mut group.pending { if pe.consumer != consumer_name { continue; } if Self::cmp_ids(&pe.id, &normalized) == std::cmp::Ordering::Less { continue; } // Find the entry in the stream if let Some(entry) = self.entries.iter().find(|e| e.id == pe.id) { pe.delivery_count += 1; pe.last_delivery = now; result.push(entry.clone()); } if let Some(c) = count && result.len() >= c { break; } } Ok(result) } } /// Acknowledge entries. Returns count acknowledged. /// If the group doesn't exist, returns 0 (not an error). pub fn ack(&mut self, group_name: &str, ids: &[&str]) -> Result { let group = match self.groups.get_mut(group_name) { Some(g) => g, None => return Ok(0), }; let mut count = 0i64; for id in ids { let before = group.pending.len(); let consumer_name = group .pending .iter() .find(|pe| pe.id == **id) .map(|pe| pe.consumer.clone()); group.pending.retain(|pe| pe.id != **id); if group.pending.len() < before { count += 1; if let Some(cname) = consumer_name && let Some(c) = group.consumers.get_mut(&cname) { c.num_pending -= 1; } } } Ok(count) } } /// Format a range bound for XRANGE/XREVRANGE. /// Returns Ok(formatted_id) or Err(error_message) if the id is invalid. pub fn format_stream_range_bound(id: &str, is_start: bool) -> Result { match id { "-" => Ok("0-0".to_string()), "+" => Ok(format!("{}-{}", u64::MAX, u64::MAX)), _ => { // Handle exclusive prefix '(' if let Some(rest) = id.strip_prefix('(') { if rest == "-" || rest == "+" { return Err("ERR Invalid stream ID specified as stream command argument"); } // Normalize using the same rules as non-exclusive bounds first: // - For start: "X" → "X-0" // - For end: "X" → "X-MAX" // Then apply the exclusive adjustment. let normalized = if rest.contains('-') { rest.to_string() } else if is_start { format!("{}-0", rest) } else { format!("{}-{}", rest, u64::MAX) }; // Validate the ID let (ms, seq) = Stream::parse_id(&normalized) .map_err(|_| "ERR Invalid stream ID specified as stream command argument")?; if is_start { // Exclusive start: we want entries strictly after this ID // Increment seq (or ms if seq overflows) if seq == u64::MAX { Ok(Stream::format_id(ms + 1, 0)) } else { Ok(Stream::format_id(ms, seq + 1)) } } else { // Exclusive end: we want entries strictly before this ID // Decrement seq (or ms if seq is 0) if seq == 0 { if ms == 0 { // Nothing before 0-0 Ok("0-0".to_string()) } else { Ok(Stream::format_id(ms - 1, u64::MAX)) } } else { Ok(Stream::format_id(ms, seq - 1)) } } } else { // Validate the ID let base = if id.contains('-') { id.to_string() } else if is_start { format!("{}-0", id) } else { format!("{}-{}", id, u64::MAX) }; // Validate let parts: Vec<&str> = base.splitn(2, '-').collect(); parts[0] .parse::() .map_err(|_| "ERR Invalid stream ID specified as stream command argument")?; if parts.len() > 1 { parts[1].parse::().map_err( |_| "ERR Invalid stream ID specified as stream command argument", )?; } Ok(base) } } } } // HyperLogLog is implemented in src/hll.rs. ================================================ FILE: miniredis/tests/cmd_auth.rs ================================================ mod helpers; use helpers::*; // ── AUTH ────────────────────────────────────────────────────────────── #[tokio::test] async fn test_auth_no_password_configured() { let (_m, mut c) = start().await; // AUTH without any password configured → specific error must_fail!(c, "AUTH", "foo"; "AUTH called without any password configured"); } #[tokio::test] async fn test_auth_wrong_args() { let (_m, mut c) = start().await; // No args must_fail!(c, "AUTH"; "wrong number of arguments"); // Too many args must_fail!(c, "AUTH", "a", "b", "c"; "syntax error"); } #[tokio::test] async fn test_auth_default_user() { let (m, mut c) = start().await; m.require_auth("secret"); // Without auth, commands should fail must_fail!(c, "PING"; "NOAUTH"); // Wrong password must_fail!(c, "AUTH", "wrongpass"; "WRONGPASS"); // Correct password must_ok!(c, "AUTH", "secret"); // Now commands work let v: String = redis::cmd("PING").query_async(&mut c).await.unwrap(); assert_eq!(v, "PONG"); } #[tokio::test] async fn test_auth_user_password() { let (m, mut c) = start().await; m.require_user_auth("hello", "world"); // Without auth, commands should fail must_fail!(c, "PING"; "NOAUTH"); // Wrong password must_fail!(c, "AUTH", "hello", "wrongpass"; "WRONGPASS"); // Wrong username must_fail!(c, "AUTH", "goodbye", "world"; "WRONGPASS"); // Correct user + password must_ok!(c, "AUTH", "hello", "world"); // Now commands work let v: String = redis::cmd("PING").query_async(&mut c).await.unwrap(); assert_eq!(v, "PONG"); } // ── HELLO ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_hello_basic() { let (_m, mut c) = start().await; // HELLO 2 should return server info let v: redis::Value = redis::cmd("HELLO") .arg("2") .query_async(&mut c) .await .unwrap(); // Should be an array with key-value pairs match v { redis::Value::Array(ref items) => { assert!(items.len() >= 12); // at least 6 key-value pairs } _ => panic!("expected array from HELLO, got {:?}", v), } } #[tokio::test] async fn test_hello_errors() { let (_m, mut c) = start().await; // No args must_fail!(c, "HELLO"; "wrong number of arguments"); // Non-integer version must_fail!(c, "HELLO", "foo"; "Protocol version is not an integer"); // Unsupported version must_fail!(c, "HELLO", "1"; "NOPROTO"); must_fail!(c, "HELLO", "4"; "NOPROTO"); } #[tokio::test] async fn test_hello_auth() { let (m, mut c) = start().await; m.require_auth("secret"); // HELLO with AUTH should authenticate let v: redis::Value = redis::cmd("HELLO") .arg("2") .arg("AUTH") .arg("default") .arg("secret") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(_) => {} // success _ => panic!("expected array from HELLO, got {:?}", v), } // Should be authenticated now let v: String = redis::cmd("PING").query_async(&mut c).await.unwrap(); assert_eq!(v, "PONG"); } #[tokio::test] async fn test_hello_auth_wrong_password() { let (m, mut c) = start().await; m.require_auth("secret"); // HELLO with wrong AUTH should fail must_fail!(c, "HELLO", "2", "AUTH", "default", "wrong"; "WRONGPASS"); } #[tokio::test] async fn test_hello_syntax_errors() { let (_m, mut c) = start().await; // AUTH with missing args must_fail!(c, "HELLO", "2", "AUTH", "foo"; "Syntax error in HELLO option"); // SETNAME with missing arg must_fail!(c, "HELLO", "2", "AUTH", "foo", "bar", "SETNAME"; "Syntax error in HELLO option"); // Unknown option must_fail!(c, "HELLO", "2", "BOGUS"; "Syntax error in HELLO option"); } ================================================ FILE: miniredis/tests/cmd_bit.rs ================================================ // Ported from ../miniredis/cmd_string_test.go (bit operations) mod helpers; #[tokio::test] async fn test_getbit() { let (_m, mut c) = helpers::start().await; // \x08 = 0b00001000 let _: () = redis::cmd("SET") .arg("findme") .arg(b"\x08" as &[u8]) .query_async(&mut c) .await .unwrap(); must_int!(c, "GETBIT", "findme", "0"; 0); must_int!(c, "GETBIT", "findme", "4"; 1); must_int!(c, "GETBIT", "findme", "5"; 0); // Non-existing key must_int!(c, "GETBIT", "nosuch", "1"; 0); must_int!(c, "GETBIT", "nosuch", "1000"; 0); // Errors must_fail!(c, "GETBIT", "foo"; "wrong number of arguments"); must_fail!(c, "GETBIT", "foo", "noint"; "not an integer"); must_ok!(c, "SET", "str", "val"); // Not wrong type for getbit — strings are valid } #[tokio::test] async fn test_setbit() { let (_m, mut c) = helpers::start().await; // \x08 = 0b00001000 let _: () = redis::cmd("SET") .arg("findme") .arg(b"\x08" as &[u8]) .query_async(&mut c) .await .unwrap(); // Clear bit 4 (was 1) must_int!(c, "SETBIT", "findme", "4", "0"; 1); // Set bit 4 (was 0) must_int!(c, "SETBIT", "findme", "4", "1"; 0); // Non-existing key — creates it must_int!(c, "SETBIT", "nosuch", "0", "1"; 0); let v: Vec = redis::cmd("GET") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x80]); // Extends short values let _: () = redis::cmd("SET") .arg("short") .arg(b"\x00\x00" as &[u8]) .query_async(&mut c) .await .unwrap(); must_int!(c, "SETBIT", "short", "32", "1"; 0); let v: Vec = redis::cmd("GET") .arg("short") .query_async(&mut c) .await .unwrap(); assert_eq!(v.len(), 5); assert_eq!(v[4], 0x80); // Errors must_fail!(c, "SETBIT", "foo"; "wrong number of arguments"); must_fail!(c, "SETBIT", "foo", "noint", "1"; "not an integer"); must_fail!(c, "SETBIT", "foo", "1", "noint"; "not an integer"); must_fail!(c, "SETBIT", "foo", "-3", "0"; "not an integer"); must_fail!(c, "SETBIT", "foo", "3", "2"; "out of range"); } #[tokio::test] async fn test_bitcount() { let (_m, mut c) = helpers::start().await; // 'a' = 0x61 = 0b01100001 → 3 bits must_ok!(c, "SET", "countme", "a"); must_int!(c, "BITCOUNT", "countme"; 3); // 'aaaaa' → 15 bits must_ok!(c, "SET", "countme", "aaaaa"); must_int!(c, "BITCOUNT", "countme"; 15); // Non-existing must_int!(c, "BITCOUNT", "nosuch"; 0); // With range: 'abcd' // a=0x61(3) b=0x62(3) c=0x63(4) d=0x64(3) must_ok!(c, "SET", "foo", "abcd"); must_int!(c, "BITCOUNT", "foo", "0", "0"; 3); must_int!(c, "BITCOUNT", "foo", "0", "3"; 13); must_int!(c, "BITCOUNT", "foo", "2", "-2"; 4); // only 'c' // Errors must_fail!(c, "BITCOUNT"; "wrong number of arguments"); must_fail!(c, "BITCOUNT", "foo", "noint", "12"; "not an integer"); must_fail!(c, "BITCOUNT", "foo", "12", "noint"; "not an integer"); } #[tokio::test] async fn test_bitop() { let (_m, mut c) = helpers::start().await; // AND must_ok!(c, "SET", "a", "a"); // 0x61 must_ok!(c, "SET", "b", "b"); // 0x62 must_int!(c, "BITOP", "AND", "bitand", "a", "b"; 1); let v: Vec = redis::cmd("GET") .arg("bitand") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x60]); // '`' // AND with different lengths must_ok!(c, "SET", "a2", "aa"); must_ok!(c, "SET", "b2", "bbbb"); must_int!(c, "BITOP", "AND", "bitand2", "a2", "b2"; 4); let v: Vec = redis::cmd("GET") .arg("bitand2") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x60, 0x60, 0x00, 0x00]); // OR must_int!(c, "BITOP", "OR", "bitor", "a2", "b2"; 4); let v: Vec = redis::cmd("GET") .arg("bitor") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x63, 0x63, 0x62, 0x62]); // "ccbb" // XOR must_int!(c, "BITOP", "XOR", "bitxor", "a2", "b2"; 4); let v: Vec = redis::cmd("GET") .arg("bitxor") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x03, 0x03, 0x62, 0x62]); // NOT must_int!(c, "BITOP", "NOT", "bitnot", "a"; 1); let v: Vec = redis::cmd("GET") .arg("bitnot") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec![0x9e]); // Single arg copy must_int!(c, "BITOP", "AND", "copy", "a"; 1); let v: String = redis::cmd("GET") .arg("copy") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "a"); // Errors must_fail!(c, "BITOP"; "wrong number of arguments"); must_fail!(c, "BITOP", "AND"; "wrong number of arguments"); must_fail!(c, "BITOP", "WHAT", "dest", "key"; "syntax error"); must_fail!(c, "BITOP", "NOT", "foo", "bar", "baz"; "BITOP NOT"); } #[tokio::test] async fn test_bitpos() { let (_m, mut c) = helpers::start().await; // \xff\xf0\x00 = all 1s, 4 more 1s, all 0s let _: () = redis::cmd("SET") .arg("findme") .arg(b"\xff\xf0\x00" as &[u8]) .query_async(&mut c) .await .unwrap(); must_int!(c, "BITPOS", "findme", "0"; 12); must_int!(c, "BITPOS", "findme", "1"; 0); must_int!(c, "BITPOS", "findme", "1", "1"; 8); must_int!(c, "BITPOS", "findme", "0", "1"; 12); // Only zeros let _: () = redis::cmd("SET") .arg("zero") .arg(b"\x00\x00" as &[u8]) .query_async(&mut c) .await .unwrap(); must_int!(c, "BITPOS", "zero", "1"; -1); must_int!(c, "BITPOS", "zero", "0"; 0); // Only ones let _: () = redis::cmd("SET") .arg("one") .arg(b"\xff\xff" as &[u8]) .query_async(&mut c) .await .unwrap(); must_int!(c, "BITPOS", "one", "1"; 0); must_int!(c, "BITPOS", "one", "1", "1"; 8); must_int!(c, "BITPOS", "one", "0"; 16); // special: past the end // Non-existing must_int!(c, "BITPOS", "nosuch", "1"; -1); must_int!(c, "BITPOS", "nosuch", "0"; 0); // Errors must_fail!(c, "BITPOS"; "wrong number of arguments"); must_fail!(c, "BITPOS", "foo"; "wrong number of arguments"); must_fail!(c, "BITPOS", "foo", "noint"; "not an integer"); } ================================================ FILE: miniredis/tests/cmd_client.rs ================================================ mod helpers; #[tokio::test] async fn test_client_setname_getname() { let (_m, mut c) = helpers::start().await; // Set the client name must_ok!(c, "CLIENT", "SETNAME", "miniredis-tests"); // Get the client name must_str!(c, "CLIENT", "GETNAME"; "miniredis-tests"); } #[tokio::test] async fn test_client_getname_without_setname() { let (_m, mut c) = helpers::start().await; // Get the client name without setting it first → nil must_nil!(c, "CLIENT", "GETNAME"); } #[tokio::test] async fn test_client_setname_empty() { let (_m, mut c) = helpers::start().await; // Set then clear the client name must_ok!(c, "CLIENT", "SETNAME", "test"); must_str!(c, "CLIENT", "GETNAME"; "test"); must_ok!(c, "CLIENT", "SETNAME", ""); must_nil!(c, "CLIENT", "GETNAME"); } #[tokio::test] async fn test_client_errors() { let (_m, mut c) = helpers::start().await; must_fail!(c, "CLIENT"; "wrong number of arguments"); must_fail!(c, "CLIENT", "NOSUCHSUB"; "unknown subcommand"); } ================================================ FILE: miniredis/tests/cmd_cluster.rs ================================================ mod helpers; #[tokio::test] async fn test_cluster_slots() { let (_m, mut c) = helpers::start().await; let v: redis::Value = redis::cmd("CLUSTER") .arg("SLOTS") .query_async(&mut c) .await .unwrap(); // Should return a nested array with slot range 0-16383 match v { redis::Value::Array(slots) => { assert_eq!(slots.len(), 1); match &slots[0] { redis::Value::Array(range) => { assert!(range.len() >= 3); // start slot = 0 assert_eq!(range[0], redis::Value::Int(0)); // end slot = 16383 assert_eq!(range[1], redis::Value::Int(16383)); } _ => panic!("expected array for slot range, got {:?}", slots[0]), } } _ => panic!("expected array from CLUSTER SLOTS, got {:?}", v), } } #[tokio::test] async fn test_cluster_nodes() { let (_m, mut c) = helpers::start().await; let v: String = redis::cmd("CLUSTER") .arg("NODES") .query_async(&mut c) .await .unwrap(); assert!(v.contains("myself,master")); assert!(v.contains("connected 0-16383")); } #[tokio::test] async fn test_cluster_keyslot() { let (_m, mut c) = helpers::start().await; let v: i64 = redis::cmd("CLUSTER") .arg("KEYSLOT") .arg("{test_key}") .query_async(&mut c) .await .unwrap(); assert_eq!(v, 163); } #[tokio::test] async fn test_cluster_shards() { let (_m, mut c) = helpers::start().await; let v: redis::Value = redis::cmd("CLUSTER") .arg("SHARDS") .query_async(&mut c) .await .unwrap(); // Should return a nested array with shard info match v { redis::Value::Array(shards) => { assert_eq!(shards.len(), 1); } _ => panic!("expected array from CLUSTER SHARDS, got {:?}", v), } } #[tokio::test] async fn test_cluster_errors() { let (_m, mut c) = helpers::start().await; must_fail!(c, "CLUSTER"; "wrong number of arguments"); must_fail!(c, "CLUSTER", "NOSUCHSUB"; "unknown subcommand"); } ================================================ FILE: miniredis/tests/cmd_connection.rs ================================================ // Ported from ../miniredis/cmd_connection_test.go mod helpers; #[tokio::test] async fn test_ping() { let (_m, mut c) = helpers::start().await; // No args → PONG must_str!(c, "PING"; "PONG"); // With arg → echo must_str!(c, "PING", "hi"; "hi"); // Too many args → error must_fail!(c, "PING", "foo", "bar"; "wrong number of arguments"); } #[tokio::test] async fn test_echo() { let (_m, mut c) = helpers::start().await; must_str!(c, "ECHO", "hello\nworld"; "hello\nworld"); // Wrong number of args must_fail!(c, "ECHO"; "wrong number of arguments"); } #[tokio::test] async fn test_select() { let (m, mut c) = helpers::start().await; // Set in db 0 must_ok!(c, "SET", "foo", "bar"); // Switch to db 5, set different value must_ok!(c, "SELECT", "5"); must_ok!(c, "SET", "foo", "baz"); // Direct API: db 0 should have "bar" assert_eq!(m.get("foo"), Some("bar".to_owned())); // SELECT out of range must_fail!(c, "SELECT", "16"; "DB index is out of range"); must_fail!(c, "SELECT", "-1"; "DB index is out of range"); must_fail!(c, "SELECT", "notanumber"; "not an integer"); // Wrong number of args must_fail!(c, "SELECT"; "wrong number of arguments"); } #[tokio::test] async fn test_dbsize() { let (_m, mut c) = helpers::start().await; must_int!(c, "DBSIZE"; 0); must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "DBSIZE"; 2); } #[tokio::test] async fn test_flushdb() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "DBSIZE"; 2); must_ok!(c, "FLUSHDB"); must_int!(c, "DBSIZE"; 0); } ================================================ FILE: miniredis/tests/cmd_generic.rs ================================================ // Ported from ../miniredis/cmd_generic_test.go mod helpers; use std::time::Duration; #[tokio::test] async fn test_ttl_expire() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); // No TTL by default must_int!(c, "TTL", "foo"; -1); must_int!(c, "PTTL", "foo"; -1); assert!(m.ttl("foo").is_none()); // Set TTL with EXPIRE must_int!(c, "EXPIRE", "foo", "100"; 1); let ttl = m.ttl("foo"); assert!(ttl.is_some()); assert!(ttl.unwrap() <= Duration::from_secs(100)); // TTL command let ttl_val: i64 = redis::cmd("TTL") .arg("foo") .query_async(&mut c) .await .unwrap(); assert!(ttl_val > 0 && ttl_val <= 100); // PTTL command let pttl_val: i64 = redis::cmd("PTTL") .arg("foo") .query_async(&mut c) .await .unwrap(); assert!(pttl_val > 0 && pttl_val <= 100_000); // PERSIST removes TTL must_int!(c, "PERSIST", "foo"; 1); must_int!(c, "TTL", "foo"; -1); assert!(m.ttl("foo").is_none()); // PERSIST on key without TTL must_int!(c, "PERSIST", "foo"; 0); // EXPIRE on non-existing key must_int!(c, "EXPIRE", "nosuch", "100"; 0); // PERSIST on non-existing key must_int!(c, "PERSIST", "nosuch"; 0); // TTL/PTTL on non-existing key must_int!(c, "TTL", "nosuch"; -2); must_int!(c, "PTTL", "nosuch"; -2); // Errors must_fail!(c, "EXPIRE"; "wrong number of arguments"); must_fail!(c, "EXPIRE", "foo"; "wrong number of arguments"); must_fail!(c, "TTL"; "wrong number of arguments"); must_fail!(c, "PTTL"; "wrong number of arguments"); must_fail!(c, "PERSIST"; "wrong number of arguments"); } #[tokio::test] async fn test_expireat() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); // EXPIREAT with future timestamp let future_ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() + 100; must_int!(c, "EXPIREAT", "foo", &future_ts.to_string(); 1); let ttl = m.ttl("foo"); assert!(ttl.is_some()); // Non-existing key must_int!(c, "EXPIREAT", "nosuch", &future_ts.to_string(); 0); } #[tokio::test] async fn test_pexpire() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); must_int!(c, "PEXPIRE", "foo", "50000"; 1); let ttl = m.ttl("foo"); assert!(ttl.is_some()); assert!(ttl.unwrap() <= Duration::from_secs(50)); // Non-existing key must_int!(c, "PEXPIRE", "nosuch", "50000"; 0); } #[tokio::test] async fn test_pexpireat() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); let future_ts_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() + 100_000; must_int!(c, "PEXPIREAT", "foo", &future_ts_ms.to_string(); 1); let ttl = m.ttl("foo"); assert!(ttl.is_some()); } #[tokio::test] async fn test_type() { let (_m, mut c) = helpers::start().await; // String must_ok!(c, "SET", "str", "val"); let t: String = redis::cmd("TYPE") .arg("str") .query_async(&mut c) .await .unwrap(); assert_eq!(t, "string"); // Hash must_int!(c, "HSET", "h", "f", "v"; 1); let t: String = redis::cmd("TYPE") .arg("h") .query_async(&mut c) .await .unwrap(); assert_eq!(t, "hash"); // List must_int!(c, "RPUSH", "l", "a"; 1); let t: String = redis::cmd("TYPE") .arg("l") .query_async(&mut c) .await .unwrap(); assert_eq!(t, "list"); // Set must_int!(c, "SADD", "s", "a"; 1); let t: String = redis::cmd("TYPE") .arg("s") .query_async(&mut c) .await .unwrap(); assert_eq!(t, "set"); // Non-existing let t: String = redis::cmd("TYPE") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert_eq!(t, "none"); // Errors must_fail!(c, "TYPE"; "wrong number of arguments"); } #[tokio::test] async fn test_rename() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "val"); must_ok!(c, "RENAME", "a", "b"); must_nil!(c, "GET", "a"); must_str!(c, "GET", "b"; "val"); // Overwrite existing must_ok!(c, "SET", "c", "other"); must_ok!(c, "RENAME", "b", "c"); must_str!(c, "GET", "c"; "val"); // Non-existing source must_fail!(c, "RENAME", "nosuch", "dst"; "no such key"); // Same key must_ok!(c, "SET", "x", "v"); must_ok!(c, "RENAME", "x", "x"); must_str!(c, "GET", "x"; "v"); // Errors must_fail!(c, "RENAME"; "wrong number of arguments"); must_fail!(c, "RENAME", "a"; "wrong number of arguments"); } #[tokio::test] async fn test_renamenx() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "val"); must_int!(c, "RENAMENX", "a", "b"; 1); must_nil!(c, "GET", "a"); must_str!(c, "GET", "b"; "val"); // Target exists must_ok!(c, "SET", "c", "other"); must_int!(c, "RENAMENX", "b", "c"; 0); must_str!(c, "GET", "b"; "val"); must_str!(c, "GET", "c"; "other"); // Non-existing source must_fail!(c, "RENAMENX", "nosuch", "dst"; "no such key"); // Errors must_fail!(c, "RENAMENX"; "wrong number of arguments"); } #[tokio::test] async fn test_keys() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "alpha", "1"); must_ok!(c, "SET", "beta", "2"); must_ok!(c, "SET", "gamma", "3"); must_ok!(c, "SET", "abc", "4"); // Match all let mut result: Vec = redis::cmd("KEYS") .arg("*") .query_async(&mut c) .await .unwrap(); result.sort(); assert_eq!(result, vec!["abc", "alpha", "beta", "gamma"]); // Pattern match let mut result: Vec = redis::cmd("KEYS") .arg("a*") .query_async(&mut c) .await .unwrap(); result.sort(); assert_eq!(result, vec!["abc", "alpha"]); // Single character wildcard let result: Vec = redis::cmd("KEYS") .arg("ab?") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["abc"]); // No match let result: Vec = redis::cmd("KEYS") .arg("nosuch*") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); // Errors must_fail!(c, "KEYS"; "wrong number of arguments"); } #[tokio::test] async fn test_scan() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "key1", "1"); must_ok!(c, "SET", "key2", "2"); must_ok!(c, "SET", "other", "3"); // Basic scan let (cursor, mut keys): (String, Vec) = redis::cmd("SCAN") .arg("0") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); keys.sort(); assert_eq!(keys, vec!["key1", "key2", "other"]); // MATCH pattern let (cursor, mut keys): (String, Vec) = redis::cmd("SCAN") .arg("0") .arg("MATCH") .arg("key*") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); keys.sort(); assert_eq!(keys, vec!["key1", "key2"]); // TYPE filter must_int!(c, "SADD", "myset", "a"; 1); let (cursor, keys): (String, Vec) = redis::cmd("SCAN") .arg("0") .arg("TYPE") .arg("set") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(keys, vec!["myset"]); } #[tokio::test] async fn test_touch() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "TOUCH", "a"; 1); must_int!(c, "TOUCH", "a", "b"; 2); must_int!(c, "TOUCH", "a", "b", "nosuch"; 2); must_int!(c, "TOUCH", "nosuch"; 0); // Errors must_fail!(c, "TOUCH"; "wrong number of arguments"); } #[tokio::test] async fn test_unlink() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "UNLINK", "a", "b", "nosuch"; 2); must_nil!(c, "GET", "a"); must_nil!(c, "GET", "b"); } #[tokio::test] async fn test_randomkey() { let (_m, mut c) = helpers::start().await; // Empty database must_nil!(c, "RANDOMKEY"); must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); // Should return one of the keys let key: String = redis::cmd("RANDOMKEY").query_async(&mut c).await.unwrap(); assert!(key == "a" || key == "b"); } #[tokio::test] async fn test_wait() { let (_m, mut c) = helpers::start().await; // WAIT always returns 0 (standalone mode) must_int!(c, "WAIT", "0", "0"; 0); must_int!(c, "WAIT", "1", "100"; 0); // Errors must_fail!(c, "WAIT"; "wrong number of arguments"); } #[tokio::test] async fn test_object() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); let enc: String = redis::cmd("OBJECT") .arg("ENCODING") .arg("foo") .query_async(&mut c) .await .unwrap(); assert_eq!(enc, "raw"); must_int!(c, "OBJECT", "IDLETIME", "foo"; 0); must_int!(c, "OBJECT", "REFCOUNT", "foo"; 1); must_int!(c, "OBJECT", "FREQ", "foo"; 0); // Errors must_fail!(c, "OBJECT"; "wrong number of arguments"); } #[tokio::test] async fn test_expire_flags() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); // NX: only if no TTL must_int!(c, "EXPIRE", "foo", "100", "NX"; 1); assert!(m.ttl("foo").is_some()); // NX again should fail (TTL already set) must_int!(c, "EXPIRE", "foo", "200", "NX"; 0); // XX: only if TTL exists must_int!(c, "EXPIRE", "foo", "200", "XX"; 1); // XX on key without TTL must_ok!(c, "SET", "bar", "baz"); must_int!(c, "EXPIRE", "bar", "100", "XX"; 0); // GT: only if new TTL > old must_ok!(c, "SET", "gt", "val"); must_int!(c, "EXPIRE", "gt", "100"; 1); must_int!(c, "EXPIRE", "gt", "200", "GT"; 1); // 200 > 100 must_int!(c, "EXPIRE", "gt", "50", "GT"; 0); // 50 < 200 // LT: only if new TTL < old must_ok!(c, "SET", "lt", "val"); must_int!(c, "EXPIRE", "lt", "200"; 1); must_int!(c, "EXPIRE", "lt", "100", "LT"; 1); // 100 < 200 must_int!(c, "EXPIRE", "lt", "300", "LT"; 0); // 300 > 100 } #[tokio::test] async fn test_copy() { let (_m, mut c) = helpers::start().await; // Basic must_ok!(c, "SET", "key1", "value"); must_int!(c, "COPY", "key1", "key2"; 1); must_str!(c, "GET", "key2"; "value"); // Nonexistent source must_int!(c, "COPY", "nosuch", "to"; 0); // Existing destination (no overwrite by default) must_ok!(c, "SET", "existingkey", "value"); must_ok!(c, "SET", "newkey", "newvalue"); must_int!(c, "COPY", "newkey", "existingkey"; 0); must_str!(c, "GET", "existingkey"; "value"); // REPLACE must_ok!(c, "SET", "rkey1", "value"); must_ok!(c, "SET", "rkey2", "another"); must_int!(c, "COPY", "rkey1", "rkey2", "REPLACE"; 1); must_str!(c, "GET", "rkey2"; "value"); // List copy (deep copy) must_int!(c, "LPUSH", "l1", "original"; 1); must_int!(c, "COPY", "l1", "l2"; 1); must_int!(c, "LPUSH", "l1", "new"; 2); must_int!(c, "LLEN", "l2"; 1); // Errors must_fail!(c, "COPY"; "wrong number of arguments"); must_fail!(c, "COPY", "foo"; "wrong number of arguments"); } #[tokio::test] async fn test_move_cmd() { let (_m, mut c) = helpers::start().await; // Basic must_ok!(c, "SET", "foo", "bar!"); must_int!(c, "MOVE", "foo", "1"; 1); must_nil!(c, "GET", "foo"); // Gone from DB 0 // Source doesn't exist must_int!(c, "MOVE", "nosuch", "1"; 0); // Errors must_fail!(c, "MOVE"; "wrong number of arguments"); must_fail!(c, "MOVE", "foo"; "wrong number of arguments"); must_fail!(c, "MOVE", "foo", "noint"; "DB index is out of range"); } #[tokio::test] async fn test_expiretime() { let (_m, mut c) = helpers::start().await; // Nonexistent key must_int!(c, "EXPIRETIME", "nosuch"; -2); // No expire must_ok!(c, "SET", "noexpire", ""); must_int!(c, "EXPIRETIME", "noexpire"; -1); // With expire must_ok!(c, "SET", "foo", ""); must_int!(c, "EXPIREAT", "foo", "10413792000"; 1); must_int!(c, "EXPIRETIME", "foo"; 10413792000); } #[tokio::test] async fn test_pexpiretime() { let (_m, mut c) = helpers::start().await; // Nonexistent key must_int!(c, "PEXPIRETIME", "nosuch"; -2); // No expire must_ok!(c, "SET", "noexpire", ""); must_int!(c, "PEXPIRETIME", "noexpire"; -1); // With expire must_ok!(c, "SET", "foo", ""); must_int!(c, "PEXPIREAT", "foo", "10413792000123"; 1); must_int!(c, "PEXPIRETIME", "foo"; 10413792000123); } #[tokio::test] async fn test_dump() { let (_m, mut c) = helpers::start().await; // Missing key must_nil!(c, "DUMP", "missing-key"); // Existing key (stub returns raw string) must_ok!(c, "SET", "existing-key", "value"); must_str!(c, "DUMP", "existing-key"; "value"); // Non-string type returns nil (stub behavior) let _: i64 = redis::cmd("HSET") .arg("set-key") .arg("a") .arg("b") .query_async(&mut c) .await .unwrap(); must_nil!(c, "DUMP", "set-key"); // Errors must_fail!(c, "DUMP"; "wrong number of arguments"); } #[tokio::test] async fn test_restore() { let (_m, mut c) = helpers::start().await; // New key no TTL must_ok!(c, "RESTORE", "key-a", "0", "value-a"); must_str!(c, "GET", "key-a"; "value-a"); // Busy key must_ok!(c, "SET", "existing", "value"); must_fail!(c, "RESTORE", "existing", "0", "other"; "BUSYKEY"); // Overwrite with REPLACE must_ok!(c, "RESTORE", "existing", "0", "new-value", "REPLACE"); must_str!(c, "GET", "existing"; "new-value"); // Errors must_fail!(c, "RESTORE"; "wrong number of arguments"); must_fail!(c, "RESTORE", "key"; "wrong number of arguments"); must_fail!(c, "RESTORE", "key", "argh", "val"; "not an integer"); } ================================================ FILE: miniredis/tests/cmd_geo.rs ================================================ mod helpers; use helpers::*; // ── GEOADD ─────────────────────────────────────────────────────────── #[tokio::test] async fn test_geoadd() { let (_m, mut c) = start().await; // Add two locations must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // Re-add same member → 0 (updated, not new) must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 0); } #[tokio::test] async fn test_geoadd_errors() { let (_m, mut c) = start().await; // Wrong number of args must_fail!(c, "GEOADD"; "wrong number of arguments"); must_fail!(c, "GEOADD", "key"; "wrong number of arguments"); must_fail!(c, "GEOADD", "key", "1", "2"; "wrong number of arguments"); // Invalid longitude (out of range) must_fail!(c, "GEOADD", "broken", "-190.0", "10.0", "hi"; "invalid longitude,latitude pair"); must_fail!(c, "GEOADD", "broken", "190.0", "10.0", "hi"; "invalid longitude,latitude pair"); // Invalid latitude (out of range) must_fail!(c, "GEOADD", "broken", "10.0", "-86.0", "hi"; "invalid longitude,latitude pair"); must_fail!(c, "GEOADD", "broken", "10.0", "86.0", "hi"; "invalid longitude,latitude pair"); // Not a float must_fail!(c, "GEOADD", "broken", "notafloat", "10.0", "hi"; "not a valid float"); must_fail!(c, "GEOADD", "broken", "10.0", "notafloat", "hi"; "not a valid float"); } // ── GEOPOS ─────────────────────────────────────────────────────────── #[tokio::test] async fn test_geopos() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); // Get position — returns array of [lng, lat] let v: redis::Value = redis::cmd("GEOPOS") .arg("Sicily") .arg("Palermo") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 1); // First element should be an array of [lng, lat] match &items[0] { redis::Value::Array(coords) => { assert_eq!(coords.len(), 2); } _ => panic!("expected array for coords, got {:?}", items[0]), } } _ => panic!("expected array from GEOPOS, got {:?}", v), } // Non-existent member → nil let v: redis::Value = redis::cmd("GEOPOS") .arg("Sicily") .arg("Corleone") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 1); assert_eq!(items[0], redis::Value::Nil); } _ => panic!("expected array from GEOPOS, got {:?}", v), } } #[tokio::test] async fn test_geopos_errors() { let (_m, mut c) = start().await; must_fail!(c, "GEOPOS"; "wrong number of arguments"); must_ok!(c, "SET", "foo", "bar"); must_fail!(c, "GEOPOS", "foo"; "WRONGTYPE"); } // ── GEODIST ────────────────────────────────────────────────────────── #[tokio::test] async fn test_geodist() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // Default unit = meters let d: String = redis::cmd("GEODIST") .arg("Sicily") .arg("Palermo") .arg("Catania") .query_async(&mut c) .await .unwrap(); assert_eq!(d, "166274.1514"); // In km let d: String = redis::cmd("GEODIST") .arg("Sicily") .arg("Palermo") .arg("Catania") .arg("km") .query_async(&mut c) .await .unwrap(); assert_eq!(d, "166.2742"); } #[tokio::test] async fn test_geodist_nil() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); // Non-existent key must_nil!(c, "GEODIST", "nosuch", "a", "b"); // Non-existent member must_nil!(c, "GEODIST", "Sicily", "Palermo", "nosuch"); must_nil!(c, "GEODIST", "Sicily", "nosuch", "Palermo"); } #[tokio::test] async fn test_geodist_errors() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); must_fail!(c, "GEODIST"; "wrong number of arguments"); must_fail!(c, "GEODIST", "Sicily"; "wrong number of arguments"); must_fail!(c, "GEODIST", "Sicily", "Palermo"; "wrong number of arguments"); // Unsupported unit must_fail!(c, "GEODIST", "Sicily", "Palermo", "Catania", "miles"; "unsupported unit"); // Too many args must_fail!(c, "GEODIST", "Sicily", "Palermo", "Catania", "m", "extra"; "syntax error"); // Wrong type must_ok!(c, "SET", "foo", "bar"); must_fail!(c, "GEODIST", "foo", "Palermo", "Catania"; "WRONGTYPE"); } // ── GEORADIUS ──────────────────────────────────────────────────────── #[tokio::test] async fn test_georadius_basic() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // Basic radius query — returns member names let v: Vec = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .query_async(&mut c) .await .unwrap(); assert_eq!(v.len(), 2); assert!(v.contains(&"Palermo".to_string())); assert!(v.contains(&"Catania".to_string())); // Too small radius — no results let v: Vec = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("1") .arg("km") .query_async(&mut c) .await .unwrap(); assert_eq!(v.len(), 0); } #[tokio::test] async fn test_georadius_asc_desc() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // ASC let v: Vec = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .arg("ASC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Catania", "Palermo"]); // DESC let v: Vec = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .arg("DESC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Palermo", "Catania"]); } #[tokio::test] async fn test_georadius_count() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // COUNT 1 + ASC let v: Vec = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .arg("ASC") .arg("COUNT") .arg("1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Catania"]); // COUNT errors must_fail!(c, "GEORADIUS", "Sicily", "15", "37", "200", "km", "COUNT"; "syntax error"); must_fail!(c, "GEORADIUS", "Sicily", "15", "37", "200", "km", "COUNT", "notanumber"; "not an integer"); must_fail!(c, "GEORADIUS", "Sicily", "15", "37", "200", "km", "COUNT", "-12"; "COUNT must be > 0"); } #[tokio::test] async fn test_georadius_withdist() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // WITHDIST in km let v: redis::Value = redis::cmd("GEORADIUS") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .arg("WITHDIST") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 2); } _ => panic!("expected array from GEORADIUS WITHDIST, got {:?}", v), } } #[tokio::test] async fn test_georadius_errors() { let (_m, mut c) = start().await; // Invalid unit must_fail!(c, "GEORADIUS", "Sicily", "15", "37", "200", "mm"; "wrong number of arguments"); // Invalid float params must_fail!(c, "GEORADIUS", "Sicily", "abc", "def", "ghi", "m"; "wrong number of arguments"); } #[tokio::test] async fn test_georadius_ro() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // GEORADIUS_RO works let v: Vec = redis::cmd("GEORADIUS_RO") .arg("Sicily") .arg("15") .arg("37") .arg("200") .arg("km") .arg("ASC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Catania", "Palermo"]); // STORE not allowed in RO mode must_fail!(c, "GEORADIUS_RO", "Sicily", "15", "37", "200", "km", "STORE", "foo"; "syntax error"); must_fail!(c, "GEORADIUS_RO", "Sicily", "15", "37", "200", "km", "STOREDIST", "foo"; "syntax error"); } // ── GEORADIUSBYMEMBER ──────────────────────────────────────────────── #[tokio::test] async fn test_georadiusbymember_basic() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // Basic query let v: Vec = redis::cmd("GEORADIUSBYMEMBER") .arg("Sicily") .arg("Palermo") .arg("200") .arg("km") .query_async(&mut c) .await .unwrap(); assert_eq!(v.len(), 2); // ASC let v: Vec = redis::cmd("GEORADIUSBYMEMBER") .arg("Sicily") .arg("Palermo") .arg("200") .arg("km") .arg("ASC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Palermo", "Catania"]); // DESC let v: Vec = redis::cmd("GEORADIUSBYMEMBER") .arg("Sicily") .arg("Palermo") .arg("200") .arg("km") .arg("DESC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Catania", "Palermo"]); } #[tokio::test] async fn test_georadiusbymember_count() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); let v: Vec = redis::cmd("GEORADIUSBYMEMBER") .arg("Sicily") .arg("Palermo") .arg("200") .arg("km") .arg("ASC") .arg("COUNT") .arg("1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Palermo"]); } #[tokio::test] async fn test_georadiusbymember_missing() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); // Non-existent key → nil must_nil!(c, "GEORADIUSBYMEMBER", "Capri", "Palermo", "200", "km"); // Missing member → error must_fail!(c, "GEORADIUSBYMEMBER", "Sicily", "nosuch", "200", "km"; "could not decode requested zset member"); // Invalid unit must_fail!(c, "GEORADIUSBYMEMBER", "Sicily", "Palermo", "200", "mm"; "wrong number of arguments"); } #[tokio::test] async fn test_georadiusbymember_ro() { let (_m, mut c) = start().await; must_int!(c, "GEOADD", "Sicily", "13.361389", "38.115556", "Palermo"; 1); must_int!(c, "GEOADD", "Sicily", "15.087269", "37.502669", "Catania"; 1); // GEORADIUSBYMEMBER_RO works let v: Vec = redis::cmd("GEORADIUSBYMEMBER_RO") .arg("Sicily") .arg("Palermo") .arg("200") .arg("km") .arg("ASC") .query_async(&mut c) .await .unwrap(); assert_eq!(v, vec!["Palermo", "Catania"]); // STORE not allowed must_fail!(c, "GEORADIUSBYMEMBER_RO", "Sicily", "Palermo", "200", "km", "STORE", "foo"; "syntax error"); must_fail!(c, "GEORADIUSBYMEMBER_RO", "Sicily", "Palermo", "200", "km", "STOREDIST", "foo"; "syntax error"); } ================================================ FILE: miniredis/tests/cmd_hash.rs ================================================ // Ported from ../miniredis/cmd_hash_test.go mod helpers; #[tokio::test] async fn test_hset_hget() { let (_m, mut c) = helpers::start().await; must_int!(c, "HSET", "h", "field1", "val1"; 1); must_str!(c, "HGET", "h", "field1"; "val1"); // Overwrite existing field must_int!(c, "HSET", "h", "field1", "val2"; 0); must_str!(c, "HGET", "h", "field1"; "val2"); // Multiple fields at once must_int!(c, "HSET", "h", "a", "1", "b", "2"; 2); must_str!(c, "HGET", "h", "a"; "1"); must_str!(c, "HGET", "h", "b"; "2"); // Non-existent field must_nil!(c, "HGET", "h", "nosuch"); // Non-existent key must_nil!(c, "HGET", "nosuch", "field"); // Errors must_fail!(c, "HSET"; "wrong number of arguments"); must_fail!(c, "HSET", "h"; "wrong number of arguments"); must_fail!(c, "HSET", "h", "f"; "wrong number of arguments"); must_fail!(c, "HGET"; "wrong number of arguments"); } #[tokio::test] async fn test_hsetnx() { let (_m, mut c) = helpers::start().await; must_int!(c, "HSETNX", "h", "field", "val"; 1); must_str!(c, "HGET", "h", "field"; "val"); // Already exists must_int!(c, "HSETNX", "h", "field", "other"; 0); must_str!(c, "HGET", "h", "field"; "val"); } #[tokio::test] async fn test_hmset_hmget() { let (_m, mut c) = helpers::start().await; must_ok!(c, "HMSET", "h", "a", "1", "b", "2", "c", "3"); must_strs!(c, "HMGET", "h", "a", "b", "c"; ["1", "2", "3"]); // Missing fields return nil let result: Vec> = redis::cmd("HMGET") .arg("h") .arg("a") .arg("nosuch") .arg("c") .query_async(&mut c) .await .unwrap(); assert_eq!( result, vec![Some("1".to_string()), None, Some("3".to_string())] ); } #[tokio::test] async fn test_hdel() { let (_m, mut c) = helpers::start().await; must_ok!(c, "HMSET", "h", "a", "1", "b", "2", "c", "3"); must_int!(c, "HDEL", "h", "a", "b"; 2); must_nil!(c, "HGET", "h", "a"); must_str!(c, "HGET", "h", "c"; "3"); // Non-existent field must_int!(c, "HDEL", "h", "nosuch"; 0); // Non-existent key must_int!(c, "HDEL", "nosuch", "field"; 0); } #[tokio::test] async fn test_hexists() { let (_m, mut c) = helpers::start().await; must_int!(c, "HSET", "h", "field", "val"; 1); must_int!(c, "HEXISTS", "h", "field"; 1); must_int!(c, "HEXISTS", "h", "nosuch"; 0); must_int!(c, "HEXISTS", "nosuch", "field"; 0); } #[tokio::test] async fn test_hgetall() { let (_m, mut c) = helpers::start().await; must_ok!(c, "HMSET", "h", "a", "1", "b", "2"); // HGETALL returns field-value pairs let result: Vec = redis::cmd("HGETALL") .arg("h") .query_async(&mut c) .await .unwrap(); // Should be sorted by field name: a, 1, b, 2 assert_eq!(result, vec!["a", "1", "b", "2"]); // Empty key let result: Vec = redis::cmd("HGETALL") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_hkeys_hvals() { let (_m, mut c) = helpers::start().await; must_ok!(c, "HMSET", "h", "b", "2", "a", "1", "c", "3"); must_strs!(c, "HKEYS", "h"; ["a", "b", "c"]); must_strs!(c, "HVALS", "h"; ["1", "2", "3"]); } #[tokio::test] async fn test_hlen() { let (_m, mut c) = helpers::start().await; must_int!(c, "HLEN", "h"; 0); must_ok!(c, "HMSET", "h", "a", "1", "b", "2"); must_int!(c, "HLEN", "h"; 2); } #[tokio::test] async fn test_hincrby() { let (_m, mut c) = helpers::start().await; must_int!(c, "HINCRBY", "h", "field", "5"; 5); must_int!(c, "HINCRBY", "h", "field", "3"; 8); must_int!(c, "HINCRBY", "h", "field", "-2"; 6); // Non-integer value must_ok!(c, "HMSET", "h", "str", "notanumber"); must_fail!(c, "HINCRBY", "h", "str", "1"; "not an integer"); } #[tokio::test] async fn test_hincrbyfloat() { let (_m, mut c) = helpers::start().await; must_str!(c, "HINCRBYFLOAT", "h", "field", "1.5"; "1.5"); must_str!(c, "HINCRBYFLOAT", "h", "field", "2.5"; "4"); must_str!(c, "HINCRBYFLOAT", "h", "field", "-1"; "3"); } #[tokio::test] async fn test_hstrlen() { let (_m, mut c) = helpers::start().await; must_int!(c, "HSET", "h", "field", "hello"; 1); must_int!(c, "HSTRLEN", "h", "field"; 5); must_int!(c, "HSTRLEN", "h", "nosuch"; 0); must_int!(c, "HSTRLEN", "nosuch", "field"; 0); } #[tokio::test] async fn test_hash_wrongtype() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "str", "val"); must_fail!(c, "HGET", "str", "field"; "WRONGTYPE"); must_fail!(c, "HSET", "str", "field", "val"; "WRONGTYPE"); } #[tokio::test] async fn test_hscan() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("HSET") .arg("h") .arg("field1") .arg("value1") .arg("field2") .arg("value2") .query_async(&mut c) .await .unwrap(); // Basic scan let (cursor, vals): (String, Vec) = redis::cmd("HSCAN") .arg("h") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); // Returns [field1, value1, field2, value2] assert_eq!(vals.len(), 4); assert!(vals.contains(&"field1".to_string())); assert!(vals.contains(&"value1".to_string())); // MATCH let _: i64 = redis::cmd("HSET") .arg("h2") .arg("aap") .arg("a") .arg("noot") .arg("b") .arg("mies") .arg("m") .query_async(&mut c) .await .unwrap(); let (cursor, vals): (String, Vec) = redis::cmd("HSCAN") .arg("h2") .arg(0) .arg("MATCH") .arg("mi*") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(vals, vec!["mies", "m"]); // Errors must_fail!(c, "HSCAN"; "wrong number of arguments"); must_fail!(c, "HSCAN", "h"; "wrong number of arguments"); must_fail!(c, "HSCAN", "h", "noint"; "invalid cursor"); must_fail!(c, "HSCAN", "h", "0", "MATCH"; "syntax error"); must_fail!(c, "HSCAN", "h", "0", "COUNT"; "syntax error"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "HSCAN", "str", "0"; "WRONGTYPE"); } #[tokio::test] async fn test_hrandfield() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("HSET") .arg("h") .arg("f1") .arg("v1") .arg("f2") .arg("v2") .arg("f3") .arg("v3") .arg("f4") .arg("v4") .query_async(&mut c) .await .unwrap(); // Single field (no count) let v: String = redis::cmd("HRANDFIELD") .arg("h") .query_async(&mut c) .await .unwrap(); assert!(v.starts_with("f")); // Positive count let vals: Vec = redis::cmd("HRANDFIELD") .arg("h") .arg(2) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 2); // Count larger than hash let vals: Vec = redis::cmd("HRANDFIELD") .arg("h") .arg(10) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 4); // Negative count (allows duplicates) let vals: Vec = redis::cmd("HRANDFIELD") .arg("h") .arg(-6) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 6); // WITHVALUES let vals: Vec = redis::cmd("HRANDFIELD") .arg("h") .arg(1) .arg("WITHVALUES") .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 2); // [field, value] // Nonexistent key must_nil!(c, "HRANDFIELD", "nosuch"); // Errors must_fail!(c, "HRANDFIELD"; "wrong number of arguments"); must_fail!(c, "HRANDFIELD", "h", "noint"; "not an integer"); } #[tokio::test] async fn test_hexpire() { let (m, mut c) = helpers::start().await; // Basic expiration let _: i64 = redis::cmd("HSET") .arg("myhash") .arg("field1") .arg("value1") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("HEXPIRE") .arg("myhash") .arg(10) .arg("FIELDS") .arg(1) .arg("field1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1]); // Multiple fields let _: i64 = redis::cmd("HSET") .arg("myhash2") .arg("field1") .arg("value1") .arg("field2") .arg("value2") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("HEXPIRE") .arg("myhash2") .arg(20) .arg("FIELDS") .arg(2) .arg("field1") .arg("field2") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1, 1]); // Nonexistent field let result: Vec = redis::cmd("HEXPIRE") .arg("myhash") .arg(10) .arg("FIELDS") .arg(1) .arg("nonexistent") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![-2]); // Nonexistent key let result: Vec = redis::cmd("HEXPIRE") .arg("nokey") .arg(10) .arg("FIELDS") .arg(1) .arg("field1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![-2]); // NX option let _: i64 = redis::cmd("HSET") .arg("nxhash") .arg("f1") .arg("v1") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("HEXPIRE") .arg("nxhash") .arg(10) .arg("NX") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1]); // NX again → 0 (already has TTL) let result: Vec = redis::cmd("HEXPIRE") .arg("nxhash") .arg(20) .arg("NX") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0]); // XX option: no TTL → 0 let _: i64 = redis::cmd("HSET") .arg("xxhash") .arg("f1") .arg("v1") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("HEXPIRE") .arg("xxhash") .arg(10) .arg("XX") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0]); // Set TTL first, then XX → 1 let _: Vec = redis::cmd("HEXPIRE") .arg("xxhash") .arg(10) .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("HEXPIRE") .arg("xxhash") .arg(20) .arg("XX") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1]); // GT option let _: i64 = redis::cmd("HSET") .arg("gthash") .arg("f1") .arg("v1") .query_async(&mut c) .await .unwrap(); let _: Vec = redis::cmd("HEXPIRE") .arg("gthash") .arg(10) .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); // GT with smaller → 0 let result: Vec = redis::cmd("HEXPIRE") .arg("gthash") .arg(5) .arg("GT") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0]); // GT with larger → 1 let result: Vec = redis::cmd("HEXPIRE") .arg("gthash") .arg(20) .arg("GT") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1]); // LT option let _: i64 = redis::cmd("HSET") .arg("lthash") .arg("f1") .arg("v1") .query_async(&mut c) .await .unwrap(); let _: Vec = redis::cmd("HEXPIRE") .arg("lthash") .arg(20) .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); // LT with larger → 0 let result: Vec = redis::cmd("HEXPIRE") .arg("lthash") .arg(30) .arg("LT") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0]); // LT with smaller → 1 let result: Vec = redis::cmd("HEXPIRE") .arg("lthash") .arg(10) .arg("LT") .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1]); // Field actually expires via fast_forward let _: i64 = redis::cmd("HSET") .arg("hash6") .arg("f1") .arg("v1") .arg("f2") .arg("v2") .query_async(&mut c) .await .unwrap(); let _: Vec = redis::cmd("HEXPIRE") .arg("hash6") .arg(1) .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); m.fast_forward(std::time::Duration::from_secs(2)); must_nil!(c, "HGET", "hash6", "f1"); must_str!(c, "HGET", "hash6", "f2"; "v2"); // All fields expired → key deleted let _: i64 = redis::cmd("HSET") .arg("hash7") .arg("f1") .arg("v1") .query_async(&mut c) .await .unwrap(); let _: Vec = redis::cmd("HEXPIRE") .arg("hash7") .arg(1) .arg("FIELDS") .arg(1) .arg("f1") .query_async(&mut c) .await .unwrap(); m.fast_forward(std::time::Duration::from_secs(2)); must_int!(c, "EXISTS", "hash7"; 0); // Errors must_fail!(c, "HEXPIRE", "myhash"; "wrong number of arguments"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "HEXPIRE", "str", "10", "FIELDS", "1", "f"; "WRONGTYPE"); must_fail!(c, "HEXPIRE", "myhash", "notanumber", "FIELDS", "1", "f"; "not an integer"); must_fail!(c, "HEXPIRE", "myhash", "10", "FIELDS", "0"; "wrong number of arguments"); must_fail!(c, "HEXPIRE", "myhash", "10", "FIELDS", "2", "f"; "numfields"); must_fail!(c, "HEXPIRE", "myhash", "10", "GT", "LT", "FIELDS", "1", "f"; "GT and LT"); must_fail!(c, "HEXPIRE", "myhash", "10", "NX", "XX", "FIELDS", "1", "f"; "NX and XX"); } ================================================ FILE: miniredis/tests/cmd_hll.rs ================================================ mod helpers; use helpers::*; // ── PFADD ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_pfadd_basic() { let (_m, mut c) = start().await; // Add 3 new elements → 1 (changed) must_int!(c, "PFADD", "h", "aap", "noot", "mies"; 1); // Add duplicate → 0 (not changed) must_int!(c, "PFADD", "h", "aap"; 0); // TYPE should be "hll" must_str!(c, "TYPE", "h"; "hll"); } #[tokio::test] async fn test_pfadd_errors() { let (_m, mut c) = start().await; // Wrong type must_ok!(c, "SET", "str", "value"); must_fail!(c, "PFADD", "str", "hi"; "not a valid HyperLogLog string value"); // Wrong number of arguments (no args at all) must_fail!(c, "PFADD"; "wrong number of arguments"); } // ── PFCOUNT ────────────────────────────────────────────────────────── #[tokio::test] async fn test_pfcount_basic() { let (_m, mut c) = start().await; // Add elements one at a time for i in 0..100 { must_int!(c, "PFADD", "h1", &format!("unique-{}", i); 1); } // Add one more must_int!(c, "PFADD", "h1", "specific-value"; 1); // Duplicate additions should return 0 for _ in 0..10 { must_int!(c, "PFADD", "h1", "specific-value"; 0); } // Count should be 101 must_int!(c, "PFCOUNT", "h1"; 101); } #[tokio::test] async fn test_pfcount_multiple_keys() { let (_m, mut c) = start().await; // Create two non-overlapping HLLs must_int!(c, "PFADD", "h1", "a", "b", "c"; 1); must_int!(c, "PFADD", "h2", "d", "e"; 1); // Single key counts must_int!(c, "PFCOUNT", "h1"; 3); must_int!(c, "PFCOUNT", "h2"; 2); // Multi-key count (union) must_int!(c, "PFCOUNT", "h1", "h2"; 5); // With a non-existent key must_int!(c, "PFCOUNT", "h1", "h2", "h3"; 5); // Non-existent key alone must_int!(c, "PFCOUNT", "h9"; 0); } #[tokio::test] async fn test_pfcount_errors() { let (_m, mut c) = start().await; must_ok!(c, "SET", "str", "value"); // Wrong number of arguments must_fail!(c, "PFCOUNT"; "wrong number of arguments"); // Wrong type must_fail!(c, "PFCOUNT", "str"; "not a valid HyperLogLog string value"); // Wrong type mixed with valid key must_int!(c, "PFADD", "h1", "a"; 1); must_fail!(c, "PFCOUNT", "h1", "str"; "not a valid HyperLogLog string value"); } // ── PFMERGE ────────────────────────────────────────────────────────── #[tokio::test] async fn test_pfmerge_basic() { let (_m, mut c) = start().await; // Create two non-overlapping HLLs for i in 0..100 { must_int!(c, "PFADD", "h1", &format!("item-{}", i); 1); } for i in 100..200 { must_int!(c, "PFADD", "h3", &format!("item-{}", i); 1); } // Merge non-intersecting must_ok!(c, "PFMERGE", "res1", "h1", "h3"); let count: i64 = redis::cmd("PFCOUNT") .arg("res1") .query_async(&mut c) .await .unwrap(); assert!((195..=205).contains(&count), "expected ~200, got {}", count); } #[tokio::test] async fn test_pfmerge_overlapping() { let (_m, mut c) = start().await; // Create overlapping HLLs for i in 0..100 { must_int!(c, "PFADD", "h1", &format!("item-{}", i); 1); if i % 2 == 0 { must_int!(c, "PFADD", "h2", &format!("item-{}", i); 1); } } // h1 has 100, h2 has 50 (all in h1) must_int!(c, "PFCOUNT", "h1"; 100); must_int!(c, "PFCOUNT", "h2"; 50); // Merge overlapping → should be 100 (union) must_ok!(c, "PFMERGE", "res2", "h1", "h2"); must_int!(c, "PFCOUNT", "res2"; 100); } #[tokio::test] async fn test_pfmerge_with_empty() { let (_m, mut c) = start().await; must_int!(c, "PFADD", "h1", "a", "b", "c"; 1); // Merge with empty/non-existent key must_ok!(c, "PFMERGE", "res", "h1", "h_empty"); must_int!(c, "PFCOUNT", "res"; 3); } #[tokio::test] async fn test_pfmerge_dest_only() { let (_m, mut c) = start().await; // PFMERGE with just dest (no sources) — creates empty HLL must_ok!(c, "PFMERGE", "dest"); must_int!(c, "PFCOUNT", "dest"; 0); must_str!(c, "TYPE", "dest"; "hll"); } #[tokio::test] async fn test_pfmerge_errors() { let (_m, mut c) = start().await; must_ok!(c, "SET", "str", "value"); // Wrong number of arguments must_fail!(c, "PFMERGE"; "wrong number of arguments"); // Wrong type source must_fail!(c, "PFMERGE", "h10", "str"; "not a valid HyperLogLog string value"); } // ── DEL interaction ────────────────────────────────────────────────── #[tokio::test] async fn test_hll_del() { let (_m, mut c) = start().await; must_int!(c, "PFADD", "h", "a", "b", "c"; 1); must_int!(c, "PFCOUNT", "h"; 3); // DEL the key must_int!(c, "DEL", "h"; 1); // Count should be 0 now must_int!(c, "PFCOUNT", "h"; 0); } ================================================ FILE: miniredis/tests/cmd_list.rs ================================================ // Ported from ../miniredis/cmd_list_test.go mod helpers; #[tokio::test] async fn test_lpush_rpush() { let (_m, mut c) = helpers::start().await; must_int!(c, "LPUSH", "l", "a"; 1); must_int!(c, "LPUSH", "l", "b"; 2); must_int!(c, "RPUSH", "l", "c"; 3); // List should be: b, a, c must_strs!(c, "LRANGE", "l", "0", "-1"; ["b", "a", "c"]); // Multiple values must_int!(c, "RPUSH", "l2", "a", "b", "c"; 3); must_strs!(c, "LRANGE", "l2", "0", "-1"; ["a", "b", "c"]); must_int!(c, "LPUSH", "l3", "c", "b", "a"; 3); must_strs!(c, "LRANGE", "l3", "0", "-1"; ["a", "b", "c"]); // Errors must_fail!(c, "LPUSH"; "wrong number of arguments"); must_fail!(c, "LPUSH", "key"; "wrong number of arguments"); } #[tokio::test] async fn test_lpop_rpop() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c"; 3); must_str!(c, "LPOP", "l"; "a"); must_str!(c, "RPOP", "l"; "c"); must_str!(c, "LPOP", "l"; "b"); // Empty list must_nil!(c, "LPOP", "l"); must_nil!(c, "RPOP", "l"); // Non-existent key must_nil!(c, "LPOP", "nosuch"); } #[tokio::test] async fn test_lpop_count() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c", "d", "e"; 5); must_strs!(c, "LPOP", "l", "3"; ["a", "b", "c"]); must_strs!(c, "LPOP", "l", "10"; ["d", "e"]); } #[tokio::test] async fn test_lpushx_rpushx() { let (_m, mut c) = helpers::start().await; // PUSHX on non-existing key must_int!(c, "LPUSHX", "l", "a"; 0); must_int!(c, "RPUSHX", "l", "a"; 0); // Create the list must_int!(c, "RPUSH", "l", "a"; 1); // Now PUSHX works must_int!(c, "LPUSHX", "l", "b"; 2); must_int!(c, "RPUSHX", "l", "c"; 3); must_strs!(c, "LRANGE", "l", "0", "-1"; ["b", "a", "c"]); } #[tokio::test] async fn test_llen() { let (_m, mut c) = helpers::start().await; must_int!(c, "LLEN", "l"; 0); must_int!(c, "RPUSH", "l", "a", "b", "c"; 3); must_int!(c, "LLEN", "l"; 3); } #[tokio::test] async fn test_lindex() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c"; 3); must_str!(c, "LINDEX", "l", "0"; "a"); must_str!(c, "LINDEX", "l", "1"; "b"); must_str!(c, "LINDEX", "l", "2"; "c"); must_str!(c, "LINDEX", "l", "-1"; "c"); must_str!(c, "LINDEX", "l", "-2"; "b"); // Out of range must_nil!(c, "LINDEX", "l", "100"); must_nil!(c, "LINDEX", "l", "-100"); // Non-existent key must_nil!(c, "LINDEX", "nosuch", "0"); } #[tokio::test] async fn test_lrange() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c", "d", "e"; 5); must_strs!(c, "LRANGE", "l", "0", "-1"; ["a", "b", "c", "d", "e"]); must_strs!(c, "LRANGE", "l", "1", "3"; ["b", "c", "d"]); must_strs!(c, "LRANGE", "l", "-3", "-1"; ["c", "d", "e"]); must_strs!(c, "LRANGE", "l", "0", "100"; ["a", "b", "c", "d", "e"]); // Empty result let result: Vec = redis::cmd("LRANGE") .arg("l") .arg("10") .arg("20") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); // Non-existent key let result: Vec = redis::cmd("LRANGE") .arg("nosuch") .arg("0") .arg("-1") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_lset() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c"; 3); must_ok!(c, "LSET", "l", "1", "B"); must_strs!(c, "LRANGE", "l", "0", "-1"; ["a", "B", "c"]); // Negative index must_ok!(c, "LSET", "l", "-1", "C"); must_strs!(c, "LRANGE", "l", "0", "-1"; ["a", "B", "C"]); // Errors must_fail!(c, "LSET", "l", "100", "x"; "index out of range"); must_fail!(c, "LSET", "nosuch", "0", "x"; "no such key"); } #[tokio::test] async fn test_linsert() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "c"; 2); must_int!(c, "LINSERT", "l", "BEFORE", "c", "b"; 3); must_strs!(c, "LRANGE", "l", "0", "-1"; ["a", "b", "c"]); must_int!(c, "LINSERT", "l", "AFTER", "c", "d"; 4); must_strs!(c, "LRANGE", "l", "0", "-1"; ["a", "b", "c", "d"]); // Pivot not found must_int!(c, "LINSERT", "l", "BEFORE", "nosuch", "x"; -1); // Non-existent key must_int!(c, "LINSERT", "nosuch", "BEFORE", "a", "x"; 0); } #[tokio::test] async fn test_lrem() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "a", "c", "a"; 5); // Remove 2 from head must_int!(c, "LREM", "l", "2", "a"; 2); must_strs!(c, "LRANGE", "l", "0", "-1"; ["b", "c", "a"]); // Remove from tail must_int!(c, "RPUSH", "l2", "a", "b", "a", "c", "a"; 5); must_int!(c, "LREM", "l2", "-2", "a"; 2); must_strs!(c, "LRANGE", "l2", "0", "-1"; ["a", "b", "c"]); // Remove all must_int!(c, "RPUSH", "l3", "a", "a", "a"; 3); must_int!(c, "LREM", "l3", "0", "a"; 3); must_int!(c, "LLEN", "l3"; 0); } #[tokio::test] async fn test_ltrim() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "l", "a", "b", "c", "d", "e"; 5); must_ok!(c, "LTRIM", "l", "1", "3"); must_strs!(c, "LRANGE", "l", "0", "-1"; ["b", "c", "d"]); } #[tokio::test] async fn test_rpoplpush() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "src", "a", "b", "c"; 3); must_str!(c, "RPOPLPUSH", "src", "dst"; "c"); must_strs!(c, "LRANGE", "src", "0", "-1"; ["a", "b"]); must_strs!(c, "LRANGE", "dst", "0", "-1"; ["c"]); // Empty source must_int!(c, "RPUSH", "empty", "x"; 1); must_str!(c, "LPOP", "empty"; "x"); must_nil!(c, "RPOPLPUSH", "empty", "dst"); } #[tokio::test] async fn test_lmove() { let (_m, mut c) = helpers::start().await; must_int!(c, "RPUSH", "src", "a", "b", "c"; 3); must_str!(c, "LMOVE", "src", "dst", "RIGHT", "LEFT"; "c"); must_strs!(c, "LRANGE", "src", "0", "-1"; ["a", "b"]); must_strs!(c, "LRANGE", "dst", "0", "-1"; ["c"]); must_str!(c, "LMOVE", "src", "dst", "LEFT", "RIGHT"; "a"); must_strs!(c, "LRANGE", "src", "0", "-1"; ["b"]); must_strs!(c, "LRANGE", "dst", "0", "-1"; ["c", "a"]); } #[tokio::test] async fn test_list_wrongtype() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "str", "val"); must_fail!(c, "LPUSH", "str", "x"; "WRONGTYPE"); must_fail!(c, "LLEN", "str"; "WRONGTYPE"); must_fail!(c, "LRANGE", "str", "0", "-1"; "WRONGTYPE"); } #[tokio::test] async fn test_lpos() { let (_m, mut c) = helpers::start().await; // Build list: [aap, aap, vuur, aap, mies, aap, noot, aap] // RPUSH to get them in left-to-right order let _: i64 = redis::cmd("RPUSH") .arg("l") .arg("aap") .arg("aap") .arg("vuur") .arg("aap") .arg("mies") .arg("aap") .arg("noot") .arg("aap") .query_async(&mut c) .await .unwrap(); // Simple must_int!(c, "LPOS", "l", "aap"; 0); must_int!(c, "LPOS", "l", "vuur"; 2); must_int!(c, "LPOS", "l", "mies"; 4); must_nil!(c, "LPOS", "l", "wim"); // RANK must_int!(c, "LPOS", "l", "aap", "RANK", "1"; 0); must_int!(c, "LPOS", "l", "aap", "RANK", "2"; 1); must_int!(c, "LPOS", "l", "aap", "RANK", "3"; 3); must_int!(c, "LPOS", "l", "aap", "RANK", "-1"; 7); must_int!(c, "LPOS", "l", "aap", "RANK", "-2"; 5); // COUNT let vals: Vec = redis::cmd("LPOS") .arg("l") .arg("aap") .arg("COUNT") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(vals, vec![0, 1, 3, 5, 7]); let vals: Vec = redis::cmd("LPOS") .arg("l") .arg("aap") .arg("COUNT") .arg(3) .query_async(&mut c) .await .unwrap(); assert_eq!(vals, vec![0, 1, 3]); let vals: Vec = redis::cmd("LPOS") .arg("l") .arg("wim") .arg("COUNT") .arg(1) .query_async(&mut c) .await .unwrap(); assert!(vals.is_empty()); // RANK + COUNT let vals: Vec = redis::cmd("LPOS") .arg("l") .arg("aap") .arg("RANK") .arg(3) .arg("COUNT") .arg(2) .query_async(&mut c) .await .unwrap(); assert_eq!(vals, vec![3, 5]); // COUNT + MAXLEN let vals: Vec = redis::cmd("LPOS") .arg("l") .arg("aap") .arg("COUNT") .arg(0) .arg("MAXLEN") .arg(4) .query_async(&mut c) .await .unwrap(); assert_eq!(vals, vec![0, 1, 3]); // Errors must_fail!(c, "LPOS", "l"; "wrong number of arguments"); must_fail!(c, "LPOS", "l", "aap", "RANK"; "syntax error"); must_fail!(c, "LPOS", "l", "aap", "RANK", "0"; "can't be zero"); must_fail!(c, "LPOS", "l", "aap", "COUNT", "-1"; "can't be negative"); must_fail!(c, "LPOS", "l", "aap", "MAXLEN", "-1"; "can't be negative"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "LPOS", "str", "val"; "WRONGTYPE"); } #[tokio::test] async fn test_blpop() { let (_m, mut c) = helpers::start().await; // Non-blocking: data available let _: i64 = redis::cmd("RPUSH") .arg("ll") .arg("aap") .arg("noot") .arg("mies") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("BLPOP") .arg("ll") .arg(1) .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["ll", "aap"]); // Timeout (short) let result: Option> = redis::cmd("BLPOP") .arg("empty") .arg("0.1") .query_async(&mut c) .await .unwrap(); assert!(result.is_none()); // Errors must_fail!(c, "BLPOP"; "wrong number of arguments"); must_fail!(c, "BLPOP", "key"; "wrong number of arguments"); must_fail!(c, "BLPOP", "key", "-1"; "timeout is negative"); } #[tokio::test] async fn test_brpop() { let (_m, mut c) = helpers::start().await; // Non-blocking: data available let _: i64 = redis::cmd("RPUSH") .arg("ll") .arg("aap") .arg("noot") .arg("mies") .query_async(&mut c) .await .unwrap(); let result: Vec = redis::cmd("BRPOP") .arg("ll") .arg(1) .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["ll", "mies"]); // Timeout (short) let result: Option> = redis::cmd("BRPOP") .arg("empty") .arg("0.1") .query_async(&mut c) .await .unwrap(); assert!(result.is_none()); // Errors must_fail!(c, "BRPOP"; "wrong number of arguments"); must_fail!(c, "BRPOP", "key"; "wrong number of arguments"); must_fail!(c, "BRPOP", "key", "-1"; "timeout is negative"); } #[tokio::test] async fn test_brpoplpush() { let (_m, mut c) = helpers::start().await; // Non-blocking: data available let _: i64 = redis::cmd("RPUSH") .arg("l1") .arg("aap") .arg("noot") .arg("mies") .query_async(&mut c) .await .unwrap(); let result: String = redis::cmd("BRPOPLPUSH") .arg("l1") .arg("l2") .arg(1) .query_async(&mut c) .await .unwrap(); assert_eq!(result, "mies"); must_strs!(c, "LRANGE", "l2", "0", "-1"; ["mies"]); // Timeout (short) let result: Option = redis::cmd("BRPOPLPUSH") .arg("empty") .arg("dst") .arg("0.1") .query_async(&mut c) .await .unwrap(); assert!(result.is_none()); // Errors must_fail!(c, "BRPOPLPUSH"; "wrong number of arguments"); must_fail!(c, "BRPOPLPUSH", "key"; "wrong number of arguments"); must_fail!(c, "BRPOPLPUSH", "key", "bar"; "wrong number of arguments"); must_fail!(c, "BRPOPLPUSH", "key", "foo", "-1"; "timeout is negative"); } #[tokio::test] async fn test_blmove() { let (_m, mut c) = helpers::start().await; // Setup let _: i64 = redis::cmd("RPUSH") .arg("src") .arg("RL") .arg("RR") .arg("LL") .arg("LR") .query_async(&mut c) .await .unwrap(); let _: i64 = redis::cmd("RPUSH") .arg("dst") .arg("m1") .arg("m2") .arg("m3") .query_async(&mut c) .await .unwrap(); // RIGHT LEFT let v: String = redis::cmd("BLMOVE") .arg("src") .arg("dst") .arg("RIGHT") .arg("LEFT") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(v, "LR"); // LEFT RIGHT let v: String = redis::cmd("BLMOVE") .arg("src") .arg("dst") .arg("LEFT") .arg("RIGHT") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(v, "RL"); // Timeout (short) let result: Option = redis::cmd("BLMOVE") .arg("nosuch") .arg("dst") .arg("RIGHT") .arg("LEFT") .arg("0.1") .query_async(&mut c) .await .unwrap(); assert!(result.is_none()); // Errors must_fail!(c, "BLMOVE"; "wrong number of arguments"); must_fail!(c, "BLMOVE", "l"; "wrong number of arguments"); must_fail!(c, "BLMOVE", "l", "l"; "wrong number of arguments"); must_fail!(c, "BLMOVE", "l", "l", "l"; "wrong number of arguments"); must_fail!(c, "BLMOVE", "l", "l", "l", "l"; "wrong number of arguments"); } #[tokio::test] async fn test_brpop_tx() { // BRPOP in a transaction behaves as if the timeout triggers right away let (m, mut c) = helpers::start().await; // BRPOP on empty list inside MULTI → null (no blocking) must_ok!(c, "MULTI"); let v: String = redis::cmd("BRPOP") .arg("l1") .arg(3) .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: String = redis::cmd("SET") .arg("foo") .arg("bar") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: redis::Value = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 2); assert_eq!(items[0], redis::Value::Nil); } _ => panic!("expected array from EXEC, got {:?}", v), } // Now push something and BRPOP in MULTI → should pop it m.push("l1", &["e1"]); must_ok!(c, "MULTI"); let v: String = redis::cmd("BRPOP") .arg("l1") .arg(3) .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: String = redis::cmd("SET") .arg("foo") .arg("bar") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: redis::Value = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 2); // First result: [key, value] match &items[0] { redis::Value::Array(kv) => { assert_eq!(kv.len(), 2); } _ => panic!("expected array from BRPOP result, got {:?}", items[0]), } } _ => panic!("expected array from EXEC, got {:?}", v), } } #[tokio::test] async fn test_blpop_resource_cleanup() { // Test that a blocking BLPOP is cleaned up when the connection is closed. // Ensures the server doesn't leak resources or hang. let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut conn = client.get_multiplexed_async_connection().await.unwrap(); // Issue a short-timeout BLPOP on a non-existent key let result: Option<(String, String)> = redis::cmd("BLPOP") .arg("nonexistent") .arg("0.1") .query_async(&mut conn) .await .unwrap(); assert!(result.is_none(), "BLPOP should timeout with nil"); // Drop the connection and close the server — should not hang drop(conn); m.close().await; } #[tokio::test] async fn test_rpush_pop() { let (m, mut c) = helpers::start().await; // RPUSH must_int!(c, "RPUSH", "l", "aap", "noot", "mies"; 3); must_strs!(c, "LRANGE", "l", "0", "0"; ["aap"]); must_strs!(c, "LRANGE", "l", "-1", "-1"; ["mies"]); // Push more must_int!(c, "RPUSH", "l", "aap2", "noot2", "mies2"; 6); must_strs!(c, "LRANGE", "l", "0", "0"; ["aap"]); must_strs!(c, "LRANGE", "l", "-1", "-1"; ["mies2"]); // Direct API: Push and Pop let len = m.push("l2", &["a"]); assert_eq!(len, 1); let len = m.push("l2", &["b"]); assert_eq!(len, 2); let list = m.list("l2"); assert_eq!(list, Some(vec!["a".to_string(), "b".to_string()])); } ================================================ FILE: miniredis/tests/cmd_misc.rs ================================================ mod helpers; use helpers::*; // ── QUIT ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_quit() { let (_m, mut c) = start().await; // QUIT should return OK must_ok!(c, "QUIT"); } // ── COMMAND ───────────────────────────────────────────────────────── #[tokio::test] async fn test_command() { let (_m, mut c) = start().await; let v: redis::Value = redis::cmd("COMMAND").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 200, "expected 200 command entries"); } _ => panic!("expected array from COMMAND, got {:?}", v), } } // ── INFO ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_info_no_args() { let (_m, mut c) = start().await; let v: String = redis::cmd("INFO").query_async(&mut c).await.unwrap(); assert!(v.contains("# Clients"), "expected Clients section"); assert!( v.contains("connected_clients:"), "expected connected_clients" ); assert!(v.contains("# Stats"), "expected Stats section"); assert!( v.contains("total_connections_received:"), "expected total_connections_received" ); assert!( v.contains("total_commands_processed:"), "expected total_commands_processed" ); } #[tokio::test] async fn test_info_clients() { let (_m, mut c) = start().await; let v: String = redis::cmd("INFO") .arg("clients") .query_async(&mut c) .await .unwrap(); assert!(v.contains("# Clients"), "expected Clients section"); assert!( v.contains("connected_clients:"), "expected connected_clients" ); assert!(!v.contains("# Stats"), "should not contain Stats section"); } #[tokio::test] async fn test_info_stats() { let (_m, mut c) = start().await; let v: String = redis::cmd("INFO") .arg("stats") .query_async(&mut c) .await .unwrap(); assert!(v.contains("# Stats"), "expected Stats section"); assert!( v.contains("total_connections_received:"), "expected total_connections_received" ); assert!( !v.contains("# Clients"), "should not contain Clients section" ); } #[tokio::test] async fn test_info_invalid_section() { let (_m, mut c) = start().await; must_fail!(c, "INFO", "bogus"; "not supported"); } // ── CLIENT SETNAME/GETNAME ────────────────────────────────────────── #[tokio::test] async fn test_client_setname_getname() { let (_m, mut c) = start().await; // GETNAME before setting → nil must_nil!(c, "CLIENT", "GETNAME"); // SETNAME must_ok!(c, "CLIENT", "SETNAME", "myconn"); // GETNAME must_str!(c, "CLIENT", "GETNAME"; "myconn"); // Reset name with empty string must_ok!(c, "CLIENT", "SETNAME", ""); must_nil!(c, "CLIENT", "GETNAME"); } #[tokio::test] async fn test_client_setname_errors() { let (_m, mut c) = start().await; // Name with space must_fail!(c, "CLIENT", "SETNAME", "my name"; "cannot contain spaces"); // Name with newline must_fail!(c, "CLIENT", "SETNAME", "my\nname"; "cannot contain spaces"); // Wrong number of args must_fail!(c, "CLIENT", "SETNAME"; "wrong number of arguments"); must_fail!(c, "CLIENT", "SETNAME", "a", "b"; "wrong number of arguments"); } #[tokio::test] async fn test_client_getname_errors() { let (_m, mut c) = start().await; // Wrong number of args must_fail!(c, "CLIENT", "GETNAME", "extra"; "wrong number of arguments"); } // ── CLUSTER ───────────────────────────────────────────────────────── #[tokio::test] async fn test_cluster_slots() { let (_m, mut c) = start().await; let v: redis::Value = redis::cmd("CLUSTER") .arg("SLOTS") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 1, "expected 1 slot range"); } _ => panic!("expected array from CLUSTER SLOTS, got {:?}", v), } } #[tokio::test] async fn test_cluster_keyslot() { let (_m, mut c) = start().await; let v: i64 = redis::cmd("CLUSTER") .arg("KEYSLOT") .arg("foo") .query_async(&mut c) .await .unwrap(); assert_eq!(v, 163); } #[tokio::test] async fn test_cluster_nodes() { let (_m, mut c) = start().await; let v: String = redis::cmd("CLUSTER") .arg("NODES") .query_async(&mut c) .await .unwrap(); assert!( v.contains("myself,master"), "expected myself,master, got: {}", v ); assert!( v.contains("0-16383"), "expected 0-16383 slot range, got: {}", v ); } #[tokio::test] async fn test_cluster_shards() { let (_m, mut c) = start().await; let v: redis::Value = redis::cmd("CLUSTER") .arg("SHARDS") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(items) => { assert!(!items.is_empty(), "expected at least 1 shard"); } _ => panic!("expected array from CLUSTER SHARDS, got {:?}", v), } } #[tokio::test] async fn test_cluster_unknown() { let (_m, mut c) = start().await; must_fail!(c, "CLUSTER", "BOGUS"; "unknown subcommand"); } // ── OBJECT IDLETIME ───────────────────────────────────────────────── #[tokio::test] async fn test_object_idletime() { let (_m, mut c) = start().await; // Non-existent key → nil must_nil!(c, "OBJECT", "IDLETIME", "nosuch"); // Set a key, check idle time ≥ 0 must_ok!(c, "SET", "key", "val"); let v: i64 = redis::cmd("OBJECT") .arg("IDLETIME") .arg("key") .query_async(&mut c) .await .unwrap(); assert!(v >= 0, "expected non-negative idle time, got {}", v); } #[tokio::test] async fn test_object_idletime_errors() { let (_m, mut c) = start().await; must_fail!(c, "OBJECT"; "wrong number of arguments"); must_fail!(c, "OBJECT", "IDLETIME"; "wrong number of arguments"); must_fail!(c, "OBJECT", "BOGUS"; "unknown subcommand"); } ================================================ FILE: miniredis/tests/cmd_pubsub.rs ================================================ mod helpers; use helpers::*; // ── PUBLISH (through regular dispatch) ────────────────────────────── #[tokio::test] async fn test_publish_no_subscribers() { let (_m, mut c) = start().await; // No subscribers → 0 must_int!(c, "PUBLISH", "ch", "hello"; 0); } #[tokio::test] async fn test_publish_errors() { let (_m, mut c) = start().await; must_fail!(c, "PUBLISH"; "wrong number of arguments"); must_fail!(c, "PUBLISH", "ch"; "wrong number of arguments"); must_fail!(c, "PUBLISH", "ch", "msg", "extra"; "wrong number of arguments"); } // ── PUBSUB CHANNELS/NUMSUB/NUMPAT (through regular dispatch) ─────── #[tokio::test] async fn test_pubsub_channels_empty() { let (_m, mut c) = start().await; let v: Vec = redis::cmd("PUBSUB") .arg("CHANNELS") .query_async(&mut c) .await .unwrap(); assert!(v.is_empty()); } #[tokio::test] async fn test_pubsub_numsub_empty() { let (_m, mut c) = start().await; let v: redis::Value = redis::cmd("PUBSUB") .arg("NUMSUB") .arg("ch1") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 2); // channel name, count (0) } _ => panic!("expected array from PUBSUB NUMSUB, got {:?}", v), } } #[tokio::test] async fn test_pubsub_numpat_empty() { let (_m, mut c) = start().await; must_int!(c, "PUBSUB", "NUMPAT"; 0); } // ── SUBSCRIBE + PUBLISH via redis-rs PubSub API ──────────────────── #[tokio::test] async fn test_subscribe_and_publish() { let (m, mut c) = start().await; // Create a subscriber using redis-rs PubSub let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); // Subscribe to a channel pubsub.subscribe("ch1").await.unwrap(); // Give a moment for subscription to register tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Publish a message must_int!(c, "PUBLISH", "ch1", "hello"; 1); // Receive the message let msg = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout waiting for message") .expect("no message received"); let payload: String = msg.get_payload().unwrap(); assert_eq!(payload, "hello"); assert_eq!(msg.get_channel_name(), "ch1"); } #[tokio::test] async fn test_subscribe_multiple_channels() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.subscribe("ch1").await.unwrap(); pubsub.subscribe("ch2").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Publish to ch1 must_int!(c, "PUBLISH", "ch1", "msg1"; 1); // Publish to ch2 must_int!(c, "PUBLISH", "ch2", "msg2"; 1); // Receive both messages let msg1 = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout") .expect("no message"); let payload1: String = msg1.get_payload().unwrap(); let msg2 = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout") .expect("no message"); let payload2: String = msg2.get_payload().unwrap(); let mut payloads = vec![payload1, payload2]; payloads.sort(); assert_eq!(payloads, vec!["msg1", "msg2"]); } #[tokio::test] async fn test_psubscribe() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.psubscribe("event*").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Publish to a matching channel must_int!(c, "PUBLISH", "event123", "data"; 1); // Non-matching channel must_int!(c, "PUBLISH", "other", "data"; 0); // Receive the matching message let msg = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout") .expect("no message"); let payload: String = msg.get_payload().unwrap(); assert_eq!(payload, "data"); } #[tokio::test] async fn test_unsubscribe() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.subscribe("ch1").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Can receive must_int!(c, "PUBLISH", "ch1", "before"; 1); let msg = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout") .expect("no message"); let payload: String = msg.get_payload().unwrap(); assert_eq!(payload, "before"); // Unsubscribe pubsub.unsubscribe("ch1").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Should have 0 subscribers now must_int!(c, "PUBLISH", "ch1", "after"; 0); } #[tokio::test] async fn test_pubsub_channels_with_subscriber() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.subscribe("news").await.unwrap(); pubsub.subscribe("sports").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // PUBSUB CHANNELS should list both let mut channels: Vec = redis::cmd("PUBSUB") .arg("CHANNELS") .query_async(&mut c) .await .unwrap(); channels.sort(); assert_eq!(channels, vec!["news", "sports"]); // PUBSUB CHANNELS with pattern let channels: Vec = redis::cmd("PUBSUB") .arg("CHANNELS") .arg("n*") .query_async(&mut c) .await .unwrap(); assert_eq!(channels, vec!["news"]); } #[tokio::test] async fn test_pubsub_numsub_with_subscriber() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.subscribe("ch1").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; let v: redis::Value = redis::cmd("PUBSUB") .arg("NUMSUB") .arg("ch1") .arg("ch2") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 4); // ch1, count, ch2, count } _ => panic!("expected array from PUBSUB NUMSUB, got {:?}", v), } } #[tokio::test] async fn test_pubsub_numpat_with_subscriber() { let (m, mut c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.psubscribe("event*").await.unwrap(); pubsub.psubscribe("news*").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; must_int!(c, "PUBSUB", "NUMPAT"; 2); } // ── Direct API ────────────────────────────────────────────────────── #[tokio::test] async fn test_publish_direct_api() { let (m, _c) = start().await; let sub_client = redis::Client::open(m.redis_url()).unwrap(); let mut pubsub = sub_client.get_async_pubsub().await.unwrap(); pubsub.subscribe("ch1").await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Use direct API to publish let count = m.publish("ch1", "hello-direct"); assert_eq!(count, 1); // Receive the message let msg = tokio::time::timeout( std::time::Duration::from_secs(2), pubsub.on_message().next(), ) .await .expect("timeout") .expect("no message"); let payload: String = msg.get_payload().unwrap(); assert_eq!(payload, "hello-direct"); } // Need this import for Stream trait used by on_message() use futures_lite::StreamExt; ================================================ FILE: miniredis/tests/cmd_resp3.rs ================================================ mod helpers; use helpers::*; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; // ── Helper: raw TCP connection for RESP3 wire format tests ────────── async fn raw_connect(m: &miniredis_rs::Miniredis) -> TcpStream { TcpStream::connect(m.addr()).await.unwrap() } /// Send a RESP2 command (array of bulk strings) and return raw response bytes. async fn raw_cmd(stream: &mut TcpStream, args: &[&str]) -> Vec { // Build RESP2 array let mut cmd = format!("*{}\r\n", args.len()); for arg in args { cmd.push_str(&format!("${}\r\n{}\r\n", arg.len(), arg)); } stream.write_all(cmd.as_bytes()).await.unwrap(); stream.flush().await.unwrap(); // Read response (wait a bit for it to arrive) tokio::time::sleep(std::time::Duration::from_millis(50)).await; let mut buf = vec![0u8; 4096]; let n = stream.read(&mut buf).await.unwrap(); buf.truncate(n); buf } // ── HELLO command tests ───────────────────────────────────────────── #[tokio::test] async fn test_hello_2_returns_map_as_array() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // HELLO 2 should return a flat array (RESP2 encoding of Map) let resp = raw_cmd(&mut stream, &["HELLO", "2"]).await; let resp_str = String::from_utf8_lossy(&resp); // Should start with * (RESP2 array), containing 14 elements (7 key-value pairs) assert!( resp_str.starts_with("*14\r\n"), "expected RESP2 array *14, got: {:?}", resp_str ); } #[tokio::test] async fn test_hello_3_returns_resp3_map() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // HELLO 3 should return a RESP3 map let resp = raw_cmd(&mut stream, &["HELLO", "3"]).await; let resp_str = String::from_utf8_lossy(&resp); // Should start with % (RESP3 map) with 7 key-value pairs assert!( resp_str.starts_with("%7\r\n"), "expected RESP3 map %7, got: {:?}", resp_str ); } #[tokio::test] async fn test_hello_3_enables_resp3_null() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Switch to RESP3 let _ = raw_cmd(&mut stream, &["HELLO", "3"]).await; // GET on non-existent key should return RESP3 null: _\r\n let resp = raw_cmd(&mut stream, &["GET", "nosuch"]).await; assert_eq!( resp, b"_\r\n", "expected RESP3 null, got: {:?}", String::from_utf8_lossy(&resp) ); } #[tokio::test] async fn test_resp2_null_is_dollar_minus_one() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Without HELLO 3, GET on non-existent key should return RESP2 null: $-1\r\n let resp = raw_cmd(&mut stream, &["GET", "nosuch"]).await; assert_eq!( resp, b"$-1\r\n", "expected RESP2 null, got: {:?}", String::from_utf8_lossy(&resp) ); } // ── HGETALL RESP3 map ─────────────────────────────────────────────── #[tokio::test] async fn test_hgetall_resp2_flat_array() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Set up a hash let _ = raw_cmd(&mut stream, &["HSET", "h", "f1", "v1"]).await; // HGETALL in RESP2 mode returns flat array let resp = raw_cmd(&mut stream, &["HGETALL", "h"]).await; let resp_str = String::from_utf8_lossy(&resp); assert!( resp_str.starts_with("*2\r\n"), "expected RESP2 array *2, got: {:?}", resp_str ); } #[tokio::test] async fn test_hgetall_resp3_map() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Set up a hash let _ = raw_cmd(&mut stream, &["HSET", "h", "f1", "v1"]).await; // Switch to RESP3 let _ = raw_cmd(&mut stream, &["HELLO", "3"]).await; // HGETALL in RESP3 mode returns map let resp = raw_cmd(&mut stream, &["HGETALL", "h"]).await; let resp_str = String::from_utf8_lossy(&resp); assert!( resp_str.starts_with("%1\r\n"), "expected RESP3 map %1, got: {:?}", resp_str ); // Verify it contains the field-value pair assert!( resp_str.contains("f1"), "response should contain field 'f1'" ); assert!( resp_str.contains("v1"), "response should contain value 'v1'" ); } // ── SMEMBERS RESP3 set ────────────────────────────────────────────── #[tokio::test] async fn test_smembers_resp2_array() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; let _ = raw_cmd(&mut stream, &["SADD", "s", "a", "b"]).await; // SMEMBERS in RESP2 mode returns array let resp = raw_cmd(&mut stream, &["SMEMBERS", "s"]).await; let resp_str = String::from_utf8_lossy(&resp); assert!( resp_str.starts_with("*2\r\n"), "expected RESP2 array *2, got: {:?}", resp_str ); } #[tokio::test] async fn test_smembers_resp3_set() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; let _ = raw_cmd(&mut stream, &["SADD", "s", "a", "b"]).await; // Switch to RESP3 let _ = raw_cmd(&mut stream, &["HELLO", "3"]).await; // SMEMBERS in RESP3 mode returns set (~) let resp = raw_cmd(&mut stream, &["SMEMBERS", "s"]).await; let resp_str = String::from_utf8_lossy(&resp); assert!( resp_str.starts_with("~2\r\n"), "expected RESP3 set ~2, got: {:?}", resp_str ); } // ── Commands still work after HELLO 3 ─────────────────────────────── #[tokio::test] async fn test_commands_work_after_hello_3() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Switch to RESP3 let _ = raw_cmd(&mut stream, &["HELLO", "3"]).await; // SET should still return +OK let resp = raw_cmd(&mut stream, &["SET", "key", "value"]).await; assert_eq!(resp, b"+OK\r\n"); // GET should return bulk string let resp = raw_cmd(&mut stream, &["GET", "key"]).await; assert_eq!(resp, b"$5\r\nvalue\r\n"); // Integer commands work let resp = raw_cmd(&mut stream, &["DEL", "key"]).await; assert_eq!(resp, b":1\r\n"); } #[tokio::test] async fn test_hello_3_then_hello_2_resets() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let mut stream = raw_connect(&m).await; // Switch to RESP3 let resp = raw_cmd(&mut stream, &["HELLO", "3"]).await; assert!(String::from_utf8_lossy(&resp).starts_with("%")); // Switch back to RESP2 let resp = raw_cmd(&mut stream, &["HELLO", "2"]).await; // HELLO 2 returns Map but serialized as RESP2 flat array since we just set resp2 assert!(String::from_utf8_lossy(&resp).starts_with("*")); // GET on non-existent key should now return RESP2 null let resp = raw_cmd(&mut stream, &["GET", "nosuch"]).await; assert_eq!(resp, b"$-1\r\n"); } // ── HELLO with AUTH ───────────────────────────────────────────────── #[tokio::test] async fn test_hello_3_with_auth() { let m = miniredis_rs::Miniredis::run().await.unwrap(); m.require_auth("password"); let mut stream = raw_connect(&m).await; // HELLO 3 AUTH default password let resp = raw_cmd(&mut stream, &["HELLO", "3", "AUTH", "default", "password"]).await; let resp_str = String::from_utf8_lossy(&resp); assert!( resp_str.starts_with("%7\r\n"), "expected RESP3 map, got: {:?}", resp_str ); // Should be authenticated and in RESP3 mode let resp = raw_cmd(&mut stream, &["SET", "k", "v"]).await; assert_eq!(resp, b"+OK\r\n"); } // ── Via redis-rs client ───────────────────────────────────────────── #[tokio::test] async fn test_hello_via_redis_rs() { let (_m, mut c) = start().await; // HELLO 2 should work via redis-rs let result: Vec = redis::cmd("HELLO") .arg("2") .query_async(&mut c) .await .unwrap(); // Returns as flat array in RESP2 mode assert!(!result.is_empty()); // Commands should still work must_ok!(c, "SET", "k", "v"); must_str!(c, "GET", "k"; "v"); } #[tokio::test] async fn test_hello_invalid_version() { let (_m, mut c) = start().await; must_fail!(c, "HELLO", "4"; "NOPROTO"); must_fail!(c, "HELLO", "0"; "NOPROTO"); } ================================================ FILE: miniredis/tests/cmd_scripting.rs ================================================ mod helpers; use helpers::*; // ── EVAL basic ─────────────────────────────────────────────────────── #[tokio::test] async fn test_eval_return_string() { let (_m, mut c) = start().await; must_str!(c, "EVAL", "return 'hello'", "0"; "hello"); } #[tokio::test] async fn test_eval_return_number() { let (_m, mut c) = start().await; must_int!(c, "EVAL", "return 42", "0"; 42); } #[tokio::test] async fn test_eval_return_true() { let (_m, mut c) = start().await; must_int!(c, "EVAL", "return true", "0"; 1); } #[tokio::test] async fn test_eval_return_false() { let (_m, mut c) = start().await; must_nil!(c, "EVAL", "return false", "0"); } #[tokio::test] async fn test_eval_return_nil() { let (_m, mut c) = start().await; must_nil!(c, "EVAL", "return nil", "0"); } #[tokio::test] async fn test_eval_return_table() { let (_m, mut c) = start().await; let result: Vec = redis::cmd("EVAL") .arg("return {'a', 'b', 'c'}") .arg("0") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["a", "b", "c"]); } // ── KEYS and ARGV ──────────────────────────────────────────────────── #[tokio::test] async fn test_eval_keys_argv() { let (_m, mut c) = start().await; // KEYS[1] = "mykey", ARGV[1] = "myval" must_str!(c, "EVAL", "return KEYS[1]", "1", "mykey", "myval"; "mykey"); must_str!(c, "EVAL", "return ARGV[1]", "1", "mykey", "myval"; "myval"); } #[tokio::test] async fn test_eval_multiple_keys() { let (_m, mut c) = start().await; must_str!(c, "EVAL", "return KEYS[2]", "2", "k1", "k2"; "k2"); } // ── redis.call() ───────────────────────────────────────────────────── #[tokio::test] async fn test_eval_redis_call_set_get() { let (_m, mut c) = start().await; // Use redis.call to SET and GET let script = r#" redis.call('SET', KEYS[1], ARGV[1]) return redis.call('GET', KEYS[1]) "#; must_str!(c, "EVAL", script, "1", "foo", "bar"; "bar"); // Verify the value persists must_str!(c, "GET", "foo"; "bar"); } #[tokio::test] async fn test_eval_redis_call_incr() { let (_m, mut c) = start().await; must_ok!(c, "SET", "counter", "10"); let script = "return redis.call('INCR', KEYS[1])"; must_int!(c, "EVAL", script, "1", "counter"; 11); } #[tokio::test] async fn test_eval_redis_call_multiple() { let (_m, mut c) = start().await; let script = r#" redis.call('SET', 'k1', 'v1') redis.call('SET', 'k2', 'v2') return redis.call('MGET', 'k1', 'k2') "#; let result: Vec = redis::cmd("EVAL") .arg(script) .arg("0") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["v1", "v2"]); } // ── redis.pcall() ──────────────────────────────────────────────────── #[tokio::test] async fn test_eval_pcall_error() { let (_m, mut c) = start().await; // pcall catches errors let script = r#" local ok, err = pcall(function() return redis.call('NOSUCHCOMMAND') end) if ok then return 'no error' else return 'got error' end "#; must_str!(c, "EVAL", script, "0"; "got error"); } #[tokio::test] async fn test_eval_redis_pcall() { let (_m, mut c) = start().await; // redis.pcall returns error as table let script = r#" local res = redis.pcall('SET', 'key') if res.err then return 'error: ' .. res.err end return 'no error' "#; let result: String = redis::cmd("EVAL") .arg(script) .arg("0") .query_async(&mut c) .await .unwrap(); assert!( result.starts_with("error: "), "expected error prefix, got: {}", result ); } // ── redis.error_reply() / redis.status_reply() ────────────────────── #[tokio::test] async fn test_eval_error_reply() { let (_m, mut c) = start().await; must_fail!(c, "EVAL", "return redis.error_reply('MY_ERR custom error')", "0"; "MY_ERR"); } #[tokio::test] async fn test_eval_status_reply() { let (_m, mut c) = start().await; must_str!(c, "EVAL", "return redis.status_reply('PONG')", "0"; "PONG"); } // ── redis.sha1hex() ────────────────────────────────────────────────── #[tokio::test] async fn test_eval_sha1hex() { let (_m, mut c) = start().await; // SHA1 of empty string = da39a3ee5e6b4b0d3255bfef95601890afd80709 must_str!(c, "EVAL", "return redis.sha1hex('')", "0"; "da39a3ee5e6b4b0d3255bfef95601890afd80709"); } // ── EVALSHA ────────────────────────────────────────────────────────── #[tokio::test] async fn test_evalsha_basic() { let (_m, mut c) = start().await; // Load a script via EVAL first (caches it) must_int!(c, "EVAL", "return 42", "0"; 42); // Now get the SHA and use EVALSHA let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg("return 42") .query_async(&mut c) .await .unwrap(); must_int!(c, "EVALSHA", &sha, "0"; 42); } #[tokio::test] async fn test_evalsha_not_found() { let (_m, mut c) = start().await; must_fail!(c, "EVALSHA", "deadbeef", "0"; "No matching script"); } // ── SCRIPT LOAD / EXISTS / FLUSH ───────────────────────────────────── #[tokio::test] async fn test_script_load() { let (_m, mut c) = start().await; let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg("return 'loaded'") .query_async(&mut c) .await .unwrap(); // SHA should be a 40-char hex string assert_eq!(sha.len(), 40); // EVALSHA should work now must_str!(c, "EVALSHA", &sha, "0"; "loaded"); } #[tokio::test] async fn test_script_exists() { let (_m, mut c) = start().await; let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg("return 1") .query_async(&mut c) .await .unwrap(); // EXISTS should find it let result: Vec = redis::cmd("SCRIPT") .arg("EXISTS") .arg(&sha) .arg("deadbeef") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1, 0]); } #[tokio::test] async fn test_script_flush() { let (_m, mut c) = start().await; let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg("return 1") .query_async(&mut c) .await .unwrap(); // Flush all scripts must_ok!(c, "SCRIPT", "FLUSH"); // Should no longer exist let result: Vec = redis::cmd("SCRIPT") .arg("EXISTS") .arg(&sha) .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0]); } #[tokio::test] async fn test_script_flush_sync() { let (_m, mut c) = start().await; must_ok!(c, "SCRIPT", "FLUSH", "SYNC"); } // ── Error handling ─────────────────────────────────────────────────── #[tokio::test] async fn test_eval_errors() { let (_m, mut c) = start().await; // Wrong number of args must_fail!(c, "EVAL"; "wrong number of arguments"); must_fail!(c, "EVAL", "return 1"; "wrong number of arguments"); // Invalid numkeys must_fail!(c, "EVAL", "return 1", "abc"; "not an integer"); // Negative numkeys must_fail!(c, "EVAL", "return 1", "-1"; "negative"); // numkeys > remaining args must_fail!(c, "EVAL", "return 1", "2", "key1"; "greater than number of args"); } #[tokio::test] async fn test_eval_syntax_error() { let (_m, mut c) = start().await; must_fail!(c, "EVAL", "this is not valid lua!!", "0"; "Error compiling script"); } #[tokio::test] async fn test_eval_runtime_error() { let (_m, mut c) = start().await; // redis.call with wrong type should propagate error must_ok!(c, "SET", "str", "value"); must_fail!(c, "EVAL", "return redis.call('LPUSH', 'str', 'v')", "0"; "WRONGTYPE"); } // ── Script using various data types ────────────────────────────────── #[tokio::test] async fn test_eval_with_hash() { let (_m, mut c) = start().await; let script = r#" redis.call('HSET', KEYS[1], 'field1', 'val1') redis.call('HSET', KEYS[1], 'field2', 'val2') return redis.call('HGET', KEYS[1], 'field1') "#; must_str!(c, "EVAL", script, "1", "myhash"; "val1"); } #[tokio::test] async fn test_eval_with_list() { let (_m, mut c) = start().await; let script = r#" redis.call('RPUSH', KEYS[1], 'a', 'b', 'c') return redis.call('LLEN', KEYS[1]) "#; must_int!(c, "EVAL", script, "1", "mylist"; 3); } // ── SCRIPT subcommand errors ───────────────────────────────────────── #[tokio::test] async fn test_script_errors() { let (_m, mut c) = start().await; must_fail!(c, "SCRIPT"; "wrong number of arguments"); must_fail!(c, "SCRIPT", "NOSUCHSUB"; "unknown subcommand"); must_fail!(c, "SCRIPT", "LOAD"; "wrong number of arguments"); must_fail!(c, "SCRIPT", "EXISTS"; "wrong number of arguments"); } // ── EVAL_RO / EVALSHA_RO ───────────────────────────────────────────── #[tokio::test] async fn test_eval_ro_read() { let (_m, mut c) = start().await; must_ok!(c, "SET", "k", "v"); // Read-only should allow reads must_str!(c, "EVAL_RO", "return redis.call('GET', KEYS[1])", "1", "k"; "v"); } #[tokio::test] async fn test_eval_ro_write_blocked() { let (_m, mut c) = start().await; // Read-only should block writes must_fail!(c, "EVAL_RO", "return redis.call('SET', 'k', 'v')", "0"; "Write commands are not allowed"); } // ── EVALSHA_RO ─────────────────────────────────────────────────────── #[tokio::test] async fn test_evalsha_ro_read() { let (_m, mut c) = start().await; let script = "return redis.call('GET', KEYS[1])"; let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg(script) .query_async(&mut c) .await .unwrap(); must_ok!(c, "SET", "readonly", "foo"); // Read-only should allow reads must_str!(c, "EVALSHA_RO", &sha, "1", "readonly"; "foo"); } #[tokio::test] async fn test_evalsha_ro_write_blocked() { let (_m, mut c) = start().await; let write_script = "return redis.call('SET', KEYS[1], ARGV[1])"; let sha: String = redis::cmd("SCRIPT") .arg("LOAD") .arg(write_script) .query_async(&mut c) .await .unwrap(); // Read-only should block writes must_fail!(c, "EVALSHA_RO", &sha, "1", "key1", "value1"; "Write commands are not allowed"); } // ── redis.call() error cases ───────────────────────────────────────── #[tokio::test] async fn test_eval_call_errors() { let (_m, mut c) = start().await; // redis.call() with no args must_fail!(c, "EVAL", "redis.call()", "0"; "Please specify at least one argument"); // redis.call with table arg must_fail!(c, "EVAL", "redis.call({})", "0"; "must be strings or integers"); // redis.call with number arg (number as command name is treated as string "1") must_fail!(c, "EVAL", "redis.call(1)", "0"; "Unknown Redis command"); } // ── redis.log() ────────────────────────────────────────────────────── #[tokio::test] async fn test_eval_log() { let (_m, mut c) = start().await; // redis.log should succeed and return nil must_nil!(c, "EVAL", "redis.log(redis.LOG_NOTICE, 'hello')", "0"); } // ── redis.replicate_commands() / redis.set_repl() ──────────────────── #[tokio::test] async fn test_eval_replicate_commands() { let (_m, mut c) = start().await; // replicate_commands returns true (always enabled) must_int!(c, "EVAL", "return redis.replicate_commands()", "0"; 1); } #[tokio::test] async fn test_eval_set_repl() { let (_m, mut c) = start().await; // set_repl is a no-op, takes an integer argument must_nil!(c, "EVAL", "redis.set_repl(0)", "0"); } ================================================ FILE: miniredis/tests/cmd_server.rs ================================================ mod helpers; use helpers::*; // ── DBSIZE ────────────────────────────────────────────────────────── #[tokio::test] async fn test_dbsize() { let (_m, mut c) = start().await; must_int!(c, "DBSIZE"; 0); must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "DBSIZE"; 2); must_int!(c, "DEL", "a"; 1); must_int!(c, "DBSIZE"; 1); } // ── FLUSHDB ───────────────────────────────────────────────────────── #[tokio::test] async fn test_flushdb() { let (_m, mut c) = start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "DBSIZE"; 2); must_ok!(c, "FLUSHDB"); must_int!(c, "DBSIZE"; 0); } // ── FLUSHALL ──────────────────────────────────────────────────────── #[tokio::test] async fn test_flushall() { let (_m, mut c) = start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SELECT", "1"); must_ok!(c, "SET", "b", "2"); must_ok!(c, "FLUSHALL"); must_int!(c, "DBSIZE"; 0); must_ok!(c, "SELECT", "0"); must_int!(c, "DBSIZE"; 0); } // ── TIME ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_time() { let (_m, mut c) = start().await; let v: redis::Value = redis::cmd("TIME").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(items) => { assert_eq!(items.len(), 2); } _ => panic!("expected array from TIME, got {:?}", v), } // Too many args must_fail!(c, "TIME", "extra"; "wrong number of arguments"); } // ── INFO ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_info() { let (_m, mut c) = start().await; // No section → returns both clients and stats let v: String = redis::cmd("INFO").query_async(&mut c).await.unwrap(); assert!( v.contains("# Clients"), "expected Clients section, got: {}", v ); assert!( v.contains("connected_clients"), "expected connected_clients, got: {}", v ); // Specific section let v: String = redis::cmd("INFO") .arg("clients") .query_async(&mut c) .await .unwrap(); assert!( v.contains("connected_clients"), "expected connected_clients, got: {}", v ); let v: String = redis::cmd("INFO") .arg("stats") .query_async(&mut c) .await .unwrap(); assert!( v.contains("total_connections_received"), "expected total_connections_received, got: {}", v ); // Invalid section must_fail!(c, "INFO", "bogus"; "not supported"); } // ── SWAPDB ────────────────────────────────────────────────────────── #[tokio::test] async fn test_swapdb() { let (_m, mut c) = start().await; // Set key in DB 0 must_ok!(c, "SET", "key0", "val0"); // Switch to DB 1 and set key must_ok!(c, "SELECT", "1"); must_ok!(c, "SET", "key1", "val1"); // Swap DB 0 and DB 1 must_ok!(c, "SWAPDB", "0", "1"); // Now DB 1 should have key0 must_str!(c, "GET", "key0"; "val0"); must_nil!(c, "GET", "key1"); // Switch to DB 0, should have key1 must_ok!(c, "SELECT", "0"); must_str!(c, "GET", "key1"; "val1"); must_nil!(c, "GET", "key0"); } #[tokio::test] async fn test_swapdb_errors() { let (_m, mut c) = start().await; must_fail!(c, "SWAPDB"; "wrong number of arguments"); must_fail!(c, "SWAPDB", "0"; "wrong number of arguments"); must_fail!(c, "SWAPDB", "abc", "0"; "invalid first DB index"); must_fail!(c, "SWAPDB", "0", "abc"; "invalid second DB index"); must_fail!(c, "SWAPDB", "0", "99"; "DB index is out of range"); must_fail!(c, "SWAPDB", "-1", "0"; "DB index is out of range"); } // ── MEMORY USAGE ──────────────────────────────────────────────────── #[tokio::test] async fn test_memory_usage() { let (_m, mut c) = start().await; // Non-existent key → nil must_nil!(c, "MEMORY", "USAGE", "nosuch"); // String key must_ok!(c, "SET", "foo", "bar"); let v: i64 = redis::cmd("MEMORY") .arg("USAGE") .arg("foo") .query_async(&mut c) .await .unwrap(); assert!(v > 0, "expected positive memory usage, got {}", v); } #[tokio::test] async fn test_memory_errors() { let (_m, mut c) = start().await; must_fail!(c, "MEMORY"; "wrong number of arguments"); must_fail!(c, "MEMORY", "USAGE"; "wrong number of arguments"); must_fail!(c, "MEMORY", "BOGUS"; "unknown subcommand"); } ================================================ FILE: miniredis/tests/cmd_set.rs ================================================ // Ported from ../miniredis/cmd_set_test.go mod helpers; #[tokio::test] async fn test_sadd() { let (m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "a", "b", "c"; 3); must_int!(c, "SCARD", "s"; 3); must_int!(c, "SADD", "s", "a", "b", "d"; 1); // only d is new // SMEMBERS must_strs_sorted!(c, "SMEMBERS", "s"; ["a", "b", "c", "d"]); // Non-existing let members: Vec = redis::cmd("SMEMBERS") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert!(members.is_empty()); // Direct API assert_eq!(m.key_type("s"), "set"); // Errors must_fail!(c, "SADD"; "wrong number of arguments"); must_fail!(c, "SADD", "s"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "SADD", "str", "x"; "WRONGTYPE"); must_fail!(c, "SMEMBERS", "str"; "WRONGTYPE"); must_fail!(c, "SCARD", "str"; "WRONGTYPE"); } #[tokio::test] async fn test_sismember() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "a", "b"; 2); must_int!(c, "SISMEMBER", "s", "a"; 1); must_int!(c, "SISMEMBER", "s", "b"; 1); must_int!(c, "SISMEMBER", "s", "nosuch"; 0); // Non-existing key must_int!(c, "SISMEMBER", "nosuch", "a"; 0); // Errors must_fail!(c, "SISMEMBER"; "wrong number of arguments"); must_fail!(c, "SISMEMBER", "s"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "SISMEMBER", "str", "x"; "WRONGTYPE"); } #[tokio::test] async fn test_smismember() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "a", "b", "c"; 3); let result: Vec = redis::cmd("SMISMEMBER") .arg("s") .arg("a") .arg("x") .arg("c") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![1, 0, 1]); // Non-existing key let result: Vec = redis::cmd("SMISMEMBER") .arg("nosuch") .arg("a") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec![0, 0]); // Errors must_fail!(c, "SMISMEMBER"; "wrong number of arguments"); must_fail!(c, "SMISMEMBER", "s"; "wrong number of arguments"); } #[tokio::test] async fn test_srem() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "a", "b", "c"; 3); must_int!(c, "SREM", "s", "a", "d"; 1); must_strs_sorted!(c, "SMEMBERS", "s"; ["b", "c"]); // Remove from non-existing key must_int!(c, "SREM", "nosuch", "a"; 0); // Errors must_fail!(c, "SREM"; "wrong number of arguments"); must_fail!(c, "SREM", "s"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "SREM", "str", "x"; "WRONGTYPE"); } #[tokio::test] async fn test_smove() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "src", "a", "b", "c"; 3); must_int!(c, "SADD", "dst", "x"; 1); must_int!(c, "SMOVE", "src", "dst", "a"; 1); must_strs_sorted!(c, "SMEMBERS", "src"; ["b", "c"]); must_strs_sorted!(c, "SMEMBERS", "dst"; ["a", "x"]); // Move non-existing member must_int!(c, "SMOVE", "src", "dst", "nosuch"; 0); // Move from non-existing key must_int!(c, "SMOVE", "nosuch", "dst", "a"; 0); // Move last member must_int!(c, "SADD", "single", "x"; 1); must_int!(c, "SMOVE", "single", "dst", "x"; 1); // Errors must_fail!(c, "SMOVE"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "SMOVE", "str", "dst", "x"; "WRONGTYPE"); must_fail!(c, "SMOVE", "src", "str", "x"; "WRONGTYPE"); } #[tokio::test] async fn test_sdiff() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b", "c"; 3); must_int!(c, "SADD", "s2", "b", "c", "d"; 3); must_strs_sorted!(c, "SDIFF", "s1", "s2"; ["a"]); // Single set must_strs_sorted!(c, "SDIFF", "s1"; ["a", "b", "c"]); // Three sets must_int!(c, "SADD", "s3", "a"; 1); let result: Vec = redis::cmd("SDIFF") .arg("s1") .arg("s2") .arg("s3") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); // Non-existing key must_strs_sorted!(c, "SDIFF", "s1", "nosuch"; ["a", "b", "c"]); // Errors must_fail!(c, "SDIFF"; "wrong number of arguments"); } #[tokio::test] async fn test_sdiffstore() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b", "c"; 3); must_int!(c, "SADD", "s2", "b", "c", "d"; 3); must_int!(c, "SDIFFSTORE", "dst", "s1", "s2"; 1); must_strs_sorted!(c, "SMEMBERS", "dst"; ["a"]); // Errors must_fail!(c, "SDIFFSTORE"; "wrong number of arguments"); must_fail!(c, "SDIFFSTORE", "dst"; "wrong number of arguments"); } #[tokio::test] async fn test_sinter() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b", "c"; 3); must_int!(c, "SADD", "s2", "b", "c", "d"; 3); must_strs_sorted!(c, "SINTER", "s1", "s2"; ["b", "c"]); // Single set must_strs_sorted!(c, "SINTER", "s1"; ["a", "b", "c"]); // Three sets must_int!(c, "SADD", "s3", "b"; 1); must_strs_sorted!(c, "SINTER", "s1", "s2", "s3"; ["b"]); // Non-existing key → empty let result: Vec = redis::cmd("SINTER") .arg("s1") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); // Errors must_fail!(c, "SINTER"; "wrong number of arguments"); } #[tokio::test] async fn test_sinterstore() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b", "c"; 3); must_int!(c, "SADD", "s2", "b", "c", "d"; 3); must_int!(c, "SINTERSTORE", "dst", "s1", "s2"; 2); must_strs_sorted!(c, "SMEMBERS", "dst"; ["b", "c"]); // Empty intersection with non-existing key must_int!(c, "SINTERSTORE", "dst2", "s1", "nosuch"; 0); // Errors must_fail!(c, "SINTERSTORE"; "wrong number of arguments"); must_fail!(c, "SINTERSTORE", "dst"; "wrong number of arguments"); } #[tokio::test] async fn test_sunion() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b"; 2); must_int!(c, "SADD", "s2", "b", "c"; 2); must_strs_sorted!(c, "SUNION", "s1", "s2"; ["a", "b", "c"]); // Single set must_strs_sorted!(c, "SUNION", "s1"; ["a", "b"]); // Three sets must_int!(c, "SADD", "s3", "d"; 1); must_strs_sorted!(c, "SUNION", "s1", "s2", "s3"; ["a", "b", "c", "d"]); // Non-existing key must_strs_sorted!(c, "SUNION", "s1", "nosuch"; ["a", "b"]); // Errors must_fail!(c, "SUNION"; "wrong number of arguments"); } #[tokio::test] async fn test_sunionstore() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b"; 2); must_int!(c, "SADD", "s2", "b", "c"; 2); must_int!(c, "SUNIONSTORE", "dst", "s1", "s2"; 3); must_strs_sorted!(c, "SMEMBERS", "dst"; ["a", "b", "c"]); // Errors must_fail!(c, "SUNIONSTORE"; "wrong number of arguments"); must_fail!(c, "SUNIONSTORE", "dst"; "wrong number of arguments"); } #[tokio::test] async fn test_scard() { let (_m, mut c) = helpers::start().await; must_int!(c, "SCARD", "nosuch"; 0); must_int!(c, "SADD", "s", "a", "b", "c"; 3); must_int!(c, "SCARD", "s"; 3); // Errors must_fail!(c, "SCARD"; "wrong number of arguments"); } #[tokio::test] async fn test_set_wrongtype() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "str", "val"); must_fail!(c, "SADD", "str", "x"; "WRONGTYPE"); must_fail!(c, "SREM", "str", "x"; "WRONGTYPE"); must_fail!(c, "SCARD", "str"; "WRONGTYPE"); must_fail!(c, "SMEMBERS", "str"; "WRONGTYPE"); must_fail!(c, "SISMEMBER", "str", "x"; "WRONGTYPE"); } #[tokio::test] async fn test_spop() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "aap", "noot"; 2); // SPOP returns a member let v: String = redis::cmd("SPOP") .arg("s") .query_async(&mut c) .await .unwrap(); assert!(v == "aap" || v == "noot"); // One left, pop it let v2: String = redis::cmd("SPOP") .arg("s") .query_async(&mut c) .await .unwrap(); assert!(v2 == "aap" || v2 == "noot"); assert_ne!(v, v2); // Key is now gone must_int!(c, "SCARD", "s"; 0); // Non-existing key must_nil!(c, "SPOP", "nosuch"); // With count must_int!(c, "SADD", "s2", "a", "b", "c", "d"; 4); let vals: Vec = redis::cmd("SPOP") .arg("s2") .arg(2) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 2); must_int!(c, "SCARD", "s2"; 2); // Errors must_ok!(c, "SET", "str", "val"); must_fail!(c, "SPOP", "str"; "WRONGTYPE"); must_fail!(c, "SPOP", "str", "-12"; "out of range"); } #[tokio::test] async fn test_srandmember() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s", "aap", "noot", "mies"; 3); // Without count let v: String = redis::cmd("SRANDMEMBER") .arg("s") .query_async(&mut c) .await .unwrap(); assert!(v == "aap" || v == "noot" || v == "mies"); // Set still has all members (non-destructive) must_int!(c, "SCARD", "s"; 3); // Positive count let vals: Vec = redis::cmd("SRANDMEMBER") .arg("s") .arg(2) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 2); // No duplicates with positive count assert_ne!(vals[0], vals[1]); // Positive count larger than set let vals: Vec = redis::cmd("SRANDMEMBER") .arg("s") .arg(10) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 3); // Negative count allows duplicates let vals: Vec = redis::cmd("SRANDMEMBER") .arg("s") .arg(-5) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 5); // Non-existing key must_nil!(c, "SRANDMEMBER", "nosuch"); let vals: Vec = redis::cmd("SRANDMEMBER") .arg("nosuch") .arg(1) .query_async(&mut c) .await .unwrap(); assert!(vals.is_empty()); // Errors must_fail!(c, "SRANDMEMBER"; "wrong number of arguments"); must_fail!(c, "SRANDMEMBER", "s", "noint"; "not an integer"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "SRANDMEMBER", "str"; "WRONGTYPE"); } #[tokio::test] async fn test_sscan() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "set", "value1", "value2"; 2); // Basic scan let (cursor, vals): (String, Vec) = redis::cmd("SSCAN") .arg("set") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); let mut sorted = vals.clone(); sorted.sort(); assert_eq!(sorted, vec!["value1", "value2"]); // MATCH must_int!(c, "SADD", "s2", "aap", "noot", "mies"; 3); let (cursor, vals): (String, Vec) = redis::cmd("SSCAN") .arg("s2") .arg(0) .arg("MATCH") .arg("mi*") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(vals, vec!["mies"]); // COUNT (accepted but ignored in miniredis) let (cursor, _vals): (String, Vec) = redis::cmd("SSCAN") .arg("set") .arg(0) .arg("COUNT") .arg(200) .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); // Errors must_fail!(c, "SSCAN"; "wrong number of arguments"); must_fail!(c, "SSCAN", "set"; "wrong number of arguments"); must_fail!(c, "SSCAN", "set", "noint"; "invalid cursor"); must_fail!(c, "SSCAN", "set", "0", "MATCH"; "syntax error"); must_fail!(c, "SSCAN", "set", "0", "COUNT"; "syntax error"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "SSCAN", "str", "0"; "WRONGTYPE"); } #[tokio::test] async fn test_sintercard() { let (_m, mut c) = helpers::start().await; must_int!(c, "SADD", "s1", "a", "b", "c"; 3); must_int!(c, "SADD", "s2", "b", "c", "d"; 3); // Basic must_int!(c, "SINTERCARD", "2", "s1", "s2"; 2); // With LIMIT > result must_int!(c, "SINTERCARD", "2", "s1", "s2", "LIMIT", "15"; 2); // LIMIT 0 (unlimited) must_int!(c, "SINTERCARD", "2", "s1", "s2", "LIMIT", "0"; 2); // LIMIT 1 must_int!(c, "SINTERCARD", "2", "s1", "s2", "LIMIT", "1"; 1); // Multi intersection must_int!(c, "SADD", "s3", "c", "d", "e"; 3); must_int!(c, "SINTERCARD", "3", "s1", "s2", "s3"; 1); // Non-existing key must_int!(c, "SINTERCARD", "2", "s1", "NOT_A_KEY"; 0); // Errors must_fail!(c, "SINTERCARD", "two", "k1", "k2"; "numkeys"); must_fail!(c, "SINTERCARD", "2", "k1", "k2", "LIMIT", "five"; "not an integer"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "SINTERCARD", "1", "str"; "WRONGTYPE"); } ================================================ FILE: miniredis/tests/cmd_sorted_set.rs ================================================ // Ported from ../miniredis/cmd_sorted_set_test.go mod helpers; #[tokio::test] async fn test_zadd() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); must_int!(c, "ZCARD", "z"; 3); // Update existing must_int!(c, "ZADD", "z", "4", "a"; 0); must_int!(c, "ZCARD", "z"; 3); // Errors must_fail!(c, "ZADD"; "wrong number of arguments"); must_fail!(c, "ZADD", "z"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "ZADD", "str", "1", "a"; "WRONGTYPE"); } #[tokio::test] async fn test_zadd_nx_xx() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b"; 2); // NX: only add new must_int!(c, "ZADD", "z", "NX", "10", "a", "3", "c"; 1); // a should still be 1 let score: f64 = redis::cmd("ZSCORE") .arg("z") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 1.0); // XX: only update existing must_int!(c, "ZADD", "z", "XX", "10", "a", "4", "d"; 0); // a should be updated let score: f64 = redis::cmd("ZSCORE") .arg("z") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 10.0); // d should not exist let d: Option = redis::cmd("ZSCORE") .arg("z") .arg("d") .query_async(&mut c) .await .unwrap(); assert!(d.is_none()); // XX and NX together must_fail!(c, "ZADD", "z", "XX", "NX", "1", "a"; "XX and NX"); } #[tokio::test] async fn test_zadd_ch() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b"; 2); // CH: count changed (new + updated) must_int!(c, "ZADD", "z", "CH", "10", "a", "2", "b", "3", "c"; 2); // a was updated (1->10), c was new = 2 changes } #[tokio::test] async fn test_zadd_incr() { let (_m, mut c) = helpers::start().await; // INCR mode let score: String = redis::cmd("ZADD") .arg("z") .arg("INCR") .arg("5") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, "5"); let score: String = redis::cmd("ZADD") .arg("z") .arg("INCR") .arg("3") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, "8"); } #[tokio::test] async fn test_zscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1.5", "a", "2.5", "b"; 2); let score: f64 = redis::cmd("ZSCORE") .arg("z") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 1.5); // Non-existing member let score: Option = redis::cmd("ZSCORE") .arg("z") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert!(score.is_none()); // Non-existing key must_nil!(c, "ZSCORE", "nosuch", "a"); // Errors must_fail!(c, "ZSCORE"; "wrong number of arguments"); } #[tokio::test] async fn test_zmscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b"; 2); let scores: Vec> = redis::cmd("ZMSCORE") .arg("z") .arg("a") .arg("nosuch") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(scores, vec![Some(1.0), None, Some(2.0)]); } #[tokio::test] async fn test_zincrby() { let (_m, mut c) = helpers::start().await; let score: String = redis::cmd("ZINCRBY") .arg("z") .arg("1.5") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, "1.5"); let score: String = redis::cmd("ZINCRBY") .arg("z") .arg("2.5") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(score, "4"); // Errors must_fail!(c, "ZINCRBY"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "val"); must_fail!(c, "ZINCRBY", "str", "1", "a"; "WRONGTYPE"); } #[tokio::test] async fn test_zrank() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); must_int!(c, "ZRANK", "z", "a"; 0); must_int!(c, "ZRANK", "z", "b"; 1); must_int!(c, "ZRANK", "z", "c"; 2); // Non-existing member must_nil!(c, "ZRANK", "z", "nosuch"); // Non-existing key must_nil!(c, "ZRANK", "nosuch", "a"); // ZREVRANK must_int!(c, "ZREVRANK", "z", "a"; 2); must_int!(c, "ZREVRANK", "z", "c"; 0); } #[tokio::test] async fn test_zrem() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); must_int!(c, "ZREM", "z", "a", "nosuch"; 1); must_int!(c, "ZCARD", "z"; 2); // Non-existing key must_int!(c, "ZREM", "nosuch", "a"; 0); // Errors must_fail!(c, "ZREM"; "wrong number of arguments"); must_fail!(c, "ZREM", "z"; "wrong number of arguments"); } #[tokio::test] async fn test_zrange() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c", "4", "d"; 4); must_strs!(c, "ZRANGE", "z", "0", "-1"; ["a", "b", "c", "d"]); must_strs!(c, "ZRANGE", "z", "1", "2"; ["b", "c"]); must_strs!(c, "ZRANGE", "z", "0", "0"; ["a"]); // Empty range let result: Vec = redis::cmd("ZRANGE") .arg("z") .arg("10") .arg("20") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); // Non-existing key let result: Vec = redis::cmd("ZRANGE") .arg("nosuch") .arg("0") .arg("-1") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_zrevrange() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); must_strs!(c, "ZREVRANGE", "z", "0", "-1"; ["c", "b", "a"]); must_strs!(c, "ZREVRANGE", "z", "0", "1"; ["c", "b"]); } #[tokio::test] async fn test_zrangebyscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c", "4", "d"; 4); must_strs!(c, "ZRANGEBYSCORE", "z", "-inf", "+inf"; ["a", "b", "c", "d"]); must_strs!(c, "ZRANGEBYSCORE", "z", "2", "3"; ["b", "c"]); must_strs!(c, "ZRANGEBYSCORE", "z", "(1", "3"; ["b", "c"]); must_strs!(c, "ZRANGEBYSCORE", "z", "1", "(3"; ["a", "b"]); // LIMIT must_strs!(c, "ZRANGEBYSCORE", "z", "-inf", "+inf", "LIMIT", "1", "2"; ["b", "c"]); // Empty let result: Vec = redis::cmd("ZRANGEBYSCORE") .arg("z") .arg("10") .arg("20") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_zrevrangebyscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c", "4", "d"; 4); must_strs!(c, "ZREVRANGEBYSCORE", "z", "+inf", "-inf"; ["d", "c", "b", "a"]); must_strs!(c, "ZREVRANGEBYSCORE", "z", "3", "2"; ["c", "b"]); } #[tokio::test] async fn test_zrangebylex() { let (_m, mut c) = helpers::start().await; // All same score for lex ordering must_int!(c, "ZADD", "z", "0", "a", "0", "b", "0", "c", "0", "d"; 4); must_strs!(c, "ZRANGEBYLEX", "z", "-", "+"; ["a", "b", "c", "d"]); must_strs!(c, "ZRANGEBYLEX", "z", "[b", "[c"; ["b", "c"]); must_strs!(c, "ZRANGEBYLEX", "z", "(a", "[c"; ["b", "c"]); must_strs!(c, "ZRANGEBYLEX", "z", "[b", "(d"; ["b", "c"]); // LIMIT must_strs!(c, "ZRANGEBYLEX", "z", "-", "+", "LIMIT", "1", "2"; ["b", "c"]); } #[tokio::test] async fn test_zrevrangebylex() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "0", "a", "0", "b", "0", "c", "0", "d"; 4); must_strs!(c, "ZREVRANGEBYLEX", "z", "+", "-"; ["d", "c", "b", "a"]); must_strs!(c, "ZREVRANGEBYLEX", "z", "[c", "[b"; ["c", "b"]); } #[tokio::test] async fn test_zlexcount() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "0", "a", "0", "b", "0", "c", "0", "d"; 4); must_int!(c, "ZLEXCOUNT", "z", "-", "+"; 4); must_int!(c, "ZLEXCOUNT", "z", "[b", "[c"; 2); must_int!(c, "ZLEXCOUNT", "z", "(a", "[c"; 2); // Non-existing key must_int!(c, "ZLEXCOUNT", "nosuch", "-", "+"; 0); } #[tokio::test] async fn test_zcount() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); must_int!(c, "ZCOUNT", "z", "-inf", "+inf"; 3); must_int!(c, "ZCOUNT", "z", "1", "2"; 2); must_int!(c, "ZCOUNT", "z", "(1", "3"; 2); // Non-existing key must_int!(c, "ZCOUNT", "nosuch", "-inf", "+inf"; 0); } #[tokio::test] async fn test_zremrangebyrank() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c", "4", "d"; 4); must_int!(c, "ZREMRANGEBYRANK", "z", "1", "2"; 2); must_strs!(c, "ZRANGE", "z", "0", "-1"; ["a", "d"]); } #[tokio::test] async fn test_zremrangebyscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c", "4", "d"; 4); must_int!(c, "ZREMRANGEBYSCORE", "z", "2", "3"; 2); must_strs!(c, "ZRANGE", "z", "0", "-1"; ["a", "d"]); } #[tokio::test] async fn test_zremrangebylex() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "0", "a", "0", "b", "0", "c", "0", "d"; 4); must_int!(c, "ZREMRANGEBYLEX", "z", "[b", "[c"; 2); must_strs!(c, "ZRANGE", "z", "0", "-1"; ["a", "d"]); } #[tokio::test] async fn test_zunionstore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z1", "1", "a", "2", "b"; 2); must_int!(c, "ZADD", "z2", "3", "b", "4", "c"; 2); must_int!(c, "ZUNIONSTORE", "dst", "2", "z1", "z2"; 3); // a=1, b=2+3=5, c=4 let score: f64 = redis::cmd("ZSCORE") .arg("dst") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 5.0); // Non-existing key must_int!(c, "ZUNIONSTORE", "dst2", "2", "z1", "nosuch"; 2); // Errors must_fail!(c, "ZUNIONSTORE"; "wrong number of arguments"); } #[tokio::test] async fn test_zinterstore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z1", "1", "a", "2", "b", "3", "c"; 3); must_int!(c, "ZADD", "z2", "10", "b", "20", "c", "30", "d"; 3); must_int!(c, "ZINTERSTORE", "dst", "2", "z1", "z2"; 2); // b=2+10=12, c=3+20=23 let score: f64 = redis::cmd("ZSCORE") .arg("dst") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 12.0); // WEIGHTS must_int!(c, "ZINTERSTORE", "dst2", "2", "z1", "z2", "WEIGHTS", "2", "1"; 2); let score: f64 = redis::cmd("ZSCORE") .arg("dst2") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 14.0); // 2*2 + 10*1 // AGGREGATE MIN must_int!(c, "ZINTERSTORE", "dst3", "2", "z1", "z2", "AGGREGATE", "MIN"; 2); let score: f64 = redis::cmd("ZSCORE") .arg("dst3") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(score, 2.0); } #[tokio::test] async fn test_zpopmin_zpopmax() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "a", "2", "b", "3", "c"; 3); // ZPOPMIN let result: Vec = redis::cmd("ZPOPMIN") .arg("z") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["a", "1"]); // ZPOPMAX let result: Vec = redis::cmd("ZPOPMAX") .arg("z") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["c", "3"]); must_int!(c, "ZCARD", "z"; 1); // Empty set let result: Vec = redis::cmd("ZPOPMIN") .arg("nosuch") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_zrange_withscores() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1.5", "a", "2.5", "b"; 2); let result: Vec = redis::cmd("ZRANGE") .arg("z") .arg("0") .arg("-1") .arg("WITHSCORES") .query_async(&mut c) .await .unwrap(); assert_eq!(result, vec!["a", "1.5", "b", "2.5"]); } #[tokio::test] async fn test_sorted_set_wrongtype() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "str", "val"); must_fail!(c, "ZADD", "str", "1", "a"; "WRONGTYPE"); must_fail!(c, "ZCARD", "str"; "WRONGTYPE"); must_fail!(c, "ZSCORE", "str", "a"; "WRONGTYPE"); must_fail!(c, "ZRANK", "str", "a"; "WRONGTYPE"); must_fail!(c, "ZREM", "str", "a"; "WRONGTYPE"); must_fail!(c, "ZRANGE", "str", "0", "-1"; "WRONGTYPE"); } #[tokio::test] async fn test_zinter() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("ZADD") .arg("h1") .arg(1) .arg("field1") .arg(2) .arg("field2") .arg(3) .arg("field3") .query_async(&mut c) .await .unwrap(); let _: i64 = redis::cmd("ZADD") .arg("h2") .arg(1) .arg("field1") .arg(2) .arg("field2") .arg(4) .arg("field4") .query_async(&mut c) .await .unwrap(); // Basic intersection must_strs!(c, "ZINTER", "2", "h1", "h2"; ["field1", "field2"]); // With WITHSCORES must_strs!(c, "ZINTER", "2", "h1", "h2", "WITHSCORES"; ["field1", "2", "field2", "4"]); // Errors must_fail!(c, "ZINTER"; "wrong number of arguments"); must_fail!(c, "ZINTER", "noint", "k"; "not an integer"); } #[tokio::test] async fn test_zunion() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("ZADD") .arg("h1") .arg(1) .arg("field1") .arg(2) .arg("field2") .query_async(&mut c) .await .unwrap(); let _: i64 = redis::cmd("ZADD") .arg("h2") .arg(1) .arg("field1") .arg(2) .arg("field2") .query_async(&mut c) .await .unwrap(); // Basic union must_strs!(c, "ZUNION", "2", "h1", "h2"; ["field1", "field2"]); // With WITHSCORES (sum by default) must_strs!(c, "ZUNION", "2", "h1", "h2", "WITHSCORES"; ["field1", "2", "field2", "4"]); // AGGREGATE MIN must_strs!(c, "ZUNION", "2", "h1", "h2", "AGGREGATE", "MIN", "WITHSCORES"; ["field1", "1", "field2", "2"]); // Errors must_fail!(c, "ZUNION"; "wrong number of arguments"); must_fail!(c, "ZUNION", "2"; "wrong number of arguments"); must_fail!(c, "ZUNION", "noint", "k"; "not an integer"); } #[tokio::test] async fn test_zrandmember() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("ZADD") .arg("z") .arg(1) .arg("one") .arg(2) .arg("two") .arg(3) .arg("three") .query_async(&mut c) .await .unwrap(); // Without count let v: String = redis::cmd("ZRANDMEMBER") .arg("z") .query_async(&mut c) .await .unwrap(); assert!(v == "one" || v == "two" || v == "three"); // Nonexistent key must_nil!(c, "ZRANDMEMBER", "nosuch"); // Positive count let vals: Vec = redis::cmd("ZRANDMEMBER") .arg("z") .arg(2) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 2); // Positive count larger than set let vals: Vec = redis::cmd("ZRANDMEMBER") .arg("z") .arg(10) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 3); // Count 0 let vals: Vec = redis::cmd("ZRANDMEMBER") .arg("z") .arg(0) .query_async(&mut c) .await .unwrap(); assert!(vals.is_empty()); // Negative count let vals: Vec = redis::cmd("ZRANDMEMBER") .arg("z") .arg(-5) .query_async(&mut c) .await .unwrap(); assert_eq!(vals.len(), 5); // Nonexistent key with count let vals: Vec = redis::cmd("ZRANDMEMBER") .arg("nosuch") .arg(40) .query_async(&mut c) .await .unwrap(); assert!(vals.is_empty()); // Errors must_fail!(c, "ZRANDMEMBER"; "wrong number of arguments"); must_fail!(c, "ZRANDMEMBER", "z", "noint"; "not an integer"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "ZRANDMEMBER", "str", "1"; "WRONGTYPE"); } #[tokio::test] async fn test_issue10_float_scores() { let (_m, mut c) = helpers::start().await; // Regression: ZRANGEBYSCORE with exact float boundaries let _: i64 = redis::cmd("ZADD") .arg("key") .arg(3.3) .arg("element") .query_async(&mut c) .await .unwrap(); must_strs!(c, "ZRANGEBYSCORE", "key", "3.3", "3.3"; ["element"]); // No match let result: Vec = redis::cmd("ZRANGEBYSCORE") .arg("key") .arg("4.3") .arg("4.3") .query_async(&mut c) .await .unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn test_zscan() { let (_m, mut c) = helpers::start().await; let _: i64 = redis::cmd("ZADD") .arg("z") .arg(1.0) .arg("field1") .arg(2.0) .arg("field2") .query_async(&mut c) .await .unwrap(); // Basic scan let (cursor, vals): (String, Vec) = redis::cmd("ZSCAN") .arg("z") .arg(0) .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(vals.len(), 4); // field1, 1, field2, 2 assert!(vals.contains(&"field1".to_string())); assert!(vals.contains(&"field2".to_string())); // COUNT (accepted but ignored) let (cursor, vals): (String, Vec) = redis::cmd("ZSCAN") .arg("z") .arg(0) .arg("COUNT") .arg(200) .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(vals.len(), 4); // MATCH let _: i64 = redis::cmd("ZADD") .arg("z") .arg(3.0) .arg("aap") .arg(4.0) .arg("noot") .arg(5.0) .arg("mies") .query_async(&mut c) .await .unwrap(); let (cursor, vals): (String, Vec) = redis::cmd("ZSCAN") .arg("z") .arg(0) .arg("MATCH") .arg("mi*") .query_async(&mut c) .await .unwrap(); assert_eq!(cursor, "0"); assert_eq!(vals, vec!["mies", "5"]); // Errors must_fail!(c, "ZSCAN"; "wrong number of arguments"); must_fail!(c, "ZSCAN", "z"; "wrong number of arguments"); must_fail!(c, "ZSCAN", "z", "noint"; "invalid cursor"); must_fail!(c, "ZSCAN", "z", "0", "MATCH"; "syntax error"); must_fail!(c, "ZSCAN", "z", "0", "COUNT"; "syntax error"); must_ok!(c, "SET", "str", "val"); must_fail!(c, "ZSCAN", "str", "0"; "WRONGTYPE"); } #[tokio::test] async fn test_sorted_set_infinity() { let (_m, mut c) = helpers::start().await; // Add with infinity scores must_int!(c, "ZADD", "zinf", "inf", "plus_inf", "-inf", "minus_inf", "10", "ten"; 3); must_int!(c, "ZCARD", "zinf"; 3); // Check ordering: -inf, 10, +inf must_strs!(c, "ZRANGE", "zinf", "0", "-1"; ["minus_inf", "ten", "plus_inf"]); } #[tokio::test] async fn test_sorted_set_zrank_withscore() { let (_m, mut c) = helpers::start().await; must_int!(c, "ZADD", "z", "1", "one", "2", "two", "3", "three"; 3); // ZRANK with WITHSCORE let v: redis::Value = redis::cmd("ZRANK") .arg("z") .arg("three") .arg("WITHSCORE") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 2); assert_eq!(items[0], redis::Value::Int(2)); } _ => panic!("expected array from ZRANK WITHSCORE, got {:?}", v), } // ZREVRANK with WITHSCORE let v: redis::Value = redis::cmd("ZREVRANK") .arg("z") .arg("one") .arg("WITHSCORE") .query_async(&mut c) .await .unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 2); assert_eq!(items[0], redis::Value::Int(2)); } _ => panic!("expected array from ZREVRANK WITHSCORE, got {:?}", v), } } ================================================ FILE: miniredis/tests/cmd_stream.rs ================================================ mod helpers; use helpers::*; // ── XADD / XLEN ───────────────────────────────────────────────────── #[tokio::test] async fn test_xadd_basic() { let (_m, mut c) = start().await; // XADD with explicit ID let id: String = redis::cmd("XADD") .arg("s") .arg("1-1") .arg("name") .arg("a") .query_async(&mut c) .await .unwrap(); assert_eq!(id, "1-1"); must_int!(c, "XLEN", "s"; 1); // Second entry let id2: String = redis::cmd("XADD") .arg("s") .arg("2-1") .arg("name") .arg("b") .query_async(&mut c) .await .unwrap(); assert_eq!(id2, "2-1"); must_int!(c, "XLEN", "s"; 2); // TYPE must_str!(c, "TYPE", "s"; "stream"); } #[tokio::test] async fn test_xadd_auto_id() { let (_m, mut c) = start().await; // Auto-generated ID let id: String = redis::cmd("XADD") .arg("s") .arg("*") .arg("name") .arg("a") .query_async(&mut c) .await .unwrap(); assert!(id.contains('-'), "expected id with '-', got {}", id); must_int!(c, "XLEN", "s"; 1); } #[tokio::test] async fn test_xadd_partial_id() { let (_m, mut c) = start().await; // Partial auto-sequence: "123-*" let id: String = redis::cmd("XADD") .arg("s") .arg("123-*") .arg("name") .arg("a") .query_async(&mut c) .await .unwrap(); assert!( id.starts_with("123-"), "expected id starting with '123-', got {}", id ); // Second with same ms should increment seq let id2: String = redis::cmd("XADD") .arg("s") .arg("123-*") .arg("name") .arg("b") .query_async(&mut c) .await .unwrap(); assert!( id2.starts_with("123-"), "expected id starting with '123-', got {}", id2 ); assert_ne!(id, id2); } #[tokio::test] async fn test_xadd_maxlen() { let (_m, mut c) = start().await; // Add entries with MAXLEN trimming for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg("MAXLEN") .arg("3") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_int!(c, "XLEN", "s"; 3); } #[tokio::test] async fn test_xadd_minid() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // XADD with MINID trimming redis::cmd("XADD") .arg("s") .arg("MINID") .arg("4") .arg("6-0") .arg("v") .arg("6") .query_async::(&mut c) .await .unwrap(); // Only entries >= 4-0 should remain (4-0, 5-0, 6-0) must_int!(c, "XLEN", "s"; 3); } #[tokio::test] async fn test_xadd_nomkstream() { let (_m, mut c) = start().await; // NOMKSTREAM on non-existing key should not create stream let result: redis::RedisResult> = redis::cmd("XADD") .arg("nosuch") .arg("NOMKSTREAM") .arg("*") .arg("field") .arg("value") .query_async(&mut c) .await; match result { Ok(None) => {} // Expected: nil response Ok(Some(v)) => panic!("expected nil, got {:?}", v), Err(e) => panic!("unexpected error: {:?}", e), } must_int!(c, "XLEN", "nosuch"; 0); } #[tokio::test] async fn test_xadd_errors() { let (_m, mut c) = start().await; // Wrong number of args must_fail!(c, "XADD"; "wrong number of arguments"); must_fail!(c, "XADD", "s"; "wrong number of arguments"); must_fail!(c, "XADD", "s", "*"; "wrong number of arguments"); // Odd field-value pairs must_fail!(c, "XADD", "s", "*", "field"; "wrong number of arguments"); // Invalid ID (0-0) must_fail!(c, "XADD", "s", "0-0", "f", "v"; "must be greater than 0-0"); // Wrong type must_ok!(c, "SET", "str", "value"); must_fail!(c, "XADD", "str", "*", "f", "v"; "WRONGTYPE"); } #[tokio::test] async fn test_xadd_duplicate_id() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-1") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); // Same ID should fail must_fail!(c, "XADD", "s", "1-1", "f", "v"; "equal or smaller"); // Smaller ID should also fail must_fail!(c, "XADD", "s", "1-0", "f", "v"; "equal or smaller"); } // ── XLEN ───────────────────────────────────────────────────────────── #[tokio::test] async fn test_xlen() { let (_m, mut c) = start().await; must_int!(c, "XLEN", "nosuch"; 0); redis::cmd("XADD") .arg("s") .arg("1-1") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_int!(c, "XLEN", "s"; 1); // Wrong type must_ok!(c, "SET", "str", "value"); must_fail!(c, "XLEN", "str"; "WRONGTYPE"); } // ── XRANGE / XREVRANGE ────────────────────────────────────────────── #[tokio::test] async fn test_xrange() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Full range let result: redis::Value = redis::cmd("XRANGE") .arg("s") .arg("-") .arg("+") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 5); // Partial range let result: redis::Value = redis::cmd("XRANGE") .arg("s") .arg("2") .arg("4") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 3); // With COUNT let result: redis::Value = redis::cmd("XRANGE") .arg("s") .arg("-") .arg("+") .arg("COUNT") .arg("2") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 2); } #[tokio::test] async fn test_xrevrange() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Full reverse range let result: redis::Value = redis::cmd("XREVRANGE") .arg("s") .arg("+") .arg("-") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 5); // First entry should be the highest ID let first = as_array(&entries[0]); let id = as_string(&first[0]); assert_eq!(id, "5-0"); // With COUNT let result: redis::Value = redis::cmd("XREVRANGE") .arg("s") .arg("+") .arg("-") .arg("COUNT") .arg("2") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 2); } #[tokio::test] async fn test_xrange_errors() { let (_m, mut c) = start().await; must_fail!(c, "XRANGE"; "wrong number of arguments"); must_fail!(c, "XRANGE", "s"; "wrong number of arguments"); must_fail!(c, "XRANGE", "s", "-"; "wrong number of arguments"); // Wrong type must_ok!(c, "SET", "str", "value"); must_fail!(c, "XRANGE", "str", "-", "+"; "WRONGTYPE"); } // ── XREAD ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_xread() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Read all from beginning let result: redis::Value = redis::cmd("XREAD") .arg("STREAMS") .arg("s") .arg("0") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); assert_eq!(streams.len(), 1); // stream = [name, entries] let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 3); // Read from after 1-0 let result: redis::Value = redis::cmd("XREAD") .arg("STREAMS") .arg("s") .arg("1-0") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 2); } #[tokio::test] async fn test_xread_count() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } let result: redis::Value = redis::cmd("XREAD") .arg("COUNT") .arg("2") .arg("STREAMS") .arg("s") .arg("0") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 2); } #[tokio::test] async fn test_xread_multi_streams() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s1") .arg("1-0") .arg("v") .arg("1") .query_async::(&mut c) .await .unwrap(); redis::cmd("XADD") .arg("s2") .arg("1-0") .arg("v") .arg("2") .query_async::(&mut c) .await .unwrap(); let result: redis::Value = redis::cmd("XREAD") .arg("STREAMS") .arg("s1") .arg("s2") .arg("0") .arg("0") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); assert_eq!(streams.len(), 2); } #[tokio::test] async fn test_xread_errors() { let (_m, mut c) = start().await; must_fail!(c, "XREAD"; "wrong number of arguments"); must_fail!(c, "XREAD", "STREAMS"; "wrong number of arguments"); } // ── XDEL ───────────────────────────────────────────────────────────── #[tokio::test] async fn test_xdel() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Delete one entry must_int!(c, "XDEL", "s", "2-0"; 1); must_int!(c, "XLEN", "s"; 2); // Delete already-deleted must_int!(c, "XDEL", "s", "2-0"; 0); // Delete non-existing key must_int!(c, "XDEL", "nosuch", "1-0"; 0); } // ── XTRIM ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_xtrim_maxlen() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Trim to 3 must_int!(c, "XTRIM", "s", "MAXLEN", "3"; 2); must_int!(c, "XLEN", "s"; 3); // Check first remaining entry is 3-0 let result: redis::Value = redis::cmd("XRANGE") .arg("s") .arg("-") .arg("+") .arg("COUNT") .arg("1") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); let first = as_array(&entries[0]); assert_eq!(as_string(&first[0]), "3-0"); } #[tokio::test] async fn test_xtrim_minid() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } // Trim to MINID 3 must_int!(c, "XTRIM", "s", "MINID", "3"; 2); must_int!(c, "XLEN", "s"; 3); } // ── XINFO ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_xinfo_stream() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } let result: redis::Value = redis::cmd("XINFO") .arg("STREAM") .arg("s") .query_async(&mut c) .await .unwrap(); let items = as_array(&result); // Should contain key-value pairs: length, groups, last-generated-id, ... assert!( items.len() >= 6, "expected at least 6 items, got {}", items.len() ); // Find "length" key let mut found_length = false; for chunk in items.chunks(2) { if as_string(&chunk[0]) == "length" { assert_eq!(as_int(&chunk[1]), 3); found_length = true; } } assert!(found_length, "expected 'length' field in XINFO STREAM"); } #[tokio::test] async fn test_xinfo_groups() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); let result: redis::Value = redis::cmd("XINFO") .arg("GROUPS") .arg("s") .query_async(&mut c) .await .unwrap(); let groups = as_array(&result); assert_eq!(groups.len(), 1); } #[tokio::test] async fn test_xinfo_errors() { let (_m, mut c) = start().await; must_fail!(c, "XINFO"; "wrong number of arguments"); must_fail!(c, "XINFO", "STREAM", "nosuch"; "no such key"); } // ── XGROUP ─────────────────────────────────────────────────────────── #[tokio::test] async fn test_xgroup_create() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // Duplicate group must_fail!(c, "XGROUP", "CREATE", "s", "g1", "0"; "BUSYGROUP"); } #[tokio::test] async fn test_xgroup_create_mkstream() { let (_m, mut c) = start().await; // Create group on non-existing stream with MKSTREAM must_ok!(c, "XGROUP", "CREATE", "nosuch", "g1", "0", "MKSTREAM"); must_int!(c, "XLEN", "nosuch"; 0); // Stream exists now, even though empty must_str!(c, "TYPE", "nosuch"; "stream"); } #[tokio::test] async fn test_xgroup_destroy() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); must_int!(c, "XGROUP", "DESTROY", "s", "g1"; 1); must_int!(c, "XGROUP", "DESTROY", "s", "g1"; 0); } #[tokio::test] async fn test_xgroup_createconsumer() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); must_int!(c, "XGROUP", "CREATECONSUMER", "s", "g1", "c1"; 1); // Already exists must_int!(c, "XGROUP", "CREATECONSUMER", "s", "g1", "c1"; 0); } #[tokio::test] async fn test_xgroup_delconsumer() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); must_int!(c, "XGROUP", "CREATECONSUMER", "s", "g1", "c1"; 1); must_int!(c, "XGROUP", "DELCONSUMER", "s", "g1", "c1"; 0); // 0 pending } #[tokio::test] async fn test_xgroup_errors() { let (_m, mut c) = start().await; must_fail!(c, "XGROUP"; "wrong number of arguments"); // Non-existing stream without MKSTREAM must_fail!(c, "XGROUP", "CREATE", "nosuch", "g1", "0"; "requires the key to exist"); } // ── XREADGROUP ─────────────────────────────────────────────────────── #[tokio::test] async fn test_xreadgroup() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // Read new entries let result: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); assert_eq!(streams.len(), 1); let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 3); } #[tokio::test] async fn test_xreadgroup_count() { let (_m, mut c) = start().await; for i in 1..=5 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // Read with COUNT let result: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("COUNT") .arg("2") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 2); } #[tokio::test] async fn test_xreadgroup_redelivery() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("v") .arg("1") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // First read - new message let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // Re-read from PEL let result: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg("0") .query_async(&mut c) .await .unwrap(); let streams = as_array(&result); let stream = as_array(&streams[0]); let entries = as_array(&stream[1]); assert_eq!(entries.len(), 1); } #[tokio::test] async fn test_xreadgroup_nogroup() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_fail!(c, "XREADGROUP", "GROUP", "nosuch", "c1", "STREAMS", "s", ">"; "NOGROUP"); } // ── XACK ───────────────────────────────────────────────────────────── #[tokio::test] async fn test_xack() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // Read all let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // ACK one must_int!(c, "XACK", "s", "g1", "1-0"; 1); // ACK same again = 0 must_int!(c, "XACK", "s", "g1", "1-0"; 0); // ACK multiple must_int!(c, "XACK", "s", "g1", "2-0", "3-0"; 2); } // ── XPENDING ───────────────────────────────────────────────────────── #[tokio::test] async fn test_xpending_summary() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // Read all let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // Summary mode let result: redis::Value = redis::cmd("XPENDING") .arg("s") .arg("g1") .query_async(&mut c) .await .unwrap(); let items = as_array(&result); assert_eq!(items.len(), 4); // First item: count assert_eq!(as_int(&items[0]), 3); } #[tokio::test] async fn test_xpending_detail() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // Detail mode let result: redis::Value = redis::cmd("XPENDING") .arg("s") .arg("g1") .arg("-") .arg("+") .arg("10") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 3); } // ── XCLAIM ─────────────────────────────────────────────────────────── #[tokio::test] async fn test_xclaim() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // c1 reads all let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // c2 claims 1-0 from c1 let result: redis::Value = redis::cmd("XCLAIM") .arg("s") .arg("g1") .arg("c2") .arg("0") .arg("1-0") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 1); } #[tokio::test] async fn test_xclaim_justid() { let (_m, mut c) = start().await; redis::cmd("XADD") .arg("s") .arg("1-0") .arg("f") .arg("v") .query_async::(&mut c) .await .unwrap(); must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // JUSTID - return only IDs, not full entries let result: redis::Value = redis::cmd("XCLAIM") .arg("s") .arg("g1") .arg("c2") .arg("0") .arg("1-0") .arg("JUSTID") .query_async(&mut c) .await .unwrap(); let entries = as_array(&result); assert_eq!(entries.len(), 1); // Should be a string (ID), not an array assert!(matches!(entries[0], redis::Value::BulkString(_))); } // ── XAUTOCLAIM ─────────────────────────────────────────────────────── #[tokio::test] async fn test_xautoclaim() { let (_m, mut c) = start().await; for i in 1..=3 { redis::cmd("XADD") .arg("s") .arg(format!("{}-0", i)) .arg("v") .arg(format!("{}", i)) .query_async::(&mut c) .await .unwrap(); } must_ok!(c, "XGROUP", "CREATE", "s", "g1", "0"); // c1 reads all let _: redis::Value = redis::cmd("XREADGROUP") .arg("GROUP") .arg("g1") .arg("c1") .arg("STREAMS") .arg("s") .arg(">") .query_async(&mut c) .await .unwrap(); // XAUTOCLAIM with 0 min-idle-time (claims all pending) let result: redis::Value = redis::cmd("XAUTOCLAIM") .arg("s") .arg("g1") .arg("c2") .arg("0") .arg("0-0") .query_async(&mut c) .await .unwrap(); let items = as_array(&result); assert!( items.len() >= 2, "expected at least 2 items (next-id, entries), got {}", items.len() ); // Second item: claimed entries let entries = as_array(&items[1]); assert_eq!(entries.len(), 3); } // ── Helper functions ───────────────────────────────────────────────── fn as_array(v: &redis::Value) -> &Vec { match v { redis::Value::Array(a) => a, _ => panic!("expected array, got {:?}", v), } } fn as_string(v: &redis::Value) -> String { match v { redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(), redis::Value::SimpleString(s) => s.clone(), _ => panic!("expected string, got {:?}", v), } } fn as_int(v: &redis::Value) -> i64 { match v { redis::Value::Int(i) => *i, _ => panic!("expected int, got {:?}", v), } } ================================================ FILE: miniredis/tests/cmd_string.rs ================================================ // Ported from ../miniredis/cmd_string_test.go mod helpers; use std::time::Duration; #[tokio::test] async fn test_set() { let (m, mut c) = helpers::start().await; // Basic SET/GET must_ok!(c, "SET", "foo", "bar"); must_str!(c, "GET", "foo"; "bar"); m.check_get("foo", "bar"); // Overwrite must_ok!(c, "SET", "foo", "baz"); must_str!(c, "GET", "foo"; "baz"); // Non-existent must_nil!(c, "GET", "nosuch"); // Wrong number of args must_fail!(c, "SET"; "wrong number of arguments"); must_fail!(c, "SET", "foo"; "wrong number of arguments"); must_fail!(c, "GET"; "wrong number of arguments"); } #[tokio::test] async fn test_set_nx() { let (_m, mut c) = helpers::start().await; // NX: set only if not exists must_ok!(c, "SET", "foo", "bar", "NX"); must_str!(c, "GET", "foo"; "bar"); // Second NX should fail (return nil) must_nil!(c, "SET", "foo", "baz", "NX"); // Value unchanged must_str!(c, "GET", "foo"; "bar"); } #[tokio::test] async fn test_set_xx() { let (_m, mut c) = helpers::start().await; // XX: set only if exists — key doesn't exist yet must_nil!(c, "SET", "foo", "bar", "XX"); must_nil!(c, "GET", "foo"); // Now create it must_ok!(c, "SET", "foo", "bar"); // XX should work now must_ok!(c, "SET", "foo", "baz", "XX"); must_str!(c, "GET", "foo"; "baz"); } #[tokio::test] async fn test_set_ex() { let (m, mut c) = helpers::start().await; // SET with EX must_ok!(c, "SET", "foo", "bar", "EX", "10"); // TTL should be set let ttl = m.ttl("foo"); assert!(ttl.is_some()); assert!(ttl.unwrap() <= Duration::from_secs(10)); // Invalid EX must_fail!(c, "SET", "foo", "bar", "EX", "0"; "invalid expire time"); must_fail!(c, "SET", "foo", "bar", "EX", "-1"; "invalid expire time"); must_fail!(c, "SET", "foo", "bar", "EX", "notanumber"; "not an integer"); } #[tokio::test] async fn test_set_px() { let (m, mut c) = helpers::start().await; // SET with PX must_ok!(c, "SET", "foo", "bar", "PX", "10000"); let ttl = m.ttl("foo"); assert!(ttl.is_some()); assert!(ttl.unwrap() <= Duration::from_secs(10)); // Invalid PX must_fail!(c, "SET", "foo", "bar", "PX", "0"; "invalid expire time"); must_fail!(c, "SET", "foo", "bar", "PX", "-1"; "invalid expire time"); } #[tokio::test] async fn test_set_keepttl() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar", "EX", "100"); // Overwrite with KEEPTTL must_ok!(c, "SET", "foo", "baz", "KEEPTTL"); must_str!(c, "GET", "foo"; "baz"); let ttl = m.ttl("foo"); assert!(ttl.is_some(), "TTL should be preserved"); } #[tokio::test] async fn test_set_get() { let (_m, mut c) = helpers::start().await; // GET option — return old value must_nil!(c, "SET", "foo", "bar", "GET"); must_str!(c, "SET", "foo", "baz", "GET"; "bar"); must_str!(c, "GET", "foo"; "baz"); } #[tokio::test] async fn test_setnx() { let (_m, mut c) = helpers::start().await; must_int!(c, "SETNX", "foo", "bar"; 1); must_str!(c, "GET", "foo"; "bar"); must_int!(c, "SETNX", "foo", "baz"; 0); must_str!(c, "GET", "foo"; "bar"); // Wrong number of args must_fail!(c, "SETNX"; "wrong number of arguments"); must_fail!(c, "SETNX", "foo"; "wrong number of arguments"); } #[tokio::test] async fn test_getset() { let (_m, mut c) = helpers::start().await; must_nil!(c, "GETSET", "foo", "bar"); must_str!(c, "GETSET", "foo", "baz"; "bar"); must_str!(c, "GET", "foo"; "baz"); // Wrong number of args must_fail!(c, "GETSET"; "wrong number of arguments"); must_fail!(c, "GETSET", "foo"; "wrong number of arguments"); } #[tokio::test] async fn test_del() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_ok!(c, "SET", "c", "3"); // DEL multiple must_int!(c, "DEL", "a", "b"; 2); must_nil!(c, "GET", "a"); must_nil!(c, "GET", "b"); must_str!(c, "GET", "c"; "3"); // DEL non-existent must_int!(c, "DEL", "nosuch"; 0); // Wrong number of args must_fail!(c, "DEL"; "wrong number of arguments"); } #[tokio::test] async fn test_exists() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "a", "1"); must_ok!(c, "SET", "b", "2"); must_int!(c, "EXISTS", "a"; 1); must_int!(c, "EXISTS", "nosuch"; 0); // Multiple keys must_int!(c, "EXISTS", "a", "b", "nosuch"; 2); // Wrong number of args must_fail!(c, "EXISTS"; "wrong number of arguments"); } #[tokio::test] async fn test_setex() { let (m, mut c) = helpers::start().await; must_ok!(c, "SETEX", "foo", "10", "bar"); must_str!(c, "GET", "foo"; "bar"); let ttl = m.ttl("foo"); assert!(ttl.is_some()); // Errors must_fail!(c, "SETEX", "foo", "0", "bar"; "invalid expire time"); must_fail!(c, "SETEX", "foo", "-1", "bar"; "invalid expire time"); must_fail!(c, "SETEX"; "wrong number of arguments"); } #[tokio::test] async fn test_psetex() { let (m, mut c) = helpers::start().await; must_ok!(c, "PSETEX", "foo", "10000", "bar"); must_str!(c, "GET", "foo"; "bar"); let ttl = m.ttl("foo"); assert!(ttl.is_some()); must_fail!(c, "PSETEX", "foo", "0", "bar"; "invalid expire time"); } #[tokio::test] async fn test_incr_decr() { let (_m, mut c) = helpers::start().await; must_int!(c, "INCR", "counter"; 1); must_int!(c, "INCR", "counter"; 2); must_int!(c, "INCRBY", "counter", "5"; 7); must_int!(c, "DECR", "counter"; 6); must_int!(c, "DECRBY", "counter", "3"; 3); // Non-integer value must_ok!(c, "SET", "str", "notanumber"); must_fail!(c, "INCR", "str"; "not an integer"); // Errors must_fail!(c, "INCR"; "wrong number of arguments"); must_fail!(c, "INCRBY"; "wrong number of arguments"); } #[tokio::test] async fn test_incrbyfloat() { let (_m, mut c) = helpers::start().await; must_str!(c, "INCRBYFLOAT", "f", "1.5"; "1.5"); must_str!(c, "INCRBYFLOAT", "f", "2.5"; "4"); must_str!(c, "INCRBYFLOAT", "f", "-1"; "3"); } #[tokio::test] async fn test_mget_mset() { let (_m, mut c) = helpers::start().await; must_ok!(c, "MSET", "a", "1", "b", "2", "c", "3"); must_strs!(c, "MGET", "a", "b", "c"; ["1", "2", "3"]); // Missing key returns nil let result: Vec> = redis::cmd("MGET") .arg("a") .arg("nosuch") .arg("c") .query_async(&mut c) .await .unwrap(); assert_eq!( result, vec![Some("1".to_string()), None, Some("3".to_string())] ); } #[tokio::test] async fn test_msetnx() { let (_m, mut c) = helpers::start().await; must_int!(c, "MSETNX", "a", "1", "b", "2"; 1); must_str!(c, "GET", "a"; "1"); // Any key exists → all fail must_int!(c, "MSETNX", "a", "x", "c", "3"; 0); must_str!(c, "GET", "a"; "1"); must_nil!(c, "GET", "c"); // c was NOT set } #[tokio::test] async fn test_strlen() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "hello"); must_int!(c, "STRLEN", "foo"; 5); must_int!(c, "STRLEN", "nosuch"; 0); } #[tokio::test] async fn test_append() { let (_m, mut c) = helpers::start().await; must_int!(c, "APPEND", "key", "hello"; 5); must_int!(c, "APPEND", "key", " world"; 11); must_str!(c, "GET", "key"; "hello world"); } #[tokio::test] async fn test_getrange() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "hello world"); must_str!(c, "GETRANGE", "foo", "0", "4"; "hello"); must_str!(c, "GETRANGE", "foo", "-5", "-1"; "world"); must_str!(c, "GETRANGE", "foo", "0", "-1"; "hello world"); } #[tokio::test] async fn test_setrange() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "hello world"); must_int!(c, "SETRANGE", "foo", "6", "Redis"; 11); must_str!(c, "GET", "foo"; "hello Redis"); // Extending must_int!(c, "SETRANGE", "bar", "5", "hi"; 7); // Should be zero-padded let val: Vec = redis::cmd("GET") .arg("bar") .query_async(&mut c) .await .unwrap(); assert_eq!(val, vec![0, 0, 0, 0, 0, b'h', b'i']); } #[tokio::test] async fn test_getdel() { let (_m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); must_str!(c, "GETDEL", "foo"; "bar"); must_nil!(c, "GET", "foo"); // Non-existent must_nil!(c, "GETDEL", "nosuch"); } #[tokio::test] async fn test_getex() { let (m, mut c) = helpers::start().await; must_ok!(c, "SET", "foo", "bar"); // GETEX with EX must_str!(c, "GETEX", "foo", "EX", "100"; "bar"); assert!(m.ttl("foo").is_some()); // GETEX with PERSIST must_str!(c, "GETEX", "foo", "PERSIST"; "bar"); assert!(m.ttl("foo").is_none()); // Non-existent must_nil!(c, "GETEX", "nosuch"); } ================================================ FILE: miniredis/tests/cmd_tls.rs ================================================ use std::sync::Arc; use miniredis_rs::Miniredis; use tokio::io::{AsyncReadExt, AsyncWriteExt}; /// Generate a self-signed TLS certificate and return (server_config, cert_der). fn generate_tls_config() -> (Arc, Vec) { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); let cert_der_bytes = cert.cert.der().to_vec(); let cert_der = rustls::pki_types::CertificateDer::from(cert_der_bytes.clone()); let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into()); let config = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert_der], key_der) .unwrap(); (Arc::new(config), cert_der_bytes) } /// Create a TLS client connector that trusts the given cert. fn make_tls_connector(cert_der: &[u8]) -> tokio_rustls::TlsConnector { let mut root_store = rustls::RootCertStore::empty(); root_store .add(rustls::pki_types::CertificateDer::from(cert_der.to_vec())) .unwrap(); let config = rustls::ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); tokio_rustls::TlsConnector::from(Arc::new(config)) } /// Send a RESP2 command over a TLS stream and return the raw response. async fn tls_cmd( stream: &mut tokio_rustls::client::TlsStream, args: &[&str], ) -> Vec { let mut cmd = format!("*{}\r\n", args.len()); for arg in args { cmd.push_str(&format!("${}\r\n{}\r\n", arg.len(), arg)); } stream.write_all(cmd.as_bytes()).await.unwrap(); stream.flush().await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(50)).await; let mut buf = vec![0u8; 4096]; let n = stream.read(&mut buf).await.unwrap(); buf.truncate(n); buf } // ── Tests ─────────────────────────────────────────────────────────── #[tokio::test] async fn test_tls_server_starts() { let (tls_config, _) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); assert!(m.port() > 0); assert!(m.tls_url().starts_with("rediss://")); } #[tokio::test] async fn test_tls_direct_api_works() { let (tls_config, _) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); // Direct API works regardless of TLS m.set("key", "value"); assert_eq!(m.get("key"), Some("value".to_string())); } #[tokio::test] async fn test_tls_ping() { let (tls_config, cert_der) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); let connector = make_tls_connector(&cert_der); let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap(); let server_name = rustls::pki_types::ServerName::try_from("localhost").unwrap(); let mut tls = connector.connect(server_name, tcp).await.unwrap(); let resp = tls_cmd(&mut tls, &["PING"]).await; assert_eq!(resp, b"+PONG\r\n"); } #[tokio::test] async fn test_tls_set_get() { let (tls_config, cert_der) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); let connector = make_tls_connector(&cert_der); let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap(); let server_name = rustls::pki_types::ServerName::try_from("localhost").unwrap(); let mut tls = connector.connect(server_name, tcp).await.unwrap(); let resp = tls_cmd(&mut tls, &["SET", "foo", "bar"]).await; assert_eq!(resp, b"+OK\r\n"); let resp = tls_cmd(&mut tls, &["GET", "foo"]).await; assert_eq!(resp, b"$3\r\nbar\r\n"); // Verify via direct API assert_eq!(m.get("foo"), Some("bar".to_string())); } #[tokio::test] async fn test_tls_multiple_commands() { let (tls_config, cert_der) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); let connector = make_tls_connector(&cert_der); let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap(); let server_name = rustls::pki_types::ServerName::try_from("localhost").unwrap(); let mut tls = connector.connect(server_name, tcp).await.unwrap(); // Run several commands over TLS let resp = tls_cmd(&mut tls, &["SET", "k1", "v1"]).await; assert_eq!(resp, b"+OK\r\n"); let resp = tls_cmd(&mut tls, &["SET", "k2", "v2"]).await; assert_eq!(resp, b"+OK\r\n"); let resp = tls_cmd(&mut tls, &["DEL", "k1"]).await; assert_eq!(resp, b":1\r\n"); let resp = tls_cmd(&mut tls, &["EXISTS", "k1"]).await; assert_eq!(resp, b":0\r\n"); let resp = tls_cmd(&mut tls, &["GET", "k2"]).await; assert_eq!(resp, b"$2\r\nv2\r\n"); } #[tokio::test] async fn test_plain_tcp_to_tls_server_fails() { let (tls_config, _) = generate_tls_config(); let m = Miniredis::run_tls(tls_config).await.unwrap(); // Plain TCP should not get a valid response from TLS server let mut stream = tokio::net::TcpStream::connect(m.addr()).await.unwrap(); let cmd = b"*1\r\n$4\r\nPING\r\n"; stream.write_all(cmd).await.unwrap(); stream.flush().await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(100)).await; let mut buf = vec![0u8; 1024]; let result = tokio::time::timeout(std::time::Duration::from_millis(200), stream.read(&mut buf)).await; match result { Ok(Ok(0)) => {} // connection closed - expected Ok(Ok(n)) => { // Got bytes but they shouldn't be a valid RESP response let resp = &buf[..n]; assert_ne!(resp, b"+PONG\r\n", "should not get PONG without TLS"); } Ok(Err(_)) => {} // error - expected Err(_) => {} // timeout - expected } } ================================================ FILE: miniredis/tests/cmd_transactions.rs ================================================ mod helpers; use helpers::*; // ── Error cases ────────────────────────────────────────────────────── #[tokio::test] async fn test_exec_without_multi() { let (_m, mut c) = start().await; must_fail!(c, "EXEC"; "EXEC without MULTI"); } #[tokio::test] async fn test_discard_without_multi() { let (_m, mut c) = start().await; must_fail!(c, "DISCARD"; "DISCARD without MULTI"); } #[tokio::test] async fn test_multi_nested() { let (_m, mut c) = start().await; // First MULTI → OK must_ok!(c, "MULTI"); // Second MULTI → error must_fail!(c, "MULTI"; "MULTI calls can not be nested"); // Clean up must_ok!(c, "DISCARD"); } // ── Basic MULTI / EXEC ────────────────────────────────────────────── #[tokio::test] async fn test_multi_basic() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); // Clean up must_ok!(c, "DISCARD"); } #[tokio::test] async fn test_simple_transaction() { let (_m, mut c) = start().await; // MULTI must_ok!(c, "MULTI"); // SET is queued let v: String = redis::cmd("SET") .arg("aap") .arg("1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // EXEC returns array of results let v: Vec = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); assert_eq!(v, vec!["OK"]); // Key should now be set must_str!(c, "GET", "aap"; "1"); // Commands should be back to normal mode must_ok!(c, "SET", "aap", "2"); must_str!(c, "GET", "aap"; "2"); } #[tokio::test] async fn test_multi_exec_multiple_commands() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); let v: String = redis::cmd("SET") .arg("k1") .arg("v1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: String = redis::cmd("SET") .arg("k2") .arg("v2") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: String = redis::cmd("GET") .arg("k1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // EXEC let v: redis::Value = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); // Should be Array [OK, OK, "v1"] match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 3); } _ => panic!("expected array from EXEC, got {:?}", v), } // Verify must_str!(c, "GET", "k1"; "v1"); must_str!(c, "GET", "k2"; "v2"); } // ── DISCARD ────────────────────────────────────────────────────────── #[tokio::test] async fn test_discard_transaction() { let (_m, mut c) = start().await; // Pre-set a key must_ok!(c, "SET", "aap", "noot"); // MULTI + queue a change must_ok!(c, "MULTI"); let v: String = redis::cmd("SET") .arg("aap") .arg("mies") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // DISCARD must_ok!(c, "DISCARD"); // Key should still have original value must_str!(c, "GET", "aap"; "noot"); } // ── WATCH ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_watch_basic() { let (_m, mut c) = start().await; must_ok!(c, "WATCH", "foo"); } #[tokio::test] async fn test_watch_in_multi() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); // WATCH inside MULTI should error must_fail!(c, "WATCH", "foo"; "WATCH inside MULTI"); must_ok!(c, "DISCARD"); } #[tokio::test] async fn test_watch_exec_success() { let (_m, mut c) = start().await; // Set initial value must_ok!(c, "SET", "one", "two"); // WATCH the key must_ok!(c, "WATCH", "one"); // MULTI + GET must_ok!(c, "MULTI"); let v: String = redis::cmd("GET") .arg("one") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // EXEC — no changes to watched key, so it should succeed let v: Vec = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); assert_eq!(v, vec!["two"]); } #[tokio::test] async fn test_watch_exec_fail() { let (_m, mut c1, mut c2) = start_two_clients().await; // Set initial value must_ok!(c1, "SET", "one", "two"); // c1: WATCH the key must_ok!(c1, "WATCH", "one"); // c2: Modify the watched key must_ok!(c2, "SET", "one", "three"); // c1: MULTI + GET + EXEC → should return nil (WATCH abort) must_ok!(c1, "MULTI"); let v: String = redis::cmd("GET") .arg("one") .query_async(&mut c1) .await .unwrap(); assert_eq!(v, "QUEUED"); // EXEC should return nil because the watched key was modified let v: redis::Value = redis::cmd("EXEC").query_async(&mut c1).await.unwrap(); assert_eq!(v, redis::Value::Nil); // We're no longer in a transaction; key has the value set by c2 must_str!(c1, "GET", "one"; "three"); } // ── UNWATCH ────────────────────────────────────────────────────────── #[tokio::test] async fn test_unwatch() { let (_m, mut c1, mut c2) = start_two_clients().await; // Set initial value must_ok!(c1, "SET", "one", "two"); // c1: WATCH the key must_ok!(c1, "WATCH", "one"); // c1: UNWATCH — cancels the watch must_ok!(c1, "UNWATCH"); // c2: Modify the key (would have triggered WATCH failure, but we unwatched) must_ok!(c2, "SET", "one", "three"); // c1: MULTI + SET + EXEC → should succeed because we unwatched must_ok!(c1, "MULTI"); let v: String = redis::cmd("SET") .arg("one") .arg("four") .query_async(&mut c1) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: Vec = redis::cmd("EXEC").query_async(&mut c1).await.unwrap(); assert_eq!(v, vec!["OK"]); // Key should have the value from our transaction must_str!(c1, "GET", "one"; "four"); } // ── Transaction with pipe().atomic() ───────────────────────────────── #[tokio::test] async fn test_pipe_atomic() { let (_m, mut c) = start().await; // Use the redis crate's built-in atomic pipe (MULTI/EXEC wrapper) let (v1, v2): (String, i64) = redis::pipe() .atomic() .cmd("SET") .arg("k") .arg("hello") .cmd("INCR") .arg("counter") .query_async(&mut c) .await .unwrap(); assert_eq!(v1, "OK"); assert_eq!(v2, 1); must_str!(c, "GET", "k"; "hello"); must_int!(c, "GET", "counter"; 1); } // ── MULTI with unknown command → EXECABORT ─────────────────────────── #[tokio::test] async fn test_tx_queue_err() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); // Valid command let v: String = redis::cmd("SET") .arg("aap") .arg("mies") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // Unknown command → error and dirty transaction must_fail!(c, "NOSUCHCOMMAND", "arg"; "unknown command"); // Another valid command still queues let v: String = redis::cmd("SET") .arg("noot") .arg("vuur") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); // EXEC should fail with EXECABORT because of the unknown command must_fail!(c, "EXEC"; "Transaction discarded"); // Nothing should have been executed must_nil!(c, "GET", "aap"); } // ── EVAL/EVALSHA inside MULTI/EXEC ─────────────────────────────────── #[tokio::test] async fn test_lua_tx_eval() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); let v: String = redis::cmd("EVAL") .arg("return {ARGV[1]}") .arg("0") .arg("key1") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: redis::Value = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 1); } _ => panic!("expected array from EXEC, got {:?}", v), } } #[tokio::test] async fn test_lua_tx_evalsha() { let (_m, mut c) = start().await; must_ok!(c, "MULTI"); // SCRIPT LOAD inside MULTI let v: String = redis::cmd("SCRIPT") .arg("LOAD") .arg("return {KEYS[1],ARGV[1]}") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let script_sha = "bfbf458525d6a0b19200bfd6db3af481156b367b"; let v: String = redis::cmd("EVALSHA") .arg(script_sha) .arg("1") .arg("key1") .arg("key2") .query_async(&mut c) .await .unwrap(); assert_eq!(v, "QUEUED"); let v: redis::Value = redis::cmd("EXEC").query_async(&mut c).await.unwrap(); match v { redis::Value::Array(ref items) => { assert_eq!(items.len(), 2); } _ => panic!("expected array from EXEC, got {:?}", v), } } ================================================ FILE: miniredis/tests/direct_api.rs ================================================ use miniredis_rs::Miniredis; // ── String operations ──────────────────────────────────────────────── #[tokio::test] async fn test_direct_set_get() { let m = Miniredis::run().await.unwrap(); m.set("key", "value"); assert_eq!(m.get("key"), Some("value".to_string())); assert_eq!(m.get("nosuch"), None); } #[tokio::test] async fn test_direct_incr() { let m = Miniredis::run().await.unwrap(); assert_eq!(m.incr("counter", 1), 1); assert_eq!(m.incr("counter", 5), 6); assert_eq!(m.incr("counter", -2), 4); } #[tokio::test] async fn test_direct_check_get() { let m = Miniredis::run().await.unwrap(); m.set("k", "v"); m.check_get("k", "v"); } // ── Key management ─────────────────────────────────────────────────── #[tokio::test] async fn test_direct_del_exists() { let m = Miniredis::run().await.unwrap(); m.set("k", "v"); assert!(m.exists("k")); assert!(m.del("k")); assert!(!m.exists("k")); assert!(!m.del("k")); } #[tokio::test] async fn test_direct_keys() { let m = Miniredis::run().await.unwrap(); m.set("b", "1"); m.set("a", "2"); m.set("c", "3"); assert_eq!(m.keys(), vec!["a", "b", "c"]); } #[tokio::test] async fn test_direct_key_type() { let m = Miniredis::run().await.unwrap(); m.set("str", "v"); assert_eq!(m.key_type("str"), "string"); assert_eq!(m.key_type("nosuch"), "none"); } #[tokio::test] async fn test_direct_db_size() { let m = Miniredis::run().await.unwrap(); assert_eq!(m.db_size(), 0); m.set("k1", "v"); m.set("k2", "v"); assert_eq!(m.db_size(), 2); } #[tokio::test] async fn test_direct_flush() { let m = Miniredis::run().await.unwrap(); m.set("k1", "v"); m.set("k2", "v"); m.flush_db(); assert_eq!(m.db_size(), 0); } // ── List operations ────────────────────────────────────────────────── #[tokio::test] async fn test_direct_list_push_pop() { let m = Miniredis::run().await.unwrap(); assert_eq!(m.push("l", &["a", "b", "c"]), 3); assert_eq!( m.list("l"), Some(vec!["a".to_string(), "b".to_string(), "c".to_string()]) ); assert_eq!(m.pop("l"), Some("c".to_string())); assert_eq!(m.lpop("l"), Some("a".to_string())); assert_eq!(m.list("l"), Some(vec!["b".to_string()])); } #[tokio::test] async fn test_direct_list_lpush() { let m = Miniredis::run().await.unwrap(); m.lpush("l", "a"); m.lpush("l", "b"); assert_eq!(m.list("l"), Some(vec!["b".to_string(), "a".to_string()])); } #[tokio::test] async fn test_direct_check_list() { let m = Miniredis::run().await.unwrap(); m.push("l", &["x", "y", "z"]); m.check_list("l", &["x", "y", "z"]); } // ── Set operations ─────────────────────────────────────────────────── #[tokio::test] async fn test_direct_set_add_members() { let m = Miniredis::run().await.unwrap(); assert_eq!(m.set_add("s", &["a", "b", "c"]), 3); assert_eq!(m.set_add("s", &["b", "d"]), 1); // only d is new let members = m.members("s").unwrap(); assert_eq!(members, vec!["a", "b", "c", "d"]); } #[tokio::test] async fn test_direct_is_member() { let m = Miniredis::run().await.unwrap(); m.set_add("s", &["a", "b"]); assert!(m.is_member("s", "a")); assert!(!m.is_member("s", "c")); assert!(!m.is_member("nosuch", "a")); } #[tokio::test] async fn test_direct_check_set() { let m = Miniredis::run().await.unwrap(); m.set_add("s", &["c", "a", "b"]); m.check_set("s", &["a", "b", "c"]); } // ── Hash operations ────────────────────────────────────────────────── #[tokio::test] async fn test_direct_hash() { let m = Miniredis::run().await.unwrap(); m.hset("h", "f1", "v1"); m.hset("h", "f2", "v2"); assert_eq!(m.hget("h", "f1"), Some("v1".to_string())); assert_eq!(m.hget("h", "nosuch"), None); let keys = m.hkeys("h").unwrap(); assert_eq!(keys, vec!["f1", "f2"]); } #[tokio::test] async fn test_direct_hdel() { let m = Miniredis::run().await.unwrap(); m.hset("h", "f1", "v1"); assert!(m.hdel("h", "f1")); assert!(!m.hdel("h", "f1")); assert_eq!(m.hget("h", "f1"), None); } // ── Sorted set operations ──────────────────────────────────────────── #[tokio::test] async fn test_direct_sorted_set() { let m = Miniredis::run().await.unwrap(); assert!(m.zadd("ss", 1.0, "a")); assert!(m.zadd("ss", 3.0, "c")); assert!(m.zadd("ss", 2.0, "b")); // Update existing assert!(!m.zadd("ss", 1.5, "a")); assert_eq!(m.zscore("ss", "a"), Some(1.5)); assert_eq!(m.zscore("ss", "nosuch"), None); let members = m.zmembers("ss").unwrap(); assert_eq!(members, vec!["a", "b", "c"]); // sorted by score } // ── Stream operations ──────────────────────────────────────────────── #[tokio::test] async fn test_direct_stream() { let m = Miniredis::run().await.unwrap(); let id = m.xadd("stream", "1-0", &[("field", "value")]); assert_eq!(id, "1-0"); assert_eq!(m.key_type("stream"), "stream"); } // ── HyperLogLog operations ────────────────────────────────────────── #[tokio::test] async fn test_direct_hll() { let m = Miniredis::run().await.unwrap(); assert!(m.pfadd("hll", &["a", "b", "c"])); assert!(!m.pfadd("hll", &["a", "b"])); // no new elements assert_eq!(m.pfcount("hll"), 3); } // ── TTL operations ─────────────────────────────────────────────────── #[tokio::test] async fn test_direct_ttl() { let m = Miniredis::run().await.unwrap(); m.set("k", "v"); assert_eq!(m.ttl("k"), None); m.set_ttl("k", std::time::Duration::from_secs(60)); assert!(m.ttl("k").is_some()); } // ── Select DB ──────────────────────────────────────────────────────── #[tokio::test] async fn test_direct_select_db() { let mut m = Miniredis::run().await.unwrap(); m.set("k", "db0"); m.select(1); m.set("k", "db1"); assert_eq!(m.get("k"), Some("db1".to_string())); m.select(0); assert_eq!(m.get("k"), Some("db0".to_string())); } // ── Connection counting ────────────────────────────────────────────── #[tokio::test] async fn test_direct_connection_count() { let m = Miniredis::run().await.unwrap(); // Before any connections assert_eq!(m.total_connection_count(), 0); // Create a client connection let client = redis::Client::open(m.redis_url()).unwrap(); let mut conn = client.get_multiplexed_async_connection().await.unwrap(); // Small delay for the connection to register tokio::time::sleep(std::time::Duration::from_millis(50)).await; assert!(m.total_connection_count() > 0); assert!(m.current_connection_count() > 0); // Do a command to confirm it works let _: String = redis::cmd("PING").query_async(&mut conn).await.unwrap(); drop(conn); } // ── Fast forward ───────────────────────────────────────────────────── #[tokio::test] async fn test_direct_fast_forward() { let m = Miniredis::run().await.unwrap(); m.set("k", "v"); m.set_ttl("k", std::time::Duration::from_secs(10)); // Key exists before expiration assert!(m.exists("k")); // Fast forward past TTL m.fast_forward(std::time::Duration::from_secs(11)); // Key should be gone assert!(!m.exists("k")); } // ── Auth ───────────────────────────────────────────────────────────── #[tokio::test] async fn test_direct_require_auth() { let m = Miniredis::run().await.unwrap(); m.require_auth("secret"); // Connection without auth should fail let client = redis::Client::open(m.redis_url()).unwrap(); let mut conn = client.get_multiplexed_async_connection().await.unwrap(); let result: redis::RedisResult = redis::cmd("PING").query_async(&mut conn).await; assert!(result.is_err()); } // ── DB() access ───────────────────────────────────────────────────── #[tokio::test] async fn test_direct_db_access() { let m = Miniredis::run().await.unwrap(); // Set key in DB 0 m.set("key0", "val0"); // Set key in DB 5 via db() m.db(5).set("key5", "val5"); // Keys are isolated assert_eq!(m.db(0).get("key0"), Some("val0".to_string())); assert_eq!(m.db(0).get("key5"), None); assert_eq!(m.db(5).get("key5"), Some("val5".to_string())); assert_eq!(m.db(5).get("key0"), None); // db() methods assert!(m.db(5).exists("key5")); assert_eq!(m.db(5).key_type("key5"), "string"); assert_eq!(m.db(5).db_size(), 1); assert_eq!(m.db(5).keys(), vec!["key5".to_string()]); } // ── Restart ───────────────────────────────────────────────────────── #[tokio::test] async fn test_direct_restart() { let mut m = Miniredis::run().await.unwrap(); m.set("before", "restart"); let old_addr = m.addr(); m.close().await; tokio::task::yield_now().await; m.restart().await.unwrap(); let new_addr = m.addr(); // Port should change (new random port) assert_ne!(old_addr.port(), new_addr.port()); // Data should be preserved assert_eq!(m.get("before"), Some("restart".to_string())); // New connections should work let client = redis::Client::open(m.redis_url()).unwrap(); let mut conn = client.get_multiplexed_async_connection().await.unwrap(); let v: String = redis::cmd("GET") .arg("before") .query_async(&mut conn) .await .unwrap(); assert_eq!(v, "restart"); } // ── Dump ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_direct_dump() { let m = Miniredis::run().await.unwrap(); m.set("str", "hello"); m.push("mylist", &["a", "b", "c"]); m.set_add("myset", &["x", "y"]); m.hset("myhash", "f1", "v1"); m.zadd("myzset", 1.5, "member1"); let dump = m.dump(); assert!(dump.contains("- str\n"), "missing string key"); assert!(dump.contains("\"hello\""), "missing string value"); assert!(dump.contains("- mylist\n"), "missing list key"); assert!(dump.contains("\"a\""), "missing list element"); assert!(dump.contains("- myset\n"), "missing set key"); assert!(dump.contains("- myhash\n"), "missing hash key"); assert!(dump.contains("f1:"), "missing hash field"); assert!(dump.contains("- myzset\n"), "missing zset key"); assert!(dump.contains("1.5:"), "missing zset score"); } #[tokio::test] async fn test_direct_db_dump() { let m = Miniredis::run().await.unwrap(); m.db(3).set("k", "v"); // DB 0 dump should be empty let dump0 = m.db(0).dump(); assert!(dump0.is_empty(), "DB 0 should be empty"); // DB 3 dump should have the key let dump3 = m.db(3).dump(); assert!(dump3.contains("- k\n"), "DB 3 should have key k"); } ================================================ FILE: miniredis/tests/helpers/mod.rs ================================================ use miniredis_rs::Miniredis; use redis::aio::MultiplexedConnection; /// Spin up a server + connected client — equivalent to Go's `runWithClient(t)`. pub async fn start() -> (Miniredis, MultiplexedConnection) { let m = Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let conn = client.get_multiplexed_async_connection().await.unwrap(); (m, conn) } /// Spin up a server + two connected clients. #[allow(dead_code)] pub async fn start_two_clients() -> (Miniredis, MultiplexedConnection, MultiplexedConnection) { let m = Miniredis::run().await.unwrap(); let c1 = redis::Client::open(m.redis_url()) .unwrap() .get_multiplexed_async_connection() .await .unwrap(); let c2 = redis::Client::open(m.redis_url()) .unwrap() .get_multiplexed_async_connection() .await .unwrap(); (m, c1, c2) } // ── Assertion macros ───────────────────────────────────────────────── /// Execute a command and assert it returns "OK". #[macro_export] macro_rules! must_ok { ($conn:expr, $cmd:expr $(, $arg:expr)*) => {{ let result: String = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); assert_eq!(result, "OK"); }}; } /// Execute a command and assert it returns a specific string. #[macro_export] macro_rules! must_str { ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{ let result: String = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); assert_eq!(result, $expected); }}; } /// Execute a command and assert it returns a specific integer. #[macro_export] macro_rules! must_int { ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{ let result: i64 = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); assert_eq!(result, $expected as i64); }}; } /// Execute a command and assert it returns nil/null. #[macro_export] macro_rules! must_nil { ($conn:expr, $cmd:expr $(, $arg:expr)*) => {{ let result: Option = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); assert_eq!(result, None); }}; } /// Execute a command and assert it returns an error containing the given substring. #[macro_export] macro_rules! must_fail { ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{ let result: redis::RedisResult = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await; match result { Err(e) => { let msg = e.to_string(); assert!( msg.contains($expected), "error {msg:?} does not contain {:?}", $expected, ); } Ok(v) => { panic!( "expected error containing {:?}, got {:?}", $expected, v, ); } } }}; } /// Execute a command and assert it returns a list of strings (sorted before comparison). #[macro_export] macro_rules! must_strs_sorted { ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{ let mut result: Vec = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); result.sort(); let mut expected: Vec = $expected.iter().map(|s: &&str| s.to_string()).collect(); expected.sort(); assert_eq!(result, expected); }}; } /// Execute a command and assert it returns a list of strings (order preserved). #[macro_export] macro_rules! must_strs { ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{ let result: Vec = redis::cmd($cmd) $(.arg($arg))* .query_async(&mut $conn) .await .unwrap(); let expected: Vec = $expected.iter().map(|s: &&str| s.to_string()).collect(); assert_eq!(result, expected); }}; } /// Execute a command and assert boolean result (1 = true, 0 = false). #[macro_export] macro_rules! must_1 { ($conn:expr, $cmd:expr $(, $arg:expr)*) => { must_int!($conn, $cmd $(, $arg)* ; 1); }; } #[macro_export] macro_rules! must_0 { ($conn:expr, $cmd:expr $(, $arg:expr)*) => { must_int!($conn, $cmd $(, $arg)* ; 0); }; } ================================================ FILE: miniredis/tests/integration-go/Makefile ================================================ MINIREDIS_RS_BIN = ../../../target/release/miniredis-rs-server TESTDATA = ../../testdata .PHONY: test build test: build $(TESTDATA)/ca.crt $(TESTDATA)/server.crt $(TESTDATA)/client.crt PATH="$(dir $(abspath $(MINIREDIS_RS_BIN))):$(PATH)" INT=1 go test -count=1 -v -timeout 120s ./... build: cd ../../.. && cargo build --release --features tls --bin miniredis-rs-server # Generate test TLS certificates if they don't exist. $(TESTDATA)/ca.key: openssl genrsa -out $@ 2048 $(TESTDATA)/ca.crt: $(TESTDATA)/ca.key openssl req -new -x509 -key $< -out $@ -days 3650 -subj "/CN=Test CA" \ -addext "basicConstraints=critical,CA:TRUE" $(TESTDATA)/server.key: openssl genrsa -out $@ 2048 $(TESTDATA)/server.crt: $(TESTDATA)/server.key $(TESTDATA)/ca.crt $(TESTDATA)/ca.key openssl req -new -key $(TESTDATA)/server.key -subj "/CN=Server" \ -addext "subjectAltName=DNS:Server" | \ openssl x509 -req -CA $(TESTDATA)/ca.crt -CAkey $(TESTDATA)/ca.key \ -CAcreateserial -out $@ -days 3650 \ -copy_extensions copyall $(TESTDATA)/client.key: openssl genrsa -out $@ 2048 $(TESTDATA)/client.crt: $(TESTDATA)/client.key $(TESTDATA)/ca.crt $(TESTDATA)/ca.key openssl req -new -key $(TESTDATA)/client.key -subj "/CN=Client" | \ openssl x509 -req -CA $(TESTDATA)/ca.crt -CAkey $(TESTDATA)/ca.key \ -CAcreateserial -out $@ -days 3650 ================================================ FILE: miniredis/tests/integration-go/README.md ================================================ # Integration Tests These are the integration tests from the original [miniredis](https://github.com/alicebob/miniredis) Go implementation (`integration/`), adapted to compare miniredis-rs against miniredis-go at the RESP byte level. ## How it works Each test starts two servers — miniredis-rs (as a subprocess) and miniredis-go (in-process) — then runs identical Redis commands against both and compares the raw RESP responses. ## Running ```bash make test ``` This builds `miniredis-rs-server` (with TLS) and runs the Go tests with `INT=1` set. Tests are skipped without `INT=1`. ## Changes from upstream The test files (`*_test.go`) and the comparison framework (`test.go`) are kept identical to upstream, with the following exceptions: ### `ephemeral.go` Rewritten to start `miniredis-rs-server` as a subprocess (instead of `redis-server`). Config is passed via stdin; the server prints `PORT=` to stdout when ready. ### `tls.go` and `testdata/` The test certificates use a proper CA hierarchy because rustls's `WebPkiClientVerifier` rejects self-signed `CA:TRUE` certificates used as end-entity certs (which the upstream certs are). The upstream certs work with Go's `crypto/tls` but not with rustls. - `ca.crt` / `ca.key` — self-signed CA - `server.crt` / `server.key` — server cert signed by CA (`SAN: DNS:Server`) - `client.crt` / `client.key` — client cert signed by CA Both sides use `ca.crt` as the trust root. ### `generic_test.go` `TestFastForward` uses `c.real.Do("MINIREDIS.FASTFORWARD", "200")` instead of `time.Sleep(200ms)`. miniredis-rs (like miniredis-go) uses mock time for TTLs, so wall-clock sleep doesn't expire keys. The custom `MINIREDIS.FASTFORWARD ` command advances mock time on the subprocess, matching what `c.miniredis.FastForward()` does for miniredis-go in-process. ================================================ FILE: miniredis/tests/integration-go/cluster_test.go ================================================ package main import "testing" func TestCluster(t *testing.T) { skip(t) testCluster(t, func(c *client) { // c.DoLoosly("CLUSTER", "SLOTS") c.DoLoosely("CLUSTER", "KEYSLOT", "{test}") c.DoLoosely("CLUSTER", "NODES") c.Error("wrong number", "CLUSTER") // c.DoLoosely("CLUSTER", "SHARDS") }, ) } ================================================ FILE: miniredis/tests/integration-go/command_test.go ================================================ package main import "testing" func TestCommand(t *testing.T) { t.Skip("not sure about this one yet") testRaw(t, func(c *client) { c.DoLoosely("COMMAND") }) } ================================================ FILE: miniredis/tests/integration-go/connection_test.go ================================================ package main import ( "testing" ) func TestEcho(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ECHO", "hello world") c.Do("ECHO", "42") c.Do("ECHO", "3.1415") c.Error("wrong number", "ECHO", "hello", "world") c.Error("wrong number", "ECHO") c.Error("wrong number", "eChO", "hello", "world") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("ECHO", "hi") c.Do("EXEC") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Error("wrong number", "ECHO") c.Error("discarded", "EXEC") }) } func TestPing(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("PING") c.Do("PING", "hello world") c.Error("wrong number", "PING", "hello", "world") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("PING", "hi") c.Do("EXEC") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("PING", "hi again") c.Do("EXEC") }) } func TestSelect(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("GET", "foo") c.Do("SELECT", "2") c.Do("GET", "foo") c.Do("SET", "foo", "bar2") c.Do("GET", "foo") c.Error("wrong number", "SELECT") c.Error("out of range", "SELECT", "-1") c.Error("not an integer", "SELECT", "aap") c.Error("wrong number", "SELECT", "1", "2") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SET", "foo", "bar") c.Do("GET", "foo") c.Do("SELECT", "2") c.Do("GET", "foo") c.Do("SET", "foo", "bar2") c.Do("GET", "foo") c.Do("EXEC") c.Do("GET", "foo") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SELECT", "-1") c.Do("EXEC") }) } func TestAuth(t *testing.T) { skip(t) testAuth(t, "supersecret", func(c *client) { c.Error("Authentication required", "PING") c.Error("Authentication required", "SET", "foo", "bar") c.Error("wrong number", "SET") c.Error("Authentication required", "SET", "foo", "bar", "baz") c.Error("Authentication required", "GET", "foo") c.Error("wrong number", "AUTH") c.Error("invalid", "AUTH", "nosecret") c.Error("invalid", "AUTH", "nosecret", "bar") c.Error("syntax error", "AUTH", "nosecret", "bar", "bar") c.Do("AUTH", "supersecret") c.Do("SET", "foo", "bar") c.Do("GET", "foo") }, ) testUserAuth(t, map[string]string{ "agent1": "supersecret", "agent2": "dragon", }, func(c *client) { c.Error("Authentication required", "PING") c.Error("Authentication required", "SET", "foo", "bar") c.Error("wrong number", "SET") c.Error("Authentication required", "SET", "foo", "bar", "baz") c.Error("Authentication required", "GET", "foo") c.Error("wrong number", "AUTH") c.Error("invalid", "AUTH", "nosecret") c.Error("invalid", "AUTH", "agent100", "supersecret") c.Error("syntax error", "AUTH", "agent100", "supersecret", "supersecret") c.Error("invalid", "AUTH", "agent1", "bzzzt") c.Do("AUTH", "agent1", "supersecret") c.Do("SET", "foo", "bar") c.Do("GET", "foo") // go back to invalid user c.Error("invalid", "AUTH", "agent100", "supersecret") c.Do("GET", "foo") // still agent1 }, ) testRaw(t, func(c *client) { c.Error("wrong number", "AUTH") c.Error("without any", "AUTH", "foo") c.Error("invalid", "AUTH", "foo", "bar") c.Error("syntax error", "AUTH", "foo", "bar", "bar") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("AUTH", "apassword") c.Do("EXEC") }) } func TestHello(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s", "aap") // sets have resp3 specific code c.DoLoosely("HELLO", "3") c.Do("SMEMBERS", "s") c.DoLoosely("HELLO", "2") c.Do("SMEMBERS", "s") c.Error("not an integer", "HELLO", "twoandahalf") c.DoLoosely("HELLO", "3", "AUTH", "default", "foo") c.DoLoosely("HELLO", "3", "AUTH", "default", "foo", "SETNAME", "foo") c.DoLoosely("HELLO", "3", "SETNAME", "foo") // errors c.Error("Syntax error", "HELLO", "3", "default", "foo") c.Error("not an integer", "HELLO", "three", "AUTH", "default", "foo") c.Error("Syntax error", "HELLO", "3", "AUTH", "default") c.Error("unsupported", "HELLO", "-1", "foo") c.Error("unsupported", "HELLO", "0", "foo") c.Error("unsupported", "HELLO", "1", "foo") c.Error("unsupported", "HELLO", "4", "foo") c.Error("Syntax error", "HELLO", "3", "default", "foo", "SETNAME") c.Error("Syntax error", "HELLO", "3", "SETNAME") }, ) testAuth(t, "secret", func(c *client) { c.Error("Authentication required", "SADD", "s", "aap") // sets have resp3 specific code c.Error("invalid", "HELLO", "3", "AUTH", "default", "foo") c.Error("invalid", "HELLO", "3", "AUTH", "wrong", "secret") c.DoLoosely("HELLO", "3", "AUTH", "default", "secret") c.Do("SMEMBERS", "s") c.DoLoosely("HELLO", "3", "AUTH", "default", "secret") // again! c.Do("SMEMBERS", "s") c.DoLoosely("HELLO", "2", "AUTH", "default", "secret") // again! c.Do("SMEMBERS", "s") c.DoLoosely("HELLO", "3", "AUTH", "default", "wrong") c.Do("SMEMBERS", "s") }, ) testUserAuth(t, map[string]string{ "sesame": "open", }, func(c *client) { c.Error("Authentication required", "SADD", "s", "aap") // sets have resp3 specific code c.Error("invalid", "HELLO", "3", "AUTH", "foo", "bar") c.Error("invalid", "HELLO", "3", "AUTH", "sesame", "close") c.Error("Authentication required", "SMEMBERS", "s") c.DoLoosely("HELLO", "3", "AUTH", "sesame", "open123") c.Error("Authentication required", "SMEMBERS", "s") }, ) } ================================================ FILE: miniredis/tests/integration-go/ephemeral.go ================================================ package main // Start a miniredis-rs server on a random port. import ( "bufio" "fmt" "os/exec" "strings" ) const executable = "miniredis-rs-server" type ephemeral exec.Cmd // Redis starts a miniredis-rs on a random port. Will panic if that // doesn't work. // Returns something which you'll have to Close(), and a string to give to Dial() func Redis() (*ephemeral, string) { return runRedis("") } // RedisAuth starts a miniredis-rs on a random port with authentication enabled. func RedisAuth(passwd string) (*ephemeral, string) { return runRedis(fmt.Sprintf("requirepass %s", passwd)) } // RedisUserAuth starts a miniredis-rs on a random port with ACL rules enabled. func RedisUserAuth(users map[string]string) (*ephemeral, string) { acls := "user default on -@all +hello\n" for user, pass := range users { acls += fmt.Sprintf("user %s on +@all ~* >%s\n", user, pass) } return runRedis(acls) } // RedisCluster starts a miniredis-rs on a random port in cluster mode. func RedisCluster() (*ephemeral, string) { return runRedis("cluster-enabled yes\ncluster-config-file nodes.conf") } func RedisTLS() (*ephemeral, string) { return runRedis(` tls-port 0 tls-cert-file ../../testdata/server.crt tls-key-file ../../testdata/server.key tls-ca-cert-file ../../testdata/ca.crt `) } func runRedis(extraConfig string) (*ephemeral, string) { c := exec.Command(executable, "-") stdin, err := c.StdinPipe() if err != nil { panic(err) } stdout, err := c.StdoutPipe() if err != nil { panic(err) } c.Stderr = nil // inherit stderr for debugging if err := c.Start(); err != nil { panic(fmt.Sprintf("starting %s: %s", executable, err)) } // Send config and close stdin so the server knows config is complete. fmt.Fprintf(stdin, "port 0\nbind 127.0.0.1\nappendonly no\n%s", extraConfig) stdin.Close() // Read the PORT= readiness line from stdout. scanner := bufio.NewScanner(stdout) if !scanner.Scan() { c.Process.Kill() c.Wait() panic("miniredis-rs-server: no readiness line on stdout") } line := scanner.Text() if !strings.HasPrefix(line, "PORT=") { c.Process.Kill() c.Wait() panic(fmt.Sprintf("miniredis-rs-server: unexpected output: %q", line)) } port := strings.TrimPrefix(line, "PORT=") addr := fmt.Sprintf("127.0.0.1:%s", port) e := ephemeral(*c) return &e, addr } func (e *ephemeral) Close() { ((*exec.Cmd)(e)).Process.Kill() ((*exec.Cmd)(e)).Wait() } ================================================ FILE: miniredis/tests/integration-go/generic_test.go ================================================ package main import ( "strings" "testing" "time" ) func TestKeys(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "one", "1") c.Do("SET", "two", "2") c.Do("SET", "three", "3") c.Do("SET", "four", "4") c.DoSorted("KEYS", `*o*`) c.DoSorted("KEYS", `t??`) c.DoSorted("KEYS", `t?*`) c.DoSorted("KEYS", `*`) c.DoSorted("KEYS", `t*`) c.DoSorted("KEYS", `t\*`) c.DoSorted("KEYS", `[tf]*`) // zero length key c.Do("SET", "", "nothing") c.Do("GET", "") // Simple failure cases c.Error("wrong number", "KEYS") c.Error("wrong number", "KEYS", "foo", "bar") }) testRaw(t, func(c *client) { c.Do("SET", "[one]", "1") c.Do("SET", "two", "2") c.DoSorted("KEYS", `[\[o]*`) c.DoSorted("KEYS", `\[*`) c.DoSorted("KEYS", `*o*`) c.DoSorted("KEYS", `[]*`) // nothing }) } func TestRandom(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RANDOMKEY") // A random key from a DB with a single key. We can test that. c.Do("SET", "one", "1") c.Do("RANDOMKEY") // Simple failure cases c.Error("wrong number", "RANDOMKEY", "bar") }) } func TestUnknownCommand(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Error("unknown", "nosuch") c.Error("unknown", "noSUCH") c.Error("unknown", "noSUCH", "1", "2", "3") }) } func TestQuit(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("QUIT") }) } func TestExists(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "a", "3") c.Do("HSET", "b", "c", "d") c.Do("EXISTS", "a", "b") c.Do("EXISTS", "a", "b", "q") c.Do("EXISTS", "a", "b", "b", "b", "a", "q") // Error cases c.Error("wrong number", "EXISTS") }) } func TestRename(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No 'a' key c.Error("no such", "RENAME", "a", "b") // Move a key with the TTL. c.Do("SET", "a", "3") c.Do("EXPIRE", "a", "123") c.Do("SET", "b", "12") c.Do("RENAME", "a", "b") c.Do("EXISTS", "a") c.Do("GET", "a") c.Do("TYPE", "a") c.Do("TTL", "a") c.Do("EXISTS", "b") c.Do("GET", "b") c.Do("TYPE", "b") c.Do("TTL", "b") // move a key without TTL c.Do("SET", "nottl", "3") c.Do("RENAME", "nottl", "stillnottl") c.Do("TTL", "nottl") c.Do("TTL", "stillnottl") // Error cases c.Error("wrong number", "RENAME") c.Error("wrong number", "RENAME", "a") c.Error("wrong number", "RENAME", "a", "b", "toomany") }) } func TestRenamenx(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No 'a' key c.Error("no such", "RENAMENX", "a", "b") c.Do("SET", "a", "value") c.Do("SET", "str", "value") c.Do("RENAMENX", "a", "str") c.Do("EXISTS", "a") c.Do("EXISTS", "str") c.Do("GET", "a") c.Do("GET", "str") c.Do("RENAMENX", "a", "nosuch") c.Do("EXISTS", "a") c.Do("EXISTS", "nosuch") // Error cases c.Error("wrong number", "RENAMENX") c.Error("wrong number", "RENAMENX", "a") c.Error("wrong number", "RENAMENX", "a", "b", "toomany") }) } func TestScan(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No keys yet c.Do("SCAN", "0") c.Do("SET", "key", "value") c.Do("SCAN", "0") c.Do("SCAN", "0", "COUNT", "12") c.Do("SCAN", "0", "cOuNt", "12") c.Do("SET", "anotherkey", "value") c.Do("SCAN", "0", "MATCH", "anoth*") c.Do("SCAN", "0", "MATCH", "anoth*", "COUNT", "100") c.Do("SCAN", "0", "COUNT", "100", "MATCH", "anoth*") c.Do("SADD", "setkey", "setitem") c.Do("SCAN", "0", "TYPE", "set") c.Do("SCAN", "0", "tYpE", "sEt") c.Do("SCAN", "0", "TYPE", "not-a-type") c.Do("SCAN", "0", "TYPE", "set", "MATCH", "setkey") c.Do("SCAN", "0", "TYPE", "set", "COUNT", "100") c.Do("SCAN", "0", "TYPE", "set", "MATCH", "setkey", "COUNT", "100") // SCAN may return a higher count of items than requested (See https://redis.io/docs/manual/keyspace/), so we must query all items. c.DoLoosely("SCAN", "0", "COUNT", "100") // cursor differs // Can't really test multiple keys. // c.Do("SET", "key2", "value2") // c.Do("SCAN", "0") // Error cases c.Error("wrong number", "SCAN") c.Error("invalid cursor", "SCAN", "noint") c.Error("not an integer", "SCAN", "0", "COUNT", "noint") c.Error("syntax error", "SCAN", "0", "COUNT", "0") c.Error("syntax error", "SCAN", "0", "COUNT") c.Error("syntax error", "SCAN", "0", "MATCH") c.Error("syntax error", "SCAN", "0", "garbage") c.Error("syntax error", "SCAN", "0", "COUNT", "12", "MATCH", "foo", "garbage") c.Error("syntax error", "SCAN", "0", "TYPE") }) } func TestFastForward(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "key1", "value") c.Do("SET", "key", "value", "PX", "100") c.DoSorted("KEYS", "*") c.miniredis.FastForward(200 * time.Millisecond) c.real.Do("MINIREDIS.FASTFORWARD", "200") c.DoSorted("KEYS", "*") }) testRaw(t, func(c *client) { c.Error("invalid expire", "SET", "key1", "value", "PX", "-100") c.Error("invalid expire", "SET", "key2", "value", "EX", "-100") c.Error("invalid expire", "SET", "key3", "value", "EX", "0") c.DoSorted("KEYS", "*") c.Do("SET", "key4", "value") c.DoSorted("KEYS", "*") c.Do("EXPIRE", "key4", "-100") c.DoSorted("KEYS", "*") c.Do("SET", "key4", "value") c.DoSorted("KEYS", "*") c.Do("EXPIRE", "key4", "0") c.DoSorted("KEYS", "*") }) } func TestProto(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ECHO", strings.Repeat("X", 1<<24)) }) } func TestSwapdb(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "key1", "val1") c.Do("SWAPDB", "0", "1") c.Do("SELECT", "1") c.Do("GET", "key1") c.Do("SWAPDB", "1", "1") c.Do("GET", "key1") c.Error("wrong number", "SWAPDB") c.Error("wrong number", "SWAPDB", "1") c.Error("wrong number", "SWAPDB", "1", "2", "3") c.Error("invalid first", "SWAPDB", "foo", "2") c.Error("invalid second", "SWAPDB", "1", "bar") c.Error("invalid first", "SWAPDB", "foo", "bar") c.Error("out of range", "SWAPDB", "-1", "2") c.Error("out of range", "SWAPDB", "1", "-2") // c.Do("SWAPDB", "1", "1000") // miniredis has no upperlimit }) // SWAPDB with transactions testRaw2(t, func(c1, c2 *client) { c1.Do("SET", "foo", "foooooo") c1.Do("MULTI") c1.Do("SWAPDB", "0", "2") c1.Do("GET", "foo") c2.Do("GET", "foo") c1.Do("EXEC") c1.Do("GET", "foo") c2.Do("GET", "foo") }) } func TestDel(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "one", "1") c.Do("SET", "two", "2") c.Do("SET", "three", "3") c.Do("SET", "four", "4") c.Do("DEL", "one") c.DoSorted("KEYS", "*") c.Do("DEL", "twoooo") c.DoSorted("KEYS", "*") c.Do("DEL", "two", "four") c.DoSorted("KEYS", "*") c.Error("wrong number", "DEL") c.DoSorted("KEYS", "*") }) } func TestUnlink(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "one", "1") c.Do("SET", "two", "2") c.Do("SET", "three", "3") c.Do("SET", "four", "4") c.Do("UNLINK", "one") c.DoSorted("KEYS", "*") c.Do("UNLINK", "twoooo") c.DoSorted("KEYS", "*") c.Do("UNLINK", "two", "four") c.DoSorted("KEYS", "*") c.Error("wrong number", "UNLINK") c.DoSorted("KEYS", "*") }) } func TestTouch(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "a", "some value") c.Do("TOUCH", "a") c.Do("GET", "a") c.Do("TTL", "a") c.Do("TOUCH", "a", "foobar", "a") c.Error("wrong number", "TOUCH") }) } func TestPersist(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("EXPIRE", "foo", "12") c.Do("TTL", "foo") c.Do("PERSIST", "foo") c.Do("TTL", "foo") }) } func TestCopy(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Error("wrong number", "COPY") c.Error("wrong number", "COPY", "a") c.Error("syntax", "COPY", "a", "b", "c") c.Error("syntax", "COPY", "a", "b", "DB") c.Error("range", "COPY", "a", "b", "DB", "-1") c.Error("integer", "COPY", "a", "b", "DB", "foo") c.Error("syntax", "COPY", "a", "b", "DB", "1", "REPLACE", "foo") c.Do("SET", "a", "1") c.Do("COPY", "a", "b") // returns 1 - successfully copied c.Do("EXISTS", "b") c.Do("GET", "b") c.Do("TYPE", "b") c.Do("COPY", "nonexistent", "c") // returns 1 - not successfully copied c.Do("RENAME", "b", "c") // rename the copied key t.Run("replace option", func(t *testing.T) { c.Do("SET", "fromme", "1") c.Do("HSET", "replaceme", "foo", "bar") c.Do("COPY", "fromme", "replaceme", "REPLACE") c.Do("TYPE", "replaceme") c.Do("GET", "replaceme") }) t.Run("different DB", func(t *testing.T) { c.Do("SELECT", "2") c.Do("SET", "fromme", "1") c.Do("COPY", "fromme", "replaceme", "DB", "3") c.Do("EXISTS", "replaceme") // your value is in another db c.Do("SELECT", "3") c.Do("EXISTS", "replaceme") c.Do("TYPE", "replaceme") c.Do("GET", "replaceme") }) c.Do("SELECT", "0") t.Run("copy to self", func(t *testing.T) { // copy to self is never allowed c.Do("SET", "double", "1") c.Error("the same", "COPY", "double", "double") c.Error("the same", "COPY", "double", "double", "REPLACE") c.Do("COPY", "double", "double", "DB", "2") // different DB is fine c.Do("SELECT", "2") c.Do("TYPE", "double") c.Error("the same", "COPY", "noexisting", "noexisting") // "copy to self?" check comes before key check }) c.Do("SELECT", "0") // deep copies? t.Run("hash", func(t *testing.T) { c.Do("HSET", "temp", "paris", "12") c.Do("HSET", "temp", "oslo", "-5") c.Do("COPY", "temp", "temp2") c.Do("TYPE", "temp2") c.Do("HGET", "temp2", "oslo") c.Do("HSET", "temp2", "oslo", "-7") c.Do("HGET", "temp", "oslo") c.Do("HGET", "temp2", "oslo") }) t.Run("list set", func(t *testing.T) { c.Do("LPUSH", "list", "aap", "noot", "mies") c.Do("COPY", "list", "list2") c.Do("TYPE", "list2") c.Do("LSET", "list", "1", "vuur") c.Do("LRANGE", "list", "0", "-1") c.Do("LRANGE", "list2", "0", "-1") }) t.Run("list", func(t *testing.T) { c.Do("LPUSH", "list", "aap", "noot", "mies") c.Do("COPY", "list", "list2") c.Do("TYPE", "list2") c.Do("LPUSH", "list", "vuur") c.Do("LRANGE", "list", "0", "-1") c.Do("LRANGE", "list2", "0", "-1") }) t.Run("set", func(t *testing.T) { c.Do("SADD", "set", "aap", "noot", "mies") c.Do("COPY", "set", "set2") c.Do("TYPE", "set2") c.DoSorted("SMEMBERS", "set2") c.Do("SADD", "set", "vuur") c.DoSorted("SMEMBERS", "set") c.DoSorted("SMEMBERS", "set2") }) t.Run("sorted set", func(t *testing.T) { c.Do("ZADD", "zset", "1", "aap", "2", "noot", "3", "mies") c.Do("COPY", "zset", "zset2") c.Do("TYPE", "zset2") c.Do("ZCARD", "zset") c.Do("ZCARD", "zset2") c.Do("ZADD", "zset", "4", "vuur") c.Do("ZCARD", "zset") c.Do("ZCARD", "zset2") }) t.Run("stream", func(t *testing.T) { c.Do("XADD", "planets", "0-1", "name", "Mercury", ) c.Do("COPY", "planets", "planets2") c.Do("XLEN", "planets2") c.Do("TYPE", "planets2") c.Do("XADD", "planets", "18446744073709551000-0", "name", "Earth", ) c.Do("XLEN", "planets") c.Do("XLEN", "planets2") }) t.Run("stream", func(t *testing.T) { c.Do("PFADD", "hlog", "42") c.DoApprox(2, "PFCOUNT", "hlog") c.Do("COPY", "hlog", "hlog2") // c.Do("TYPE", "hlog2") broken c.Do("PFADD", "hlog", "44") c.Do("PFCOUNT", "hlog") c.Do("PFCOUNT", "hlog2") }) }) } func TestClient(t *testing.T) { skip(t) testRaw(t, func(c *client) { // Try to get the client name without setting it first c.Do("CLIENT", "GETNAME") c.Do("CLIENT", "SETNAME", "miniredis-tests") c.Do("CLIENT", "GETNAME") c.Do("CLIENT", "SETNAME", "miniredis-tests2") c.Do("CLIENT", "GETNAME") c.Do("CLIENT", "SETNAME", "") c.Do("CLIENT", "GETNAME") c.Error("wrong number", "CLIENT") c.Error("unknown subcommand", "CLIENT", "FOOBAR") c.Error("wrong number", "CLIENT", "GETNAME", "foo") c.Error("contain spaces", "CLIENT", "SETNAME", "miniredis tests") c.Error("contain spaces", "CLIENT", "SETNAME", "miniredis\ntests") }) testRaw2(t, func(c1, c2 *client) { c1.Do("MULTI") c1.Do("CLIENT", "SETNAME", "conn-c1") c1.Do("CLIENT", "GETNAME") c2.Do("CLIENT", "GETNAME") // not set yet c1.Do("EXEC") c1.Do("CLIENT", "GETNAME") c2.Do("CLIENT", "GETNAME") }) } func TestObject(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("OBJECT", "IDLETIME", "foo") c.Do("SET", "foo", "bar") c.Do("OBJECT", "IDLETIME", "foo") c.Do("GET", "foo") c.Do("OBJECT", "IDLETIME", "foo") c.Error("number", "OBJECT") c.Error("unknown subcommand 'foo'", "OBJECT", "foo") c.Error("object|idletime", "OBJECT", "IDLETIME") c.Error("wrong number", "OBJECT", "IDLETIME", "foo", "bar") c.Do("MULTI") c.Do("OBJECT", "IDLETIME", "foo") c.Error("object|idletime", "OBJECT", "IDLETIME", "bar", "baz") c.Error("object|idletime", "OBJECT", "IDLETIME") c.Error("Transaction discarded", "EXEC") }) } ================================================ FILE: miniredis/tests/integration-go/geo_test.go ================================================ package main import ( "testing" ) func TestGeoadd(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("GEOADD", "Sicily", "13.361389", "38.115556", "Palermo", "15.087269", "37.502669", "Catania", ) c.Do("ZRANGE", "Sicily", "0", "-1") c.Do("ZRANGE", "Sicily", "0", "-1", "WITHSCORES") c.Do("GEOADD", "mountains", "86.9248308", "27.9878675", "Everest", "142.1993050", "11.3299030", "Challenger Deep", "31.132", "29.976", "Pyramids", ) c.Do("ZRANGE", "mountains", "0", "-1") c.Do("GEOADD", // re-add an existing one "mountains", "86.9248308", "27.9878675", "Everest", ) c.Do("ZRANGE", "mountains", "0", "-1") c.Do("GEOADD", // update "mountains", "86.9248308", "28.000", "Everest", ) c.Do("ZRANGE", "mountains", "0", "-1") // failure cases c.Error("invalid", "GEOADD", "err", "186.9248308", "27.9878675", "not the Everest") c.Error("invalid", "GEOADD", "err", "-186.9248308", "27.9878675", "not the Everest") c.Error("invalid", "GEOADD", "err", "86.9248308", "87.9878675", "not the Everest") c.Error("invalid", "GEOADD", "err", "86.9248308", "-87.9", "not the Everest") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "GEOADD", "str", "86.9248308", "27.9878675", "Everest") c.Error("wrong number", "GEOADD") c.Error("wrong number", "GEOADD", "foo") c.Error("wrong number", "GEOADD", "foo", "86.9248308") c.Error("wrong number", "GEOADD", "foo", "86.9248308", "27.9878675") c.Do("GEOADD", "foo", "86.9248308", "27.9878675", "") c.Error("not a valid float", "GEOADD", "foo", "eight", "27.9878675", "bar") c.Error("not a valid float", "GEOADD", "foo", "86.9248308", "seven", "bar") // failures in a transaction c.Do("MULTI") c.Error("wrong number", "GEOADD", "foo") c.Error("discarded", "EXEC") c.Do("MULTI") c.Do("GEOADD", "foo", "eight", "27.9878675", "bar") c.Do("EXEC") // 2nd key is invalid c.Do("MULTI") c.Do("GEOADD", "two", "86.9248308", "28.000", "Everest", "eight", "27.9878675", "bar", ) c.Do("EXEC") c.Do("ZRANGE", "two", "0", "-1") }) } func TestGeopos(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("GEOADD", "Sicily", "13.361389", "38.115556", "Palermo", "15.087269", "37.502669", "Catania", ) c.Do("GEOPOS", "Sicily") c.DoRounded(3, "GEOPOS", "Sicily", "Palermo") c.Do("GEOPOS", "Sicily", "nosuch") c.DoRounded(3, "GEOPOS", "Sicily", "Catania", "Palermo") c.DoRounded(3, "GEOPOS", "Sicily", "Catania", "Catania", "Palermo") c.Do("GEOPOS", "nosuch", "Palermo") // failure cases c.Error("wrong number", "GEOPOS") c.Do("SET", "foo", "bar") c.Error("wrong kind", "GEOPOS", "foo", "Palermo") }) } func TestGeodist(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("GEOADD", "Sicily", "13.361389", "38.115556", "Palermo", "15.087269", "37.502669", "Catania", ) c.DoRounded(2, "GEODIST", "Sicily", "Palermo", "Catania") c.DoRounded(2, "GEODIST", "Sicily", "Catania", "Palermo") c.Do("GEODIST", "Sicily", "nosuch", "Palermo") c.Do("GEODIST", "Sicily", "Catania", "nosuch") c.Do("GEODIST", "nosuch", "Catania", "Palermo") c.DoRounded(2, "GEODIST", "Sicily", "Palermo", "Catania", "m") c.Do("GEODIST", "Sicily", "Palermo", "Catania", "km") c.Do("GEODIST", "Sicily", "Palermo", "Catania", "KM") c.Do("GEODIST", "Sicily", "Palermo", "Catania", "mi") c.DoRounded(2, "GEODIST", "Sicily", "Palermo", "Catania", "ft") c.Do("GEODIST", "Sicily", "Palermo", "Palermo") c.Error("unsupported unit", "GEODIST", "Sicily", "Palermo", "Palermo", "yards") c.Error("wrong number", "GEODIST") c.Error("wrong number", "GEODIST", "Sicily") c.Error("wrong number", "GEODIST", "Sicily", "Palermo") c.Error("syntax error", "GEODIST", "Sicily", "Palermo", "Palermo", "miles", "too many") c.Error("unsupported unit provided. please use M, KM, FT, MI", "GEODIST", "Sicily", "Palermo", "Catania", "foobar") c.Do("SET", "string", "123") c.Error("wrong kind", "GEODIST", "string", "a", "b") }) } func TestGeoradius(t *testing.T) { skip(t) t.Run("basic", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "KM") c.Do("GEORADIUS", "stations", "1.0", "1.0", "1", "km") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "ft", "WITHDIST") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "m", "WITHDIST") // redis has more precision in the coords c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "m", "WITHCOORD") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHDIST", "WITHCOORD") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHCOORD", "WITHDIST") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHCOORD", "WITHCOORD", "WITHCOORD") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHDIST", "WITHDIST", "WITHDIST") // FIXME: the distances don't quite match for miles or km c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "mi", "WITHDIST") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHDIST") // Sorting c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "DESC") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "DESC", "ASC") // COUNT c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "COUNT", "1") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "COUNT", "2") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "COUNT", "999") c.Error("syntax error", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "COUNT") c.Error("COUNT must", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "COUNT", "0") c.Error("COUNT must", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "COUNT", "-12") c.Error("not an integer", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "COUNT", "foobar") // non-existing key c.Do("GEORADIUS", "foo", "-73.9718893", "40.7728773", "4", "km") // no error in redis, for some reason // c.Do("GEORADIUS", "foo", "-73.9718893", "40.7728773", "4", "km", "FOOBAR") c.Error("syntax error", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "FOOBAR") // GEORADIUS_RO c.Do("GEORADIUS_RO", "stations", "-73.9718893", "40.7728773", "4", "km") c.Do("GEORADIUS_RO", "stations", "1.0", "1.0", "1", "km") c.Error("syntax error", "GEORADIUS_RO", "stations", "-73.9718893", "40.7728773", "4", "km", "STORE", "bar") c.Error("syntax error", "GEORADIUS_RO", "stations", "-73.9718893", "40.7728773", "4", "km", "STOREDIST", "bar") c.Error("syntax error", "GEORADIUS_RO", "stations", "-73.9718893", "40.7728773", "4", "km", "STORE") c.Error("syntax error", "GEORADIUS_RO", "stations", "-73.9718893", "40.7728773", "4", "km", "STOREDIST") }) }) t.Run("STORE", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) // plain store c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STORE", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.Do("ZRANGE", "foo", "0", "-1", "WITHSCORES") // Yeah, valid: c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STORE", "") c.Do("ZRANGE", "", "0", "-1") // store with count, and overwrite existing key c.Do("EXPIRE", "foo", "999") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "COUNT", "1", "STORE", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.Do("TTL", "foo") // store should overwrite c.Do("SET", "taken", "123") c.Do("EXPIRE", "taken", "999") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STORE", "taken") c.Do("TYPE", "taken") c.Do("ZRANGE", "taken", "0", "-1") c.Do("TTL", "taken") // errors c.Error("syntax error", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STORE") c.Error("not compatible", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHDIST", "STORE", "foo") c.Error("not compatible", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHCOORD", "STORE", "foo") }) }) t.Run("STOREDIST", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) // plain store c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.DoRounded(3, "ZRANGE", "foo", "0", "-1", "WITHSCORES") // plain store, meter c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "m", "STOREDIST", "meter") c.Do("ZRANGE", "meter", "0", "-1") c.DoRounded(3, "ZRANGE", "meter", "0", "-1", "WITHSCORES") // Yeah, valid: c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST", "") c.Do("ZRANGE", "", "0", "-1") // STOREDIST with count c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "ASC", "COUNT", "1", "STOREDIST", "foo") c.Do("ZRANGE", "foo", "0", "-1") // store should overwrite c.Do("SET", "taken", "123") c.Do("EXPIRE", "taken", "9999") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST", "taken") c.Do("TYPE", "taken") c.Do("ZRANGE", "taken", "0", "-1") c.Do("TTL", "taken") // multiple keys c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST", "n1", "STOREDIST", "n2", "STOREDIST", "n3") c.Do("TYPE", "n1") c.Do("TYPE", "n2") c.Do("TYPE", "n3") // STORE and STOREDIST c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST", "a", "STORE", "b") c.Do("TYPE", "a") c.Do("ZRANGE", "a", "0", "-1") c.Do("TYPE", "b") c.Do("ZRANGE", "b", "0", "-1") // errors c.Error("syntax error", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "STOREDIST") c.Error("not compatible", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHDIST", "STOREDIST", "foo") c.Error("not compatible", "GEORADIUS", "stations", "-73.9718893", "40.7728773", "400", "km", "WITHCOORD", "STOREDIST", "foo") }) }) } func TestGeoradiusByMember(t *testing.T) { skip(t) t.Run("basic", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km") // c.Do("GEORADIUSBYMEMBER", "stations", "1.0", "1.0", "1", "km") // Not valid test c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "ft", "WITHDIST") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "m", "WITHDIST") // redis has more precision in the coords c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "m", "WITHCOORD") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHDIST", "WITHCOORD") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHCOORD", "WITHDIST") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHCOORD", "WITHCOORD", "WITHCOORD") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHDIST", "WITHDIST", "WITHDIST") // FIXME: the distances don't quite match for miles or km c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "mi", "WITHDIST") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHDIST") // Sorting c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "DESC") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "DESC", "ASC") // COUNT c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "COUNT", "1") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "COUNT", "2") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "COUNT", "999") c.Error("syntax error", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "COUNT") c.Error("COUNT must", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "COUNT", "0") c.Error("COUNT must", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "COUNT", "-12") c.Error("not an integer", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "COUNT", "foobar") // non-existing key // c.Do("GEORADIUSBYMEMBER", "foo", "Astor Pl", "4", "km") // Failing // geo_test.go:268: value error. expected: []interface {}{} got: case: main.command{cmd:"GEORADIUSBYMEMBER", args:[]interface {}{"foo", "Astor Pl", "4", "km"}, error:false, sort:false, loosely:false, errorSub:"", receiveOnly:false, roundFloats:0, closeChan:false} // no error in redis, for some reason // c.Do("GEORADIUSBYMEMBER", "foo", "Astor Pl", "4", "km", "FOOBAR") c.Error("syntax error", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "FOOBAR") // GEORADIUSBYMEMBER_RO c.Do("GEORADIUSBYMEMBER_RO", "stations", "Astor Pl", "4", "km") // c.Do("GEORADIUSBYMEMBER_RO", "stations", "1.0", "1.0", "1", "km") // Not a valid test c.Error("syntax error", "GEORADIUSBYMEMBER_RO", "stations", "Astor Pl", "4", "km", "STORE", "bar") c.Error("syntax error", "GEORADIUSBYMEMBER_RO", "stations", "Astor Pl", "4", "km", "STOREDIST", "bar") c.Error("syntax error", "GEORADIUSBYMEMBER_RO", "stations", "Astor Pl", "4", "km", "STORE") c.Error("syntax error", "GEORADIUSBYMEMBER_RO", "stations", "Astor Pl", "4", "km", "STOREDIST") }) }) t.Run("STORE", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) // plain store c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STORE", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.Do("ZRANGE", "foo", "0", "-1", "WITHSCORES") // Yeah, valid: c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STORE", "") c.Do("ZRANGE", "", "0", "-1") // store with count, and overwrite existing key c.Do("EXPIRE", "foo", "999") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "COUNT", "1", "STORE", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.Do("TTL", "foo") // store should overwrite c.Do("SET", "taken", "123") c.Do("EXPIRE", "taken", "999") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STORE", "taken") c.Do("TYPE", "taken") c.Do("ZRANGE", "taken", "0", "-1") c.Do("TTL", "taken") // errors c.Error("syntax error", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STORE") c.Error("not compatible", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHDIST", "STORE", "foo") c.Error("not compatible", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHCOORD", "STORE", "foo") }) }) t.Run("STOREDIST", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", ) // plain store c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST", "foo") c.Do("ZRANGE", "foo", "0", "-1") c.DoRounded(3, "ZRANGE", "foo", "0", "-1", "WITHSCORES") // plain store, meter c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "m", "STOREDIST", "meter") c.Do("ZRANGE", "meter", "0", "-1") c.DoRounded(3, "ZRANGE", "meter", "0", "-1", "WITHSCORES") // Yeah, valid: c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST", "") c.Do("ZRANGE", "", "0", "-1") // STOREDIST with count c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "ASC", "COUNT", "1", "STOREDIST", "foo") c.Do("ZRANGE", "foo", "0", "-1") // store should overwrite c.Do("SET", "taken", "123") c.Do("EXPIRE", "taken", "9999") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST", "taken") c.Do("TYPE", "taken") c.Do("ZRANGE", "taken", "0", "-1") c.Do("TTL", "taken") // multiple keys c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST", "n1", "STOREDIST", "n2", "STOREDIST", "n3") c.Do("TYPE", "n1") c.Do("TYPE", "n2") c.Do("TYPE", "n3") // STORE and STOREDIST c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST", "a", "STORE", "b") c.Do("TYPE", "a") c.Do("ZRANGE", "a", "0", "-1") c.Do("TYPE", "b") c.Do("ZRANGE", "b", "0", "-1") // errors c.Error("syntax error", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "STOREDIST") c.Error("not compatible", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHDIST", "STOREDIST", "foo") c.Error("not compatible", "GEORADIUSBYMEMBER", "stations", "Astor Pl", "400", "km", "WITHCOORD", "STOREDIST", "foo") }) }) } // a bit longer testset func TestGeo(t *testing.T) { skip(t) // some subway stations // https://data.cityofnewyork.us/Transportation/Subway-Stations/arq3-7z49/data testRaw(t, func(c *client) { c.Do("GEOADD", "stations", "-73.99106999861966", "40.73005400028978", "Astor Pl", "-74.00019299927328", "40.71880300107709", "Canal St", "-73.98384899986625", "40.76172799961419", "50th St", "-73.97499915116808", "40.68086213682956", "Bergen St", "-73.89488591154061", "40.66471445143568", "Pennsylvania Ave", "-73.90087000018522", "40.88466700064975", "238th St", "-73.95806670661364", "40.800581558114956", "Cathedral Pkwy (110th St)", "-73.94085899871263", "40.67991899941601", "Kingston - Throop Aves", "-73.8987883783301", "40.74971952935675", "65th St", "-73.92901818461539", "40.75196004401078", "36th St", "-73.98740940202974", "40.71830605618619", "Delancey St - Essex St", "-73.89165772702445", "40.67802821447783", "Van Siclen Ave", "-73.87962599910783", "40.68152000045683", "Norwood Ave", "-73.84443500029684", "40.69516599823373", "104th-102nd Sts", "-73.98177094440949", "40.690648119969794", "DeKalb Ave", "-73.82758075034528", "40.58326843810286", "Beach 105th St", "-73.81365140419632", "40.58809156457325", "Beach 90th St", "-73.89175225349464", "40.829987446384116", "Freeman St", "-73.89661738461646", "40.822142131170786", "Intervale Ave", "-73.90074099998965", "40.85609299881864", "182nd-183rd Sts", "-73.91013600050078", "40.84589999983414", "174th-175th Sts", "-73.91843200082253", "40.83376899862797", "167th St", "-73.8456249984179", "40.75462199881262", "Mets - Willets Point", "-73.86952700103515", "40.74914499948836", "Junction Blvd", "-73.83003000262508", "40.75959999915012", "Flushing - Main St", "-73.83256900003744", "40.846809998885504", "Buhre Ave", "-73.92613800014134", "40.81047600117261", "3rd Ave - 138th St", "-73.85122199961472", "40.83425499825462", "Castle Hill Ave", "-74.0041310005885", "40.713064999433136", "Brooklyn Bridge - City Hall", "-73.8470359987544", "40.836488000608156", "Zerega Ave", "-73.9767132992584", "40.75180742981634", "Grand Central - 42nd St", "-73.98207600148947", "40.74608099909145", "33rd St", "-73.9510700015425", "40.78567199998607", "96th St", "-73.95987399886047", "40.77362000074615", "77th St", "-73.91038357033376", "40.68285130087804", "Chauncey St", "-73.98310999909673", "40.67731566735096", "Union St", "-73.8820347465864", "40.74237007972169", "Elmhurst Ave", "-73.92078599933306", "40.678822000873375", "Ralph Ave", "-73.86748067850041", "40.8571924091606", "Pelham Pkwy", "-73.86613410538703", "40.877839385172024", "Gun Hill Rd", "-73.8543153107622", "40.898286515575286", "Nereid Ave (238 St)", "-73.9580997367769", "40.67076515344894", "Franklin Ave", "-73.89306639507903", "40.823976841237396", "Simpson St", "-73.86835609178098", "40.848768666338934", "Bronx Park East", "-73.95007934590994", "40.65665931376077", "Winthrop St", "-73.88940491730106", "40.665517963059635", "Van Siclen Ave", "-73.9273847542618", "40.81830344372315", "149th St - Grand Concourse", "-73.92569199505733", "40.82823032742169", "161st St - Yankee Stadium", "-73.9679670004732", "40.762526000304575", "Lexington Ave - 59th St", "-73.90409799875945", "40.81211799827203", "E 149th St", "-73.87451599929486", "40.82952100156747", "Morrison Av - Soundview", "-73.8862829985325", "40.82652500055904", "Whitlock Ave", "-73.86761799923673", "40.8315090005233", "St Lawrence Ave", "-73.90298400173006", "40.745630001138395", "Woodside - 61st St", "-73.75540499924732", "40.603995001687544", "Far Rockaway - Mott Ave", "-73.976336575218", "40.77551939729258", "72nd St", "-73.96460245687166", "40.79161879767014", "96th St", "-73.93956099985425", "40.84071899990795", "168th St", "-73.8935090000331", "40.86697799999945", "Kingsbridge Rd", "-73.98459099904711", "40.754184001312545", "42nd St - Bryant Pk", "-73.96203130426609", "40.6616334551018", "Prospect Park", "-73.99534882595742", "40.63147876093745", "55th St", "-73.81701287135405", "40.70289855287313", "Jamaica - Van Wyck", "-73.8303702709878", "40.714034819571026", "Kew Gardens - Union Tpke", "-73.80800471963833", "40.700382424235", "Sutphin Blvd - Archer Av", "-73.94605470266329", "40.747768121414325", "Court Sq - 23rd St", "-73.85286048434907", "40.726505475813006", "67th Ave", "-73.87722085669182", "40.736813418197144", "Grand Ave - Newtown", "-73.97817199965161", "40.63611866666291", "Ditmas Ave", "-73.95999000137212", "40.68888900026455", "Classon Ave", "-73.95031225606621", "40.706126576274166", "Broadway", "-73.95024799996972", "40.71407200064717", "Lorimer St", "-73.9019160004208", "40.66914500061398", "Sutter Ave", "-73.90395860491864", "40.68886654246024", "Wilson Ave", "-73.9166388842194", "40.686415270704344", "Halsey St", "-73.94735499884204", "40.703844000042096", "Lorimer St", "-74.01151599772157", "40.634970999647166", "8th Ave", "-73.929861999118", "40.7564420005104", "36th Ave", "-73.92582299919906", "40.761431998800546", "Broadway", "-73.98676800153976", "40.75461199851542", "Times Sq - 42nd St", "-73.97918899989101", "40.75276866674217", "Grand Central - 42nd St", "-73.95762400074634", "40.67477166685263", "Park Pl", "-73.83216299845388", "40.68433100001238", "111th St", "-74.00030814755975", "40.732254493367876", "W 4th St - Washington Sq (Lower)", "-73.97192000069982", "40.75710699989316", "51st St", "-73.97621799859327", "40.78864400073892", "86th St", "-73.85736239521543", "40.89314324138378", "233rd St", "-73.98220899995783", "40.77344000052039", "66th St - Lincoln Ctr", "-73.89054900017344", "40.82094799852307", "Hunts Point Ave", "-74.0062770001748", "40.72285399778783", "Canal St", "-73.83632199755944", "40.84386300128381", "Middletown Rd", "-73.98659900207888", "40.739864000474604", "23rd St", "-73.94526400039679", "40.74702299889643", "Court Sq", "-73.98192900232715", "40.76824700063689", "59th St - Columbus Circle", "-73.9489160009391", "40.74221599986316", "Hunters Point Ave", "-73.9956570016487", "40.74408099989751", "23rd St", "-74.00536700180581", "40.728251000730204", "Houston St", "-73.83768300060997", "40.681711001091195", "104th St", "-73.81583268782963", "40.60840218069683", "Broad Channel", "-73.96850099975177", "40.57631166708091", "Ocean Pkwy", "-73.95358099875249", "40.74262599969749", "Vernon Blvd - Jackson Ave", "-73.96387000158042", "40.76814100049679", "68th St - Hunter College", "-73.9401635351909", "40.750635651014804", "Queensboro Plz", "-73.8438529979573", "40.680428999588415", "Rockaway Blvd", "-73.98995099881881", "40.734673000996125", "Union Sq - 14th St", "-73.95352200064022", "40.68962700158444", "Bedford - Nostrand Aves", "-73.97973580592873", "40.66003568810021", "15th St - Prospect Park", "-73.98025117900944", "40.66624469001985", "7th Ave", "-73.97577599917474", "40.65078166803418", "Ft Hamilton Pkwy", "-73.97972116229084", "40.64427200012998", "Church Ave", "-73.96435779623125", "40.64390459860419", "Beverly Rd", "-73.96288246192114", "40.65049324646484", "Church Ave", "-73.96269486837261", "40.63514193733789", "Newkirk Ave", "-73.96145343987648", "40.65507304163716", "Parkside Ave", "-73.9709563319228", "40.6752946951032", "Grand Army Plaza", "-73.97754993539385", "40.68442016526762", "Atlantic Av - Barclay's Center", "-73.91194599726617", "40.678339999883505", "Rockaway Ave", "-73.97537499833149", "40.68711899950771", "Fulton St", "-73.9667959986695", "40.68809400106055", "Clinton - Washington Aves", "-73.97285279191024", "40.67710217983294", "7th Ave", "-73.97678343963167", "40.684488323453685", "Atlantic Av - Barclay's Center", "-73.97880999956767", "40.683665667279435", "Atlantic Av - Barclay's Center", "-73.99015100090539", "40.692403999991036", "Borough Hall", "-73.83591899965162", "40.672096999172844", "Aqueduct Racetrack", "-73.86049500117254", "40.85436399966426", "Morris Park", "-73.85535900043564", "40.858984999820116", "Pelham Pkwy", "-73.95042600099683", "40.68043800006226", "Nostrand Ave", "-73.98040679874578", "40.68831058019022", "Nevins St", "-73.96422203748425", "40.67203223545925", "Eastern Pkwy - Bklyn Museum", "-73.94884798381702", "40.64512351894373", "Beverly Rd", "-73.94945514035334", "40.6508606878022", "Church Ave", "-73.94829990822407", "40.63999124275311", "Newkirk Ave", "-73.94754120734406", "40.63284240700742", "Brooklyn College - Flatbush Ave", "-73.95072891124937", "40.6627729934283", "Sterling St", "-73.93293256081851", "40.66897831107809", "Crown Hts - Utica Ave", "-73.94215978392963", "40.66948144864978", "Kingston Ave", "-73.95118300016523", "40.724479997808274", "Nassau Ave", "-73.95442500146235", "40.73126699971465", "Greenpoint Ave", "-73.95783200075729", "40.708383000017925", "Marcy Ave", "-73.95348800038457", "40.706889998054", "Hewes St", "-73.92984899935611", "40.81322399958908", "138th St - Grand Concourse", "-73.9752485052734", "40.76008683231326", "5th Ave - 53rd St", "-73.96907237490204", "40.75746830782865", "Lexington Ave - 53rd St", "-73.98869800128737", "40.74545399979951", "28th St", "-73.9879368338264", "40.74964456009442", "Herald Sq - 34th St", "-73.98168087489128", "40.73097497580066", "1st Ave", "-73.98622899953202", "40.755983000570076", "Times Sq - 42nd St", "-73.9514239994525", "40.71277400073426", "Metropolitan Ave", "-73.94049699874644", "40.71157600064823", "Grand St", "-73.94394399869037", "40.714575998363635", "Graham Ave", "-73.95666499806525", "40.71717399858899", "Bedford Ave", "-73.93979284713505", "40.70739106438455", "Montrose Ave", "-73.94381559597835", "40.74630503357145", "Long Island City - Court Sq", "-73.9495999997552", "40.7441286664954", "21st St", "-73.93285137679598", "40.75276306140845", "39th Ave", "-73.95035999879713", "40.82655099962194", "145th St", "-73.94488999901047", "40.8340410001399", "157th St", "-73.97232299915696", "40.79391900121471", "96th St", "-73.96837899960818", "40.799446000334825", "103rd St", "-73.95182200176913", "40.79907499977324", "Central Park North (110th St)", "-73.96137008267617", "40.796060739904526", "103rd St", "-73.98197000159583", "40.77845300068614", "72nd St", "-73.97209794937208", "40.78134608418206", "81st St", "-73.83692369387158", "40.71804465348743", "75th Ave", "-73.96882849429672", "40.78582304678557", "86th St", "-73.9668470005456", "40.80396699961484", "Cathedral Pkwy (110th St)", "-73.96410999757751", "40.807722001230864", "116th St - Columbia University", "-73.94549500011411", "40.807753999182815", "125th St", "-73.94077000106708", "40.8142290003391", "135th St", "-73.94962500096905", "40.802097999133004", "116th St", "-73.90522700122354", "40.850409999510234", "Tremont Ave", "-73.95367600087873", "40.82200799968475", "137th St - City College", "-73.93624499873299", "40.82042099969279", "145th St", "-73.91179399884471", "40.8484800012369", "176th St", "-73.9076840015997", "40.85345300155693", "Burnside Ave", "-73.91339999846983", "40.83930599964156", "170th St", "-73.94013299907257", "40.840555999148535", "168th St", "-73.9335959996056", "40.84950499974065", "181st St", "-73.92941199742039", "40.85522500175836", "191st St", "-73.93970399761596", "40.84739100072403", "175th St", "-73.77601299999507", "40.59294299908617", "Beach 44th St", "-73.7885219980118", "40.59237400121235", "Beach 60th St", "-73.82052058959523", "40.58538569133279", "Beach 98th St", "-73.83559008701239", "40.580955865573515", "Rockaway Park - Beach 116 St", "-73.76817499939688", "40.59539800166876", "Beach 36th St", "-73.76135299762073", "40.60006600105881", "Beach 25th St", "-73.80328900021885", "40.707571999615695", "Parsons Blvd", "-73.79347419927721", "40.710517502784", "169th St", "-73.86269999830412", "40.749865000555545", "103rd St - Corona Plaza", "-73.85533399834884", "40.75172999941711", "111th St", "-73.86161820097203", "40.729763972422425", "63rd Dr - Rego Park", "-73.86504999877702", "40.67704400054478", "Grant Ave", "-73.97991700056134", "40.78393399959032", "79th St", "-73.9030969995401", "40.67534466640805", "Atlantic Ave", "-74.00290599855235", "40.73342200104225", "Christopher St - Sheridan Sq", "-73.82579799906613", "40.68595099878361", "Ozone Park - Lefferts Blvd", "-73.98769099825152", "40.755477001982506", "Times Sq - 42nd St", "-73.97595787413822", "40.576033818103646", "W 8th St - NY Aquarium", "-73.99336500134324", "40.74721499918219", "28th St", "-73.98426400110407", "40.743069999259035", "28th St", "-73.82812100059289", "40.85246199951662", "Pelham Bay Park", "-73.84295199925012", "40.839892001013915", "Westchester Sq - E Tremont Ave", "-73.99787100060406", "40.741039999802105", "18th St", "-73.97604100111508", "40.751431000286864", "Grand Central - 42nd St", "-73.7969239998421", "40.59092700078133", "Beach 67th St", "-74.00049500225435", "40.73233799774325", "W 4th St - Washington Sq (Upper)", "-73.86008700006875", "40.69242699966103", "85th St - Forest Pky", "-73.85205199740794", "40.69370399880105", "Woodhaven Blvd", "-73.83679338454697", "40.697114810696476", "111th St", "-73.82834900017954", "40.700481998515315", "121st St", "-73.90393400118631", "40.69551800114878", "Halsey St", "-73.9109757182647", "40.699471062427136", "Myrtle - Wyckoff Aves", "-73.88411070800329", "40.6663149325969", "New Lots Ave", "-73.8903580002471", "40.67270999906104", "Van Siclen Ave", "-73.8851940021643", "40.679777998961164", "Cleveland St", "-73.90056237226057", "40.66405727094644", "Livonia Ave", "-73.90244864183562", "40.66358900181724", "Junius St", "-73.90895833584449", "40.66261748815223", "Rockaway Ave", "-73.90185000017287", "40.64665366739528", "Canarsie - Rockaway Pkwy", "-73.89954769388724", "40.65046878544699", "E 105th St", "-73.91633025007947", "40.6615297898075", "Saratoga Ave", "-73.92252118536001", "40.66476678877493", "Sutter Ave - Rutland Road", "-73.89927796057142", "40.65891477368527", "New Lots Ave", "-73.90428999746412", "40.67936600147369", "Broadway Junction", "-73.89852600159652", "40.676998000003756", "Alabama Ave", "-73.88074999747269", "40.6741300014559", "Shepherd Ave", "-73.87392925215778", "40.68315265707736", "Crescent St", "-73.87332199882995", "40.689616000838754", "Cypress Hills", "-73.86728799944963", "40.691290001246735", "75th St - Eldert Ln", "-73.8964029993185", "40.746324999410284", "69th St", "-73.8912051289911", "40.746867573829114", "74th St - Broadway", "-73.86943208612348", "40.73309737380972", "Woodhaven Blvd - Queens Mall", "-73.91217899939602", "40.69945400090837", "Myrtle - Wyckoff Aves", "-73.90758199885423", "40.70291899894902", "Seneca Ave", "-73.91823200219723", "40.70369299961644", "DeKalb Ave", "-73.91254899891254", "40.744149001021576", "52nd St", "-73.91352174995538", "40.756316952608096", "46th St", "-73.90606508052358", "40.752824829236076", "Northern Blvd", "-73.91843500103973", "40.74313200060382", "46th St", "-73.88369700071884", "40.747658999559135", "82nd St - Jackson Hts", "-73.87661299986985", "40.74840800060913", "90th St - Elmhurst Av", "-73.83030100071032", "40.66047600004959", "Howard Beach - JFK Airport", "-73.83405799948723", "40.668234001699815", "Aqueduct - North Conduit Av", "-73.82069263637443", "40.70916181536946", "Briarwood - Van Wyck Blvd", "-73.84451672012669", "40.72159430953587", "Forest Hills - 71st Av", "-73.81083299897232", "40.70541799906764", "Sutphin Blvd", "-73.80109632298924", "40.70206737621188", "Jamaica Ctr - Parsons / Archer", "-73.86021461772737", "40.88802825863786", "225th St", "-73.87915899874777", "40.82858400108929", "Elder Ave", "-73.89643499897414", "40.816103999972405", "Longwood Ave", "-73.91809500109238", "40.77003699949086", "Astoria Blvd", "-73.9120340001031", "40.775035666523664", "Astoria - Ditmars Blvd", "-73.9077019387083", "40.81643746686396", "Jackson Ave", "-73.90177778730917", "40.81948726483844", "Prospect Ave", "-73.91404199994753", "40.8053680007636", "Cypress Ave", "-73.88769359812888", "40.837195550170605", "174th St", "-73.86723422851625", "40.86548337793927", "Allerton Ave", "-73.90765699936489", "40.80871900090143", "E 143rd St - St Mary's St", "-73.89717400101743", "40.867760000885795", "Kingsbridge Rd", "-73.89006400069478", "40.87341199980121", "Bedford Park Blvd - Lehman College", "-73.93647000005559", "40.82388000080457", "Harlem - 148 St", "-73.9146849986034", "40.84443400092679", "Mt Eden Ave", "-73.89774900102401", "40.861295998683495", "Fordham Rd", "-73.91779099745928", "40.84007499993004", "170th St", "-73.88713799889574", "40.87324399861646", "Bedford Park Blvd", "-73.90983099923551", "40.87456099941789", "Marble Hill - 225th St", "-73.90483400107873", "40.87885599817935", "231st St", "-73.91527899954356", "40.86944399946045", "215th St", "-73.91881900132312", "40.864614000525854", "207th St", "-73.91989900100465", "40.86807199999737", "Inwood - 207th St", "-73.89858300049647", "40.88924800011476", "Van Cortlandt Park - 242nd St", "-73.87996127877184", "40.84020763241799", "West Farms Sq - E Tremont Av", "-73.8625097078866", "40.883887974625274", "219th St", "-73.88465499988732", "40.87974999947229", "Mosholu Pkwy", "-73.87885499918691", "40.87481100011182", "Norwood - 205th St", "-73.86705361747603", "40.87125880254771", "Burke Ave", "-73.83859099802153", "40.87866300037311", "Baychester Ave", "-73.8308340021742", "40.88829999901007", "Eastchester - Dyre Ave", "-73.78381700176453", "40.712645666744045", "Jamaica - 179th St", "-73.8506199987954", "40.903125000541245", "Wakefield - 241st St", "-73.95924499945693", "40.670342666584396", "Botanic Garden", "-73.90526176305106", "40.68286062551184", "Bushwick - Aberdeen", "-73.90311757920684", "40.67845624842869", "Broadway Junction", "-73.84638400151765", "40.86952599962676", "Gun Hill Rd", "-73.87334609510884", "40.8418630412186", "E 180th St", "-73.92553600006474", "40.86053100138796", "Dyckman St", "-73.95837200097044", "40.815580999978934", "125th St", "-73.95582700110425", "40.68059566598263", "Franklin Ave - Fulton St", "-73.92672247438611", "40.81833014409742", "149th St - Grand Concourse", "-73.91779152760981", "40.816029252510006", "3rd Ave - 149th St", "-73.92139999784426", "40.83553699933672", "167th St", "-73.91923999909432", "40.80756599987699", "Brook Ave", "-73.93099699953838", "40.74458699983993", "33rd St", "-73.9240159984882", "40.74378100149132", "40th St", "-73.94408792823116", "40.824766360871905", "145th St", "-73.93820899811622", "40.8301349999812", "155th St", "-73.92565099775477", "40.827904998845845", "161st St - Yankee Stadium", "-73.93072899914027", "40.67936399950546", "Utica Ave", "-73.9205264716827", "40.75698735912575", "Steinway St", "-73.92850899927413", "40.69317200129202", "Kosciuszko St", "-73.92215600150752", "40.689583999013905", "Gates Ave", "-73.92724299902838", "40.69787300011831", "Central Ave", "-73.91972000188625", "40.69866000123805", "Knickerbocker Ave", "-73.9214790001739", "40.76677866673298", "30th Ave", "-73.9229130000312", "40.706606665988716", "Jefferson St", "-73.93314700024209", "40.70615166680729", "Morgan Ave", "-73.93713823965695", "40.74891771986323", "Queens Plz", "-73.97697099965796", "40.62975466638584", "18th Ave", "-74.0255099996266", "40.629741666886915", "77th St", "-74.02337699950728", "40.63496666682377", "Bay Ridge Ave", "-73.9946587805514", "40.636260890961395", "50th St", "-74.00535100046275", "40.63138566722445", "Ft Hamilton Pkwy", "-73.98682900011477", "40.59770366695856", "25th Ave", "-73.9936762000529", "40.601950461572315", "Bay Pky", "-73.98452199846113", "40.617108999866005", "20th Ave", "-73.99045399865993", "40.620686997680025", "18th Ave", "-74.03087600085765", "40.61662166725951", "Bay Ridge - 95th St", "-74.0283979999864", "40.62268666715025", "86th St", "-74.00058287431507", "40.61315892569516", "79th St", "-73.99884094850685", "40.61925870977273", "71st St", "-73.99817432157568", "40.60467699816932", "20th Ave", "-74.00159259239406", "40.60773573171741", "18th Ave", "-73.99685724994863", "40.626224462922195", "62nd St", "-73.99635300025969", "40.62484166725887", "New Utrecht Ave", "-73.97337641974885", "40.59592482551748", "Ave U", "-73.9723553085244", "40.603258405128265", "Kings Hwy", "-73.96135378598797", "40.577710196642435", "Brighton Beach", "-73.95405791257907", "40.58654754707536", "Sheepshead Bay", "-73.95581122316301", "40.59930895095475", "Ave U", "-73.95760873538083", "40.608638645396006", "Kings Hwy", "-73.97908400099428", "40.597235999920436", "Ave U", "-73.98037300229343", "40.60405899980493", "Kings Hwy", "-73.97459272818807", "40.580738758491464", "Neptune Ave", "-73.97426599968905", "40.589449666625285", "Ave X", "-73.98376500045946", "40.58884066651933", "Bay 50th St", "-73.97818899936274", "40.59246500088859", "Gravesend - 86th St", "-73.97300281528751", "40.608842808949916", "Ave P", "-73.97404850873143", "40.61435671190883", "Ave N", "-73.9752569782215", "40.62073162316788", "Bay Pky", "-73.9592431052215", "40.617397744443736", "Ave M", "-73.98178001069293", "40.61145578989005", "Bay Pky", "-73.97606933170925", "40.62501744019143", "Ave I", "-73.96069316246925", "40.625022819915166", "Ave J", "-73.96151793942495", "40.62920837758969", "Ave H", "-73.95507827493762", "40.59532169111695", "Neck Rd", "-73.94193761457447", "40.75373927087553", "21st St - Queensbridge", "-73.98598400026407", "40.76245599925997", "50th St", "-73.98169782344476", "40.76297015245628", "7th Ave", "-73.98133100227702", "40.75864100159815", "47th-50th Sts - Rockefeller Ctr", "-73.97736800085171", "40.76408500081713", "57th St", "-73.96608964413245", "40.76461809442373", "Lexington Ave - 63rd St", "-73.95323499978866", "40.75917199967108", "Roosevelt Island - Main St", "-73.98164872301398", "40.768249531776064", "59th St - Columbus Circle", "-73.98420956591096", "40.759801973870694", "49th St", "-73.98072973372128", "40.76456552501829", "57th St", "-73.97334700047045", "40.764810999755284", "5th Ave - 59th St", "-73.96737501711436", "40.762708855394564", "Lexington Ave - 59th St", "-73.99105699913983", "40.75037300003949", "34th St - Penn Station", "-73.98749500051885", "40.75528999995681", "Times Sq - 42nd St", "-74.00762309323994", "40.71016216530185", "Fulton St", "-74.00858473570133", "40.714111000774025", "Chambers St", "-73.98973500085859", "40.757307998551504", "42nd St - Port Authority Bus Term", "-73.94906699890156", "40.69461899903765", "Myrtle-Willoughby Aves", "-73.9502340010257", "40.70037666622154", "Flushing Ave", "-73.99276500471389", "40.742954317826005", "23rd St", "-73.98777189072918", "40.74978939990011", "Herald Sq - 34th St", "-73.98503624034139", "40.68840847580642", "Hoyt - Schermerhorn Sts", "-73.98721815267317", "40.692470636847084", "Jay St - MetroTech", "-73.99017700122197", "40.713855001020406", "East Broadway", "-73.98807806807719", "40.71868074219453", "Delancey St - Essex St", "-73.98993800003434", "40.72340166574911", "Lower East Side - 2nd Ave", "-73.94137734838365", "40.70040440298112", "Flushing Ave", "-73.9356230012996", "40.6971950005145", "Myrtle Ave", "-73.98977899938897", "40.67027166728493", "4th Av - 9th St", "-73.99589172790934", "40.67364106090412", "Smith - 9th Sts", "-73.99075649573565", "40.68611054725977", "Bergen St", "-73.98605667854612", "40.69225539645323", "Jay St - MetroTech", "-73.99181830901125", "40.694196480776995", "Court St", "-73.99053886181645", "40.73587226699812", "Union Sq - 14th St", "-73.98934400102907", "40.74130266729", "23rd St", "-73.99287200067424", "40.66541366712979", "Prospect Ave", "-73.98830199974512", "40.670846666842756", "4th Av - 9th St", "-73.98575000112093", "40.73269099971662", "3rd Ave", "-73.99066976901818", "40.73476331217923", "Union Sq - 14th St", "-73.89654800103929", "40.67454199987086", "Liberty Ave", "-73.90531600055341", "40.67833366608023", "Broadway Junction", "-74.01788099953987", "40.6413616662838", "59th St", "-74.01000600074939", "40.648938666612814", "45th St", "-74.00354899951809", "40.65514366633887", "36th St", "-73.99444874451204", "40.64648407726636", "9th Ave", "-74.01403399986317", "40.64506866735981", "53rd St", "-73.9942022375285", "40.640912711444656", "Ft Hamilton Pkwy", "-73.99809099974297", "40.66039666692321", "25th St", "-73.99494697998841", "40.68027335170176", "Carroll St", "-74.00373899843763", "40.72622700129312", "Spring St", "-73.93796900205011", "40.851694999744616", "181st St", "-73.93417999964333", "40.85902199892482", "190th St", "-73.95479778057312", "40.80505813344211", "116th St", "-73.95224799734774", "40.811071672994565", "125th St", "-73.99770200045987", "40.72432866597571", "Prince St", "-73.99250799849149", "40.73046499853991", "8th St - NYU", "-74.00657099970202", "40.70941599925865", "Fulton St", "-74.00881099997359", "40.713050999077694", "Park Pl", "-74.00926600170112", "40.71547800011327", "Chambers St", "-73.98506379575646", "40.69054418535472", "Hoyt St", "-73.98999799960687", "40.693218999611084", "Borough Hall", "-73.90387900151532", "40.85840700040842", "183rd St", "-73.90103399921699", "40.86280299988937", "Fordham Rd", "-74.00974461517701", "40.71256392680817", "World Trade Center", "-74.0052290023424", "40.72082400007119", "Canal St - Holland Tunnel", "-73.94151400082208", "40.83051799929251", "155th St", "-73.93989200188344", "40.83601299923096", "163rd St - Amsterdam Av", "-74.00793800110387", "40.71002266658424", "Fulton St", "-74.00340673031336", "40.71323378962671", "Chambers St", "-73.99982638545937", "40.71817387697391", "Canal St", "-74.00698581780337", "40.71327233111697", "City Hall", "-74.0018260000577", "40.71946500105898", "Canal St", "-74.01316895919258", "40.701730507574474", "South Ferry", "-74.01400799803432", "40.70491399928076", "Bowling Green", "-74.01186199860112", "40.70755700086603", "Wall St", "-74.0130072374272", "40.703142373599135", "Whitehall St", "-74.01297456253795", "40.707744756294474", "Rector St", "-73.8958980017196", "40.70622599823048", "Fresh Pond Rd", "-73.88957722978091", "40.711431305058255", "Middle Village - Metropolitan Ave", "-74.01378300119742", "40.707512999521775", "Rector St", "-74.01218800112292", "40.7118350008202", "Cortlandt St", "-74.00950899856461", "40.710367998822136", "Fulton St", "-74.01105599991755", "40.706476001106005", "Broad St", "-74.01113196473266", "40.7105129841524", "Cortlandt St", "-74.00909999844257", "40.706820999753376", "Wall St", "-73.92727099960726", "40.865490998968916", "Dyckman St", "-73.99375299913589", "40.71826699954992", "Grand St", "-73.99620399876055", "40.725296998738045", "Broadway - Lafayette St", "-73.99380690654237", "40.720246883147254", "Bowery", "-74.00105471306033", "40.718814263587134", "Canal St", "-73.99804100117201", "40.74590599939995", "23rd St", "-73.99339099970578", "40.752287000775894", "34th St - Penn Station", "-73.89129866519697", "40.74653969115889", "Jackson Hts - Roosevelt Av", "-74.00020100063497", "40.737825999728116", "14th St", "-73.94753480879213", "40.817905559212676", "135th St", "-73.99620899921355", "40.73822799969515", "14th St", "-73.99775078874781", "40.73774146981052", "6th Ave", "-74.00257800104762", "40.73977666638199", "8th Ave", "-74.00168999937027", "40.740893000193296", "14th St", "-73.9504262489579", "40.66993815093054", "Nostrand Ave", "-73.99308599821961", "40.69746599996469", "Clark St", "-73.95684800014614", "40.68137966658742", "Franklin Ave", "-73.96583799857275", "40.68326299912644", "Clinton - Washington Aves", "-73.90307500005954", "40.70441200087814", "Forest Ave", "-73.94424999687163", "40.795020000113105", "110th St", "-73.95558899985132", "40.77949199820952", "86th St", "-73.98688499993673", "40.699742667691574", "York St", "-73.99053100065458", "40.69933699977884", "High St", "-73.97394599849406", "40.68611300020567", "Lafayette Ave", "-73.95058920022207", "40.667883603536815", "President St", "-73.87875099990931", "40.886037000253324", "Woodlawn", "-73.99465900006331", "40.72591466682659", "Bleecker St", "-73.94747800152219", "40.79060000008452", "103rd St", "-73.87210600099675", "40.675376998239365", "Euclid Ave", "-73.85147000026086", "40.67984300135503", "88th St", "-73.96379005505493", "40.6409401651401", "Cortelyou Rd", "-73.9416169983714", "40.7986290002001", "116th St", "-73.86081600108396", "40.83322599927859", "Parkchester", "-74.00688600277107", "40.719318001302135", "Franklin St", "-73.85899200206335", "40.67937100115432", "80th St", "-73.98196299856706", "40.75382100064824", "5th Ave - Bryant Pk", "-73.99714100006673", "40.72230099999366", "Spring St", "-73.93759400055725", "40.804138000587244", "125th St", "-73.9812359981396", "40.57728100006751", "Coney Island - Stillwell Av", "-74.00219709442206", "40.75544635961596", "34th St - Hudson Yards", "-73.95836178682246", "40.76880251014895", "72nd St", "-73.95177090964917", "40.77786104333163", "86th St", "-73.9470660219183", "40.784236650177654", "96th St", ) c.Do("ZRANGE", "stations", "0", "-1") c.Do("ZRANGE", "stations", "0", "-1", "WITHSCORES") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "WITHDIST") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "WITHCOORD") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "ASC") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "DESC") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "DESC", "COUNT", "3") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "ASC", "COUNT", "3") c.DoRounded(3, "GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "ASC", "COUNT", "99999") c.DoSorted("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km") c.DoLoosely("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "WITHDIST") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "WITHDIST", "ASC") c.DoLoosely("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "WITHCOORD") c.DoRounded(3, "GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "WITHCOORD", "ASC") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "ASC") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "DESC") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "DESC", "COUNT", "3") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "ASC", "COUNT", "3") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "ASC", "COUNT", "99999") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "STORE", "res") c.Do("ZRANGE", "res", "0", "-1", "WITHSCORES") c.Do("GEORADIUS", "stations", "-73.9718893", "40.7728773", "4", "km", "STOREDIST", "resd") c.DoLoosely("ZRANGE", "resd", "0", "-1", "WITHSCORES") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "STORE", "resbymem") c.Do("ZRANGE", "resbymem", "0", "-1", "WITHSCORES") c.Do("GEORADIUSBYMEMBER", "stations", "Astor Pl", "4", "km", "STOREDIST", "resbymemd") c.DoLoosely("ZRANGE", "resbymemd", "0", "-1", "WITHSCORES") }) } ================================================ FILE: miniredis/tests/integration-go/go.mod ================================================ module github.com/encoredev/encore/miniredis/tests/integration-go go 1.22 require github.com/alicebob/miniredis/v2 v2.37.0 require github.com/yuin/gopher-lua v1.1.1 // indirect ================================================ FILE: miniredis/tests/integration-go/go.sum ================================================ github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= ================================================ FILE: miniredis/tests/integration-go/hash_test.go ================================================ package main import ( "testing" ) func TestHash(t *testing.T) { skip(t) t.Run("basics", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("HSET", "aap", "noot", "mies") c.Do("HGET", "aap", "noot") c.Do("HMGET", "aap", "noot") c.Do("HLEN", "aap") c.Do("HKEYS", "aap") c.Do("HVALS", "aap") c.Do("HSET", "aaa", "bb", "1", "cc", "2") c.Do("HGET", "aaa", "bb") c.Do("HGET", "aaa", "cc") c.Do("HDEL", "aap", "noot") c.Do("HGET", "aap", "noot") c.Do("EXISTS", "aap") // key is gone // failure cases c.Error("wrong number", "HSET", "aap", "noot") c.Error("wrong number", "HGET", "aap") c.Error("wrong number", "HMGET", "aap") c.Error("wrong number", "HLEN") c.Error("wrong number", "HKEYS") c.Error("wrong number", "HVALS") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "HSET", "str", "noot", "mies") c.Error("wrong kind", "HGET", "str", "noot") c.Error("wrong kind", "HMGET", "str", "noot") c.Error("wrong kind", "HLEN", "str") c.Error("wrong kind", "HKEYS", "str") c.Error("wrong kind", "HVALS", "str") c.Error("wrong number", "HSET") c.Error("wrong number", "HSET", "a1") c.Error("wrong number", "HSET", "a1", "b") c.Error("wrong number", "HSET", "a2", "b", "c", "d") }) }) t.Run("tx", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("MULTI") c.Do("HSET", "aap", "noot", "mies", "vuur", "wim") c.Do("EXEC") c.Do("MULTI") c.Do("HSET", "aap", "noot", "mies", "vuur") // uneven arg count c.Do("EXEC") }) }) t.Run("expire", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("HSET", "aap", "noot", "mies") c.Do("HEXPIRE", "aap", "3", "FIELDS", "2", "noot", "vuur") c.Error("wrong number", "HEXPIRE", "aap", "3", "FIELDS", "0") c.Error("wrong number", "HEXPIRE", "aap", "3") c.Error("wrong number", "HEXPIRE", "aap", "3", "FIELDS") c.Error("wrong number", "HEXPIRE", "aap", "-3", "FIELDS", "0") c.Error("wrong number", "HEXPIRE", "aap", "noot", "3") c.Error("not an int", "HEXPIRE", "aap", "3.14", "FIELDS", "noot", "3.14") c.Error("numfields", "HEXPIRE", "aap", "3", "FIELDS", "3", "noot", "vuur") }) }) } func TestHashSetnx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HSETNX", "aap", "noot", "mies") c.Do("EXISTS", "aap") c.Do("HEXISTS", "aap", "noot") c.Do("HSETNX", "aap", "noot", "mies2") c.Do("HGET", "aap", "noot") // failure cases c.Error("wrong number", "HSETNX", "aap") c.Error("wrong number", "HSETNX", "aap", "noot") c.Error("wrong number", "HSETNX", "aap", "noot", "too", "many") }) } func TestHashDelExists(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HSET", "aap", "noot", "mies") c.Do("HSET", "aap", "vuur", "wim") c.Do("HEXISTS", "aap", "noot") c.Do("HEXISTS", "aap", "vuur") c.Do("HDEL", "aap", "noot") c.Do("HEXISTS", "aap", "noot") c.Do("HEXISTS", "aap", "vuur") c.Do("HEXISTS", "nosuch", "vuur") // failure cases c.Error("wrong number", "HDEL") c.Error("wrong number", "HDEL", "aap") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "HDEL", "str", "key") c.Error("wrong number", "HEXISTS") c.Error("wrong number", "HEXISTS", "aap") c.Error("wrong number", "HEXISTS", "aap", "too", "many") c.Error("wrong kind", "HEXISTS", "str", "field") }) } func TestHashGetall(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HSET", "aap", "noot", "mies") c.Do("HSET", "aap", "vuur", "wim") c.DoSorted("HGETALL", "aap") c.Do("HGETALL", "nosuch") // failure cases c.Error("wrong number", "HGETALL") c.Error("wrong number", "HGETALL", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "HGETALL", "str") }) testRESP3(t, func(c *client) { c.Do("HSET", "aap", "noot", "mies") c.Do("HGETALL", "aap") c.Do("HSET", "aap", "vuur", "wim") c.DoSorted("HGETALL", "aap") c.Do("HGETALL", "nosuch") }) } func TestHmset(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HMSET", "aap", "noot", "mies", "vuur", "zus") c.Do("HGET", "aap", "noot") c.Do("HGET", "aap", "vuur") c.Do("HLEN", "aap") // failure cases c.Error("wrong number", "HMSET", "aap") c.Error("wrong number", "HMSET", "aap", "key") c.Error("wrong number", "HMSET", "aap", "key", "value", "odd") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "HMSET", "str", "key", "value") }) } func TestHashIncr(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HINCRBY", "aap", "noot", "12") c.Do("HINCRBY", "aap", "noot", "-13") c.Do("HINCRBY", "aap", "noot", "2123") c.Do("HGET", "aap", "noot") // Simple failure cases. c.Error("wrong number", "HINCRBY") c.Error("wrong number", "HINCRBY", "aap") c.Error("wrong number", "HINCRBY", "aap", "noot") c.Error("not an integer", "HINCRBY", "aap", "noot", "noint") c.Error("wrong number", "HINCRBY", "aap", "noot", "12", "toomany") c.Do("SET", "str", "value") c.Error("wrong kind", "HINCRBY", "str", "value", "12") c.Do("HINCRBY", "aap", "noot", "12") }) testRaw(t, func(c *client) { c.Do("HINCRBYFLOAT", "aap", "noot", "12.3") c.Do("HINCRBYFLOAT", "aap", "noot", "-13.1") c.Do("HINCRBYFLOAT", "aap", "noot", "200") c.Do("HGET", "aap", "noot") // Simple failure cases. c.Error("wrong number", "HINCRBYFLOAT") c.Error("wrong number", "HINCRBYFLOAT", "aap") c.Error("wrong number", "HINCRBYFLOAT", "aap", "noot") c.Error("not a valid float", "HINCRBYFLOAT", "aap", "noot", "noint") c.Error("wrong number", "HINCRBYFLOAT", "aap", "noot", "12", "toomany") c.Do("SET", "str", "value") c.Error("wrong kind", "HINCRBYFLOAT", "str", "value", "12") c.Do("HINCRBYFLOAT", "aap", "noot", "12") }) } func TestHscan(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No set yet c.Do("HSCAN", "h", "0") c.Do("HSET", "h", "key1", "value1") c.Do("HSCAN", "h", "0") c.Do("HSCAN", "h", "0", "COUNT", "12") c.Do("HSCAN", "h", "0", "cOuNt", "12") c.Do("HSET", "h", "anotherkey", "value2") c.Do("HSCAN", "h", "0", "MATCH", "anoth*") c.Do("HSCAN", "h", "0", "MATCH", "anoth*", "COUNT", "100") c.Do("HSCAN", "h", "0", "COUNT", "100", "MATCH", "anoth*") // Can't really test multiple keys. // c.Do("SET", "key2", "value2") // c.Do("SCAN", "0") // Error cases c.Error("wrong number", "HSCAN") c.Error("wrong number", "HSCAN", "noint") c.Error("not an integer", "HSCAN", "h", "0", "COUNT", "noint") c.Error("syntax error", "HSCAN", "h", "0", "COUNT") c.Error("syntax error", "HSCAN", "h", "0", "MATCH") c.Error("syntax error", "HSCAN", "h", "0", "garbage") c.Error("syntax error", "HSCAN", "h", "0", "COUNT", "12", "MATCH", "foo", "garbage") // c.Do("HSCAN", "nosuch", "0", "COUNT", "garbage") c.Do("SET", "str", "1") c.Error("wrong kind", "HSCAN", "str", "0") }) } func TestHstrlen(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HSTRLEN", "hash", "foo") c.Do("HSET", "hash", "foo", "bar") c.Do("HSTRLEN", "hash", "foo") c.Do("HSTRLEN", "hash", "nosuch") c.Do("HSTRLEN", "nosuch", "nosuch") c.Error("wrong number", "HSTRLEN") c.Error("wrong number", "HSTRLEN", "foo") c.Error("wrong number", "HSTRLEN", "foo", "baz", "bar") c.Do("SET", "str", "1") c.Error("wrong kind", "HSTRLEN", "str", "bar") }) } func TestHrandfield(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("HSET", "one", "foo", "bar") c.Do("HRANDFIELD", "one") c.Do("HRANDFIELD", "one", "0") c.Do("HRANDFIELD", "one", "1") c.Do("HRANDFIELD", "one", "2") // limited to 1 c.Do("HRANDFIELD", "one", "3") // limited to 1 c.Do("HRANDFIELD", "one", "-1") c.Do("HRANDFIELD", "one", "-2") // padded c.Do("HRANDFIELD", "one", "-3") // padded c.Do("HSET", "more", "foo", "bar", "baz", "bak") c.DoLoosely("HRANDFIELD", "more") c.Do("HRANDFIELD", "more", "0") c.DoLoosely("HRANDFIELD", "more", "1") c.DoLoosely("HRANDFIELD", "more", "2") c.DoLoosely("HRANDFIELD", "more", "3") // limited to 2 c.DoLoosely("HRANDFIELD", "more", "-1") c.DoLoosely("HRANDFIELD", "more", "-2") c.DoLoosely("HRANDFIELD", "more", "-3") // length padded to 3 c.Do("HRANDFIELD", "nosuch", "1") c.Do("HRANDFIELD", "nosuch", "2") c.Do("HRANDFIELD", "nosuch", "3") c.Do("HRANDFIELD", "nosuch", "0") c.Do("HRANDFIELD", "nosuch") c.Do("HRANDFIELD", "nosuch", "-1") // still empty c.Do("HRANDFIELD", "nosuch", "-2") // still empty c.Do("HRANDFIELD", "nosuch", "-3") // still empty c.DoLoosely("HRANDFIELD", "one", "2") c.DoLoosely("HRANDFIELD", "one", "7") c.DoLoosely("HRANDFIELD", "one", "2", "WITHVALUE") c.DoLoosely("HRANDFIELD", "one", "7", "WITHVALUE") c.Error("ERR syntax error", "HRANDFIELD", "foo", "1", "2") c.Error("ERR wrong number", "HRANDFIELD") }) } ================================================ FILE: miniredis/tests/integration-go/hll_test.go ================================================ package main import ( "math/rand" "testing" ) func TestHll(t *testing.T) { skip(t) t.Run("basics", func(t *testing.T) { testRaw(t, func(c *client) { // Add 100 unique random values to h1 and 50 of these 100 to h2 for i := 0; i < 100; i++ { value := randomStr(10) c.Do("PFADD", "h1", value) if i%2 == 0 { c.Do("PFADD", "h2", value) } } for i := 0; i < 100; i++ { c.Do("PFADD", "h3", randomStr(10)) } // Merge non-intersecting hlls { c.Do( "PFMERGE", "res1", "h1", // count 100 "h3", // count 100 ) c.DoApprox(2, "PFCOUNT", "res1") } // Merge intersecting hlls { c.Do( "PFMERGE", "res2", "h1", // count 100 "h2", // count 50 (all 50 are presented in h1) ) c.DoApprox(2, "PFCOUNT", "res2") } // Merge all hlls { c.Do( "PFMERGE", "res3", "h1", // count 100 "h2", // count 50 (all 50 are presented in h1) "h3", // count 100 "h4", // empty key ) c.DoApprox(2, "PFCOUNT", "res3") } // failure cases c.Error("wrong number", "PFADD") c.Error("wrong number", "PFCOUNT") c.Error("wrong number", "PFMERGE") c.Do("SET", "str", "I am a string") c.Error("not a valid HyperLogLog", "PFADD", "str", "noot", "mies") c.Error("not a valid HyperLogLog", "PFCOUNT", "str", "h1") c.Error("not a valid HyperLogLog", "PFMERGE", "str", "noot") c.Error("not a valid HyperLogLog", "PFMERGE", "noot", "str") c.Do("DEL", "h1", "h2", "h3", "h4", "res1", "res2", "res3") c.Do("PFCOUNT", "h1", "h2", "h3", "h4", "res1", "res2", "res3") }) }) t.Run("tx", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("MULTI") c.Do("PFADD", "h1", "noot", "mies", "vuur", "wim") c.Do("PFADD", "h2", "noot1", "mies1", "vuur1", "wim1") c.Do("PFMERGE", "h3", "h1", "h2") c.Do("PFCOUNT", "h1") c.Do("PFCOUNT", "h2") c.Do("PFCOUNT", "h3") c.Do("EXEC") }) }) } const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randomStr(length int) string { rand.Seed(42) b := make([]byte, length) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } ================================================ FILE: miniredis/tests/integration-go/list_test.go ================================================ package main import ( "sync" "testing" "time" ) func TestLPushLpop(t *testing.T) { skip(t) t.Run("without count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("LPUSH", "l", "aap", "noot", "mies") c.Do("TYPE", "l") c.Do("LPUSH", "l", "more", "keys") c.Do("LRANGE", "l", "0", "-1") c.Do("LRANGE", "l", "0", "6") c.Do("LRANGE", "l", "2", "6") c.Do("LRANGE", "l", "-100", "-100") c.Do("LRANGE", "nosuch", "2", "6") c.Do("LPOP", "l") c.Do("LPOP", "l") c.Do("LPOP", "l") c.Do("LPOP", "l") c.Do("LPOP", "l") c.Do("LPOP", "l") c.Do("EXISTS", "l") c.Do("LPOP", "nosuch") // failure cases c.Error("wrong number", "LPUSH") c.Error("wrong number", "LPUSH", "l") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LPUSH", "str", "noot", "mies") c.Error("wrong number", "LRANGE") c.Error("wrong number", "LRANGE", "key") c.Error("wrong number", "LRANGE", "key", "2") c.Error("wrong number", "LRANGE", "key", "2", "6", "toomany") c.Error("not an integer", "LRANGE", "key", "noint", "6") c.Error("not an integer", "LRANGE", "key", "2", "noint") c.Error("wrong number", "LPOP") }) }) t.Run("with count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("LPUSH", "l", "aap", "noot", "mies") c.Do("LPOP", "l", "0") c.Do("LPOP", "l", "2") c.Do("LPOP", "l", "2") c.Error("out of range", "LPOP", "l", "-42") c.Do("LPOP", "nosuch", "2") c.Error("wrong number", "LPOP", "nosuch", "2", "foobar") }) }) t.Run("resp3", func(t *testing.T) { testRESP3(t, func(c *client) { c.Do("LPUSH", "l", "aap", "noot", "mies") c.Do("LPOP", "l") c.Do("LPOP", "l", "9") c.Do("LPOP", "l") c.Do("LPOP", "l", "9") c.Do("LPOP", "nosuch") c.Do("LPOP", "nosuch", "9") }) }) } func TestLPushx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("LPUSHX", "l", "aap") c.Do("EXISTS", "l") c.Do("LRANGE", "l", "0", "-1") c.Do("LPUSH", "l", "noot") c.Do("LPUSHX", "l", "mies") c.Do("EXISTS", "l") c.Do("LRANGE", "l", "0", "-1") c.Do("LPUSHX", "l", "even", "more", "arguments") // failure cases c.Error("wrong number", "LPUSHX") c.Error("wrong number", "LPUSHX", "l") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LPUSHX", "str", "mies") }) } func TestRPushRPop(t *testing.T) { skip(t) t.Run("without count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("TYPE", "l") c.Do("RPUSH", "l", "more", "keys") c.Do("LRANGE", "l", "0", "-1") c.Do("LRANGE", "l", "0", "6") c.Do("LRANGE", "l", "2", "6") c.Do("RPOP", "l") c.Do("RPOP", "l") c.Do("RPOP", "l") c.Do("RPOP", "l") c.Do("RPOP", "l") c.Do("RPOP", "l") c.Do("EXISTS", "l") c.Do("RPOP", "nosuch") // failure cases c.Error("wrong number", "RPUSH") c.Error("wrong number", "RPUSH", "l") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "RPUSH", "str", "noot", "mies") c.Error("wrong number", "RPOP") }) }) t.Run("with count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("RPOP", "l", "0") c.Do("RPOP", "l", "2") c.Do("RPOP", "l", "99") c.Do("RPOP", "l", "99") c.Do("RPOP", "nosuch", "99") }) }) t.Run("resp3", func(t *testing.T) { testRESP3(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("RPOP", "l") c.Do("RPOP", "l", "9") c.Do("RPOP", "l") c.Do("RPOP", "l", "9") }) }) } func TestLinxed(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("LINDEX", "l", "0") c.Do("LINDEX", "l", "1") c.Do("LINDEX", "l", "2") c.Do("LINDEX", "l", "3") c.Do("LINDEX", "l", "4") c.Do("LINDEX", "l", "44444") c.Error("not an integer", "LINDEX", "l", "-0") c.Do("LINDEX", "l", "-1") c.Do("LINDEX", "l", "-2") c.Do("LINDEX", "l", "-3") c.Do("LINDEX", "l", "-4") c.Do("LINDEX", "l", "-4000") // failure cases c.Error("wrong number", "LINDEX") c.Error("wrong number", "LINDEX", "l") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LINDEX", "str", "1") c.Error("not an integer", "LINDEX", "l", "noint") c.Error("wrong number", "LINDEX", "l", "1", "too many") }) } func TestLpos(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "aap", "mies", "aap", "vuur", "aap", "aap") c.Do("LPOS", "l", "app") c.Do("LPOS", "l", "noot") c.Do("LPOS", "l", "mies") c.Do("LPOS", "l", "vuur") c.Do("LPOS", "l", "wim") c.Do("LPOS", "l", "app", "RANK", "1") c.Do("LPOS", "l", "app", "RANK", "4") c.Do("LPOS", "l", "app", "RANK", "5") c.Do("LPOS", "l", "app", "RANK", "6") c.Do("LPOS", "l", "app", "RANK", "-1") c.Do("LPOS", "l", "app", "RANK", "-4") c.Do("LPOS", "l", "app", "RANK", "-5") c.Do("LPOS", "l", "app", "RANK", "-6") c.Do("LPOS", "l", "wim", "COUNT", "1") c.Do("LPOS", "l", "aap", "COUNT", "1") c.Do("LPOS", "l", "aap", "COUNT", "3") c.Do("LPOS", "l", "aap", "COUNT", "5") c.Do("LPOS", "l", "aap", "COUNT", "100") c.Do("LPOS", "l", "aap", "COUNT", "0") c.Do("LPOS", "l", "aap", "RANK", "3", "COUNT", "2") c.Do("LPOS", "l", "aap", "RANK", "3", "COUNT", "3") c.Do("LPOS", "l", "aap", "RANK", "5", "COUNT", "100") c.Do("LPOS", "l", "aap", "RANK", "-3", "COUNT", "2") c.Do("LPOS", "l", "aap", "RANK", "-3", "COUNT", "3") c.Do("LPOS", "l", "aap", "RANK", "-5", "COUNT", "100") c.Do("LPOS", "l", "aap", "RANK", "4", "MAXLEN", "6") c.Do("LPOS", "l", "aap", "RANK", "4", "MAXLEN", "7") c.Do("LPOS", "l", "aap", "RANK", "-4", "MAXLEN", "5") c.Do("LPOS", "l", "aap", "RANK", "-4", "MAXLEN", "6") c.Do("LPOS", "l", "aap", "COUNT", "0", "MAXLEN", "1") c.Do("LPOS", "l", "aap", "COUNT", "0", "MAXLEN", "4") c.Do("LPOS", "l", "aap", "COUNT", "0", "MAXLEN", "7") c.Do("LPOS", "l", "aap", "COUNT", "0", "MAXLEN", "8") c.Do("LPOS", "l", "aap", "COUNT", "2", "MAXLEN", "0") c.Do("LPOS", "l", "aap", "COUNT", "1", "MAXLEN", "0") c.Do("LPOS", "l", "aap", "RANK", "4", "COUNT", "2", "MAXLEN", "0") c.Do("LPOS", "l", "aap", "RANK", "4", "COUNT", "2", "MAXLEN", "7") c.Do("LPOS", "l", "aap", "RANK", "4", "COUNT", "2", "MAXLEN", "6") c.Do("LPOS", "l", "aap", "RANK", "-3", "COUNT", "2", "MAXLEN", "0") c.Do("LPOS", "l", "aap", "RANK", "-3", "COUNT", "2", "MAXLEN", "4") c.Do("LPOS", "l", "aap", "RANK", "-3", "COUNT", "2", "MAXLEN", "3") // failure cases c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LPOS", "str", "aap") c.Error("wrong number", "LPOS", "l") c.Error("syntax error", "LPOS", "l", "aap", "RANK") c.Error("syntax error", "LPOS", "l", "aap", "RANK", "1", "COUNT") c.Error("syntax error", "LPOS", "l", "aap", "RANK", "1", "COUNT", "1", "MAXLEN") c.Error("syntax error", "LPOS", "l", "aap", "RANK", "1", "COUNT", "1", "MAXLEN", "1", "RANK") c.Error("syntax error", "LPOS", "l", "aap", "RANKS", "1") c.Error("syntax error", "LPOS", "l", "aap", "RANK", "1", "COUNTING", "1") c.Error("syntax error", "LPOS", "l", "aap", "RANK", "1", "MAXLENGTH", "1") c.Error("not an integer", "LPOS", "l", "aap", "RANK", "not_an_int") c.Error("can't be zero", "LPOS", "l", "aap", "RANK", "0") c.Error("can't be negative", "LPOS", "l", "aap", "COUNT", "-1") c.Error("can't be negative", "LPOS", "l", "aap", "COUNT", "not_an_int") c.Error("can't be negative", "LPOS", "l", "aap", "MAXLEN", "-1") c.Error("can't be negative", "LPOS", "l", "aap", "MAXLEN", "not_an_int") c.Error("can't be negative", "LPOS", "l", "aap", "MAXLEN", "-1", "RANK", "not_an_int", "COUNT", "-1") c.Error("not an integer", "LPOS", "l", "aap", "RANK", "not_an_int", "COUNT", "-1", "MAXLEN", "-1") c.Error("can't be negative", "LPOS", "l", "aap", "COUNT", "-1", "MAXLEN", "-1", "RANK", "not_an_int") }) } func TestLlen(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("LLEN", "l") c.Do("LLEN", "nosuch") // failure cases c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LLEN", "str") c.Error("wrong number", "LLEN") c.Error("wrong number", "LLEN", "l", "too many") }) } func TestLtrim(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("LTRIM", "l", "0", "1") c.Do("LRANGE", "l", "0", "-1") c.Do("RPUSH", "l2", "aap", "noot", "mies", "vuur") c.Do("LTRIM", "l2", "-2", "-1") c.Do("LRANGE", "l2", "0", "-1") c.Do("RPUSH", "l3", "aap", "noot", "mies", "vuur") c.Do("LTRIM", "l3", "-2", "-1000") c.Do("LRANGE", "l3", "0", "-1") // remove the list c.Do("RPUSH", "l4", "aap") c.Do("LTRIM", "l4", "0", "-999") c.Do("EXISTS", "l4") // failure cases c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LTRIM", "str", "0", "1") c.Error("wrong number", "LTRIM", "l", "0", "1", "toomany") c.Error("not an integer", "LTRIM", "l", "noint", "1") c.Error("not an integer", "LTRIM", "l", "0", "noint") c.Error("wrong number", "LTRIM", "l", "0") c.Error("wrong number", "LTRIM", "l") c.Error("wrong number", "LTRIM") }) } func TestLrem(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies", "mies", "mies") c.Do("LREM", "l", "1", "mies") c.Do("LRANGE", "l", "0", "-1") c.Do("RPUSH", "l2", "aap", "noot", "mies", "mies", "mies") c.Do("LREM", "l2", "-2", "mies") c.Do("LRANGE", "l2", "0", "-1") c.Do("RPUSH", "l3", "aap", "noot", "mies", "mies", "mies") c.Do("LREM", "l3", "0", "mies") c.Do("LRANGE", "l3", "0", "-1") // remove the list c.Do("RPUSH", "l4", "aap") c.Do("LREM", "l4", "999", "aap") c.Do("EXISTS", "l4") // failure cases c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LREM", "str", "0", "aap") c.Error("wrong number", "LREM", "l", "0", "aap", "toomany") c.Error("not an integer", "LREM", "l", "noint", "aap") c.Error("wrong number", "LREM", "l", "0") c.Error("wrong number", "LREM", "l") c.Error("wrong number", "LREM") }) } func TestLset(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies", "mies", "mies") c.Do("LSET", "l", "1", "[cencored]") c.Do("LRANGE", "l", "0", "-1") c.Do("LSET", "l", "-1", "[cencored]") c.Do("LRANGE", "l", "0", "-1") c.Error("out of range", "LSET", "l", "1000", "new") c.Error("out of range", "LSET", "l", "-7000", "new") c.Error("no such key", "LSET", "nosuch", "1", "new") // failure cases c.Error("wrong number", "LSET") c.Error("wrong number", "LSET", "l") c.Error("wrong number", "LSET", "l", "0") c.Error("not an integer", "LSET", "l", "noint", "aap") c.Error("wrong number", "LSET", "l", "0", "aap", "toomany") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LSET", "str", "0", "aap") }) } func TestLinsert(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies", "mies", "mies!") c.Do("LINSERT", "l", "before", "aap", "1") c.Do("LINSERT", "l", "before", "noot", "2") c.Do("LINSERT", "l", "after", "mies!", "3") c.Do("LINSERT", "l", "after", "mies", "4") c.Do("LINSERT", "l", "after", "nosuch", "0") c.Do("LINSERT", "nosuch", "after", "nosuch", "0") c.Do("LRANGE", "l", "0", "-1") c.Do("LINSERT", "l", "AfTeR", "mies", "4") c.Do("LRANGE", "l", "0", "-1") // failure cases c.Error("wrong number", "LINSERT") c.Error("wrong number", "LINSERT", "l") c.Error("wrong number", "LINSERT", "l", "before") c.Error("wrong number", "LINSERT", "l", "before", "aap") c.Error("wrong number", "LINSERT", "l", "before", "aap", "too", "many") c.Error("syntax error", "LINSERT", "l", "What?", "aap", "noot") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LINSERT", "str", "before", "aap", "noot") }) } func TestRpoplpush(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "l", "aap", "noot", "mies") c.Do("RPOPLPUSH", "l", "l2") c.Do("LRANGE", "l", "0", "-1") c.Do("LRANGE", "2l", "0", "-1") c.Do("RPOPLPUSH", "l", "l2") c.Do("RPOPLPUSH", "l", "l2") c.Do("RPOPLPUSH", "l", "l2") // now empty c.Do("EXISTS", "l") c.Do("LRANGE", "2l", "0", "-1") c.Do("RPUSH", "round", "aap", "noot", "mies") c.Do("RPOPLPUSH", "round", "round") c.Do("LRANGE", "round", "0", "-1") c.Do("RPOPLPUSH", "round", "round") c.Do("RPOPLPUSH", "round", "round") c.Do("RPOPLPUSH", "round", "round") c.Do("RPOPLPUSH", "round", "round") c.Do("LRANGE", "round", "0", "-1") // failure cases c.Do("RPUSH", "chk", "aap", "noot", "mies") c.Error("wrong number", "RPOPLPUSH") c.Error("wrong number", "RPOPLPUSH", "chk") c.Error("wrong number", "RPOPLPUSH", "chk", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "RPOPLPUSH", "chk", "str") c.Error("wrong kind", "RPOPLPUSH", "str", "chk") c.Do("LRANGE", "chk", "0", "-1") }) } func TestRpushx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSHX", "l", "aap") c.Do("EXISTS", "l") c.Do("RPUSH", "l", "noot", "mies") c.Do("RPUSHX", "l", "vuur") c.Do("EXISTS", "l") c.Do("LRANGE", "l", "0", "-1") c.Do("RPUSHX", "l", "more", "arguments") // failure cases c.Do("RPUSH", "chk", "noot", "mies") c.Error("wrong number", "RPUSHX") c.Error("wrong number", "RPUSHX", "chk") c.Do("LRANGE", "chk", "0", "-1") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "RPUSHX", "str", "value") }) } func TestBrpop(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("LPUSH", "l", "one") c.Do("BRPOP", "l", "1") c.Do("BRPOP", "l", "0.1") c.Error("timeout is out of range", "BRPOP", "l", "inf") c.Do("EXISTS", "l") // transaction c.Do("MULTI") c.Do("BRPOP", "nosuch", "10") c.Do("EXEC") // failure cases c.Error("wrong number", "BRPOP") c.Error("wrong number", "BRPOP", "l") c.Error("not a float", "BRPOP", "l", "X") c.Error("not a float", "BRPOP", "l", "") c.Error("wrong number", "BRPOP", "1") c.Error("timeout is negative", "BRPOP", "key", "-1") }) } func TestBrpopMulti(t *testing.T) { skip(t) testMulti(t, func(c *client) { c.Do("BRPOP", "key", "1") c.Do("BRPOP", "key", "1") c.Do("BRPOP", "key", "1") c.Do("BRPOP", "key", "1") c.Do("BRPOP", "key", "1") // will timeout }, func(c *client) { c.Do("LPUSH", "key", "aap", "noot", "mies") time.Sleep(50 * time.Millisecond) c.Do("LPUSH", "key", "toon") }, ) } func TestBrpopTrans(t *testing.T) { skip(t) testMulti(t, func(c *client) { c.Do("BRPOP", "key", "1") }, func(c *client) { c.Do("MULTI") c.Do("LPUSH", "key", "toon") c.Do("EXEC") }, ) } func TestBlpop(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("LPUSH", "l", "one") c.Do("BLPOP", "l", "1") c.Do("EXISTS", "l") // failure cases c.Error("wrong number", "BLPOP") c.Error("wrong number", "BLPOP", "l") c.Error("not a float", "BLPOP", "l", "X") c.Error("not a float", "BLPOP", "l", "") c.Error("wrong number", "BLPOP", "1") c.Error("timeout is negative", "BLPOP", "key", "-1") }) testMulti(t, func(c *client) { c.Do("BLPOP", "key", "1") c.Do("BLPOP", "key", "1") c.Do("BLPOP", "key", "1") c.Do("BLPOP", "key", "1") c.Do("BLPOP", "key", "1") // will timeout }, func(c *client) { c.Do("LPUSH", "key", "aap", "noot", "mies") time.Sleep(10 * time.Millisecond) c.Do("LPUSH", "key", "toon") }, ) } func TestBrpoplpush(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("LPUSH", "l", "one") c.Do("BRPOPLPUSH", "l", "l2", "0.1") c.Do("EXISTS", "l") c.Do("EXISTS", "l2") c.Do("LRANGE", "l", "0", "-1") c.Do("LRANGE", "l2", "0", "-1") // failure cases c.Error("wrong number", "BRPOPLPUSH") c.Error("wrong number", "BRPOPLPUSH", "l") c.Error("wrong number", "BRPOPLPUSH", "l", "x") c.Error("wrong number", "BRPOPLPUSH", "1") c.Error("timeout is negative", "BRPOPLPUSH", "from", "to", "-1") c.Error("out of range", "BRPOPLPUSH", "from", "to", "inf") c.Error("wrong number", "BRPOPLPUSH", "from", "to", "-1", "xxx") }) wg := &sync.WaitGroup{} wg.Add(1) testMulti(t, func(c *client) { c.Do("BRPOPLPUSH", "from", "to", "1") c.Do("BRPOPLPUSH", "from", "to", "1") c.Do("BRPOPLPUSH", "from", "to", "1") c.Do("BRPOPLPUSH", "from", "to", "1") c.Do("BRPOPLPUSH", "from", "to", "1") // will timeout wg.Done() }, func(c *client) { c.Do("LPUSH", "from", "aap", "noot", "mies") time.Sleep(20 * time.Millisecond) c.Do("LPUSH", "from", "toon") wg.Wait() c.Do("LRANGE", "from", "0", "-1") c.Do("LRANGE", "to", "0", "-1") }, ) } func TestLmove(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "src", "LR", "LL", "RR", "RL") c.Do("LMOVE", "src", "dst", "LEFT", "RIGHT") c.Do("LRANGE", "src", "0", "-1") c.Do("LRANGE", "dst", "0", "-1") c.Do("LMOVE", "src", "dst", "RIGHT", "LEFT") c.Do("LMOVE", "src", "dst", "LEFT", "LEFT") c.Do("LMOVE", "src", "dst", "RIGHT", "RIGHT") // now empty c.Do("EXISTS", "src") c.Do("LRANGE", "dst", "0", "-1") // Cycle left to right c.Do("RPUSH", "round", "aap", "noot", "mies") c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") c.Do("LRANGE", "round", "0", "-1") c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") c.Do("LMOVE", "round", "round", "LEFT", "RIGHT") c.Do("LRANGE", "round", "0", "-1") // Cycle right to left c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") c.Do("LRANGE", "round", "0", "-1") c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") c.Do("LMOVE", "round", "round", "RIGHT", "LEFT") c.Do("LRANGE", "round", "0", "-1") // Cycle same side c.Do("LMOVE", "round", "round", "LEFT", "LEFT") c.Do("LRANGE", "round", "0", "-1") c.Do("LMOVE", "round", "round", "RIGHT", "RIGHT") c.Do("LRANGE", "round", "0", "-1") // failure cases c.Do("RPUSH", "chk", "aap", "noot", "mies") c.Error("wrong number", "LMOVE") c.Error("wrong number", "LMOVE", "chk") c.Error("wrong number", "LMOVE", "chk", "dst") c.Error("wrong number", "LMOVE", "chk", "dst", "chk") c.Error("wrong number", "LMOVE", "chk", "dst", "chk", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "LMOVE", "chk", "str", "LEFT", "LEFT") c.Error("wrong kind", "LMOVE", "str", "chk", "LEFT", "LEFT") c.Do("LRANGE", "chk", "0", "-1") }) } func TestBlmove(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("RPUSH", "src", "LR", "LL", "RR", "RL") c.Do("BLMOVE", "src", "dst", "LEFT", "RIGHT", "0") c.Do("LRANGE", "src", "0", "-1") c.Do("LRANGE", "dst", "0", "-1") c.Do("BLMOVE", "src", "dst", "RIGHT", "LEFT", "0") c.Do("BLMOVE", "src", "dst", "LEFT", "LEFT", "0") c.Do("BLMOVE", "src", "dst", "RIGHT", "RIGHT", "0") // now empty c.Do("EXISTS", "src") c.Do("LRANGE", "dst", "0", "-1") // Cycle left to right c.Do("RPUSH", "round", "aap", "noot", "mies") c.Do("BLMOVE", "round", "round", "LEFT", "RIGHT", "0") c.Do("LRANGE", "round", "0", "-1") c.Do("BLMOVE", "round", "round", "LEFT", "RIGHT", "0") c.Do("BLMOVE", "round", "round", "LEFT", "RIGHT", "0") c.Do("BLMOVE", "round", "round", "LEFT", "RIGHT", "0") c.Do("BLMOVE", "round", "round", "LEFT", "RIGHT", "0") c.Do("LRANGE", "round", "0", "-1") // Cycle right to left c.Do("BLMOVE", "round", "round", "RIGHT", "LEFT", "0") c.Do("LRANGE", "round", "0", "-1") c.Do("BLMOVE", "round", "round", "RIGHT", "LEFT", "0") c.Do("BLMOVE", "round", "round", "RIGHT", "LEFT", "0") c.Do("BLMOVE", "round", "round", "RIGHT", "LEFT", "0") c.Do("BLMOVE", "round", "round", "RIGHT", "LEFT", "0") c.Do("LRANGE", "round", "0", "-1") // Cycle same side c.Do("BLMOVE", "round", "round", "LEFT", "LEFT", "0") c.Do("LRANGE", "round", "0", "-1") c.Do("BLMOVE", "round", "round", "RIGHT", "RIGHT", "0") c.Do("LRANGE", "round", "0", "-1") // TTL c.Do("LPUSH", "test", "1") c.Do("EXPIRE", "test", "1000") c.Do("TTL", "test") c.Do("BLMOVE", "test", "test", "LEFT", "LEFT", "1") c.Do("TTL", "test") // failure cases c.Do("RPUSH", "chk", "aap", "noot", "mies") c.Error("wrong number", "LMOVE") c.Error("wrong number", "LMOVE", "chk") c.Error("wrong number", "LMOVE", "chk", "dst") c.Error("wrong number", "LMOVE", "chk", "dst", "chk") c.Error("wrong number", "LMOVE", "chk", "dst", "chk", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "BLMOVE", "chk", "str", "LEFT", "LEFT", "0") c.Error("wrong kind", "BLMOVE", "str", "chk", "LEFT", "LEFT", "0") c.Do("LRANGE", "chk", "0", "-1") }) wg := &sync.WaitGroup{} wg.Add(1) testMulti(t, func(c *client) { c.Do("BLMOVE", "from", "to", "RIGHT", "LEFT", "1") c.Do("BLMOVE", "from", "to", "RIGHT", "LEFT", "1") c.Do("BLMOVE", "from", "to", "RIGHT", "LEFT", "1") c.Do("BLMOVE", "from", "to", "RIGHT", "LEFT", "1") c.Do("BLMOVE", "from", "to", "RIGHT", "LEFT", "1") // will timeout wg.Done() }, func(c *client) { c.Do("LPUSH", "from", "aap", "noot", "mies") time.Sleep(20 * time.Millisecond) c.Do("LPUSH", "from", "toon") wg.Wait() c.Do("LRANGE", "from", "0", "-1") c.Do("LRANGE", "to", "0", "-1") }, ) } ================================================ FILE: miniredis/tests/integration-go/pubsub_test.go ================================================ package main import ( "sync" "testing" ) func TestSubscribe(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Error("wrong number", "SUBSCRIBE") c.Do("SUBSCRIBE", "foo") c.Do("UNSUBSCRIBE") c.Do("SUBSCRIBE", "foo") c.Do("UNSUBSCRIBE", "foo") c.Do("SUBSCRIBE", "foo", "bar") c.Receive() c.Do("UNSUBSCRIBE", "foo", "bar") c.Receive() c.Do("SUBSCRIBE", "-1") c.Do("UNSUBSCRIBE", "-1") c.Do("UNSUBSCRIBE") }) } func TestPsubscribe(t *testing.T) { skip(t) testRaw2(t, func(c1, c2 *client) { c1.Error("wrong number", "PSUBSCRIBE") c1.Do("PSUBSCRIBE", "foo") c2.Do("PUBLISH", "foo", "hi") c1.Receive() c1.Do("PUNSUBSCRIBE") c1.Do("PSUBSCRIBE", "foo") c2.Do("PUBLISH", "foo", "hi2") c1.Receive() c1.Do("PUNSUBSCRIBE", "foo") c1.Do("PSUBSCRIBE", "foo", "bar") c1.Receive() c2.Do("PUBLISH", "foo", "hi3") c1.Receive() c1.Do("PUNSUBSCRIBE", "foo", "bar") c1.Receive() c1.Do("PSUBSCRIBE", "f?o") c2.Do("PUBLISH", "foo", "hi4") c1.Receive() c1.Do("PUNSUBSCRIBE", "f?o") c1.Do("PSUBSCRIBE", "f*o") c2.Do("PUBLISH", "foo", "hi5") c1.Receive() c1.Do("PUNSUBSCRIBE", "f*o") c1.Do("PSUBSCRIBE", "f[oO]o") c2.Do("PUBLISH", "foo", "hi6") c1.Receive() c1.Do("PUNSUBSCRIBE", "f[oO]o") c1.Do("PSUBSCRIBE", `f\?o`) c2.Do("PUBLISH", "f?o", "hi7") c1.Receive() c1.Do("PUNSUBSCRIBE", `f\?o`) c1.Do("PSUBSCRIBE", `f\*o`) c2.Do("PUBLISH", "f*o", "hi8") c1.Receive() c1.Do("PUNSUBSCRIBE", `f\*o`) c1.Do("PSUBSCRIBE", "f\\[oO]o") c2.Do("PUBLISH", "f[oO]o", "hi9") c1.Receive() c1.Do("PUNSUBSCRIBE", "f\\[oO]o") c1.Do("PSUBSCRIBE", `f\\oo`) c2.Do("PUBLISH", `f\\oo`, "hi10") c1.Do("PUNSUBSCRIBE", `f\\oo`) c1.Do("PSUBSCRIBE", "-1") c2.Do("PUBLISH", "foo", "hi11") c1.Do("PUNSUBSCRIBE", "-1") }) testRaw2(t, func(c1, c2 *client) { c1.Do("PSUBSCRIBE", "news*") c2.Do("PUBLISH", "news", "fire!") c1.Receive() }) testRaw2(t, func(c1, c2 *client) { c1.Do("PSUBSCRIBE", "news") // no pattern c2.Do("PUBLISH", "news", "fire!") c1.Receive() }) testRaw(t, func(c *client) { c.Do("PUNSUBSCRIBE") c.Do("PUNSUBSCRIBE") }) } func TestPublish(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Error("wrong number", "PUBLISH") c.Error("wrong number", "PUBLISH", "foo") c.Do("PUBLISH", "foo", "bar") c.Error("wrong number", "PUBLISH", "foo", "bar", "deadbeef") c.Do("PUBLISH", "-1", "-2") }) } func TestPubSub(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Error("wrong number", "PUBSUB") c.Error("subcommand", "PUBSUB", "FOO") c.Do("PUBSUB", "CHANNELS") c.Do("PUBSUB", "CHANNELS", "foo") c.Error("wrong number", "PUBSUB", "CHANNELS", "foo", "bar") c.Do("PUBSUB", "CHANNELS", "f?o") c.Do("PUBSUB", "CHANNELS", "f*o") c.Do("PUBSUB", "CHANNELS", "f[oO]o") c.Do("PUBSUB", "CHANNELS", "f\\?o") c.Do("PUBSUB", "CHANNELS", "f\\*o") c.Do("PUBSUB", "CHANNELS", "f\\[oO]o") c.Do("PUBSUB", "CHANNELS", "f\\\\oo") c.Do("PUBSUB", "CHANNELS", "-1") c.Do("PUBSUB", "NUMSUB") c.Do("PUBSUB", "NUMSUB", "foo") c.Do("PUBSUB", "NUMSUB", "foo", "bar") c.Do("PUBSUB", "NUMSUB", "-1") c.Do("PUBSUB", "NUMPAT") c.Error("wrong number", "PUBSUB", "NUMPAT", "foo") }) } func TestPubsubFull(t *testing.T) { skip(t) testRaw2(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "news", "sport") c1.Receive() c2.Do("PUBLISH", "news", "revolution!") c2.Do("PUBLISH", "news", "alien invasion!") c2.Do("PUBLISH", "sport", "lady biked too fast") c2.Do("PUBLISH", "gossip", "man bites dog") c1.Receive() c1.Receive() c1.Receive() c1.Do("UNSUBSCRIBE", "news", "sport") c1.Receive() }) testRESP3Pair(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "news", "sport") c1.Receive() c2.Do("PUBLISH", "news", "fire!") c1.Receive() c1.Do("UNSUBSCRIBE", "news", "sport") c1.Receive() }) } func TestPubsubMulti(t *testing.T) { skip(t) var wg1 sync.WaitGroup wg1.Add(2) testMulti(t, func(c *client) { c.Do("SUBSCRIBE", "news", "sport") c.Receive() wg1.Done() c.Receive() c.Receive() c.Receive() c.Do("UNSUBSCRIBE", "news", "sport") c.Receive() }, func(c *client) { c.Do("SUBSCRIBE", "sport") wg1.Done() c.Receive() c.Do("UNSUBSCRIBE", "sport") }, func(c *client) { wg1.Wait() c.Do("PUBLISH", "news", "revolution!") c.Do("PUBLISH", "news", "alien invasion!") c.Do("PUBLISH", "sport", "lady biked too fast") }, ) } func TestPubsubSelect(t *testing.T) { skip(t) testRaw2(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "news", "sport") c1.Receive() c2.Do("SELECT", "3") c2.Do("PUBLISH", "news", "revolution!") c1.Receive() }) } func TestPubsubMode(t *testing.T) { skip(t) // most commands aren't allowed in publish mode t.Run("basic", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("SUBSCRIBE", "news", "sport") c.Receive() c.Do("PING") c.Do("PING", "foo") c.Error("are allowed", "ECHO", "foo") c.Error("are allowed", "HGET", "foo", "bar") c.Error("are allowed", "SET", "foo", "bar") c.Do("QUIT") }) }) t.Run("tx", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("SUBSCRIBE", "news") // failWith(e, "PING"), // failWith(e, "PSUBSCRIBE"), // failWith(e, "PUNSUBSCRIBE"), // failWith(e, "QUIT"), // failWith(e, "SUBSCRIBE"), // failWith(e, "UNSUBSCRIBE"), c.Error("are allowed", "APPEND", "foo", "foo") c.Error("are allowed", "AUTH", "foo") c.Error("are allowed", "BITCOUNT", "foo") c.Error("are allowed", "BITOP", "OR", "foo", "bar") c.Error("are allowed", "BITPOS", "foo", "0") c.Error("are allowed", "BLPOP", "key", "1") c.Error("are allowed", "BRPOP", "key", "1") c.Error("are allowed", "BRPOPLPUSH", "foo", "bar", "1") c.Error("are allowed", "DBSIZE") c.Error("are allowed", "DECR", "foo") c.Error("are allowed", "DECRBY", "foo", "3") c.Error("are allowed", "DEL", "foo") c.Error("are allowed", "DISCARD") c.Error("are allowed", "ECHO", "foo") c.Error("are allowed", "EVAL", "foo", "{}") c.Error("are allowed", "EVALSHA", "foo", "{}") c.Error("are allowed", "EXEC") c.Error("are allowed", "EXISTS", "foo") c.Error("are allowed", "EXPIRE", "foo", "12") c.Error("are allowed", "EXPIREAT", "foo", "12") c.Error("are allowed", "FLUSHALL") c.Error("are allowed", "FLUSHDB") c.Error("are allowed", "GET", "foo") c.Error("are allowed", "GETEX", "foo") c.Error("are allowed", "GETBIT", "foo", "12") c.Error("are allowed", "GETRANGE", "foo", "12", "12") c.Error("are allowed", "GETSET", "foo", "bar") c.Error("are allowed", "HDEL", "foo", "bar") c.Error("are allowed", "HEXISTS", "foo", "bar") c.Error("are allowed", "HGET", "foo", "bar") c.Error("are allowed", "HGETALL", "foo") c.Error("are allowed", "HINCRBY", "foo", "bar", "12") c.Error("are allowed", "HINCRBYFLOAT", "foo", "bar", "12.34") c.Error("are allowed", "HKEYS", "foo") c.Error("are allowed", "HLEN", "foo") c.Error("are allowed", "HMGET", "foo", "bar") c.Error("are allowed", "HMSET", "foo", "bar", "baz") c.Error("are allowed", "HSCAN", "foo", "0") c.Error("are allowed", "HSET", "foo", "bar", "baz") c.Error("are allowed", "HSETNX", "foo", "bar", "baz") c.Error("are allowed", "HVALS", "foo") c.Error("are allowed", "INCR", "foo") c.Error("are allowed", "INCRBY", "foo", "12") c.Error("are allowed", "INCRBYFLOAT", "foo", "12.34") c.Error("are allowed", "KEYS", "*") c.Error("are allowed", "LINDEX", "foo", "0") c.Error("are allowed", "LINSERT", "foo", "after", "bar", "0") c.Error("are allowed", "LLEN", "foo") c.Error("are allowed", "LPOP", "foo") c.Error("are allowed", "LPUSH", "foo", "bar") c.Error("are allowed", "LPUSHX", "foo", "bar") c.Error("are allowed", "LRANGE", "foo", "1", "1") c.Error("are allowed", "LREM", "foo", "0", "bar") c.Error("are allowed", "LSET", "foo", "0", "bar") c.Error("are allowed", "LTRIM", "foo", "0", "0") c.Error("are allowed", "MGET", "foo", "bar") c.Error("are allowed", "MOVE", "foo", "bar") c.Error("are allowed", "MSET", "foo", "bar") c.Error("are allowed", "MSETNX", "foo", "bar") c.Error("are allowed", "MULTI") c.Error("are allowed", "PERSIST", "foo") c.Error("are allowed", "PEXPIRE", "foo", "12") c.Error("are allowed", "PEXPIREAT", "foo", "12") c.Error("are allowed", "PSETEX", "foo", "12", "bar") c.Error("are allowed", "PTTL", "foo") c.Error("are allowed", "PUBLISH", "foo", "bar") c.Error("are allowed", "PUBSUB", "CHANNELS") c.Error("are allowed", "RANDOMKEY") c.Error("are allowed", "RENAME", "foo", "bar") c.Error("are allowed", "RENAMENX", "foo", "bar") c.Error("are allowed", "RPOP", "foo") c.Error("are allowed", "RPOPLPUSH", "foo", "bar") c.Error("are allowed", "RPUSH", "foo", "bar") c.Error("are allowed", "RPUSHX", "foo", "bar") c.Error("are allowed", "SADD", "foo", "bar") c.Error("are allowed", "SCAN", "0") c.Error("are allowed", "SCARD", "foo") c.Error("are allowed", "SCRIPT", "FLUSH") c.Error("are allowed", "SDIFF", "foo") c.Error("are allowed", "SDIFFSTORE", "foo", "bar") c.Error("are allowed", "SELECT", "12") c.Error("are allowed", "SET", "foo", "bar") c.Error("are allowed", "SETBIT", "foo", "0", "1") c.Error("are allowed", "SETEX", "foo", "12", "bar") c.Error("are allowed", "SETNX", "foo", "bar") c.Error("are allowed", "SETRANGE", "foo", "0", "bar") c.Error("are allowed", "SINTER", "foo", "bar") c.Error("are allowed", "SINTERSTORE", "foo", "bar", "baz") c.Error("are allowed", "SISMEMBER", "foo", "bar") c.Error("are allowed", "SMEMBERS", "foo") c.Error("are allowed", "SMOVE", "foo", "bar", "baz") c.Error("are allowed", "SPOP", "foo") c.Error("are allowed", "SRANDMEMBER", "foo") c.Error("are allowed", "SREM", "foo", "bar", "baz") c.Error("are allowed", "SSCAN", "foo", "0") c.Error("are allowed", "STRLEN", "foo") c.Error("are allowed", "SUNION", "foo", "bar") c.Error("are allowed", "SUNIONSTORE", "foo", "bar", "baz") c.Error("are allowed", "TIME") c.Error("are allowed", "TTL", "foo") c.Error("are allowed", "TYPE", "foo") c.Error("are allowed", "UNWATCH") c.Error("are allowed", "WATCH", "foo") c.Error("are allowed", "ZADD", "foo", "INCR", "1", "bar") c.Error("are allowed", "ZCARD", "foo") c.Error("are allowed", "ZCOUNT", "foo", "0", "1") c.Error("are allowed", "ZINCRBY", "foo", "bar", "12") c.Error("are allowed", "ZINTERSTORE", "foo", "1", "bar") c.Error("are allowed", "ZLEXCOUNT", "foo", "-", "+") c.Error("are allowed", "ZRANGE", "foo", "0", "-1") c.Error("are allowed", "ZRANGEBYLEX", "foo", "-", "+") c.Error("are allowed", "ZRANGEBYSCORE", "foo", "0", "1") c.Error("are allowed", "ZRANK", "foo", "bar") c.Error("are allowed", "ZREM", "foo", "bar") c.Error("are allowed", "ZREMRANGEBYLEX", "foo", "-", "+") c.Error("are allowed", "ZREMRANGEBYRANK", "foo", "0", "1") c.Error("are allowed", "ZREMRANGEBYSCORE", "foo", "0", "1") c.Error("are allowed", "ZREVRANGE", "foo", "0", "-1") c.Error("are allowed", "ZREVRANGEBYLEX", "foo", "+", "-") c.Error("are allowed", "ZREVRANGEBYSCORE", "foo", "0", "1") c.Error("are allowed", "ZREVRANK", "foo", "bar") c.Error("are allowed", "ZSCAN", "foo", "0") c.Error("are allowed", "ZSCORE", "foo", "bar") c.Error("are allowed", "ZUNIONSTORE", "foo", "1", "bar") }) }) } func TestSubscriptions(t *testing.T) { skip(t) testRaw2(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "foo", "bar", "foo") c2.Do("PUBSUB", "NUMSUB") c1.Do("UNSUBSCRIBE", "bar", "bar", "bar") c2.Do("PUBSUB", "NUMSUB") }) } func TestPubsubUnsub(t *testing.T) { skip(t) testRaw2(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "news", "sport") c1.Receive() c2.DoSorted("PUBSUB", "CHANNELS") c1.Do("QUIT") c2.DoSorted("PUBSUB", "CHANNELS") }) } func TestPubsubTx(t *testing.T) { skip(t) // publish is in a tx testRaw2(t, func(c1, c2 *client) { c1.Do("SUBSCRIBE", "foo") c2.Do("MULTI") c2.Do("PUBSUB", "CHANNELS") c2.Do("PUBLISH", "foo", "hello one") c2.Error("wrong number", "GET") c2.Do("PUBLISH", "foo", "hello two") c2.Error("discarded", "EXEC") c2.Do("PUBLISH", "foo", "post tx") c1.Receive() }) // SUBSCRIBE is in a tx testRaw2(t, func(c1, c2 *client) { c1.Do("MULTI") c1.Do("SUBSCRIBE", "foo") c2.Do("PUBSUB", "CHANNELS") c1.Do("EXEC") c2.Do("PUBSUB", "CHANNELS") c1.Error("are allowed", "MULTI") // we're in SUBSCRIBE mode }) // DISCARDing a tx prevents from entering publish mode testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SUBSCRIBE", "foo") c.Do("DISCARD") c.Do("PUBSUB", "CHANNELS") }) // UNSUBSCRIBE is in a tx testRaw2(t, func(c1, c2 *client) { c1.Do("MULTI") c1.Do("SUBSCRIBE", "foo") c1.Do("UNSUBSCRIBE", "foo") c2.Do("PUBSUB", "CHANNELS") c1.Do("EXEC") c2.Do("PUBSUB", "CHANNELS") c1.Do("PUBSUB", "CHANNELS") }) // PSUBSCRIBE is in a tx testRaw2(t, func(c1, c2 *client) { c1.Do("MULTI") c1.Do("PSUBSCRIBE", "foo") c2.Do("PUBSUB", "NUMPAT") c1.Do("EXEC") c2.Do("PUBSUB", "NUMPAT") c1.Error("are allowed", "MULTI") // we're in SUBSCRIBE mode }) // PUNSUBSCRIBE is in a tx testRaw2(t, func(c1, c2 *client) { c1.Do("MULTI") c1.Do("PSUBSCRIBE", "foo") c1.Do("PUNSUBSCRIBE", "foo") c2.Do("PUBSUB", "NUMPAT") c1.Do("EXEC") c2.Do("PUBSUB", "NUMPAT") c1.Do("PUBSUB", "NUMPAT") }) } ================================================ FILE: miniredis/tests/integration-go/script_test.go ================================================ package main import ( "testing" ) func TestScript(t *testing.T) { skip(t) t.Run("EVAL", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("EVAL", "return 42", "0") c.Do("EVAL", "", "0") c.Do("EVAL", "return 42", "1", "foo") c.Do("EVAL", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", "2", "key1", "key2", "first", "second") c.Do("EVAL", "return {ARGV[1]}", "0", "first") c.Do("EVAL", "return {ARGV[1]}", "0", "first\nwith\nnewlines!\r\r\n\t!") c.Do("EVAL", "return redis.call('GET', 'nosuch')==false", "0") c.Do("EVAL", "return redis.call('GET', 'nosuch')==nil", "0") c.Do("EVAL", "local a = redis.call('MGET', 'bar'); return a[1] == false", "0") c.Do("EVAL", "local a = redis.call('MGET', 'bar'); return a[1] == nil", "0") c.Do("EVAL", "return redis.call('ZRANGE', 'q', 0, -1)", "0") c.Do("EVAL", "return redis.call('LPOP', 'foo')", "0") c.Do("EVAL_RO", "return 42", "0") c.Do("EVAL_RO", "return 42+2", "0") c.Error("Write commands are not allowed", "EVAL_RO", "return redis.call('LPOP', 'foo')", "0") // failure cases c.Error("wrong number", "EVAL") c.Error("wrong number", "EVAL", "return 42") c.Error("wrong number", "EVAL", "[") c.Error("not an integer", "EVAL", "return 42", "return 43") c.Error("greater", "EVAL", "return 42", "1") c.Error("negative", "EVAL", "return 42", "-1") c.Error("wrong number", "EVAL", "42") }) }) t.Run("SCRIPT", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("SCRIPT", "LOAD", "return 42") c.Do("SCRIPT", "LOAD", "return 42") c.Do("SCRIPT", "LOAD", "return 43") c.Do("SCRIPT", "EXISTS", "1fa00e76656cc152ad327c13fe365858fd7be306") c.Do("SCRIPT", "EXISTS", "0", "1fa00e76656cc152ad327c13fe365858fd7be306") c.Do("SCRIPT", "EXISTS", "0") c.Error("wrong number", "SCRIPT", "EXISTS") c.Do("SCRIPT", "FLUSH") c.Do("SCRIPT", "EXISTS", "1fa00e76656cc152ad327c13fe365858fd7be306") c.Do("SCRIPT", "FLUSH", "ASYNC") c.Do("SCRIPT", "FLUSH", "SyNc") c.Error("wrong number", "SCRIPT") c.Error("wrong number", "SCRIPT", "LOAD", "return 42", "return 42") c.DoLoosely("SCRIPT", "LOAD", "]") c.Error("wrong number", "SCRIPT", "LOAD", "]", "foo") c.Error("wrong number", "SCRIPT", "LOAD") c.Error("only support", "SCRIPT", "FLUSH", "foo") c.Error("only support", "SCRIPT", "FLUSH", "ASYNC", "foo") c.Error("unknown subcommand", "SCRIPT", "FOO") }) }) t.Run("EVALSHA", func(t *testing.T) { sha1 := "1fa00e76656cc152ad327c13fe365858fd7be306" // "return 42" sha2 := "bfbf458525d6a0b19200bfd6db3af481156b367b" // keys[1], argv[1] testRaw(t, func(c *client) { c.Do("SCRIPT", "LOAD", "return 42") c.Do("SCRIPT", "LOAD", "return {KEYS[1],ARGV[1]}") c.Do("EVALSHA", sha1, "0") c.Do("EVALSHA", sha2, "0") c.Do("EVALSHA", sha2, "0", "foo") c.Do("EVALSHA", sha2, "1", "foo") c.Do("EVALSHA", sha2, "1", "foo", "bar") c.Do("EVALSHA", sha2, "1", "foo", "bar", "baz") c.Do("SCRIPT", "FLUSH") c.Error("Please use EVAL", "EVALSHA", sha1, "0") c.Do("SCRIPT", "LOAD", "return 42") c.Error("wrong number", "EVALSHA", sha1) c.Error("wrong number", "EVALSHA") c.Error("wrong number", "EVALSHA", "nosuch") c.Error("Please use EVAL", "EVALSHA", "nosuch", "0") }) }) t.Run("combined", func(t *testing.T) { sha1 := "1fa00e76656cc152ad327c13fe365858fd7be306" // "return 42" testRaw(t, func(c *client) { // EVAL stores the script c.Do("EVAL", "return 42", "0") c.Do("SCRIPT", "EXISTS", sha1) c.Do("EVALSHA", sha1, "0") // doesn't store the script on syntax error c.Error("compiling", "EVAL", "return '<-syntax error", "0") c.Do("SCRIPT", "EXISTS", "015cb4913729c68a7209188bbdee1b1ca19358bf") c.Error("NOSCRIPT", "EVALSHA", "015cb4913729c68a7209188bbdee1b1ca19358bf", "0") // does store the script on arg errors c.Do("SCRIPT", "FLUSH") c.Error("not an int", "EVAL", "return 42", "notanumber") c.Do("SCRIPT", "EXISTS", sha1) c.Error("NOSCRIPT", "EVALSHA", sha1, "0") }) }) t.Run("setresp", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("EVAL", `redis.setresp(3); redis.call("SET", "foo", 12); return redis.call("GET", "foo")`, "0") c.Do("SCRIPT", "LOAD", `redis.setresp(3)`) c.Do("EVALSHA", "d204691e560b5b17f19626b50f84c2dcadff7ed5", "0") c.Do("EVAL", `return redis.setresp(3)`, "0") c.Do("EVAL", `return redis.setresp(2)`, "0") c.Error("RESP version must be 2 or 3", "EVAL", `return redis.setresp(4)`, "0") }) testRESP3(t, func(c *client) { c.Do("SCRIPT", "LOAD", `redis.setresp(3)`) c.Do("EVALSHA", "d204691e560b5b17f19626b50f84c2dcadff7ed5", "0") c.Do("EVAL", `redis.setresp(3); redis.call("SET", "foo", 12); return redis.call("GET", "foo")`, "0") }) }) } func TestLua(t *testing.T) { skip(t) // basic datatype things datatypes := func(c *client) { c.Do("EVAL", "", "0") c.Do("EVAL", "return 42", "0") c.Do("EVAL", "return 42, 43", "0") c.Do("EVAL", "return true", "0") c.Do("EVAL", "return 'foo'", "0") c.Do("EVAL", "return 3.1415", "0") c.Do("EVAL", "return 3.9999", "0") c.Do("EVAL", "return {1,'foo'}", "0") c.Do("EVAL", "return {1,'foo',nil,'foo'}", "0") c.Do("EVAL", "return 3.9999+3", "0") c.Do("EVAL", "return 3.99+0.0001", "0") c.Do("EVAL", "return 3.9999+0.201", "0") c.Do("EVAL", "return {{1}}", "0") c.Do("EVAL", "return {1,{1,{1,'bar'}}}", "0") c.Do("EVAL", "return nil", "0") } testRaw(t, datatypes) testRESP3(t, datatypes) // special returns testRaw(t, func(c *client) { c.Error("oops", "EVAL", "return {err = 'oops'}", "0") c.Do("EVAL", "return {1,{err = 'oops'}}", "0") c.Error("oops", "EVAL", "return redis.error_reply('oops2')", "0") c.Do("EVAL", "return {1,redis.error_reply('oops')}", "0") c.Error("oops", "EVAL", "return {err = 'oops', noerr = true}", "0") // doc error? c.Error("oops", "EVAL", "return {1, 2, err = 'oops'}", "0") // doc error? c.Do("EVAL", "return {ok = 'great'}", "0") c.Do("EVAL", "return {1,{ok = 'great'}}", "0") c.Do("EVAL", "return redis.status_reply('great')", "0") c.Do("EVAL", "return {1,redis.status_reply('great')}", "0") c.Do("EVAL", "return {ok = 'great', notok = 'yes'}", "0") // doc error? c.Do("EVAL", "return {1, 2, ok = 'great', notok = 'yes'}", "0") // doc error? c.Error("type of arguments", "EVAL", "return redis.error_reply(1)", "0") c.Error("type of arguments", "EVAL", "return redis.error_reply()", "0") c.Error("type of arguments", "EVAL", "return redis.error_reply(redis.error_reply('foo'))", "0") c.Error("type of arguments", "EVAL", "return redis.status_reply(1)", "0") c.Error("type of arguments", "EVAL", "return redis.status_reply()", "0") c.Error("type of arguments", "EVAL", "return redis.status_reply(redis.status_reply('foo'))", "0") c.ErrorTheSame("ERR ", "EVAL", "return redis.error_reply('')", "0") c.ErrorTheSame("ERR ", "EVAL", "return redis.error_reply('-')", "0") c.ErrorTheSame("ERR foo", "EVAL", "return redis.error_reply('foo')", "0") c.ErrorTheSame("ERR foo", "EVAL", "return redis.error_reply('-foo')", "0") c.ErrorTheSame("foo bar", "EVAL", "return redis.error_reply('foo bar')", "0") c.ErrorTheSame("foo bar", "EVAL", "return redis.error_reply('-foo bar')", "0") }) // state inside lua testRaw(t, func(c *client) { c.Do("EVAL", "redis.call('SELECT', 3); redis.call('SET', 'foo', 'bar')", "0") c.Do("GET", "foo") c.Do("SELECT", "3") c.Do("GET", "foo") }) // lua env testRaw(t, func(c *client) { // c.Do("EVAL", "print(1)", "0") c.Do("EVAL", `return string.format('%q', "pretty string")`, "0") c.Error("Script attempted to access nonexistent global variable", "EVAL", "foob.clock()", "0") c.DoLoosely("EVAL", "os.clock()", "0") c.Error("attempt to call", "EVAL", "os.exit(42)", "0") c.Do("EVAL", "return table.concat({1,2,3})", "0") c.Do("EVAL", "return math.abs(-42)", "0") c.Error("Script attempted to access nonexistent global variable", "EVAL", `return utf8.len("hello world")`, "0") // c.Error("Script attempted to access nonexistent global variable", "EVAL", `require("utf8")`, "0") c.Do("EVAL", `return coroutine.running()`, "0") }) // sha1hex testRaw(t, func(c *client) { c.Do("EVAL", `return redis.sha1hex("foo")`, "0") c.Do("SET", "bar", "32") c.Do("EVAL", `return redis.sha1hex(KEYS["bar"])`, "0") c.Do("EVAL", `return redis.sha1hex(KEYS[1])`, "1", "bar") c.Do("EVAL", `return redis.sha1hex(nil)`, "0") c.Do("EVAL", `return redis.sha1hex(42)`, "0") c.Do("EVAL", `return redis.sha1hex({})`, "0") c.Do("EVAL", `return redis.sha1hex(KEYS[1])`, "0") c.Error( "wrong number of arguments", "EVAL", `return redis.sha1hex()`, "0", ) c.Error( "wrong number of arguments", "EVAL", `return redis.sha1hex(1, 2)`, "0", ) }) // cjson module testRaw(t, func(c *client) { c.Do("EVAL", `return cjson.decode('{"id":"foo"}')['id']`, "0") // c.Do("SET", "foo", `{"value":42}`) // c.Do("EVAL", `return KEYS[1]`, 1, "foo") // c.Do("EVAL", `return cjson.decode(KEYS[1])['value']`, 1, "foo") c.Do("EVAL", `return cjson.decode(ARGV[1])['value']`, "0", `{"value":"42"}`) c.Do("EVAL", `return redis.call("SET", "enc", cjson.encode({["foo"]="bar"}))`, "0") c.Do("EVAL", `return redis.call("SET", "enc", cjson.encode({["foo"]={["foo"]=42}}))`, "0") c.Do("GET", "enc") c.Error( "bad argument #1 to ", "EVAL", `return cjson.encode()`, "0", ) c.Error( "bad argument #1 to ", "EVAL", `return cjson.encode(1, 2)`, "0", ) c.Error( "bad argument #1 to ", "EVAL", `return cjson.decode()`, "0", ) c.Error( "bad argument #1 to ", "EVAL", `return cjson.decode(1, 2)`, "0", ) }) // selected DB gets passed on to lua testRaw(t, func(c *client) { c.Do("SELECT", "3") c.Do("EVAL", "redis.call('SET', 'foo', 'bar')", "0") c.Do("GET", "foo") c.Do("SELECT", "0") c.Do("GET", "foo") }) } func TestLuaCall(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "1") c.Do("EVAL", `local foo = redis.call("GET", "foo"); redis.call("SET", "foo", foo+1)`, "0") c.Do("GET", "foo") c.Do("EVAL", `return redis.call("GET", "foo")`, "0") c.Do("EVAL", `return redis.call("SET", "foo", 42)`, "0") c.Do("EVAL", `redis.log(redis.LOG_NOTICE, "hello")`, "0") c.Do("EVAL", `local res = redis.call("GET", "foo"); return res['ok']`, "0") }) testRaw(t, func(c *client) { script := ` local result = redis.call('SET', 'mykey', 'myvalue', 'NX'); return result['ok']; ` c.Do("EVAL", script, "0") }) // datatype errors testRaw(t, func(c *client) { c.Error( "Please specify at least one argument for this redis lib call script: 23251039f40992dadef496cbfe3f3d23a6d314ce", "EVAL", `redis.call()`, "0", ) c.Error( "Lua redis lib command arguments must be strings or integers script: 2c79b56ef55f7dc96da28dddb6ba551017fb1480,", "EVAL", `redis.call({})`, "0", ) c.Error( "Unknown Redis command called from script script: 1f422cead4ec560a2473e39974d64f965b99b8b0", "EVAL", `redis.call(1)`, "0", ) c.Error( "Unknown Redis command called from script script: cd72c3c55975da213448de4e59a8674b8b21c486", "EVAL", `redis.call("1")`, "0", ) c.Error( "Lua redis lib command arguments must be strings or integers script: 40286a2418d06fc20cf71762ed4c52b5348b4bb0", "EVAL", `redis.call("ECHO", true)`, "0", ) c.Error( "Lua redis lib command arguments must be strings or integers script: d2f4e1eb2935fe53669068a377a3dc4b923eb669,", "EVAL", `redis.call("ECHO", false)`, "0", ) c.Error( "Lua redis lib command arguments must be strings or integers script: 33462f69402788110bccac05df6a8ac9c7429304,", "EVAL", `redis.call("ECHO", nil)`, "0", ) c.Error( "Lua redis lib command arguments must be strings or integers script: 180500c268449fd1a24ea520d39a4aa76d6693c2,", "EVAL", `redis.call("HELLO", {})`, "0", ) // c.Error("Error", "EVAL", `redis.call("HELLO", 1)`, "0") // c.Error("Redis command", "EVAL", `redis.call("HELLO", 3.14)`, "0") c.Error( "Lua redis lib command arguments must be strings or integers script: 32c9afc7bcb832809c41272b7a5525020b3e8bf5,", "EVAL", `redis.call("GET", {})`, "0", ) }) // call() errors testRaw(t, func(c *client) { c.Do("SET", "foo", "1") c.Error("rong number of arg", "EVAL", `redis.call("HGET", "foo")`, "0") c.Do("GET", "foo") c.Error("rong number of arg", "EVAL", `local foo = redis.call("HGET", "foo"); redis.call("SET", "res", foo)`, "0") c.Do("GET", "foo") c.Do("GET", "res") c.Error("WRONGTYPE", "EVAL", `local foo = redis.call("HGET", "foo", "bar"); redis.call("SET", "res", foo)`, "0") c.Do("GET", "foo") c.Do("GET", "res") }) // pcall() errors testRaw(t, func(c *client) { c.Do("SET", "foo", "1") c.Error( "Lua redis lib command arguments must be strings or integers script: 66acd1fa6589521219d0b0dc3c1965f4b11a3422,", "EVAL", `local foo = redis.pcall("HGET", "foo"); redis.call("SET", "res", foo)`, "0", ) c.Do("GET", "foo") c.Do("GET", "res") c.Error( "Lua redis lib command arguments must be strings or integers script: 5b67bc50d5e0ed20baae44ca5a735efa6a3e5243,", "EVAL", `local foo = redis.pcall("HGET", "foo", "bar"); redis.call("SET", "res", foo)`, "0", ) c.Do("GET", "foo") c.Do("GET", "res") }) // call() with non-allowed commands testRaw(t, func(c *client) { c.Do("SET", "foo", "1") c.Error( "This Redis command is not allowed from script script: a17bb9f079d9b5202346e82ccaa50f3b9553172b,", "EVAL", `redis.call("MULTI")`, "0", ) c.Error( "This Redis command is not allowed from script script: 56569e2c63cf8996b64922e5a26e23c60fe9f1aa,", "EVAL", `redis.call("EXEC")`, "0", ) c.Error( "This Redis command is not allowed from script script: a2457385c7980996400fc4315534dcf332d54f46,", "EVAL", `redis.call("EVAL", "redis.call(\"GET\", \"foo\")", 0)`, "0", ) c.Error( "This Redis command is not allowed from script script: ac613210b61b9f3339fd677969291675b9b703d3,", "EVAL", `redis.call("SCRIPT", "LOAD", "return 42")`, "0", ) c.Error( "This Redis command is not allowed from script script: 888b717177e29e998baf4bac6116c2a4787b4c70,", "EVAL", `redis.call("EVALSHA", "123", "0")`, "0", ) c.Error( "This Redis command is not allowed from script script: 508bef3f1ab46859dee541a8bc3b0f368ae1844f,", "EVAL", `redis.call("AUTH", "foobar")`, "0", ) c.Error( "This Redis command is not allowed from script script: 62b5d652eb4d90746a5672a450ed9e3627521df1,", "EVAL", `redis.call("WATCH", "foobar")`, "0", ) c.Error( "This Redis command is not allowed from script script: 65ea661820802737ade33d7a70582838a09fcf8d,", "EVAL", `redis.call("SUBSCRIBE", "foo")`, "0", ) c.Error( "This Redis command is not allowed from script script: 1af9ab7e7d8aa211959de33824dc075ee816ab1a,", "EVAL", `redis.call("UNSUBSCRIBE", "foo")`, "0", ) c.Error( "This Redis command is not allowed from script script: 0610e3628fbdca44e6d49736d5b59be8bab5047d,", "EVAL", `redis.call("PSUBSCRIBE", "foo")`, "0", ) c.Error( "This Redis command is not allowed from script script: ba7f784eaff4e747e31a39abd5386c432aac3140,", "EVAL", `redis.call("PUNSUBSCRIBE", "foo")`, "0", ) c.Do("EVAL", `redis.pcall("EXEC")`, "0") c.Do("GET", "foo") }) } func TestScriptNoAuth(t *testing.T) { skip(t) testAuth(t, "supersecret", func(c *client) { c.Error("Authentication required", "EVAL", `redis.call("ECHO", "foo")`, "0") c.Do("AUTH", "supersecret") c.Do("EVAL", `redis.call("ECHO", "foo")`, "0") }, ) } func TestScriptReplicate(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do( "EVAL", `redis.replicate_commands();`, "0", ) }) testRaw(t, func(c *client) { c.Do( "EVAL", `redis.set_repl(redis.REPL_NONE);`, "0", ) }) } func TestScriptTx(t *testing.T) { skip(t) sha2 := "bfbf458525d6a0b19200bfd6db3af481156b367b" // keys[1], argv[1] testRaw(t, func(c *client) { c.Do("SCRIPT", "LOAD", "return {KEYS[1],ARGV[1]}") c.Do("MULTI") c.Do("EVALSHA", sha2, "0") c.Do("EXEC") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SCRIPT", "LOAD", "return {KEYS[1],ARGV[1]}") c.Do("EVALSHA", sha2, "0") c.Do("EXEC") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SCRIPT", "LOAD", "return {") c.Do("EVALSHA", "aaaa", "0") c.DoLoosely("EXEC") c.Do("MULTI") c.Error("unknown subcommand", "SCRIPT", "FOO") }) } ================================================ FILE: miniredis/tests/integration-go/server_test.go ================================================ package main import ( "testing" ) func TestServer(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("SET", "baz", "bak") c.Do("XADD", "planets", "123-456", "name", "Earth") c.Do("DBSIZE") c.Do("SELECT", "2") c.Do("DBSIZE") c.Do("SET", "baz", "bak") c.Do("SELECT", "0") c.Do("FLUSHDB") c.Do("DBSIZE") c.Do("SELECT", "2") c.Do("DBSIZE") c.Do("FLUSHALL") c.Do("DBSIZE") c.Do("FLUSHDB", "aSyNc") c.Do("FLUSHALL", "AsYnC") // Failure cases c.Error("wrong number", "DBSIZE", "foo") c.Error("syntax error", "FLUSHDB", "foo") c.Error("syntax error", "FLUSHALL", "foo") c.Error("syntax error", "FLUSHDB", "ASYNC", "foo") c.Error("syntax error", "FLUSHDB", "ASYNC", "ASYNC") c.Error("syntax error", "FLUSHALL", "ASYNC", "foo") }) testRaw(t, func(c *client) { c.Do("SET", "plain", "hello") c.DoLoosely("MEMORY", "USAGE", "plain") c.Do("LPUSH", "alist", "hello", "42") c.DoLoosely("MEMORY", "USAGE", "alist") c.Do("HSET", "ahash", "key", "value") c.DoLoosely("MEMORY", "USAGE", "ahash") c.Do("ZADD", "asset", "0", "line") c.DoLoosely("MEMORY", "USAGE", "asset") c.Do("PFADD", "ahll", "123") c.DoLoosely("MEMORY", "USAGE", "ahll") c.Do("XADD", "astream", "0-1", "name", "Mercury") c.DoLoosely("MEMORY", "USAGE", "astream") c.DoLoosely("MEMORY", "USAGE", "nosuch") c.Error("Try MEMORY HELP", "MEMORY", "FOO") c.Error("wrong number of arguments", "MEMORY", "USAGE") c.Error("syntax error", "MEMORY", "USAGE", "too", "many") }) testRaw(t, func(c *client) { c.DoLoosely("WAIT", "1", "1") // Failure cases c.Error("wrong number", "WAIT", "1") c.Error("wrong number", "WAIT", "1", "2", "3") c.Error("wrong number", "WAIT") c.Error("not an integer", "WAIT", "foo", "0") c.Error("not an integer", "WAIT", "1", "foo") // c.Error("out of range", "WAIT", "-1", "0") // something weird going on c.Error("timeout is negative", "WAIT", "11", "-12") }) } func TestServerTLS(t *testing.T) { skip(t) testTLS(t, func(c *client) { c.Do("PING", "foo") c.Do("SET", "foo", "bar") c.Do("GET", "foo") }) } ================================================ FILE: miniredis/tests/integration-go/set_test.go ================================================ package main import ( "testing" ) func TestSet(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s", "aap", "noot", "mies") c.Do("SADD", "s", "vuur", "noot") c.Do("TYPE", "s") c.Do("EXISTS", "s") c.Do("SCARD", "s") c.DoSorted("SMEMBERS", "s") c.DoSorted("SMEMBERS", "nosuch") c.Do("SISMEMBER", "s", "aap") c.Do("SISMEMBER", "s", "nosuch") c.Do("SMISMEMBER", "s", "aap", "noot", "nosuch") c.Do("SCARD", "nosuch") c.Do("SISMEMBER", "nosuch", "nosuch") c.Do("SMISMEMBER", "nosuch", "nosuch", "nosuch") // failure cases c.Error("wrong number", "SADD") c.Error("wrong number", "SADD", "s") c.Error("wrong number", "SMEMBERS") c.Error("wrong number", "SMEMBERS", "too", "many") c.Error("wrong number", "SCARD") c.Error("wrong number", "SCARD", "too", "many") c.Error("wrong number", "SISMEMBER") c.Error("wrong number", "SISMEMBER", "few") c.Error("wrong number", "SISMEMBER", "too", "many", "arguments") c.Error("wrong number", "SMISMEMBER") c.Error("wrong number", "SMISMEMBER", "few") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SADD", "str", "noot", "mies") c.Error("wrong kind", "SMEMBERS", "str") c.Error("wrong kind", "SISMEMBER", "str", "noot") c.Error("wrong kind", "SMISMEMBER", "str", "noot") c.Error("wrong kind", "SCARD", "str") }) testRESP3(t, func(c *client) { c.Do("SMEMBERS", "q") c.Do("SADD", "q", "aap") c.Do("SMEMBERS", "q") c.Do("SISMEMBER", "q", "aap") c.Do("SISMEMBER", "q", "noot") c.Do("SMISMEMBER", "q", "aap", "noot", "nosuch") }) } func TestSetMove(t *testing.T) { skip(t) // Move a set around testRaw(t, func(c *client) { c.Do("SADD", "s", "aap", "noot", "mies") c.Do("RENAME", "s", "others") c.DoSorted("SMEMBERS", "s") c.DoSorted("SMEMBERS", "others") c.Do("MOVE", "others", "2") c.DoSorted("SMEMBERS", "others") c.Do("SELECT", "2") c.DoSorted("SMEMBERS", "others") }) } func TestSetDel(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s", "aap", "noot", "mies") c.Do("SREM", "s", "noot", "nosuch") c.Do("SCARD", "s") c.DoSorted("SMEMBERS", "s") // failure cases c.Error("wrong number", "SREM") c.Error("wrong number", "SREM", "s") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SREM", "str", "noot") }) } func TestSetSMove(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s", "aap", "noot", "mies") c.Do("SMOVE", "s", "s2", "aap") c.Do("SCARD", "s") c.Do("SCARD", "s2") c.Do("SMOVE", "s", "s2", "nosuch") c.Do("SCARD", "s") c.Do("SCARD", "s2") c.Do("SMOVE", "s", "nosuch", "noot") c.Do("SCARD", "s") c.Do("SCARD", "s2") c.Do("SMOVE", "s", "s2", "mies") c.Do("SCARD", "s") c.Do("EXISTS", "s") c.Do("SCARD", "s2") c.Do("EXISTS", "s2") c.Do("SMOVE", "s2", "s2", "mies") c.Do("SADD", "s5", "aap") c.Do("SADD", "s6", "aap") c.Do("SMOVE", "s5", "s6", "aap") // failure cases c.Error("wrong number", "SMOVE") c.Error("wrong number", "SMOVE", "s") c.Error("wrong number", "SMOVE", "s", "s2") c.Error("wrong number", "SMOVE", "s", "s2", "too", "many") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SMOVE", "str", "s2", "noot") c.Error("wrong kind", "SMOVE", "s2", "str", "noot") }) } func TestSetSpop(t *testing.T) { skip(t) t.Run("without count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("SADD", "s", "aap") c.Do("SPOP", "s") c.Do("EXISTS", "s") c.Do("SPOP", "nosuch") c.Do("SADD", "s", "aap") c.Do("SADD", "s", "noot") c.Do("SADD", "s", "mies") c.Do("SADD", "s", "noot") c.Do("SCARD", "s") c.DoLoosely("SMEMBERS", "s") c.Do("SPOP", "s", "0") // failure cases c.Error("wrong number", "SPOP") c.Do("SADD", "s", "aap") c.Error("out of range", "SPOP", "s", "s2") c.Error("out of range", "SPOP", "s", "-1") c.Error("out of range", "SPOP", "nosuch", "s2") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SPOP", "str") }) }) t.Run("with count", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("SADD", "s", "aap") c.Do("SADD", "s", "noot") c.Do("SADD", "s", "mies") c.Do("SADD", "s", "vuur") c.DoLoosely("SPOP", "s", "2") c.Do("EXISTS", "s") c.Do("SCARD", "s") c.DoLoosely("SPOP", "s", "200") c.Do("SPOP", "s", "1") c.Do("SPOP", "s", "0") c.Do("SCARD", "s") c.Do("SPOP", "nosuch", "1") c.Do("SPOP", "nosuch", "0") // failure cases c.Error("out of range", "SPOP", "foo", "one") c.Error("out of range", "SPOP", "foo", "-4") }) }) } func TestSetSrandmember(t *testing.T) { skip(t) testRaw(t, func(c *client) { // Set with a single member... c.Do("SADD", "s", "aap") c.Do("SRANDMEMBER", "s") c.Do("SRANDMEMBER", "s", "1") c.Do("SRANDMEMBER", "s", "5") c.Do("SRANDMEMBER", "s", "-1") c.Do("SRANDMEMBER", "s", "-5") c.Do("SRANDMEMBER", "s", "0") c.Do("SRANDMEMBER", "nosuch") c.Do("SRANDMEMBER", "nosuch", "1") // failure cases c.Error("wrong number", "SRANDMEMBER") c.Error("not an integer", "SRANDMEMBER", "s", "noint") c.Error("syntax error", "SRANDMEMBER", "s", "1", "toomany") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SRANDMEMBER", "str") }) testRESP3(t, func(c *client) { c.Do("SADD", "q", "aap") c.Do("SRANDMEMBER", "q") c.Do("SRANDMEMBER", "q", "1") c.Do("SRANDMEMBER", "q", "0") }) } func TestSetSdiff(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s1", "aap", "noot", "mies") c.Do("SADD", "s2", "noot", "mies", "vuur") c.Do("SADD", "s3", "mies", "wim") c.DoSorted("SDIFF", "s1") c.DoSorted("SDIFF", "s1", "s2") c.DoSorted("SDIFF", "s1", "s2", "s3") c.Do("SDIFF", "nosuch") c.Do("SDIFF", "s1", "nosuch", "s2", "nosuch", "s3") c.Do("SDIFF", "s1", "s1") c.Do("SDIFFSTORE", "res", "s3", "nosuch", "s1") c.Do("SMEMBERS", "res") // failure cases c.Error("wrong number", "SDIFF") c.Error("wrong number", "SDIFFSTORE") c.Error("wrong number", "SDIFFSTORE", "key") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SDIFF", "s1", "str") c.Error("wrong kind", "SDIFF", "nosuch", "str") c.Error("wrong kind", "SDIFF", "str", "s1") c.Error("wrong kind", "SDIFFSTORE", "res", "str", "s1") c.Error("wrong kind", "SDIFFSTORE", "res", "s1", "str") }) testRESP3(t, func(c *client) { c.Do("SADD", "s1", "aap", "noot", "mies") c.Do("SADD", "s2", "noot", "mies", "vuur") c.DoSorted("SDIFF", "s1") c.DoSorted("SDIFF", "s1", "s2") c.Do("SDIFFSTORE", "res", "s1", "s2") c.Do("SMEMBERS", "res") }) } func TestSetSinter(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s1", "aap", "noot", "mies") c.Do("SADD", "s2", "noot", "mies", "vuur") c.Do("SADD", "s3", "mies", "wim") c.DoSorted("SINTER", "s1") c.DoSorted("SINTER", "s1", "s2") c.DoSorted("SINTER", "s1", "s2", "s3") c.Do("SINTER", "nosuch") c.Do("SINTER", "s1", "nosuch", "s2", "nosuch", "s3") c.DoSorted("SINTER", "s1", "s1") c.Do("SINTERSTORE", "res", "s3", "nosuch", "s1") c.Do("SMEMBERS", "res") // failure cases c.Error("wrong number", "SINTER") c.Error("wrong number", "SINTERSTORE") c.Error("wrong number", "SINTERSTORE", "key") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SINTER", "s1", "str") c.Error("wrong kind", "SINTER", "nosuch", "str") c.Error("wrong kind", "SINTER", "str", "nosuch") c.Error("wrong kind", "SINTER", "str", "s1") c.Error("wrong kind", "SINTERSTORE", "res", "str", "s1") c.Error("wrong kind", "SINTERSTORE", "res", "s1", "str") }) } func TestSetSintercard(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SADD", "s1", "aap", "noot", "mies") c.Do("SADD", "s2", "noot", "mies", "vuur") c.Do("SADD", "s3", "mies", "wim") c.Do("SINTERCARD", "1", "s1") c.Do("SINTERCARD", "2", "s1", "s2") c.Do("SINTERCARD", "3", "s1", "s2", "s3") c.Do("SINTERCARD", "1", "nosuch") c.Do("SINTERCARD", "5", "s1", "nosuch", "s2", "nosuch", "s3") c.Do("SINTERCARD", "2", "s1", "s1") c.Do("SINTERCARD", "1", "s1", "LIMIT", "1") c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "0") c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "1") c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "2") c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "3") // failure cases c.Error("wrong number", "SINTERCARD") c.Error("wrong number", "SINTERCARD", "0") c.Error("wrong number", "SINTERCARD", "") c.Error("wrong number", "SINTERCARD", "s1") c.Error("greater than 0", "SINTERCARD", "s1", "s2") c.Error("greater than 0", "SINTERCARD", "-2", "s1", "s2") c.Error("greater than 0", "SINTERCARD", "-1", "s1", "s2") c.Error("greater than 0", "SINTERCARD", "0", "s1", "s2") c.Error("syntax error", "SINTERCARD", "1", "s1", "s2") c.Error("can't be greater", "SINTERCARD", "3", "s1", "s2") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SINTERCARD", "2", "s1", "str") c.Error("wrong kind", "SINTERCARD", "2", "nosuch", "str") c.Error("wrong kind", "SINTERCARD", "2", "str", "nosuch") c.Error("wrong kind", "SINTERCARD", "2", "str", "s1") c.Error("can't be negative", "SINTERCARD", "2", "s1", "s2", "LIMIT", "-1") }) } func TestSetSunion(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SUNION", "s1", "aap", "noot", "mies") c.Do("SUNION", "s2", "noot", "mies", "vuur") c.Do("SUNION", "s3", "mies", "wim") c.Do("SUNION", "s1") c.Do("SUNION", "s1", "s2") c.Do("SUNION", "s1", "s2", "s3") c.Do("SUNION", "nosuch") c.Do("SUNION", "s1", "nosuch", "s2", "nosuch", "s3") c.Do("SUNION", "s1", "s1") c.Do("SUNIONSTORE", "res", "s3", "nosuch", "s1") c.Do("SMEMBERS", "res") // failure cases c.Error("wrong number", "SUNION") c.Error("wrong number", "SUNIONSTORE") c.Error("wrong number", "SUNIONSTORE", "key") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SUNION", "s1", "str") c.Error("wrong kind", "SUNION", "nosuch", "str") c.Error("wrong kind", "SUNION", "str", "s1") c.Error("wrong kind", "SUNIONSTORE", "res", "str", "s1") c.Error("wrong kind", "SUNIONSTORE", "res", "s1", "str") }) } func TestSscan(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No set yet c.Do("SSCAN", "set", "0") c.Do("SADD", "set", "key1") c.Do("SSCAN", "set", "0") c.Do("SSCAN", "set", "0", "COUNT", "12") c.Do("SSCAN", "set", "0", "cOuNt", "12") c.Do("SADD", "set", "anotherkey") c.Do("SSCAN", "set", "0", "MATCH", "anoth*") c.Do("SSCAN", "set", "0", "MATCH", "anoth*", "COUNT", "100") c.Do("SSCAN", "set", "0", "COUNT", "100", "MATCH", "anoth*") // c.DoLoosely("SSCAN", "set", "0", "COUNT", "1") // cursor differs // unstable test c.DoLoosely("SSCAN", "set", "0", "COUNT", "2") // cursor differs // Can't really test multiple keys. // c.Do("SET", "key2", "value2") // c.Do("SCAN", "0") // Error cases c.Error("wrong number", "SSCAN") c.Error("wrong number", "SSCAN", "noint") c.Error("not an integer", "SSCAN", "set", "0", "COUNT", "noint") c.Error("syntax error", "SSCAN", "set", "0", "COUNT", "0") c.Error("syntax error", "SSCAN", "set", "0", "COUNT") c.Error("syntax error", "SSCAN", "set", "0", "MATCH") c.Error("syntax error", "SSCAN", "set", "0", "garbage") c.Error("syntax error", "SSCAN", "set", "0", "COUNT", "12", "MATCH", "foo", "garbage") c.Do("SET", "str", "1") c.Error("wrong kind", "SSCAN", "str", "0") }) } func TestSetNoAuth(t *testing.T) { skip(t) testAuth(t, "supersecret", func(c *client) { c.Error("Authentication required", "SET", "foo", "bar") c.Do("AUTH", "supersecret") c.Do("SET", "foo", "bar") }, ) } ================================================ FILE: miniredis/tests/integration-go/sorted_set_test.go ================================================ package main import ( "testing" ) func TestSortedSet(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies") c.Do("ZADD", "z", "1", "vuur", "4", "noot") c.Do("TYPE", "z") c.Do("EXISTS", "z") c.Do("ZCARD", "z") c.Do("ZRANK", "z", "aap") c.Do("ZRANK", "z", "noot") c.Do("ZRANK", "z", "mies") c.Do("ZRANK", "z", "vuur") c.Do("ZRANK", "z", "nosuch") c.Do("ZRANK", "z", "vuur", "WITHSCORE") c.Do("ZRANK", "nosuch", "nosuch") c.Do("ZRANK", "z", "nosuch", "WITHSCORE") c.Do("ZRANK", "nosuch", "nosuch", "WITHSCORE") c.Do("ZREVRANK", "z", "aap") c.Do("ZREVRANK", "z", "noot") c.Do("ZREVRANK", "z", "mies") c.Do("ZREVRANK", "z", "vuur") c.Do("ZREVRANK", "z", "nosuch") c.Do("ZREVRANK", "nosuch", "nosuch") c.Do("ZREVRANK", "z", "noot", "WITHSCORE") c.Do("ZREVRANK", "nosuch", "nosuch", "WITHSCORE") c.Do("ZADD", "zi", "inf", "aap", "-inf", "noot", "+inf", "mies") c.Do("ZRANK", "zi", "noot") // Double key c.Do("ZADD", "zz", "1", "aap", "2", "aap") c.Do("ZCARD", "zz") c.Do("ZPOPMAX", "zz", "2") c.Do("ZPOPMAX", "zz") c.Error("out of range", "ZPOPMAX", "zz", "-100") c.Do("ZPOPMAX", "nosuch", "1") c.Do("ZPOPMAX", "zz", "0") c.Do("ZPOPMAX", "zz", "100") c.Do("ZPOPMIN", "zz", "2") c.Do("ZPOPMIN", "zz") c.Error("out of range", "ZPOPMIN", "zz", "-100") c.Do("ZPOPMIN", "nosuch", "1") c.Do("ZPOPMIN", "zz", "0") c.Do("ZPOPMIN", "zz", "100") // failure cases c.Do("SET", "str", "I am a string") c.Error("wrong number", "ZADD") c.Error("wrong number", "ZADD", "s") c.Error("wrong number", "ZADD", "s", "1") c.Error("syntax error", "ZADD", "s", "1", "aap", "1") c.Error("not a valid float", "ZADD", "s", "nofloat", "aap") c.Error("wrong kind", "ZADD", "str", "1", "aap") c.Error("wrong number", "ZCARD") c.Error("wrong number", "ZCARD", "too", "many") c.Error("wrong kind", "ZCARD", "str") c.Error("wrong number", "ZRANK") c.Error("wrong number", "ZRANK", "key") c.Error("syntax error", "ZRANK", "key", "too", "many") c.Error("wrong kind", "ZRANK", "str", "member") c.Error("wrong number", "ZREVRANK") c.Error("wrong number", "ZREVRANK", "key") c.Error("wrong number", "ZPOPMAX") c.Error("out of range", "ZPOPMAX", "set", "noint") c.Error("syntax error", "ZPOPMAX", "set", "1", "toomany") c.Error("wrong number", "ZPOPMIN") c.Error("out of range", "ZPOPMIN", "set", "noint") c.Error("syntax error", "ZPOPMIN", "set", "1", "toomany") c.Error("syntax error", "ZRANK", "z", "nosuch", "WITHSCORES") c.Error("syntax error", "ZREVRANK", "z", "nosuch", "WITHSCORES") c.Do("RENAME", "z", "z2") c.Do("EXISTS", "z") c.Do("EXISTS", "z2") c.Do("MOVE", "z2", "3") c.Do("EXISTS", "z2") c.Do("SELECT", "3") c.Do("EXISTS", "z2") c.Do("DEL", "z2") c.Do("EXISTS", "z2") }) testRaw(t, func(c *client) { c.Do("ZADD", "z", "0", "new\nline\n") c.Do("ZADD", "z", "0", "line") c.Do("ZADD", "z", "0", "another\nnew\nline\n") c.Do("ZSCAN", "z", "0", "MATCH", "*") c.Do("ZRANGEBYLEX", "z", "[a", "[z") c.Do("ZRANGE", "z", "0", "-1", "WITHSCORES") }) testRaw(t, func(c *client) { // very small values c.Do("ZADD", "a_zset", "1.2", "one") c.Do("ZADD", "a_zset", "incr", "1.2", "one") c.DoRounded(1, "ZADD", "a_zset", "incr", "1.2", "one") // real: 3.5999999999999996, mini: 3.6 c.Do("ZADD", "a_zset", "incr", "1.2", "one") }) } func TestSortedSetAdd(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", ) c.Do("ZADD", "z", "NX", "1.1", "aap", "3", "mies", ) c.Do("ZADD", "z", "XX", "1.2", "aap", "4", "vuur", ) c.Do("ZADD", "z", "CH", "1.2", "aap", "4.1", "vuur", "5", "roos", ) c.Do("ZADD", "z", "CH", "XX", "1.2", "aap", "4.2", "vuur", "5", "roos", "5", "zand", ) c.Do("ZADD", "z", "XX", "XX", "XX", "XX", "1.2", "aap", ) c.Do("ZADD", "z", "NX", "NX", "NX", "NX", "1.2", "aap", ) c.Error("not compatible", "ZADD", "z", "XX", "NX", "1.1", "foo") c.Error("wrong number", "ZADD", "z", "XX") c.Error("wrong number", "ZADD", "z", "NX") c.Error("wrong number", "ZADD", "z", "CH") c.Error("wrong number", "ZADD", "z", "??") c.Error("syntax error", "ZADD", "z", "1.2", "aap", "XX") c.Error("syntax error", "ZADD", "z", "1.2", "aap", "CH") c.Error("wrong number", "ZADD", "z") }) testRaw(t, func(c *client) { c.Do("ZADD", "z", "INCR", "1", "aap") c.Do("ZADD", "z", "INCR", "1", "aap") c.Do("ZADD", "z", "INCR", "1", "aap") c.Do("ZADD", "z", "INCR", "-12", "aap") c.Do("ZADD", "z", "INCR", "INCR", "-12", "aap") c.Do("ZADD", "z", "CH", "INCR", "-12", "aap") // 'CH' is ignored c.Do("ZADD", "z", "INCR", "CH", "-12", "aap") // 'CH' is ignored c.Do("ZADD", "z", "INCR", "NX", "12", "aap") c.Do("ZADD", "z", "INCR", "XX", "12", "aap") c.Do("ZADD", "q", "INCR", "NX", "12", "aap") c.Do("ZADD", "q", "INCR", "XX", "12", "aap") c.Error("INCR option", "ZADD", "z", "INCR", "1", "aap", "2", "tiger") c.Error("syntax error", "ZADD", "z", "INCR", "-12") c.Error("syntax error", "ZADD", "z", "INCR", "-12", "aap", "NX") }) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "score") c.Do("ZADD", "z", "GT", "2", "score") c.Do("ZADD", "z", "LT", "1", "score") c.Error("ERR GT, LT, and/or NX options at the same time are not compatible", "ZADD", "z", "GT", "LT", "1", "score") }) testRESP3(t, func(c *client) { c.Do("ZADD", "z", "INCR", "1", "aap") }) } func TestSortedSetRange(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", "+Inf", "more stars", "-Inf", "big bang", ) c.Do("ZADD", "zs", "5", "berlin", "5", "lisbon", "5", "manila", "5", "budapest", "5", "london", "5", "singapore", "5", "amsterdam", ) t.Run("plain", func(t *testing.T) { c.Do("ZRANGE", "z", "0", "-1") c.Do("ZRANGE", "z", "0", "10", "WITHSCORES", "WITHSCORES") c.Do("ZRANGE", "z", "0", "-1", "WiThScOrEs") c.Do("ZRANGE", "z", "0", "10") c.Do("ZRANGE", "z", "0", "2") c.Do("ZRANGE", "z", "2", "20") c.Do("ZRANGE", "z", "0", "-4") c.Do("ZRANGE", "z", "2", "-4") c.Do("ZRANGE", "z", "400", "-1") c.Do("ZRANGE", "z", "300", "-110") c.Do("ZRANGE", "z", "0", "-1", "REV") c.Error("not an integer", "ZRANGE", "z", "(0", "-1") c.Error("not an integer", "ZRANGE", "z", "0", "(-1") c.Error("combination", "ZRANGE", "z", "0", "-1", "LIMIT", "1", "2") }) t.Run("byscore", func(t *testing.T) { c.Do("ZRANGE", "z", "0", "-1", "BYSCORE") c.Do("ZRANGE", "z", "0", "1000", "BYSCORE") c.Do("ZRANGE", "z", "1", "2", "BYSCORE") c.Do("ZRANGE", "z", "1", "(2", "BYSCORE") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "REV") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "0", "1") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "LIMIT", "0", "-1") c.Do("ZRANGE", "z", "-inf", "+inf", "BYSCORE", "REV", "LIMIT", "0", "1") c.Error("not a float", "ZRANGE", "z", "[1", "2", "BYSCORE") }) t.Run("bylex", func(t *testing.T) { c.Do("ZRANGE", "zs", "[be", "(ma", "BYLEX") c.Do("ZRANGE", "zs", "[be", "+", "BYLEX") c.Do("ZRANGE", "zs", "-", "(ma", "BYLEX") c.Do("ZRANGE", "zs", "-", "+", "BYLEX") c.Do("ZRANGE", "zs", "[be", "(ma", "BYLEX", "REV") c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "0", "1") c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "3") c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "-1") c.Do("ZRANGE", "zs", "-", "+", "BYLEX", "LIMIT", "1", "-1", "REV") c.Error("syntax error", "ZRANGE", "z", "[be", "[ma", "BYSCORE", "BYLEX") c.Error("range item", "ZRANGE", "z", "be", "(ma", "BYLEX") c.Error("range item", "ZRANGE", "z", "(be", "ma", "BYLEX") }) c.Do("ZADD", "zz", "0", "aap", "0", "Aap", "0", "AAP", "0", "aAP", "0", "aAp", ) c.Do("ZRANGE", "zz", "0", "-1") // failure cases c.Error("wrong number", "ZRANGE") c.Error("wrong number", "ZRANGE", "foo") c.Error("wrong number", "ZRANGE", "foo", "1") c.Error("syntax error", "ZRANGE", "foo", "2", "3", "toomany") c.Error("syntax error", "ZRANGE", "foo", "2", "3", "WITHSCORES", "toomany") c.Error("not an integer", "ZRANGE", "foo", "noint", "3") c.Error("not an integer", "ZRANGE", "foo", "2", "noint") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZRANGE", "str", "300", "-110") }) } func TestSortedSetRevRange(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", "+Inf", "more stars", "-Inf", "big bang", ) c.Do("ZREVRANGE", "z", "0", "-1") c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES") c.Do("ZREVRANGE", "z", "0", "-1", "WITHSCORES", "WITHSCORES", "WITHSCORES") c.Do("ZREVRANGE", "z", "0", "-1", "WiThScOrEs") c.Do("ZREVRANGE", "z", "0", "-2") c.Do("ZREVRANGE", "z", "0", "-1000") c.Do("ZREVRANGE", "z", "2", "-2") c.Do("ZREVRANGE", "z", "400", "-1") c.Do("ZREVRANGE", "z", "300", "-110") c.Error("syntax", "ZREVRANGE", "z", "300", "-110", "REV") // failure cases c.Error("wrong number", "ZREVRANGE") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREVRANGE", "str", "300", "-110") }) } func TestSortedSetRem(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", "+Inf", "more stars", "-Inf", "big bang", ) c.Do("ZREM", "z", "nosuch") c.Do("ZREM", "z", "mies", "nootagain") c.Do("ZRANGE", "z", "0", "-1") // failure cases c.Error("wrong number", "ZREM") c.Error("wrong number", "ZREM", "foo") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREM", "str", "member") }) } func TestSortedSetRemRangeByLex(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "12", "zero kelvin", "12", "minusfour", "12", "one", "12", "oneone", "12", "two", "12", "zwei", "12", "three", "12", "drei", "12", "inf", ) c.Do("ZRANGEBYLEX", "z", "-", "+") c.Do("ZREMRANGEBYLEX", "z", "[o", "(t") c.Do("ZRANGEBYLEX", "z", "-", "+") c.Do("ZREMRANGEBYLEX", "z", "-", "+") c.Do("ZRANGEBYLEX", "z", "-", "+") // failure cases c.Error("wrong number", "ZREMRANGEBYLEX") c.Error("wrong number", "ZREMRANGEBYLEX", "key") c.Error("wrong number", "ZREMRANGEBYLEX", "key", "[a") c.Error("wrong number", "ZREMRANGEBYLEX", "key", "[a", "[b", "c") c.Error("not valid string range", "ZREMRANGEBYLEX", "key", "!a", "[b") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREMRANGEBYLEX", "str", "[a", "[b") }) } func TestSortedSetRemRangeByRank(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "12", "zero kelvin", "12", "minusfour", "12", "one", "12", "oneone", "12", "two", "12", "zwei", "12", "three", "12", "drei", "12", "inf", ) c.Do("ZREMRANGEBYRANK", "z", "-2", "-1") c.Do("ZRANGE", "z", "0", "-1") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf") c.Do("ZREMRANGEBYRANK", "z", "-2", "-1") c.Do("ZRANGE", "z", "0", "-1") c.Do("ZREMRANGEBYRANK", "z", "0", "-1") c.Do("EXISTS", "z") c.Do("ZREMRANGEBYRANK", "nosuch", "-2", "-1") // failure cases c.Error("wrong number", "ZREMRANGEBYRANK") c.Error("wrong number", "ZREMRANGEBYRANK", "key") c.Error("wrong number", "ZREMRANGEBYRANK", "key", "0") c.Error("not an integer", "ZREMRANGEBYRANK", "key", "noint", "-1") c.Error("not an integer", "ZREMRANGEBYRANK", "key", "0", "noint") c.Error("wrong number", "ZREMRANGEBYRANK", "key", "0", "1", "too many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREMRANGEBYRANK", "str", "0", "-1") }) } func TestSortedSetRemRangeByScore(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", "+Inf", "more stars", "-Inf", "big bang", ) c.Do("ZREMRANGEBYSCORE", "z", "-inf", "(2") c.Do("ZRANGE", "z", "0", "-1") c.Do("ZREMRANGEBYSCORE", "z", "(1000", "(2000") c.Do("ZRANGE", "z", "0", "-1") c.Do("ZREMRANGEBYSCORE", "z", "-inf", "+inf") c.Do("EXISTS", "z") c.Do("ZREMRANGEBYSCORE", "nosuch", "-inf", "inf") // failure cases c.Error("wrong number", "ZREMRANGEBYSCORE") c.Error("wrong number", "ZREMRANGEBYSCORE", "key") c.Error("wrong number", "ZREMRANGEBYSCORE", "key", "0") c.Error("not a float", "ZREMRANGEBYSCORE", "key", "noint", "-1") c.Error("not a float", "ZREMRANGEBYSCORE", "key", "0", "noint") c.Error("wrong number", "ZREMRANGEBYSCORE", "key", "0", "1", "too many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZREMRANGEBYSCORE", "str", "0", "-1") }) } func TestSortedSetScore(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", ) c.Do("ZSCORE", "z", "mies") c.Do("ZSCORE", "z", "the stars") c.Do("ZSCORE", "z", "nosuch") c.Do("ZSCORE", "nosuch", "nosuch") // failure cases c.Error("wrong number", "ZSCORE") c.Error("wrong number", "ZSCORE", "foo") c.Error("wrong number", "ZSCORE", "foo", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZSCORE", "str", "member") }) } func TestSortedSetRangeByScore(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "1", "aap", "2", "noot", "3", "mies", "2", "nootagain", "3", "miesagain", "+Inf", "the stars", "+Inf", "more stars", "-Inf", "big bang", ) c.Do("ZRANGEBYSCORE", "z", "-inf", "inf") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "1", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "-1", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "1", "-2") c.Do("ZREVRANGEBYSCORE", "z", "inf", "-inf") c.Do("ZREVRANGEBYSCORE", "z", "inf", "-inf", "LIMIT", "1", "2") c.Do("ZREVRANGEBYSCORE", "z", "inf", "-inf", "LIMIT", "-1", "2") c.Do("ZREVRANGEBYSCORE", "z", "inf", "-inf", "LIMIT", "1", "-2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "WITHSCORES") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "WiThScOrEs") c.Do("ZREVRANGEBYSCORE", "z", "-inf", "inf", "WITHSCORES", "LIMIT", "1", "2") c.Do("ZRANGEBYSCORE", "z", "0", "3") c.Do("ZRANGEBYSCORE", "z", "0", "inf") c.Do("ZRANGEBYSCORE", "z", "(1", "3") c.Do("ZRANGEBYSCORE", "z", "(1", "(3") c.Do("ZRANGEBYSCORE", "z", "1", "(3") c.Do("ZRANGEBYSCORE", "z", "1", "(3", "LIMIT", "0", "2") c.Do("ZRANGEBYSCORE", "foo", "2", "3", "LIMIT", "1", "2", "WITHSCORES") c.Do("ZCOUNT", "z", "-inf", "inf") c.Do("ZCOUNT", "z", "0", "3") c.Do("ZCOUNT", "z", "0", "inf") c.Do("ZCOUNT", "z", "(2", "inf") // Bunch of limit edge cases c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "0", "7") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "0", "8") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "0", "9") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "7", "0") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "7", "1") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "7", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "8", "0") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "8", "1") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "8", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "9", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "-1", "2") c.Do("ZRANGEBYSCORE", "z", "-inf", "inf", "LIMIT", "-1", "-1") // failure cases c.Error("wrong number", "ZRANGEBYSCORE") c.Error("wrong number", "ZRANGEBYSCORE", "foo") c.Error("wrong number", "ZRANGEBYSCORE", "foo", "1") c.Error("syntax error", "ZRANGEBYSCORE", "foo", "2", "3", "toomany") c.Error("syntax error", "ZRANGEBYSCORE", "foo", "2", "3", "WITHSCORES", "toomany") c.Error("not an integer", "ZRANGEBYSCORE", "foo", "2", "3", "LIMIT", "noint", "1") c.Error("not an integer", "ZRANGEBYSCORE", "foo", "2", "3", "LIMIT", "1", "noint") c.Error("syntax error", "ZREVRANGEBYSCORE", "z", "-inf", "inf", "WITHSCORES", "LIMIT", "1", "-2", "toomany") c.Error("not a float", "ZRANGEBYSCORE", "foo", "noint", "3") c.Error("not a float", "ZRANGEBYSCORE", "foo", "[4", "3") c.Error("not a float", "ZRANGEBYSCORE", "foo", "2", "noint") c.Error("not a float", "ZRANGEBYSCORE", "foo", "4", "[3") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZRANGEBYSCORE", "str", "300", "-110") c.Error("wrong number", "ZREVRANGEBYSCORE") c.Error("not a float", "ZREVRANGEBYSCORE", "foo", "[4", "3") c.Error("wrong kind", "ZREVRANGEBYSCORE", "str", "300", "-110") c.Error("wrong number", "ZCOUNT") c.Error("not a float", "ZCOUNT", "foo", "[4", "3") c.Error("wrong kind", "ZCOUNT", "str", "300", "-110") }) // Issue #10 testRaw(t, func(c *client) { c.Do("ZADD", "key", "3.3", "element") c.Do("ZRANGEBYSCORE", "key", "3.3", "3.3") c.Do("ZRANGEBYSCORE", "key", "4.3", "4.3") c.Do("ZREVRANGEBYSCORE", "key", "3.3", "3.3") c.Do("ZREVRANGEBYSCORE", "key", "4.3", "4.3") }) } func TestSortedSetRangeByLex(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "z", "12", "zero kelvin", "12", "minusfour", "12", "one", "12", "oneone", "12", "two", "12", "zwei", "12", "three", "12", "drei", "12", "inf", ) c.Do("ZRANGEBYLEX", "z", "-", "+") c.Do("ZREVRANGEBYLEX", "z", "+", "-") c.Do("ZLEXCOUNT", "z", "-", "+") c.Do("ZRANGEBYLEX", "z", "[o", "[three") c.Do("ZREVRANGEBYLEX", "z", "[three", "[o") c.Do("ZLEXCOUNT", "z", "[o", "[three") c.Do("ZRANGEBYLEX", "z", "(o", "(z") c.Do("ZREVRANGEBYLEX", "z", "(z", "(o") c.Do("ZLEXCOUNT", "z", "(o", "(z") c.Do("ZRANGEBYLEX", "z", "+", "(z") c.Do("ZREVRANGEBYLEX", "z", "(z", "+") c.Do("ZRANGEBYLEX", "z", "(a", "-") c.Do("ZREVRANGEBYLEX", "z", "-", "(a") c.Do("ZRANGEBYLEX", "z", "(z", "(a") c.Do("ZREVRANGEBYLEX", "z", "(a", "(z") c.Do("ZRANGEBYLEX", "nosuch", "-", "+") c.Do("ZREVRANGEBYLEX", "nosuch", "+", "-") c.Do("ZLEXCOUNT", "nosuch", "-", "+") c.Do("ZRANGEBYLEX", "z", "-", "+", "LIMIT", "1", "2") c.Do("ZREVRANGEBYLEX", "z", "+", "-", "LIMIT", "1", "2") c.Do("ZRANGEBYLEX", "z", "-", "+", "LIMIT", "-1", "2") c.Do("ZREVRANGEBYLEX", "z", "+", "-", "LIMIT", "-1", "2") c.Do("ZRANGEBYLEX", "z", "-", "+", "LIMIT", "1", "-2") c.Do("ZREVRANGEBYLEX", "z", "+", "-", "LIMIT", "1", "-2") c.Do("ZADD", "z", "12", "z") c.Do("ZADD", "z", "12", "zz") c.Do("ZADD", "z", "12", "zzz") c.Do("ZADD", "z", "12", "zzzz") c.Do("ZRANGEBYLEX", "z", "[z", "+") c.Do("ZREVRANGEBYLEX", "z", "+", "[z") c.Do("ZRANGEBYLEX", "z", "(z", "+") c.Do("ZREVRANGEBYLEX", "z", "+", "(z") c.Do("ZLEXCOUNT", "z", "(z", "+") // failure cases c.Error("wrong number", "ZRANGEBYLEX") c.Error("wrong number", "ZREVRANGEBYLEX") c.Error("wrong number", "ZRANGEBYLEX", "key") c.Error("wrong number", "ZRANGEBYLEX", "key", "[a") c.Error("syntax error", "ZRANGEBYLEX", "key", "[a", "[b", "c") c.Error("not valid string range", "ZRANGEBYLEX", "key", "!a", "[b") c.Error("not valid string range", "ZRANGEBYLEX", "key", "[a", "!b") c.Error("not valid string range", "ZRANGEBYLEX", "key", "[a", "b]") c.Error("not valid string range item", "ZRANGEBYLEX", "key", "[a", "") c.Error("not valid string range item", "ZRANGEBYLEX", "key", "", "[b") c.Error("syntax error", "ZRANGEBYLEX", "key", "[a", "[b", "LIMIT") c.Error("syntax error", "ZRANGEBYLEX", "key", "[a", "[b", "LIMIT", "1") c.Error("not an integer", "ZRANGEBYLEX", "key", "[a", "[b", "LIMIT", "a", "1") c.Error("not an integer", "ZRANGEBYLEX", "key", "[a", "[b", "LIMIT", "1", "a") c.Error("syntax error", "ZRANGEBYLEX", "key", "[a", "[b", "LIMIT", "1", "1", "toomany") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZRANGEBYLEX", "str", "[a", "[b") c.Error("wrong number", "ZLEXCOUNT") c.Error("wrong number", "ZLEXCOUNT", "key") c.Error("wrong number", "ZLEXCOUNT", "key", "[a") c.Error("wrong number", "ZLEXCOUNT", "key", "[a", "[b", "c") c.Error("not valid string range", "ZLEXCOUNT", "key", "!a", "[b") c.Error("wrong kind", "ZLEXCOUNT", "str", "[a", "[b") }) testRaw(t, func(c *client) { c.Do("ZADD", "idx", "0", "ccc") c.Do("ZRANGEBYLEX", "idx", "[d", "[e") c.Do("ZRANGEBYLEX", "idx", "[c", "[d") }) } func TestSortedSetIncyby(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZINCRBY", "z", "1.0", "m") c.Do("ZINCRBY", "z", "1.0", "m") c.Do("ZINCRBY", "z", "1.0", "m") c.Do("ZINCRBY", "z", "2.0", "m") c.Do("ZINCRBY", "z", "3", "m2") c.Do("ZINCRBY", "z", "3", "m2") c.Do("ZINCRBY", "z", "3", "m2") // failure cases c.Error("wrong number", "ZINCRBY") c.Error("wrong number", "ZINCRBY", "key") c.Error("wrong number", "ZINCRBY", "key", "1.0") c.Error("not a valid float", "ZINCRBY", "key", "nofloat", "m") c.Error("wrong number", "ZINCRBY", "key", "1.0", "too", "many") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZINCRBY", "str", "1.0", "member") }) } func TestZscan(t *testing.T) { skip(t) testRaw(t, func(c *client) { // No set yet c.Do("ZSCAN", "h", "0") c.Do("ZADD", "h", "1.0", "key1") c.Do("ZSCAN", "h", "0") c.Do("ZSCAN", "h", "0", "COUNT", "12") c.Do("ZSCAN", "h", "0", "cOuNt", "12") // ZSCAN may return a higher count of items than requested (See https://redis.io/docs/manual/keyspace/), so we must query all items. c.Do("ZSCAN", "h", "0", "COUNT", "10") // cursor differs c.Do("ZADD", "h", "2.0", "anotherkey") c.Do("ZSCAN", "h", "0", "MATCH", "anoth*") c.Do("ZSCAN", "h", "0", "MATCH", "anoth*", "COUNT", "100") c.Do("ZSCAN", "h", "0", "COUNT", "100", "MATCH", "anoth*") // Can't really test multiple keys. // c.Do("SET", "key2", "value2") // c.Do("SCAN", "0") // Error cases c.Error("wrong number", "ZSCAN") c.Error("wrong number", "ZSCAN", "noint") c.Error("not an integer", "ZSCAN", "h", "0", "COUNT", "noint") c.Error("syntax error", "ZSCAN", "h", "0", "COUNT") c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "0") c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "-1") c.Error("syntax error", "ZSCAN", "h", "0", "MATCH") c.Error("syntax error", "ZSCAN", "h", "0", "garbage") c.Error("syntax error", "ZSCAN", "h", "0", "COUNT", "12", "MATCH", "foo", "garbage") // c.Do("ZSCAN", "nosuch", "0", "COUNT", "garbage") c.Do("SET", "str", "1") c.Error("wrong kind", "ZSCAN", "str", "0") }) } func TestZunion(t *testing.T) { skip(t) testRaw(t, func(c *client) { // example from the docs https://redis.io/commands/ZUNION c.Do("ZADD", "zset1", "1", "one") c.Do("ZADD", "zset1", "2", "two") c.Do("ZADD", "zset2", "1", "one") c.Do("ZADD", "zset2", "2", "two") c.Do("ZADD", "zset2", "3", "three") c.Do("ZUNION", "2", "zset1", "zset2") c.Do("ZUNION", "2", "zset1", "zset2", "WITHSCORES") }) testRaw(t, func(c *client) { c.Do("ZADD", "h1", "1.0", "key1") c.Do("ZADD", "h1", "2.0", "key2") c.Do("ZADD", "h2", "1.0", "key1") c.Do("ZADD", "h2", "4.0", "key2") c.Do("ZUNION", "2", "h1", "h2", "WITHSCORES") c.Do("ZUNION", "2", "h1", "h2", "WEIGHTS", "2.0", "12", "WITHSCORES") c.Do("ZUNION", "2", "h1", "h2", "WEIGHTS", "2", "-12", "WITHSCORES") c.Do("ZUNION", "2", "h1", "h2", "AGGREGATE", "min", "WITHSCORES") c.Do("ZUNION", "2", "h1", "h2", "AGGREGATE", "max", "WITHSCORES") c.Do("ZUNION", "2", "h1", "h2", "AGGREGATE", "sum", "WITHSCORES") // Error cases c.Error("wrong number", "ZUNION") c.Error("wrong number", "ZUNION", "noint") c.Error("at least 1", "ZUNION", "0", "f") c.Error("syntax error", "ZUNION", "2", "f") c.Error("at least 1", "ZUNION", "-1", "f") c.Error("syntax error", "ZUNION", "2", "f1", "f2", "f3") c.Error("syntax error", "ZUNION", "2", "f1", "f2", "WEIGHTS") c.Error("syntax error", "ZUNION", "2", "f1", "f2", "WEIGHTS", "1") c.Error("syntax error", "ZUNION", "2", "f1", "f2", "WEIGHTS", "1", "2", "3") c.Error("not a float", "ZUNION", "2", "f1", "f2", "WEIGHTS", "f", "2") c.Error("syntax error", "ZUNION", "2", "f1", "f2", "AGGREGATE", "foo") c.Do("SET", "str", "1") c.Error("wrong kind", "ZUNION", "1", "str") }) // not a sorted set, still fine testRaw(t, func(c *client) { c.Do("SADD", "super", "1", "2", "3") c.Do("SADD", "exclude", "3") c.Do("ZUNION", "2", "super", "exclude", "weights", "1", "0", "aggregate", "min", "withscores") }) } func TestZunionstore(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "h1", "1.0", "key1") c.Do("ZADD", "h1", "2.0", "key2") c.Do("ZADD", "h2", "1.0", "key1") c.Do("ZADD", "h2", "4.0", "key2") c.Do("ZUNIONSTORE", "res", "2", "h1", "h2") c.Do("ZRANGE", "res", "0", "-1", "WITHSCORES") c.Do("ZUNIONSTORE", "weighted", "2", "h1", "h2", "WEIGHTS", "2.0", "12") c.Do("ZRANGE", "weighted", "0", "-1", "WITHSCORES") c.Do("ZUNIONSTORE", "weighted2", "2", "h1", "h2", "WEIGHTS", "2", "-12") c.Do("ZRANGE", "weighted2", "0", "-1", "WITHSCORES") c.Do("ZUNIONSTORE", "amin", "2", "h1", "h2", "AGGREGATE", "min") c.Do("ZRANGE", "amin", "0", "-1", "WITHSCORES") c.Do("ZUNIONSTORE", "amax", "2", "h1", "h2", "AGGREGATE", "max") c.Do("ZRANGE", "amax", "0", "-1", "WITHSCORES") c.Do("ZUNIONSTORE", "asum", "2", "h1", "h2", "AGGREGATE", "sum") c.Do("ZRANGE", "asum", "0", "-1", "WITHSCORES") // Error cases c.Error("wrong number", "ZUNIONSTORE") c.Error("wrong number", "ZUNIONSTORE", "h") c.Error("wrong number", "ZUNIONSTORE", "h", "noint") c.Error("at least 1", "ZUNIONSTORE", "h", "0", "f") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f") c.Error("at least 1", "ZUNIONSTORE", "h", "-1", "f") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f1", "f2", "f3") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f1", "f2", "WEIGHTS") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f1", "f2", "WEIGHTS", "1") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f1", "f2", "WEIGHTS", "1", "2", "3") c.Error("not a float", "ZUNIONSTORE", "h", "2", "f1", "f2", "WEIGHTS", "f", "2") c.Error("syntax error", "ZUNIONSTORE", "h", "2", "f1", "f2", "AGGREGATE", "foo") c.Do("SET", "str", "1") c.Error("wrong kind", "ZUNIONSTORE", "h", "1", "str") }) // overwrite testRaw(t, func(c *client) { c.Do("ZADD", "h1", "1.0", "key1") c.Do("ZADD", "h1", "2.0", "key2") c.Do("ZADD", "h2", "1.0", "key1") c.Do("ZADD", "h2", "4.0", "key2") c.Do("SET", "str", "1") c.Do("ZUNIONSTORE", "str", "2", "h1", "h2") c.Do("TYPE", "str") c.Do("ZUNIONSTORE", "h2", "2", "h1", "h2") c.Do("ZRANGE", "h2", "0", "-1", "WITHSCORES") c.Do("TYPE", "h1") c.Do("TYPE", "h2") }) // not a sorted set, still fine testRaw(t, func(c *client) { c.Do("SADD", "super", "1", "2", "3") c.Do("SADD", "exclude", "3") c.Do("ZUNIONSTORE", "tmp", "2", "super", "exclude", "weights", "1", "0", "aggregate", "min") c.Do("ZRANGE", "tmp", "0", "-1", "withscores") }) } func TestZinter(t *testing.T) { skip(t) // ZINTER testRaw(t, func(c *client) { c.Do("ZADD", "h1", "1.0", "key1") c.Do("ZADD", "h1", "2.0", "key2") c.Do("ZADD", "h1", "3.0", "key3") c.Do("ZADD", "h2", "1.0", "key1") c.Do("ZADD", "h2", "4.0", "key2") c.Do("ZADD", "h3", "4.0", "key4") c.DoSorted("ZINTER", "2", "h1", "h2") c.DoSorted("ZINTER", "2", "h1", "h2", "WEIGHTS", "2.0", "12") c.DoSorted("ZINTER", "2", "h1", "h2", "WEIGHTS", "2", "-12") c.DoSorted("ZINTER", "2", "h1", "h2", "AGGREGATE", "min") c.DoSorted("ZINTER", "2", "h1", "h2", "AGGREGATE", "max") c.DoSorted("ZINTER", "2", "h1", "h2", "AGGREGATE", "sum") // normal set c.Do("ZADD", "q1", "2", "f1") c.Do("SADD", "q2", "f1") c.Do("ZINTER", "2", "q1", "q2") c.DoSorted("ZINTER", "2", "q1", "q2", "WITHSCORES") // Error cases c.Error("wrong number", "ZINTER") c.Error("wrong number", "ZINTER", "noint") c.Error("at least 1", "ZINTER", "0", "f") c.Error("syntax error", "ZINTER", "2", "f") c.Error("at least 1", "ZINTER", "-1", "f") c.Error("syntax error", "ZINTER", "2", "f1", "f2", "f3") c.Error("syntax error", "ZINTER", "2", "f1", "f2", "WEIGHTS") c.Error("syntax error", "ZINTER", "2", "f1", "f2", "WEIGHTS", "1") c.Error("syntax error", "ZINTER", "2", "f1", "f2", "WEIGHTS", "1", "2", "3") c.Error("not a float", "ZINTER", "2", "f1", "f2", "WEIGHTS", "f", "2") c.Error("syntax error", "ZINTER", "2", "f1", "f2", "AGGREGATE", "foo") c.Do("SET", "str", "1") c.Error("wrong kind", "ZINTER", "1", "str") }) // ZINTERSTORE testRaw(t, func(c *client) { c.Do("ZADD", "h1", "1.0", "key1") c.Do("ZADD", "h1", "2.0", "key2") c.Do("ZADD", "h1", "3.0", "key3") c.Do("ZADD", "h2", "1.0", "key1") c.Do("ZADD", "h2", "4.0", "key2") c.Do("ZADD", "h3", "4.0", "key4") c.Do("ZINTERSTORE", "res", "2", "h1", "h2") c.Do("ZRANGE", "res", "0", "-1", "WITHSCORES") c.Do("ZINTERSTORE", "weighted", "2", "h1", "h2", "WEIGHTS", "2.0", "12") c.Do("ZRANGE", "weighted", "0", "-1", "WITHSCORES") c.Do("ZINTERSTORE", "weighted2", "2", "h1", "h2", "WEIGHTS", "2", "-12") c.Do("ZRANGE", "weighted2", "0", "-1", "WITHSCORES") c.Do("ZINTERSTORE", "amin", "2", "h1", "h2", "AGGREGATE", "min") c.Do("ZRANGE", "amin", "0", "-1", "WITHSCORES") c.Do("ZINTERSTORE", "amax", "2", "h1", "h2", "AGGREGATE", "max") c.Do("ZRANGE", "amax", "0", "-1", "WITHSCORES") c.Do("ZINTERSTORE", "asum", "2", "h1", "h2", "AGGREGATE", "sum") c.Do("ZRANGE", "asum", "0", "-1", "WITHSCORES") // normal set c.Do("ZADD", "q1", "2", "f1") c.Do("SADD", "q2", "f1") c.Do("ZINTERSTORE", "dest", "2", "q1", "q2") c.Do("ZRANGE", "dest", "0", "-1", "withscores") // store into self c.Do("ZINTERSTORE", "q1", "2", "q1", "q2") c.Do("ZRANGE", "q1", "0", "-1", "withscores") c.Do("SMEMBERS", "q2") // Error cases c.Error("wrong number", "ZINTERSTORE") c.Error("wrong number", "ZINTERSTORE", "h") c.Error("wrong number", "ZINTERSTORE", "h", "noint") c.Error("at least 1", "ZINTERSTORE", "h", "0", "f") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f") c.Error("at least 1", "ZINTERSTORE", "h", "-1", "f") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f1", "f2", "f3") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f1", "f2", "WEIGHTS") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f1", "f2", "WEIGHTS", "1") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f1", "f2", "WEIGHTS", "1", "2", "3") c.Error("not a float", "ZINTERSTORE", "h", "2", "f1", "f2", "WEIGHTS", "f", "2") c.Error("syntax error", "ZINTERSTORE", "h", "2", "f1", "f2", "AGGREGATE", "foo") c.Do("SET", "str", "1") c.Error("wrong kind", "ZINTERSTORE", "h", "1", "str") }) } func TestZpopminmax(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "set:zpop", "1.0", "key1") c.Do("ZADD", "set:zpop", "2.0", "key2") c.Do("ZADD", "set:zpop", "3.0", "key3") c.Do("ZADD", "set:zpop", "4.0", "key4") c.Do("ZADD", "set:zpop", "5.0", "key5") c.Do("ZCARD", "set:zpop") c.Do("ZSCORE", "set:zpop", "key1") c.Do("ZSCORE", "set:zpop", "key5") c.Do("ZPOPMIN", "set:zpop") c.Do("ZPOPMIN", "set:zpop", "2") c.Do("ZPOPMIN", "set:zpop", "100") c.Error("out of range", "ZPOPMIN", "set:zpop", "-100") c.Do("ZPOPMAX", "set:zpop") c.Do("ZPOPMAX", "set:zpop", "2") c.Do("ZPOPMAX", "set:zpop", "100") c.Error("out of range", "ZPOPMAX", "set:zpop", "-100") c.Do("ZPOPMAX", "nosuch", "1") // Wrong args c.Error("wrong number", "ZPOPMIN") c.Error("out of range", "ZPOPMIN", "set:zpop", "h1") c.Error("syntax error", "ZPOPMIN", "set:zpop", "1", "h2") }) } func TestZrandmember(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "q", "1.0", "key1") c.Do("ZADD", "q", "2.0", "key2") c.Do("ZADD", "q", "3.0", "key3") c.Do("ZADD", "q", "4.0", "key4") c.Do("ZADD", "q", "5.0", "key5") c.Do("ZCARD", "q") c.DoLoosely("ZRANDMEMBER", "q") c.DoLoosely("ZRANDMEMBER", "q", "3") c.DoLoosely("ZRANDMEMBER", "q", "4") c.DoLoosely("ZRANDMEMBER", "q", "5") c.DoLoosely("ZRANDMEMBER", "q", "6") c.DoLoosely("ZRANDMEMBER", "q", "7") c.DoLoosely("ZRANDMEMBER", "q", "12") c.Do("ZRANDMEMBER", "q", "0") c.DoLoosely("ZRANDMEMBER", "q", "-3") c.DoLoosely("ZRANDMEMBER", "q", "-4") c.DoLoosely("ZRANDMEMBER", "q", "-5") c.DoLoosely("ZRANDMEMBER", "q", "-6") c.DoLoosely("ZRANDMEMBER", "q", "-7") c.DoLoosely("ZRANDMEMBER", "q", "-12") c.Do("ZRANDMEMBER", "nosuch") c.Do("ZRANDMEMBER", "nosuch", "4") c.Do("ZRANDMEMBER", "nosuch", "-4") c.DoLoosely("ZRANDMEMBER", "q", "2", "WITHSCORES") c.DoLoosely("ZRANDMEMBER", "q", "0", "WITHSCORES") c.DoLoosely("ZRANDMEMBER", "q", "-2", "WITHSCORES") c.DoLoosely("ZRANDMEMBER", "nosuch", "2", "WITHSCORES") c.DoLoosely("ZRANDMEMBER", "nosuch", "-2", "WITHSCORES") // Wrong args c.Error("wrong number", "ZRANDMEMBER") c.Do("SET", "str", "1") c.Error("wrong kind", "ZRANDMEMBER", "str") c.Error("not an integer", "ZRANDMEMBER", "q", "two") }) } func TestZMScore(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("ZADD", "q", "1.0", "key1") c.Do("ZADD", "q", "2.0", "key2") c.Do("ZADD", "q", "3.0", "key3") c.Do("ZADD", "q", "4.0", "key4") c.Do("ZADD", "q", "5.0", "key5") c.Do("ZMSCORE", "q", "key1") c.Do("ZMSCORE", "q", "key1 key2 key3") c.Do("ZMSCORE", "q", "nosuch") c.Do("ZMSCORE", "nosuch", "key1") c.Do("ZMSCORE", "nosuch", "key1", "key2") // failure cases c.Error("wrong number", "ZMSCORE", "q") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "ZMSCORE", "str", "key1") }) } ================================================ FILE: miniredis/tests/integration-go/stream_test.go ================================================ package main import ( "sync" "testing" "time" ) func TestStream(t *testing.T) { skip(t) t.Run("XADD", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "planets", "0-1", "name", "Mercury", ) c.DoLoosely("XADD", "planets", "*", "name", "Venus", ) c.Do("XADD", "planets", "18446744073709551000-0", "name", "Earth", ) c.Do("XADD", "planets", "18446744073709551000-*", "name", "Pluto", ) c.Do("XADD", "reallynosuchkey", "NOMKSTREAM", "*", "name", "Earth", ) c.Error("ID specified", "XADD", "planets", "18446744073709551000-0", // <-- duplicate "name", "Earth", ) c.Do("XLEN", "planets") c.Do("RENAME", "planets", "planets2") c.Do("DEL", "planets2") c.Do("XLEN", "planets") // error cases c.Error("wrong number", "XADD", "planets", "1000", "name", "Mercury", "ignored", // <-- not an even number of keys ) c.Error("ID specified", "XADD", "newplanets", "0", // <-- invalid key "foo", "bar", ) c.Error("wrong number", "XADD", "newplanets", "123-123") // no args c.Error("stream ID", "XADD", "newplanets", "123-bar", "foo", "bar") c.Error("stream ID", "XADD", "newplanets", "bar-123", "foo", "bar") c.Error("stream ID", "XADD", "newplanets", "123-123-123", "foo", "bar") c.Do("SET", "str", "I am a string") // c.Do("XADD", "str", "1000", "foo", "bar") // c.Do("XADD", "str", "invalid-key", "foo", "bar") c.Error("wrong number", "XADD", "planets") c.Error("wrong number", "XADD") }) testRaw(t, func(c *client) { c.Do("XADD", "planets", "MAXLEN", "4", "456-1", "name", "Mercury") c.Do("XADD", "planets", "MAXLEN", "4", "456-2", "name", "Mercury") c.Do("XADD", "planets", "MAXLEN", "4", "456-3", "name", "Mercury") c.Do("XADD", "planets", "MAXLEN", "4", "456-4", "name", "Mercury") c.Do("XADD", "planets", "MAXLEN", "4", "456-5", "name", "Mercury") c.Do("XADD", "planets", "MAXLEN", "4", "456-6", "name", "Mercury") c.Do("XLEN", "planets") c.Do("XADD", "planets", "MAXLEN", "~", "4", "456-7", "name", "Mercury") c.Error("not an integer", "XADD", "planets", "MAXLEN", "!", "4", "*", "name", "Mercury") c.Error("not an integer", "XADD", "planets", "MAXLEN", " ~", "4", "*", "name", "Mercury") c.Error("MAXLEN argument", "XADD", "planets", "MAXLEN", "-4", "*", "name", "Mercury") c.Error("not an integer", "XADD", "planets", "MAXLEN", "", "*", "name", "Mercury") c.Error("not an integer", "XADD", "planets", "MAXLEN", "!", "four", "*", "name", "Mercury") c.Error("not an integer", "XADD", "planets", "MAXLEN", "~", "four") c.Error("wrong number", "XADD", "planets", "MAXLEN", "~") c.Error("wrong number", "XADD", "planets", "MAXLEN") c.Do("XADD", "planets", "MAXLEN", "0", "456-8", "name", "Mercury") c.Do("XLEN", "planets") c.Do("SET", "str", "I am a string") c.Error("not an integer", "XADD", "str", "MAXLEN", "four", "*", "foo", "bar") }) testRaw(t, func(c *client) { c.Do("XADD", "planets", "MINID", "450", "450-0", "name", "Venus") c.Do("XADD", "planets", "MINID", "450", "450-1", "name", "Venus") c.Do("XADD", "planets", "MINID", "450", "456-1", "name", "Mercury") c.Do("XADD", "planets", "MINID", "450", "456-2", "name", "Mercury") c.Do("XADD", "planets", "MINID", "450", "456-3", "name", "Mercury") c.Do("XADD", "planets", "MINID", "450", "456-4", "name", "Mercury") c.Do("XADD", "planets", "MINID", "450", "456-5", "name", "Mercury") c.Do("XADD", "planets", "MINID", "450", "456-6", "name", "Mercury") c.Do("XADD", "planets", "MINID", "~", "450", "456-7", "name", "Mercury") c.Do("XLEN", "planets") c.Error("equal or smaller than the target", "XADD", "planets", "MINID", "450", "449-0", "name", "Earth") c.Error("equal or smaller than the target", "XADD", "planets", "MINID", "450", "450", "name", "Earth") c.Error("wrong number", "XADD", "planets", "MINID", "~") c.Error("wrong number", "XADD", "planets", "MINID") c.Error("wrong number", "XADD", "planets", "MINID", "100") c.Do("SET", "str", "I am a string") c.Error("key holding the wrong kind of value", "XADD", "str", "MINID", "400", "*", "foo", "bar") }) }) t.Run("transactions", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("MULTI") c.Do("XADD", "planets", "0-1", "name", "Mercury") c.Do("EXEC") c.Do("MULTI") c.Error("wrong number", "XADD", "newplanets", "123-123") // no args c.Error("discarded", "EXEC") c.Do("MULTI") c.Do("XADD", "planets", "foo-bar", "name", "Mercury") c.Do("EXEC") c.Do("MULTI") c.Do("XADD", "planets", "MAXLEN", "four", "*", "name", "Mercury") c.Do("EXEC") c.Do("MULTI") c.Do("XADD", "reallynosuchkey", "NOMKSTREAM", "MAXLEN", "four", "*", "name", "Mercury") c.Do("EXEC") }) }) t.Run("XDEL", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XDEL", "newplanets", "123-123") c.Do("XADD", "newplanets", "123-123", "foo", "bar") c.Do("XADD", "newplanets", "123-124", "baz", "bak") c.Do("XADD", "newplanets", "123-125", "bal", "bag") c.Do("XDEL", "newplanets", "123-123", "123-125", "123-123") c.Do("XREAD", "STREAMS", "newplanets", "0") c.Do("XDEL", "newplanets", "123-123") c.Do("XREAD", "STREAMS", "newplanets", "0") c.Do("XDEL", "notexisting", "123-123") c.Do("XREAD", "STREAMS", "newplanets", "0") c.Do("XADD", "gaps", "400-400", "foo", "bar") c.Do("XADD", "gaps", "400-600", "foo", "bar") c.Do("XDEL", "gaps", "400-500") c.Do("XREAD", "STREAMS", "newplanets", "0") // errors c.Do("XADD", "existing", "123-123", "foo", "bar") c.Error("wrong number", "XDEL") // no key c.Error("wrong number", "XDEL", "existing") // no id c.Error("Invalid stream ID", "XDEL", "existing", "aa-bb") c.Do("XDEL", "notexisting", "aa-bb") // invalid id c.Do("MULTI") c.Do("XDEL", "existing", "aa-bb") c.Do("EXEC") }) }) t.Run("FLUSHALL", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "planets", "0-1", "name", "Mercury") c.Do("XGROUP", "CREATE", "planets", "universe", "$") c.Do("FLUSHALL") c.Do("XREAD", "STREAMS", "planets", "0") c.Error("consumer group", "XREADGROUP", "GROUP", "universe", "alice", "STREAMS", "planets", ">") }) }) t.Run("XINFO", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "planets", "0-1", "name", "Mercury") // c.DoLoosely("XINFO", "STREAM", "planets") c.Error("unknown subcommand", "XINFO", "STREAMMM") c.Error("no such key", "XINFO", "STREAM", "foo") c.Error("wrong number", "XINFO") c.Do("SET", "scalar", "foo") c.Error("wrong kind", "XINFO", "STREAM", "scalar") c.Error("no such key", "XINFO", "GROUPS", "foo") c.Do("XINFO", "GROUPS", "planets") c.Error("no such key", "XINFO", "CONSUMERS", "foo", "bar") }) }) t.Run("XREAD", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "ordplanets", "0-1", "name", "Mercury", "greek-god", "Hermes", ) c.Do("XADD", "ordplanets", "1-0", "name", "Venus", "greek-god", "Aphrodite", ) c.Do("XADD", "ordplanets", "2-1", "greek-god", "", "name", "Earth", ) c.Do("XADD", "ordplanets", "3-0", "name", "Mars", "greek-god", "Ares", ) c.Do("XADD", "ordplanets", "4-1", "greek-god", "Dias", "name", "Jupiter", ) c.Do("XADD", "ordplanets2", "0-1", "name", "Mercury", "greek-god", "Hermes", "idx", "1") c.Do("XADD", "ordplanets2", "1-0", "name", "Venus", "greek-god", "Aphrodite", "idx", "2") c.Do("XADD", "ordplanets2", "2-1", "name", "Earth", "greek-god", "", "idx", "3") c.Do("XADD", "ordplanets2", "3-0", "greek-god", "Ares", "name", "Mars", "idx", "4") c.Do("XADD", "ordplanets2", "4-1", "name", "Jupiter", "greek-god", "Dias", "idx", "5") c.Do("XREAD", "STREAMS", "ordplanets", "0") c.Do("XREAD", "STREAMS", "ordplanets", "2") c.Do("XREAD", "STREAMS", "ordplanets", "ordplanets2", "0", "0") c.Do("XREAD", "STREAMS", "ordplanets", "ordplanets2", "2", "0") c.Do("XREAD", "STREAMS", "ordplanets", "ordplanets2", "0", "2") c.Do("XREAD", "STREAMS", "ordplanets", "ordplanets2", "1", "3") c.Do("XREAD", "STREAMS", "ordplanets", "ordplanets2", "0", "999") c.Do("XREAD", "COUNT", "1", "STREAMS", "ordplanets", "ordplanets2", "0", "0") // failure cases c.Error("wrong number", "XREAD") c.Error("wrong number", "XREAD", "STREAMS") c.Error("wrong number", "XREAD", "STREAMS", "foo") c.Do("XREAD", "STREAMS", "foo", "0") c.Error("wrong number", "XREAD", "STREAMS", "ordplanets") c.Error("Unbalanced 'xread'", "XREAD", "STREAMS", "ordplanets", "foo", "0") c.Error("wrong number", "XREAD", "COUNT") c.Error("wrong number", "XREAD", "COUNT", "notint") c.Error("wrong number", "XREAD", "COUNT", "10") // No streams c.Error("stream ID", "XREAD", "STREAMS", "foo", "notint") }) testRaw2(t, func(c, c2 *client) { c.Do("XADD", "pl", "55-88", "name", "Mercury") // something is available: doesn't block c.Do("XREAD", "BLOCK", "10", "STREAMS", "pl", "0") c.Do("XREAD", "BLOCK", "0", "STREAMS", "pl", "0") // blocks var wg sync.WaitGroup wg.Add(1) go func() { c.Do("XREAD", "BLOCK", "1000", "STREAMS", "pl", "60") wg.Done() }() time.Sleep(10 * time.Millisecond) c2.Do("XADD", "pl", "60-1", "name", "Mercury") wg.Wait() // timeout c.Do("XREAD", "BLOCK", "10", "STREAMS", "pl", "70") c.Error("not an int", "XREAD", "BLOCK", "foo", "STREAMS", "pl", "0") c.Error("negative", "XREAD", "BLOCK", "-12", "STREAMS", "pl", "0") }) // special '$' ID testRaw2(t, func(c, c2 *client) { var wg sync.WaitGroup wg.Add(1) go func() { time.Sleep(10 * time.Millisecond) c2.Do("XADD", "pl", "60-1", "name", "Mercury") wg.Done() }() wg.Wait() c.Do("XREAD", "BLOCK", "1000", "STREAMS", "pl", "$") }) // special '$' ID on non-existing stream testRaw2(t, func(c, c2 *client) { var wg sync.WaitGroup wg.Add(1) go func() { time.Sleep(10 * time.Millisecond) c2.Do("XADD", "pl", "60-1", "nosuch", "Mercury") wg.Done() }() wg.Wait() c.Do("XREAD", "BLOCK", "1000", "STREAMS", "nosuch", "$") }) }) } func TestStreamRange(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("XADD", "ordplanets", "0-1", "name", "Mercury", "greek-god", "Hermes", ) c.Do("XADD", "ordplanets", "1-0", "name", "Venus", "greek-god", "Aphrodite", ) c.Do("XADD", "ordplanets", "2-1", "greek-god", "", "name", "Earth", ) c.Do("XADD", "ordplanets", "3-0", "name", "Mars", "greek-god", "Ares", ) c.Do("XADD", "ordplanets", "4-1", "greek-god", "Dias", "name", "Jupiter", ) c.Do("XRANGE", "ordplanets", "-", "+") c.Do("XRANGE", "ordplanets", "+", "-") c.Do("XRANGE", "ordplanets", "-", "99") c.Do("XRANGE", "ordplanets", "0", "4") c.Do("XRANGE", "ordplanets", "(0", "4") c.Do("XRANGE", "ordplanets", "0", "(4") c.Do("XRANGE", "ordplanets", "(0", "(4") c.Do("XRANGE", "ordplanets", "2", "2") c.Do("XRANGE", "ordplanets", "2-0", "2-1") c.Do("XRANGE", "ordplanets", "2-1", "2-1") c.Do("XRANGE", "ordplanets", "2-1", "2-2") c.Do("XRANGE", "ordplanets", "0", "1-0") c.Do("XRANGE", "ordplanets", "0", "1-99") c.Do("XRANGE", "ordplanets", "0", "2", "COUNT", "1") c.Do("XRANGE", "ordplanets", "1-42", "3-42", "COUNT", "1") c.Do("XREVRANGE", "ordplanets", "+", "-") c.Do("XREVRANGE", "ordplanets", "-", "+") c.Do("XREVRANGE", "ordplanets", "4", "0") c.Do("XREVRANGE", "ordplanets", "(4", "0") c.Do("XREVRANGE", "ordplanets", "4", "(0") c.Do("XREVRANGE", "ordplanets", "(4", "(0") c.Do("XREVRANGE", "ordplanets", "2", "2") c.Do("XREVRANGE", "ordplanets", "2-1", "2-0") c.Do("XREVRANGE", "ordplanets", "2-1", "2-1") c.Do("XREVRANGE", "ordplanets", "2-2", "2-1") c.Do("XREVRANGE", "ordplanets", "1-0", "0") c.Do("XREVRANGE", "ordplanets", "3-42", "1-0", "COUNT", "2") c.Do("DEL", "ordplanets") // failure cases c.Error("wrong number", "XRANGE") c.Error("wrong number", "XRANGE", "foo") c.Error("wrong number", "XRANGE", "foo", "1") c.Error("syntax error", "XRANGE", "foo", "2", "3", "toomany") c.Error("not an integer", "XRANGE", "foo", "2", "3", "COUNT", "noint") c.Error("syntax error", "XRANGE", "foo", "2", "3", "COUNT", "1", "toomany") c.Error("stream ID", "XRANGE", "foo", "-", "noint") c.Error("stream ID", "XRANGE", "foo", "(-", "+") c.Error("stream ID", "XRANGE", "foo", "-", "(+") c.Do("SET", "str", "I am a string") c.Error("wrong kind", "XRANGE", "str", "-", "+") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("XADD", "ordplanets", "0-1", "name", "Mercury", "greek-god", "Hermes", ) c.Do("XLEN", "ordplanets") c.Do("XRANGE", "ordplanets", "+", "-") c.Do("XRANGE", "ordplanets", "+", "-", "COUNT", "FOOBAR") c.Do("EXEC") c.Do("XLEN", "ordplanets") c.Do("MULTI") c.Do("XRANGE", "ordplanets", "+", "foo") c.Do("EXEC") c.Do("MULTI") c.Error("wrong number", "XRANGE", "ordplanets", "+") c.Error("discarded", "EXEC") c.Do("MULTI") c.Do("XADD", "ordplanets", "123123-123", "name", "Mercury") c.Do("XDEL", "ordplanets", "123123-123") c.Do("XADD", "ordplanets", "invalid", "name", "Mercury") c.Do("EXEC") c.Do("XLEN", "ordplanets") }) } func TestStreamGroup(t *testing.T) { skip(t) t.Run("XGROUP", func(t *testing.T) { testRaw(t, func(c *client) { c.Error("to exist", "XGROUP", "CREATE", "planets", "processing", "$") c.Do("XADD", "planets", "123-500", "foo", "bar") c.Do("XGROUP", "CREATE", "planets", "processing", "$") c.DoLoosely("XINFO", "GROUPS", "planets") // lag is wrong c.Error("already exist", "XGROUP", "CREATE", "planets", "processing", "$") c.Error("to exist", "XGROUP", "DESTROY", "foo", "bar") c.Do("XGROUP", "DESTROY", "planets", "bar") c.Error("No such consumer group", "XGROUP", "DELCONSUMER", "planets", "foo", "bar") c.Do("XGROUP", "CREATECONSUMER", "planets", "processing", "alice") c.DoLoosely("XINFO", "GROUPS", "planets") // lag is wrong c.Do("XGROUP", "DELCONSUMER", "planets", "processing", "foo") c.Do("XGROUP", "DELCONSUMER", "planets", "processing", "alice") c.Do("XINFO", "CONSUMERS", "planets", "processing") c.Do("XGROUP", "DESTROY", "planets", "processing") c.Do("XINFO", "GROUPS", "planets") c.Error("wrong number of arguments", "XGROUP") c.Error("unknown subcommand 'foo'", "XGROUP", "foo") }) }) t.Run("XREADGROUP", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") // succNoResultCheck("XINFO", "STREAM", "planets"), c.Do("XADD", "planets", "42-1", "name", "Mercury") c.Do("XADD", "planets", "42-2", "name", "Neptune") c.Do("XLEN", "planets") c.Do("XREADGROUP", "GROUP", "processing", "alice", "STREAMS", "planets", ">") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "999", "STREAMS", "planets", ">") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "0", "STREAMS", "planets", ">") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "-1", "STREAMS", "planets", ">") c.Do("XACK", "planets", "processing", "42-1") c.Do("XDEL", "planets", "42-1") c.Do("XGROUP", "CREATE", "planets", "newcons", "$", "MKSTREAM") c.Do("XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", ">") c.Do("XADD", "planets", "42-3", "name", "Venus") c.Do("XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", "42-1") c.Do("XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", "42-9") c.Error("stream ID", "XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", "foo") // NOACK { c.Do("XGROUP", "CREATE", "colors", "pr", "$", "MKSTREAM") c.Do("XADD", "colors", "42-2", "name", "Green") c.Do("XREADGROUP", "GROUP", "pr", "alice", "NOACK", "STREAMS", "colors", ">") c.Do("XREADGROUP", "GROUP", "pr", "alice", "NOACK", "STREAMS", "colors", "0") c.Do("XACK", "colors", "p", "42-2") } // errors c.Error("wrong number", "XREADGROUP") c.Error("wrong number", "XREADGROUP", "GROUP") c.Error("wrong number", "XREADGROUP", "foo") c.Error("wrong number", "XREADGROUP", "GROUP", "foo") c.Error("wrong number", "XREADGROUP", "GROUP", "foo", "bar") c.Error("wrong number", "XREADGROUP", "GROUP", "foo", "bar", "ZTREAMZ") c.Error("wrong number", "XREADGROUP", "GROUP", "foo", "bar", "STREAMS", "foo") c.Error("Unbalanced", "XREADGROUP", "GROUP", "foo", "bar", "STREAMS", "foo", "bar", ">") c.Error("syntax error", "XREADGROUP", "_____", "foo", "bar", "STREAMS", "foo", ">") c.Error("consumer group", "XREADGROUP", "GROUP", "nosuch", "alice", "STREAMS", "planets", ">") c.Error("consumer group", "XREADGROUP", "GROUP", "processing", "alice", "STREAMS", "nosuchplanets", ">") c.Do("SET", "scalar", "bar") c.Error("wrong kind", "XGROUP", "CREATE", "scalar", "processing", "$", "MKSTREAM") c.Error("BUSYGROUP", "XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") }) testRaw2(t, func(c, c2 *client) { c.Do("XGROUP", "CREATE", "pl", "processing", "$", "MKSTREAM") c.Do("XADD", "pl", "55-88", "name", "Mercury") // something is available: doesn't block c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "10", "STREAMS", "pl", ">") // c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "0", "STREAMS", "pl", ">") // blocks { var wg sync.WaitGroup wg.Add(1) go func() { c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "999999", "STREAMS", "pl", ">") wg.Done() }() time.Sleep(50 * time.Millisecond) c2.Do("XADD", "pl", "60-1", "name", "Mercury") wg.Wait() } // timeout { c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "10", "STREAMS", "pl", ">") } // block is ignored if id isn't ">" { c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "9999999999", "STREAMS", "pl", "8") } // block is ignored if _any_ id isn't ">" { c.Do("XGROUP", "CREATE", "pl2", "processing", "$", "MKSTREAM") c.Do("XREADGROUP", "GROUP", "processing", "foo", "BLOCK", "9999999999", "STREAMS", "pl", "pl2", "8", ">") } c.Error("not an int", "XREADGROUP", "GROUP", "foo", "bar", "BLOCK", "foo", "STREAMS", "foo", ">") c.Error("No such", "XREADGROUP", "GROUP", "foo", "bar", "BLOCK", "999999", "STREAMS", "pl", "invalid") c.Error("negative", "XREADGROUP", "GROUP", "foo", "bar", "BLOCK", "-1", "STREAMS", "foo", ">") }) }) t.Run("XACK", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") c.Do("XADD", "planets", "4000-1", "name", "Mercury") c.Do("XADD", "planets", "4000-2", "name", "Venus") c.Do("XADD", "planets", "4000-3", "name", "not Pluto") c.Do("XADD", "planets", "4000-4", "name", "Mars") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XACK", "planets", "processing", "4000-2", "4000-3") c.Do("XACK", "planets", "processing", "4000-4") c.Do("XACK", "planets", "processing", "2000-1") c.Do("XACK", "nosuch", "processing", "0-1") c.Do("XACK", "planets", "nosuch", "0-1") // error cases c.Error("wrong number", "XACK") c.Error("wrong number", "XACK", "planets") c.Error("wrong number", "XACK", "planets", "processing") c.Error("Invalid stream", "XACK", "planets", "processing", "invalid") c.Do("SET", "scalar", "bar") c.Error("wrong kind", "XACK", "scalar", "processing", "123-456") }) }) t.Run("XPENDING", func(t *testing.T) { // summary mode testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") c.Do("XADD", "planets", "4000-1", "name", "Mercury") c.Do("XADD", "planets", "4000-2", "name", "Venus") c.Do("XADD", "planets", "4000-3", "name", "not Pluto") c.Do("XADD", "planets", "4000-4", "name", "Mars") c.Do("XREADGROUP", "GROUP", "processing", "alice", "STREAMS", "planets", ">") c.Do("XPENDING", "planets", "processing") c.Do("XACK", "planets", "processing", "4000-4") c.Do("XPENDING", "planets", "processing") c.Do("XACK", "planets", "processing", "4000-1") c.Do("XACK", "planets", "processing", "4000-2") c.Do("XACK", "planets", "processing", "4000-3") c.Do("XPENDING", "planets", "processing") // more consumers c.Do("XADD", "planets", "4000-5", "name", "Earth") c.Do("XADD", "planets", "4000-6", "name", "Neptune") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XREADGROUP", "GROUP", "processing", "bob", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XPENDING", "planets", "processing") // no entries doesn't show up in pending c.Do("XREADGROUP", "GROUP", "processing", "eve", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XPENDING", "planets", "processing") c.Do("XGROUP", "DELCONSUMER", "planets", "processing", "alice") c.Do("XPENDING", "planets", "processing") c.Do("XGROUP", "CREATE", "empty", "empty", "$", "MKSTREAM") c.Do("XPENDING", "empty", "empty", "-", "+", "999") c.Error("consumer group", "XPENDING", "foo", "processing") c.Error("consumer group", "XPENDING", "planets", "foo") // error cases c.Error("wrong number", "XPENDING") c.Error("wrong number", "XPENDING", "planets") c.Error("syntax", "XPENDING", "planets", "processing", "too many") c.Error("syntax", "XPENDING", "planets", "processing", "IDLE", "10") }) // full mode testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") c.Do("XADD", "planets", "4000-1", "name", "Mercury") c.Do("XADD", "planets", "4000-2", "name", "Venus") c.Do("XADD", "planets", "4000-3", "name", "not Pluto") c.Do("XADD", "planets", "4000-4", "name", "Mars") c.Do("XREADGROUP", "GROUP", "processing", "alice", "STREAMS", "planets", ">") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "999") c.DoLoosely("XPENDING", "planets", "processing", "4000-2", "+", "999") c.DoLoosely("XPENDING", "planets", "processing", "-", "4000-3", "999") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "1") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "0") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "-1") c.DoLoosely("XPENDING", "planets", "processing", "IDLE", "10", "-", "+", "999") c.Do("XADD", "planets", "4000-5", "name", "Earth") c.Do("XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", ">") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "999") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "999", "bob") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "999", "eve") c.DoLoosely("XPENDING", "planets", "processing", "IDLE", "10", "-", "+", "999", "eve") // update delivery counts (which we can't test thanks to the time field) c.Do("XREADGROUP", "GROUP", "processing", "bob", "STREAMS", "planets", "99") c.DoLoosely("XPENDING", "planets", "processing", "-", "+", "999", "bob") c.Error("Invalid", "XPENDING", "planets", "processing", "foo", "+", "999") c.Error("Invalid", "XPENDING", "planets", "processing", "-", "foo", "999") c.Error("not an integer", "XPENDING", "planets", "processing", "-", "+", "foo") c.Error("not an integer", "XPENDING", "planets", "processing", "IDLE", "abc", "-", "+", "999") }) }) t.Run("XAUTOCLAIM", func(t *testing.T) { // justid mode testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "colors", "pr", "$", "MKSTREAM") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "JUSTID") c.Do("XADD", "colors", "42-2", "name", "Green") c.Do("XADD", "colors", "42-3", "name", "Blue") c.Do("XREADGROUP", "GROUP", "pr", "alice", "STREAMS", "colors", ">") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "JUSTID") c.Do("XREADGROUP", "GROUP", "pr", "alice", "STREAMS", "colors", ">") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "JUSTID") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "COUNT", "1", "JUSTID") c.Do("XPENDING", "colors", "pr") c.Do("XAUTOCLAIM", "colors", "pr", "eve", "0", "0", "JUSTID") c.Do("XPENDING", "colors", "pr") c.Error("syntax error", "XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "JUSTID", "foo") c.Error("No such key", "XAUTOCLAIM", "colors", "foo", "alice", "0", "0", "JUSTID") c.Error("No such key", "XAUTOCLAIM", "foo", "pr", "alice", "0", "0", "JUSTID") c.Error("Invalid min-idle-time", "XAUTOCLAIM", "colors", "pr", "alice", "foo", "0", "JUSTID") c.Error("Invalid stream ID", "XAUTOCLAIM", "colors", "pr", "alice", "0", "foo", "JUSTID") c.Error("Invalid stream ID", "XAUTOCLAIM", "colors", "pr", "alice", "0", "-1", "JUSTID") }) // regular mode testRaw(t, func(c *client) { c.Do("XGROUP", "CREATE", "colors", "pr", "$", "MKSTREAM") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0") c.Do("XADD", "colors", "42-2", "name", "Green") c.Do("XADD", "colors", "42-3", "name", "Blue") c.Do("XREADGROUP", "GROUP", "pr", "alice", "STREAMS", "colors", ">") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0") c.Do("XREADGROUP", "GROUP", "pr", "alice", "STREAMS", "colors", ">") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0") c.Do("XAUTOCLAIM", "colors", "pr", "alice", "0", "0", "COUNT", "1") c.Do("XAUTOCLAIM", "colors", "pr", "eve", "0", "0") c.Do("XPENDING", "colors", "pr") }) }) t.Run("XCLAIM", func(t *testing.T) { testRaw(t, func(c *client) { c.Error("No such key", "XCLAIM", "planets", "processing", "alice", "0", "0-0") c.Do("XGROUP", "CREATE", "planets", "processing", "$", "MKSTREAM") c.Error("No such key", "XCLAIM", "planets", "foo", "alice", "0", "0-0") c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-0") c.DoLoosely("XINFO", "CONSUMERS", "planets", "processing") // "idle" is fiddly c.Do("XADD", "planets", "0-1", "name", "Mercury") c.Do("XADD", "planets", "0-2", "name", "Venus") c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-1") c.DoLoosely("XINFO", "CONSUMERS", "planets", "processing") // "idle" is fiddly c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-1", "0-2", "FORCE") c.Do("XINFO", "GROUPS", "planets") c.Do("XPENDING", "planets", "processing") c.Do("XDEL", "planets", "0-1") // ! c.Do("XCLAIM", "planets", "processing", "bob", "0", "0-1") c.Do("XINFO", "GROUPS", "planets") c.Do("XPENDING", "planets", "processing") c.Do("XADD", "planets", "0-3", "name", "Mercury") c.Do("XADD", "planets", "0-4", "name", "Venus") c.Do("XCLAIM", "planets", "processing", "bob", "0", "0-4", "FORCE") c.Do("XCLAIM", "planets", "processing", "bob", "0", "0-4") c.Do("XPENDING", "planets", "processing") c.Do("XREADGROUP", "GROUP", "processing", "alice", "COUNT", "1", "STREAMS", "planets", ">") c.Do("XPENDING", "planets", "processing") c.Do("XREADGROUP", "GROUP", "processing", "alice", "STREAMS", "planets", ">") c.Do("XPENDING", "planets", "processing") c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-3", "RETRYCOUNT", "10", "IDLE", "5000", "JUSTID") c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-1", "0-2", "RETRYCOUNT", "1", "TIME", "1", "JUSTID") c.Do("XCLAIM", "planets", "processing", "alice", "0", "0-1", "0-4", "RETRYCOUNT", "1", "TIME", "1", "justid") c.Do("XPENDING", "planets", "processing") c.Do("XACK", "planets", "processing", "0-1", "0-2", "0-3", "0-4") c.Do("XPENDING", "planets", "processing") c.Error("Unrecognized XCLAIM option", "XCLAIM", "planets", "processing", "alice", "0", "0-3", "RETRYCOUNT", "10", "0-4", "IDLE", "0") c.Error("Unrecognized XCLAIM option", "XCLAIM", "planets", "processing", "alice", "0", "0-3", "RETRYCOUNT", "10", "IDLE", "0", "0-4") c.Error("Invalid min-idle-time", "XCLAIM", "planets", "processing", "alice", "foo", "0-1", "JUSTID") c.Error("Invalid IDLE", "XCLAIM", "planets", "processing", "alice", "0", "0-1", "JUSTID", "IDLE", "foo") c.Error("Invalid TIME", "XCLAIM", "planets", "processing", "alice", "0", "0-1", "JUSTID", "TIME", "foo") c.Error("Invalid RETRYCOUNT", "XCLAIM", "planets", "processing", "alice", "0", "0-1", "JUSTID", "RETRYCOUNT", "foo") }) }) testRESP3(t, func(c *client) { c.DoLoosely("XINFO", "STREAM", "foo") }) } func TestStreamTrim(t *testing.T) { skip(t) t.Run("XTRIM MAXLEN", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "planets", "0-1", "name", "Mercury") c.Do("XADD", "planets", "1-0", "name", "Venus") c.Do("XADD", "planets", "2-1", "name", "Earth") c.Do("XADD", "planets", "3-0", "name", "Mars") c.Do("XADD", "planets", "4-1", "name", "Jupiter") c.Do("XTRIM", "planets", "MAXLEN", "3") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MAXLEN", "=", "3") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MAXLEN", "2") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MAXLEN", "~", "2", "LIMIT", "99") c.Do("XRANGE", "planets", "-", "+") // error cases c.Error("not an integer", "XTRIM", "planets", "MAXLEN", "abc") c.Error("arguments", "XTRIM", "planets", "MAXLEN") c.Error("without the special", "XTRIM", "planets", "MAXLEN", "3", "LIMIT", "1") }) }) t.Run("XTRIM MINID", func(t *testing.T) { testRaw(t, func(c *client) { c.Do("XADD", "planets", "0-1", "name", "Mercury") c.Do("XADD", "planets", "1-0", "name", "Venus") c.Do("XADD", "planets", "2-1", "name", "Earth") c.Do("XADD", "planets", "3-0", "name", "Mars") c.Do("XADD", "planets", "4-1", "name", "Jupiter") c.Do("XTRIM", "planets", "MINID", "1") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MINID", "=", "1") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MINID", "3") c.Do("XRANGE", "planets", "-", "+") c.Do("XTRIM", "planets", "MINID", "~", "3", "LIMIT", "1") c.Do("XRANGE", "planets", "-", "+") // error cases c.Error("arguments", "XTRIM", "planets", "MINID") c.Error("arguments", "XTRIM", "planets") c.Error("arguments", "XTRIM", "planets", "OTHER") c.Error("without the special", "XTRIM", "planets", "MINID", "3", "LIMIT", "1") c.Error("out of range", "XTRIM", "planets", "MINID", "~", "3", "LIMIT", "one") }) }) } ================================================ FILE: miniredis/tests/integration-go/string_test.go ================================================ package main import ( "strconv" "testing" ) func TestString(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("GET", "foo") c.Do("SET", "foo", "bar\bbaz") c.Do("GET", "foo") c.Do("SET", "foo", "bar", "EX", "100") c.Error("not an integer", "SET", "foo", "bar", "EX", "noint") c.Do("SET", "utf8", "❆❅❄☃") c.Do("SET", "foo", "baz", "KEEPTTL") c.Do("SET", "foo", "bar", "GET") c.Do("SET", "new", "bar", "GET") c.Do("SET", "empty", "", "GET") c.Do("SET", "empty", "filled", "GET") c.Do("SET", "empty", "", "GET") c.Do("SET", "fooexat", "bar", "EXAT", "2345678901") c.DoApprox(10, "TTL", "fooexat") c.Error("not an integer", "SET", "foo", "bar", "EXAT", "noint") c.Do("SET", "foopxat", "bar", "PXAT", "2345678901000") c.DoApprox(10, "TTL", "foopxat") c.Error("not an integer", "SET", "foo", "bar", "PXAT", "noint") // expires right away c.Do("SET", "gone", "bar", "EXAT", "123") c.Do("EXISTS", "gone") // SET NX GET c.Do("SET", "unique", "value1", "NX", "GET") c.Do("SET", "unique", "value2", "NX", "GET") c.Do("SET", "unique", "value3", "XX", "GET") c.Do("SET", "unique", "value4", "XX", "GET") c.Do("SET", "uniquer", "value5", "XX", "GET") // Failure cases c.Error("wrong number", "SET") c.Error("wrong number", "SET", "foo") c.Error("syntax error", "SET", "foo", "bar", "baz") c.Error("wrong number", "GET") c.Error("wrong number", "GET", "too", "many") c.Error("invalid expire", "SET", "foo", "bar", "EX", "0") c.Error("invalid expire", "SET", "foo", "bar", "EX", "-100") c.Error("syntax error", "SET", "both", "bar", "PXAT", "3345678901000", "EXAT", "2345678901") c.Error("invalid expire", "SET", "foo", "bar", "EXAT", "-100") c.Error("invalid expire", "SET", "foo", "bar", "PXAT", "-100") c.Error("syntax error", "SET", "both", "bar", "PX", "6", "EX", "6") c.Error("syntax error", "SET", "both", "bar", "PX", "6", "EX", "0") c.Error("syntax error", "SET", "both", "bar", "PX", "6", "PXAT", "2345678901") // Wrong type c.Do("HSET", "hash", "key", "value") c.Error("wrong kind", "GET", "hash") c.Error("wrong kind", "SET", "hash", "foo", "GET") }) } func TestStringGetSet(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("GETSET", "foo", "new") c.Do("GET", "foo") c.Do("GET", "new") c.Do("GETSET", "nosuch", "new") c.Do("GET", "nosuch") // Failure cases c.Error("wrong number", "GETSET") c.Error("wrong number", "GETSET", "foo") c.Error("wrong number", "GETSET", "foo", "bar", "baz") // Wrong type c.Do("HSET", "hash", "key", "value") c.Error("wrong kind", "GETSET", "hash", "new") }) } func TestStringGetex(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("GETEX", "missing") c.Do("SET", "foo", "bar") c.Do("GETEX", "foo") c.Do("TTL", "foo") c.Do("GETEX", "foo", "EX", "10") c.Do("TTL", "foo") // Failure cases c.Error("wrong number", "GETEX") c.Error("syntax error", "GETEX", "foo", "bar") c.Error("syntax error", "GETEX", "foo", "EX", "10", "PERSIST") c.Error("syntax error", "GETEX", "foo", "EX", "10", "PX", "10") c.Error("not an integer", "GETEX", "foo", "EX", "ten") // Wrong type c.Do("HSET", "hash", "key", "value") c.Error("wrong kind", "GETEX", "hash") c.Do("SET", "hittl", "bar") c.Do("PEXPIRE", "hittl", "999999") c.Do("GETEX", "hittl", "PERSIST") c.Do("TTL", "hittl") }) } func TestStringGetdel(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("GETDEL", "missing") c.Do("SET", "foo", "bar") c.Do("GETDEL", "foo") c.Do("EXISTS", "foo") // Failure cases c.Error("wrong number", "GETDEL") c.Error("wrong number", "GETDEL", "foo", "bar") // Wrong type c.Do("HSET", "hash", "key", "value") c.Error("wrong kind", "GETDEL", "hash") }) } func TestStringMget(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("SET", "foo2", "bar") c.Do("MGET", "foo") c.Do("MGET", "foo", "foo2") c.Do("MGET", "nosuch", "neither") c.Do("MGET", "nosuch", "neither", "foo") // Failure cases c.Error("wrong number", "MGET") // Wrong type c.Do("HSET", "hash", "key", "value") c.Do("MGET", "hash") // not an error. }) } func TestStringSetnx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SETNX", "foo", "bar") c.Do("GET", "foo") c.Do("SETNX", "foo", "bar2") c.Do("GET", "foo") // Failure cases c.Error("wrong number", "SETNX") c.Error("wrong number", "SETNX", "foo") c.Error("wrong number", "SETNX", "foo", "bar", "baz") // Wrong type c.Do("HSET", "hash", "key", "value") c.Do("SETNX", "hash", "value") }) } func TestExpire(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("EXPIRETIME", "missing") c.Do("PEXPIRETIME", "missing") c.Do("SET", "foo", "bar") c.Do("EXPIRETIME", "foo") c.Do("PEXPIRETIME", "foo") c.Do("EXPIRE", "foo", "12") c.Do("TTL", "foo") c.Do("TTL", "nosuch") c.Do("SET", "foo", "bar") c.Do("PEXPIRE", "foo", "999999") c.Do("EXPIREAT", "foo", "2234567890") c.Do("PEXPIREAT", "foo", "2234567890123") c.Do("EXPIRETIME", "foo") c.Do("PEXPIRETIME", "foo") // c.Do("PTTL", "foo") c.Do("PTTL", "nosuch") c.Do("SET", "foo", "bar") c.Do("EXPIRE", "foo", "0") c.Do("EXISTS", "foo") c.Do("SET", "foo", "bar") c.Do("EXPIRE", "foo", "-12") c.Do("EXISTS", "foo") c.Do("SET", "gt", "nice day today, right?") c.Do("EXPIRE", "gt", "10", "GT") c.Do("TTL", "gt") c.Do("EXPIRE", "gt", "10", "LT") c.Do("TTL", "gt") c.Do("EXPIRE", "gt", "3", "GT") c.Do("TTL", "gt") c.Do("EXPIRE", "gt", "999", "NX") c.Do("TTL", "gt") c.Do("EXPIRE", "gt", "999", "XX") c.Do("TTL", "gt") c.Do("PEXPIRE", "gt", "999000", "XX") c.Do("TTL", "gt") c.Do("SET", "pgt", "indeed it is") c.Do("PEXPIRE", "pgt", "10000", "LT", "XX") c.Do("TTL", "pgt") c.Error("wrong number", "EXPIRE") c.Error("wrong number", "EXPIRE", "foo") c.Error("not an integer", "EXPIRE", "foo", "noint") c.Error("Unsupported", "EXPIRE", "foo", "12", "invaLID") c.Error("Unsupported", "EXPIRE", "foo", "12", "GT", "toomany") c.Error("at the same time", "EXPIRE", "foo", "12", "GT", "LT") c.Error("at the same time", "EXPIRE", "foo", "12", "LT", "NX") c.Error("wrong number", "EXPIREAT") c.Error("wrong number", "TTL") c.Error("wrong number", "TTL", "too", "many") c.Error("wrong number", "PEXPIRE") c.Error("wrong number", "PEXPIRE", "foo") c.Error("not an integer", "PEXPIRE", "foo", "noint") c.Error("Unsupported", "PEXPIRE", "foo", "12", "toomany") c.Error("Unsupported", "PEXPIREAT", "foo", "12", "NX", "toomany") c.Error("wrong number", "PEXPIREAT") c.Error("wrong number", "PTTL") c.Error("wrong number", "PTTL", "too", "many") c.Error("wrong number", "EXPIRETIME") c.Error("wrong number", "EXPIRETIME", "too", "many") c.Error("wrong number", "PEXPIRETIME") c.Error("wrong number", "PEXPIRETIME", "too", "many") }) } func TestMset(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("MSET", "foo", "bar") c.Do("MSET", "foo", "bar", "baz", "?") c.Do("MSET", "foo", "bar", "foo", "baz") // double key c.Do("GET", "foo") // Error cases c.Error("wrong number", "MSET") c.Error("wrong number", "MSET", "foo") c.Error("wrong number", "MSET", "foo", "bar", "baz") c.Do("MSETNX", "foo", "bar", "aap", "noot") c.Do("MSETNX", "one", "two", "three", "four") c.Do("MSETNX", "11", "12", "11", "14") // double key c.Do("GET", "11") // Wrong type of key doesn't matter c.Do("HSET", "aap", "noot", "mies") c.Do("MSET", "aap", "again", "eight", "nine") c.Do("MSETNX", "aap", "again", "eight", "nine") // Error cases c.Error("wrong number", "MSETNX") c.Error("wrong number", "MSETNX", "one") c.Error("wrong number", "MSETNX", "one", "two", "three") }) } func TestSetx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SETEX", "foo", "12", "bar") c.Do("GET", "foo") c.Do("TTL", "foo") c.Error("wrong number", "SETEX", "foo") c.Error("not an integer", "SETEX", "foo", "noint", "bar") c.Error("wrong number", "SETEX", "foo", "12") c.Error("wrong number", "SETEX", "foo", "12", "bar", "toomany") c.Error("wrong number", "SETEX", "foo", "0") c.Error("wrong number", "SETEX", "foo", "-12") c.Do("PSETEX", "foo", "12", "bar") c.Do("GET", "foo") // c.Do("PTTL", "foo") // counts down too quickly to compare c.Error("wrong number", "PSETEX", "foo") c.Error("not an integer", "PSETEX", "foo", "noint", "bar") c.Error("wrong number", "PSETEX", "foo", "12") c.Error("wrong number", "PSETEX", "foo", "12", "bar", "toomany") c.Error("wrong number", "PSETEX", "foo", "0") c.Error("wrong number", "PSETEX", "foo", "-12") }) } func TestGetrange(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "The quick brown fox jumps over the lazy dog") c.Do("GETRANGE", "foo", "0", "100") c.Do("GETRANGE", "foo", "0", "0") c.Do("GETRANGE", "foo", "0", "-4") c.Do("GETRANGE", "foo", "0", "-400") c.Do("GETRANGE", "foo", "-4", "-4") c.Do("GETRANGE", "foo", "4", "2") c.Error("not an integer", "GETRANGE", "foo", "aap", "2") c.Error("not an integer", "GETRANGE", "foo", "4", "aap") c.Error("wrong number", "GETRANGE", "foo", "4", "2", "aap") c.Error("wrong number", "GETRANGE", "foo") c.Do("HSET", "aap", "noot", "mies") c.Error("wrong kind", "GETRANGE", "aap", "4", "2") }) } func TestStrlen(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "str", "The quick brown fox jumps over the lazy dog") c.Do("STRLEN", "str") // failure cases c.Error("wrong number", "STRLEN") c.Error("wrong number", "STRLEN", "str", "bar") c.Do("HSET", "hash", "key", "value") c.Error("wrong kind", "STRLEN", "hash") }) } func TestSetrange(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "The quick brown fox jumps over the lazy dog") c.Do("SETRANGE", "foo", "0", "aap") c.Do("GET", "foo") c.Do("SETRANGE", "foo", "10", "noot") c.Do("GET", "foo") c.Do("SETRANGE", "foo", "40", "overtheedge") c.Do("GET", "foo") c.Do("SETRANGE", "foo", "400", "oh, hey there") c.Do("GET", "foo") // Non existing key c.Do("SETRANGE", "nosuch", "2", "aap") c.Do("GET", "nosuch") // Error cases c.Error("wrong number", "SETRANGE", "foo") c.Error("wrong number", "SETRANGE", "foo", "1") c.Error("not an integer", "SETRANGE", "foo", "aap", "bar") c.Error("not an integer", "SETRANGE", "foo", "noint", "bar") c.Error("out of range", "SETRANGE", "foo", "-1", "bar") c.Do("HSET", "aap", "noot", "mies") c.Error("wrong kind", "SETRANGE", "aap", "4", "bar") }) } func TestIncrAndFriends(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("INCR", "aap") c.Do("INCR", "aap") c.Do("INCR", "aap") c.Do("GET", "aap") c.Do("DECR", "aap") c.Do("DECR", "noot") c.Do("DECR", "noot") c.Do("GET", "noot") c.Do("INCRBY", "noot", "100") c.Do("INCRBY", "noot", "200") c.Do("INCRBY", "noot", "300") c.Do("GET", "noot") c.Do("DECRBY", "noot", "100") c.Do("DECRBY", "noot", "200") c.Do("DECRBY", "noot", "300") c.Do("DECRBY", "noot", "400") c.Do("GET", "noot") c.Do("INCRBYFLOAT", "zus", "1.23") c.Do("INCRBYFLOAT", "zus", "3.1456") c.Do("INCRBYFLOAT", "zus", "987.65432") c.Do("GET", "zus") c.Do("INCRBYFLOAT", "whole", "300") c.Do("INCRBYFLOAT", "whole", "300") c.Do("INCRBYFLOAT", "whole", "300") c.Do("GET", "whole") c.Do("INCRBYFLOAT", "big", "12345e10") c.Do("GET", "big") // Floats are not ints. c.Do("SET", "float", "1.23") c.Error("not an integer", "INCR", "float") c.Error("not an integer", "INCRBY", "float", "12") c.Error("not an integer", "DECR", "float") c.Error("not an integer", "DECRBY", "float", "12") c.Do("SET", "str", "I'm a string") c.Error("not a valid float", "INCRBYFLOAT", "str", "123.5") // Error cases c.Do("HSET", "mies", "noot", "mies") c.Error("wrong kind", "INCR", "mies") c.Error("wrong kind", "INCRBY", "mies", "1") c.Error("not an integer", "INCRBY", "mies", "foo") c.Error("wrong kind", "DECR", "mies") c.Error("wrong kind", "DECRBY", "mies", "1") c.Error("wrong kind", "INCRBYFLOAT", "mies", "1") c.Error("not a valid float", "INCRBYFLOAT", "int", "foo") c.Error("wrong number", "INCR", "int", "err") c.Error("wrong number", "INCRBY", "int") c.Error("wrong number", "DECR", "int", "err") c.Error("wrong number", "DECRBY", "int") c.Error("wrong number", "INCRBYFLOAT", "int") // Rounding c.Do("INCRBYFLOAT", "zero", "12.3") c.Do("INCRBYFLOAT", "zero", "-13.1") // Overflow c.Do("SET", "overflow-up", "9223372036854775807") c.Error("increment or decrement would overflow", "INCR", "overflow-up") c.Error("increment or decrement would overflow", "INCRBY", "overflow-up", "1") c.Error("increment or decrement would overflow", "DECRBY", "overflow-up", "-1") c.Do("SET", "overflow-down", "-9223372036854775808") c.Error("increment or decrement would overflow", "DECR", "overflow-down") c.Error("increment or decrement would overflow", "INCRBY", "overflow-down", "-1") c.Error("increment or decrement would overflow", "DECRBY", "overflow-down", "1") // E c.Do("INCRBYFLOAT", "one", "12e12") // c.Do("INCRBYFLOAT", "one", "12e34") // FIXME c.Error("not a valid float", "INCRBYFLOAT", "one", "12e34.1") // c.Do("INCRBYFLOAT", "one", "0x12e12") // FIXME // c.Do("INCRBYFLOAT", "one", "012e12") // FIXME c.Do("INCRBYFLOAT", "two", "012") c.Error("not a valid float", "INCRBYFLOAT", "one", "0b12e12") }) } func TestBitcount(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "str", "The quick brown fox jumps over the lazy dog") c.Do("SET", "utf8", "❆❅❄☃") c.Do("BITCOUNT", "str") c.Do("BITCOUNT", "utf8") c.Do("BITCOUNT", "str", "0", "0") c.Do("BITCOUNT", "str", "1", "2") c.Do("BITCOUNT", "str", "1", "-200") c.Do("BITCOUNT", "str", "-2", "-1") c.Do("BITCOUNT", "str", "-2", "-12") c.Do("BITCOUNT", "utf8", "0", "0") c.Do("SETBIT", "A", "10", "1") c.Do("BITCOUNT", "A", "0", "100000") c.Do("BITCOUNT", "A", "0", "9223372036854775806") c.Do("BITCOUNT", "A", "0", "9223372036854775807") // max int64 c.Error("out of range", "BITCOUNT", "A", "0", "9223372036854775808") c.Error("wrong number", "BITCOUNT") c.Error("syntax error", "BITCOUNT", "wrong", "arguments") c.Error("syntax error", "BITCOUNT", "str", "4", "2", "2", "2", "2") c.Error("not an integer", "BITCOUNT", "str", "foo", "2") c.Do("HSET", "aap", "noot", "mies") c.Error("wrong kind", "BITCOUNT", "aap", "4", "2") }) } func TestBitop(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "a", "foo") c.Do("SET", "b", "aap") c.Do("SET", "c", "noot") c.Do("SET", "d", "mies") c.Do("SET", "e", "❆❅❄☃") // ANDs c.Do("BITOP", "AND", "target", "a", "b", "c", "d") c.Do("GET", "target") c.Do("BITOP", "AND", "target", "a", "nosuch", "c", "d") c.Do("GET", "target") c.Do("BITOP", "AND", "utf8", "e", "e") c.Do("GET", "utf8") c.Do("BITOP", "AND", "utf8", "b", "e") c.Do("GET", "utf8") // BITOP on only unknown keys: c.Do("BITOP", "AND", "bits", "nosuch", "nosucheither") c.Do("GET", "bits") // ORs c.Do("BITOP", "OR", "target", "a", "b", "c", "d") c.Do("GET", "target") c.Do("BITOP", "OR", "target", "a", "nosuch", "c", "d") c.Do("GET", "target") c.Do("BITOP", "OR", "utf8", "e", "e") c.Do("GET", "utf8") c.Do("BITOP", "OR", "utf8", "b", "e") c.Do("GET", "utf8") // BITOP on only unknown keys: c.Do("BITOP", "OR", "bits", "nosuch", "nosucheither") c.Do("GET", "bits") c.Do("SET", "empty", "") // BITOP on empty key c.Do("BITOP", "OR", "bits", "empty") c.Do("GET", "bits") // XORs c.Do("BITOP", "XOR", "target", "a", "b", "c", "d") c.Do("GET", "target") c.Do("BITOP", "XOR", "target", "a", "nosuch", "c", "d") c.Do("GET", "target") c.Do("BITOP", "XOR", "target", "a") c.Do("GET", "target") c.Do("BITOP", "XOR", "utf8", "e", "e") c.Do("GET", "utf8") c.Do("BITOP", "XOR", "utf8", "b", "e") c.Do("GET", "utf8") // NOTs c.Do("BITOP", "NOT", "target", "a") c.Do("GET", "target") c.Do("BITOP", "NOT", "target", "e") c.Do("GET", "target") c.Do("BITOP", "NOT", "bits", "nosuch") c.Do("GET", "bits") c.Error("wrong number", "BITOP", "AND", "utf8") c.Error("wrong number", "BITOP", "AND") c.Error("single source key", "BITOP", "NOT", "foo", "bar", "baz") c.Error("wrong number", "BITOP", "WRONGOP", "key") c.Error("wrong number", "BITOP", "WRONGOP") c.Do("HSET", "hash", "aap", "noot") c.Error("wrong kind", "BITOP", "AND", "t", "hash", "irrelevant") c.Error("wrong kind", "BITOP", "OR", "t", "hash", "irrelevant") c.Error("wrong kind", "BITOP", "XOR", "t", "hash", "irrelevant") c.Error("wrong kind", "BITOP", "NOT", "t", "hash") }) } func TestBitpos(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "a", "\x00\x0f") c.Do("SET", "b", "\xf0\xf0") c.Do("SET", "c", "\x00\x00\x00\x0f") c.Do("SET", "d", "\x00\x00\x00") c.Do("SET", "e", "\xff\xff\xff") c.Do("SET", "empty", "") c.Do("BITPOS", "a", "1") c.Do("BITPOS", "a", "0") c.Do("BITPOS", "a", "1", "1") c.Do("BITPOS", "a", "0", "1") c.Do("BITPOS", "a", "1", "1", "2") c.Do("BITPOS", "a", "0", "1", "2") c.Do("BITPOS", "a", "0", "0", "0") c.Do("BITPOS", "a", "0", "0", "-1") c.Do("BITPOS", "a", "0", "0", "-2") c.Do("BITPOS", "a", "0", "0", "-2") c.Do("BITPOS", "a", "0", "0", "-999") c.Do("BITPOS", "a", "0", "-1", "-1") c.Do("BITPOS", "a", "0", "-2", "-1") c.Do("BITPOS", "a", "0", "-2", "-999") c.Do("BITPOS", "a", "0", "-999", "-999") c.Do("BITPOS", "b", "1") c.Do("BITPOS", "b", "0") c.Do("BITPOS", "c", "1") c.Do("BITPOS", "c", "0") c.Do("BITPOS", "d", "1") c.Do("BITPOS", "d", "0") c.Do("BITPOS", "e", "1") c.Do("BITPOS", "e", "0") c.Do("BITPOS", "e", "1", "1") c.Do("BITPOS", "e", "0", "1") c.Do("BITPOS", "e", "1", "1", "2") c.Do("BITPOS", "e", "0", "1", "2") c.Do("BITPOS", "e", "1", "100", "2") c.Do("BITPOS", "e", "0", "100", "2") c.Do("BITPOS", "e", "1", "1", "0") c.Do("BITPOS", "e", "1", "1", "-1") c.Do("BITPOS", "e", "1", "1", "-2") c.Do("BITPOS", "e", "1", "1", "-2000") c.Do("BITPOS", "e", "0", "0", "0") c.Do("BITPOS", "e", "0", "0", "-1") c.Do("BITPOS", "e", "0", "1", "2") c.Do("BITPOS", "empty", "0") c.Do("BITPOS", "empty", "0", "0") c.Do("BITPOS", "empty", "0", "0", "0") c.Do("BITPOS", "empty", "0", "0", "-1") c.Do("BITPOS", "empty", "0", "-1", "-1") c.Do("BITPOS", "empty", "1") c.Do("BITPOS", "empty", "1", "0") c.Do("BITPOS", "empty", "1", "0", "0") c.Do("BITPOS", "empty", "1", "0", "-1") c.Do("BITPOS", "empty", "1", "-1", "-1") c.Do("BITPOS", "nosuch", "0") c.Do("BITPOS", "nosuch", "0", "0") c.Do("BITPOS", "nosuch", "0", "0", "0") c.Do("BITPOS", "nosuch", "1") c.Do("BITPOS", "nosuch", "1", "0") c.Do("BITPOS", "nosuch", "1", "0", "0") c.Do("HSET", "hash", "aap", "noot") c.Error("wrong kind", "BITPOS", "hash", "1") c.Error("not an integer", "BITPOS", "a", "aap") }) } func TestGetbit(t *testing.T) { skip(t) testRaw(t, func(c *client) { for i := 0; i < 100; i++ { c.Do("SET", "a", "\x00\x0f") c.Do("SET", "e", "\xff\xff\xff") c.Do("GETBIT", "nosuch", "1") c.Do("GETBIT", "nosuch", "0") // Error cases c.Do("HSET", "hash", "aap", "noot") c.Error("wrong kind", "GETBIT", "hash", "1") c.Error("not an integer", "GETBIT", "a", "aap") c.Error("wrong number", "GETBIT", "a") c.Error("wrong number", "GETBIT", "too", "1", "many") c.Do("GETBIT", "a", strconv.Itoa(i)) c.Do("GETBIT", "e", strconv.Itoa(i)) } }) } func TestSetbit(t *testing.T) { skip(t) testRaw(t, func(c *client) { for i := 0; i < 100; i++ { c.Do("SET", "a", "\x00\x0f") c.Do("SETBIT", "a", "0", "1") c.Do("GET", "a") c.Do("SETBIT", "a", "0", "0") c.Do("GET", "a") c.Do("SETBIT", "a", "13", "0") c.Do("GET", "a") c.Do("SETBIT", "nosuch", "11111", "1") c.Do("GET", "nosuch") // Error cases c.Do("HSET", "hash", "aap", "noot") c.Error("wrong kind", "SETBIT", "hash", "1", "1") c.Error("not an integer", "SETBIT", "a", "aap", "0") c.Error("not an integer", "SETBIT", "a", "0", "aap") c.Error("not an integer", "SETBIT", "a", "-1", "0") c.Error("not an integer", "SETBIT", "a", "1", "-1") c.Error("not an integer", "SETBIT", "a", "1", "2") c.Error("wrong number", "SETBIT", "too", "1", "2", "many") c.Do("GETBIT", "a", strconv.Itoa(i)) c.Do("GETBIT", "e", strconv.Itoa(i)) } }) } func TestAppend(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("APPEND", "foo", "more") c.Do("GET", "foo") c.Do("APPEND", "nosuch", "more") c.Do("GET", "nosuch") // Failure cases c.Error("wrong number", "APPEND") c.Error("wrong number", "APPEND", "foo") }) } func TestMove(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("EXPIRE", "foo", "12345") c.Do("MOVE", "foo", "2") c.Do("GET", "foo") c.Do("TTL", "foo") c.Do("SELECT", "2") c.Do("GET", "foo") c.Do("TTL", "foo") // Failure cases c.Error("wrong number", "MOVE") c.Error("wrong number", "MOVE", "foo") // c.Do("MOVE", "foo", "noint") }) // hash key testRaw(t, func(c *client) { c.Do("HSET", "hash", "key", "value") c.Do("EXPIRE", "hash", "12345") c.Do("MOVE", "hash", "2") c.Do("MGET", "hash", "key") c.Do("TTL", "hash") c.Do("SELECT", "2") c.Do("MGET", "hash", "key") c.Do("TTL", "hash") }) testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") // to current DB. c.Error("the same", "MOVE", "foo", "0") }) } ================================================ FILE: miniredis/tests/integration-go/test.go ================================================ package main import ( "fmt" "math" "os" "reflect" "sort" "strconv" "strings" "sync" "testing" "github.com/alicebob/miniredis/v2" "github.com/alicebob/miniredis/v2/proto" ) func skip(t testing.TB) { t.Helper() if os.Getenv("INT") == "" { t.Skip("INT=1 not set") } } func testRaw(t *testing.T, cb func(*client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := Redis() t.Cleanup(sReal.Close) client := newClient(t, sRealAddr, sMini) cb(client) } // like testRaw, but with two connections func testRaw2(t *testing.T, cb func(*client, *client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := Redis() t.Cleanup(sReal.Close) client1 := newClient(t, sRealAddr, sMini) client2 := newClient(t, sRealAddr, sMini) cb(client1, client2) } // like testRaw2, but with connections in Go routines func testMulti(t *testing.T, cbs ...func(*client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := Redis() t.Cleanup(sReal.Close) var wg sync.WaitGroup for _, cb := range cbs { wg.Add(1) go func(cb func(*client)) { client := newClient(t, sRealAddr, sMini) cb(client) wg.Done() }(cb) } wg.Wait() } // similar to testRaw, but redis runs with authentication enabled func testAuth(t *testing.T, passwd string, cb func(*client)) { t.Helper() sMini := miniredis.RunT(t) sMini.RequireAuth(passwd) sReal, sRealAddr := RedisAuth(passwd) t.Cleanup(sReal.Close) client := newClient(t, sRealAddr, sMini) cb(client) } // similar to testAuth, but redis runs with redis6 multiuser authentication enabled func testUserAuth(t *testing.T, users map[string]string, cb func(*client)) { t.Helper() sMini := miniredis.RunT(t) for user, pass := range users { sMini.RequireUserAuth(user, pass) } sReal, sRealAddr := RedisUserAuth(users) t.Cleanup(sReal.Close) client := newClient(t, sRealAddr, sMini) cb(client) } // similar to testRaw, but redis is started in cluster mode func testCluster(t *testing.T, cb func(*client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := RedisCluster() t.Cleanup(sReal.Close) client := newClient(t, sRealAddr, sMini) cb(client) } // similar to testRaw, but connections require TLS func testTLS(t *testing.T, cb func(*client)) { t.Helper() sMini := miniredis.NewMiniRedis() if err := sMini.StartTLS(testServerTLS(t)); err != nil { t.Fatalf("unexpected miniredis error: %s", err.Error()) } t.Cleanup(sMini.Close) sReal, sRealAddr := RedisTLS() t.Cleanup(sReal.Close) client := newClientTLS(t, sRealAddr, sMini) cb(client) } // like testRaw, but switched to RESP3 protocol. func testRESP3(t *testing.T, cb func(*client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := Redis() t.Cleanup(sReal.Close) client := newClientResp3(t, sRealAddr, sMini) cb(client) } // like testRESP3, but with two connections func testRESP3Pair(t *testing.T, cb func(*client, *client)) { t.Helper() sMini := miniredis.RunT(t) sReal, sRealAddr := Redis() t.Cleanup(sReal.Close) client1 := newClientResp3(t, sRealAddr, sMini) client2 := newClientResp3(t, sRealAddr, sMini) cb(client1, client2) } func looselyEqual(a, b interface{}) bool { switch av := a.(type) { case string: _, ok := b.(string) return ok case []byte: _, ok := b.([]byte) return ok case int64: _, ok := b.(int64) return ok case int: _, ok := b.(int) return ok case error: _, ok := b.(error) return ok case []interface{}: bv, ok := b.([]interface{}) if !ok { return false } if len(av) != len(bv) { return false } for i, v := range av { if !looselyEqual(v, bv[i]) { return false } } return true case map[interface{}]interface{}: bv, ok := b.(map[interface{}]interface{}) if !ok { return false } if len(av) != len(bv) { return false } for k, v := range av { if !looselyEqual(v, bv[k]) { return false } } return true default: panic(fmt.Sprintf("unhandled case, got a %#v / %T", a, a)) } } // round all floats func roundFloats(r interface{}, pos int) interface{} { switch ls := r.(type) { case []interface{}: var new []interface{} for _, k := range ls { new = append(new, roundFloats(k, pos)) } return new case []byte: f, err := strconv.ParseFloat(string(ls), 64) if err != nil { return ls } return []byte(fmt.Sprintf("%.[1]*f", pos, f)) case string: f, err := strconv.ParseFloat(string(ls), 64) if err != nil { return ls } return fmt.Sprintf("%.[1]*f", pos, f) default: fmt.Printf("unhandled type: %T FIXME\n", r) return nil } } // client which compares two redises type client struct { t *testing.T real, mini *proto.Client miniredis *miniredis.Miniredis // in case you need m.FastForward() and friends } func newClient(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client { t.Helper() cReal, err := proto.Dial(realAddr) if err != nil { t.Fatalf("realredis: %s", err.Error()) } cMini, err := proto.Dial(mini.Addr()) if err != nil { t.Fatalf("miniredis: %s", err.Error()) } return &client{ t: t, miniredis: mini, real: cReal, mini: cMini, } } func newClientTLS(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client { t.Helper() cfg := testClientTLS(t) cReal, err := proto.DialTLS( realAddr, cfg, ) if err != nil { t.Fatalf("realredis: %s", err.Error()) } cMini, err := proto.DialTLS( mini.Addr(), cfg, ) if err != nil { t.Fatalf("miniredis: %s", err.Error()) } return &client{ t: t, miniredis: mini, real: cReal, mini: cMini, } } func newClientResp3(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client { t.Helper() cReal, err := proto.Dial(realAddr) if err != nil { t.Fatalf("realredis: %s", err.Error()) } if _, err := cReal.Do("HELLO", "3"); err != nil { t.Fatalf("realredis HELLO: %s", err.Error()) } cMini, err := proto.Dial(mini.Addr()) if err != nil { t.Fatalf("miniredis: %s", err.Error()) } if _, err := cMini.Do("HELLO", "3"); err != nil { t.Fatalf("miniredis HELLO: %s", err.Error()) } return &client{ t: t, miniredis: mini, real: cReal, mini: cMini, } } // Do() is the main test function. The given redis command is executed on both // a real redis and on miniredis, and the returned results must be exactly the // same. See the other Do... commands for variants which are more flexible in // their comparison. func (c *client) Do(cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) if resReal != resMini { c.t.Errorf("real: %q mini: %q", string(resReal), string(resMini)) return } if strings.HasPrefix(string(resReal), "-") { c.t.Errorf("Do() returned a redis error, use c.Error(): %q", string(resReal)) } } // result must be []string, and we'll sort them before comparing func (c *client) DoSorted(cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) realStrings, err := proto.ReadStrings(resReal) if err != nil { c.t.Errorf("readstrings realredis: %s", errReal) return } miniStrings, err := proto.ReadStrings(resMini) if err != nil { c.t.Errorf("readstrings miniredis: %s", errReal) return } sort.Strings(realStrings) sort.Strings(miniStrings) if !reflect.DeepEqual(realStrings, miniStrings) { c.t.Errorf("expected: %q got: %q", realStrings, miniStrings) } } // result must kinda match (just the structure, exact values are not compared) func (c *client) DoLoosely(cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) mini, err := proto.Parse(resMini) if err != nil { c.t.Errorf("parse error miniredis: %s", err) return } real, err := proto.Parse(resReal) if err != nil { c.t.Errorf("parse error realredis: %s", err) return } if !looselyEqual(real, mini) { c.t.Errorf("expected a loose match want: %#v have: %#v", real, mini) } } // result must match, with floats rounded func (c *client) DoRounded(rounded int, cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) mini, err := proto.Parse(resMini) if err != nil { c.t.Errorf("parse error miniredis: %s", err) return } real, err := proto.Parse(resReal) if err != nil { c.t.Errorf("parse error realredis: %s", err) return } real = roundFloats(real, rounded) mini = roundFloats(mini, rounded) if !reflect.DeepEqual(real, mini) { c.t.Errorf("expected a match (rounded to %d) want: %#v have: %#v", rounded, real, mini) } } // result must be a single int, with value within threshold func (c *client) DoApprox(threshold int, cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) mini, err := proto.Parse(resMini) if err != nil { c.t.Errorf("parse error miniredis: %s", err) return } real, err := proto.Parse(resReal) if err != nil { c.t.Errorf("parse error realredis: %s", err) return } miniInt, ok := mini.(int) if !ok { c.t.Errorf("parse int error miniredis: %T found (%#v)", mini, mini) return } realInt, ok := real.(int) if !ok { c.t.Errorf("parse int error miniredis: %T found (%#v)", real, real) return } if math.Abs(float64(miniInt-realInt)) > float64(threshold) { c.t.Errorf("expected an approximated match (threshold is %d) want: %#v have: %#v", threshold, real, mini) } } // both must return an error, which much both Contain() the message. func (c *client) Error(msg string, cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } mini, err := proto.ReadError(resMini) if err != nil { c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) c.t.Errorf("parse error miniredis: %s", err) return } real, err := proto.ReadError(resReal) if err != nil { c.t.Errorf("parse error realredis: %s", err) return } if !strings.Contains(real, msg) { c.t.Errorf("expected (real)\n%q\nto contain %q", real, msg) } if !strings.Contains(mini, msg) { c.t.Errorf("expected (mini)\n%q\nto contain %q\nreal:\n%s", mini, msg, real) } // if real != mini { // c.t.Errorf("expected error:\n%q\ngot:\n%q", real, mini) // } } // both must return exactly the same error func (c *client) ErrorTheSame(msg string, cmd string, args ...string) { c.t.Helper() resReal, errReal := c.real.Do(append([]string{cmd}, args...)...) if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Do(append([]string{cmd}, args...)...) if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } mini, err := proto.ReadError(resMini) if err != nil { c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) c.t.Errorf("parse error miniredis: %s", err) return } real, err := proto.ReadError(resReal) if err != nil { c.t.Errorf("parse error realredis: %s", err) return } if real != msg { c.t.Errorf("expected (real)\n%q\nto contain %q", real, msg) } if mini != msg { c.t.Errorf("expected (mini)\n%q\nto contain %q\nreal:\n%s", mini, msg, real) } // real == msg && mini == msg => real == mini, so we don't want to check it explicitly } // only receive a command, which can't be an error func (c *client) Receive() { c.t.Helper() resReal, errReal := c.real.Read() if errReal != nil { c.t.Errorf("error from realredis: %s", errReal) return } resMini, errMini := c.mini.Read() if errMini != nil { c.t.Errorf("error from miniredis: %s", errMini) return } // c.t.Logf("real:%q mini:%q", string(resReal), string(resMini)) if strings.HasPrefix(resReal, "-") { c.t.Errorf("error from realredis: %q", string(resReal)) } if strings.HasPrefix(resMini, "-") { c.t.Errorf("error from miniredis: %q", string(resMini)) } } ================================================ FILE: miniredis/tests/integration-go/tls.go ================================================ package main import ( "crypto/tls" "crypto/x509" "io/ioutil" "testing" ) func testServerTLS(t *testing.T) *tls.Config { cert, err := tls.LoadX509KeyPair("../../testdata/server.crt", "../../testdata/server.key") if err != nil { t.Fatal(err) } cp := x509.NewCertPool() rootca, err := ioutil.ReadFile("../../testdata/ca.crt") if err != nil { t.Fatal(err) } if !cp.AppendCertsFromPEM(rootca) { t.Fatal("ca cert err") } return &tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ServerName: "Server", ClientCAs: cp, } } func testClientTLS(t *testing.T) *tls.Config { cert, err := tls.LoadX509KeyPair("../../testdata/client.crt", "../../testdata/client.key") if err != nil { t.Fatal(err) } cp := x509.NewCertPool() rootca, err := ioutil.ReadFile("../../testdata/ca.crt") if err != nil { t.Fatal(err) } if !cp.AppendCertsFromPEM(rootca) { t.Fatal("ca cert err") } return &tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: "Server", RootCAs: cp, } } ================================================ FILE: miniredis/tests/integration-go/tx_test.go ================================================ package main import ( "testing" ) func TestTx(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SET", "AAP", "1") c.Do("GET", "AAP") c.Do("EXEC") c.Do("GET", "AAP") }) // empty testRaw(t, func(c *client) { c.Do("MULTI") c.Do("EXEC") }) // err: Double MULTI testRaw(t, func(c *client) { c.Do("MULTI") c.Error("nested", "MULTI") }) // err: No MULTI testRaw(t, func(c *client) { c.Error("without MULTI", "EXEC") }) // Errors in the MULTI sequence testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SET", "foo", "bar") c.Error("wrong number", "SET", "foo") c.Do("SET", "foo", "bar") c.Error("EXECABORT", "EXEC") }) // Simple WATCH testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("WATCH", "foo") c.Do("MULTI") c.Do("GET", "foo") c.Do("EXEC") }) // Simple UNWATCH testRaw(t, func(c *client) { c.Do("SET", "foo", "bar") c.Do("WATCH", "foo") c.Do("UNWATCH") c.Do("MULTI") c.Do("GET", "foo") c.Do("EXEC") }) // UNWATCH in a MULTI. Yep. Weird. testRaw(t, func(c *client) { c.Do("WATCH", "foo") c.Do("MULTI") c.Do("UNWATCH") // Valid. Somehow. c.Do("EXEC") }) // Test whether all these commands support transactions. testRaw(t, func(c *client) { c.Do("MULTI") c.Do("GET", "str") c.Do("GETEX", "str") c.Do("SET", "str", "bar") c.Do("SETNX", "str", "bar") c.Do("GETSET", "str", "bar") c.Do("MGET", "str", "bar") c.Do("MSET", "str", "bar") c.Do("MSETNX", "str", "bar") c.Do("SETEX", "str", "12", "newv") c.Do("PSETEX", "str", "12", "newv") c.Do("STRLEN", "str") c.Do("APPEND", "str", "more") c.Do("GETRANGE", "str", "0", "2") c.Do("SETRANGE", "str", "0", "B") c.Do("EXEC") c.Do("GET", "str") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SET", "bits", "\xff\x00") c.Do("BITCOUNT", "bits") c.Do("BITOP", "OR", "bits", "bits", "nosuch") c.Do("BITPOS", "bits", "1") c.Do("GETBIT", "bits", "12") c.Do("SETBIT", "bits", "12", "1") c.Do("EXEC") c.Do("GET", "bits") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("INCR", "number") c.Do("INCRBY", "number", "12") c.Do("INCRBYFLOAT", "number", "12.2") c.Do("DECR", "number") c.Do("GET", "number") c.Do("DECRBY", "number", "2") c.Do("GET", "number") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("HSET", "hash", "foo", "bar") c.Do("HDEL", "hash", "foo") c.Do("HEXISTS", "hash", "foo") c.Do("HSET", "hash", "foo", "bar22") c.Do("HSETNX", "hash", "foo", "bar22") c.Do("HGET", "hash", "foo") c.Do("HMGET", "hash", "foo", "baz") c.Do("HLEN", "hash") c.Do("HGETALL", "hash") c.Do("HKEYS", "hash") c.Do("HVALS", "hash") }) testRaw(t, func(c *client) { c.Do("MULTI") c.Do("SET", "key", "foo") c.Do("TYPE", "key") c.Do("EXPIRE", "key", "12") c.Do("TTL", "key") c.Do("PEXPIRE", "key", "12") c.Do("PTTL", "key") c.Do("PERSIST", "key") c.Do("DEL", "key") c.Do("TYPE", "key") c.Do("EXEC") }) // BITOP OPs are checked after the transaction. testRaw(t, func(c *client) { c.Do("MULTI") c.Do("BITOP", "BROKEN", "str", "") c.Do("EXEC") }) // fail on invalid command testRaw(t, func(c *client) { c.Do("MULTI") c.Error("wrong number", "GET") c.Error("Transaction discarded", "EXEC") }) /* FIXME // fail on unknown command testRaw(t, func(c *client) { c.Do("MULTI") c.Do("NOSUCH") c.Do("EXEC") }) */ // failed EXEC cleaned up the tx testRaw(t, func(c *client) { c.Do("MULTI") c.Error("wrong number", "GET") c.Error("Transaction discarded", "EXEC") c.Do("MULTI") }) testRaw2(t, func(c1, c2 *client) { c1.Do("WATCH", "foo") c1.Do("MULTI") c2.Do("SET", "foo", "12") c2.Error("without", "EXEC") // nil c1.Do("EXEC") // 0-length }) } ================================================ FILE: miniredis/tests/smoke.rs ================================================ use redis::AsyncCommands; #[tokio::test] async fn smoke_ping() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let pong: String = redis::cmd("PING").query_async(&mut con).await.unwrap(); assert_eq!(pong, "PONG"); m.close().await; } #[tokio::test] async fn smoke_set_get() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let _: () = con.set("foo", "bar").await.unwrap(); let val: String = con.get("foo").await.unwrap(); assert_eq!(val, "bar"); m.close().await; } #[tokio::test] async fn smoke_set_get_direct() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); // Set via client let _: () = con.set("key1", "value1").await.unwrap(); // Read via direct API m.check_get("key1", "value1"); // Set via direct API m.set("key2", "value2"); // Read via client let val: String = con.get("key2").await.unwrap(); assert_eq!(val, "value2"); m.close().await; } #[tokio::test] async fn smoke_del_exists() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let _: () = con.set("k", "v").await.unwrap(); assert!(m.exists("k")); let deleted: i64 = con.del("k").await.unwrap(); assert_eq!(deleted, 1); assert!(!m.exists("k")); m.close().await; } #[tokio::test] async fn smoke_echo() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let result: String = redis::cmd("ECHO") .arg("hello world") .query_async(&mut con) .await .unwrap(); assert_eq!(result, "hello world"); m.close().await; } #[tokio::test] async fn smoke_dbsize_flushdb() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let _: () = con.set("a", "1").await.unwrap(); let _: () = con.set("b", "2").await.unwrap(); let size: i64 = redis::cmd("DBSIZE").query_async(&mut con).await.unwrap(); assert_eq!(size, 2); let _: () = redis::cmd("FLUSHDB").query_async(&mut con).await.unwrap(); let size: i64 = redis::cmd("DBSIZE").query_async(&mut con).await.unwrap(); assert_eq!(size, 0); m.close().await; } #[tokio::test] async fn smoke_set_nx_xx() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); // NX: set only if not exists let result: bool = con.set_nx("nxkey", "first").await.unwrap(); assert!(result); let result: bool = con.set_nx("nxkey", "second").await.unwrap(); assert!(!result); let val: String = con.get("nxkey").await.unwrap(); assert_eq!(val, "first"); m.close().await; } #[tokio::test] async fn smoke_select_db() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); let _: () = con.set("key", "db0").await.unwrap(); // SELECT 1 and set a different value let _: () = redis::cmd("SELECT") .arg(1) .query_async(&mut con) .await .unwrap(); let _: () = con.set("key", "db1").await.unwrap(); // Back to db 0 let _: () = redis::cmd("SELECT") .arg(0) .query_async(&mut con) .await .unwrap(); let val: String = con.get("key").await.unwrap(); assert_eq!(val, "db0"); m.close().await; } #[tokio::test] async fn smoke_fast_forward() { let m = miniredis_rs::Miniredis::run().await.unwrap(); let client = redis::Client::open(m.redis_url()).unwrap(); let mut con = client.get_multiplexed_async_connection().await.unwrap(); // SET with EX 10 let _: () = redis::cmd("SET") .arg("temp") .arg("val") .arg("EX") .arg(10) .query_async(&mut con) .await .unwrap(); // Key should exist let val: Option = con.get("temp").await.unwrap(); assert_eq!(val, Some("val".to_owned())); // Fast forward 11 seconds m.fast_forward(std::time::Duration::from_secs(11)); // Key should be gone (lazy expiration on next access) let val: Option = con.get("temp").await.unwrap(); assert_eq!(val, None); m.close().await; } ================================================ FILE: parser/encoding/rpc.go ================================================ package encoding import ( "fmt" "reflect" "slices" "sort" "strings" "github.com/cockroachdb/errors" "github.com/golang/protobuf/proto" "encr.dev/pkg/idents" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) // ParameterLocation is the request/response home of the parameter type ParameterLocation string const ( Undefined ParameterLocation = "undefined" // Parameter location is Undefined Header ParameterLocation = "header" // Parameter is placed in the HTTP header Query ParameterLocation = "query" // Parameter is placed in the query string Body ParameterLocation = "body" // Parameter is placed in the body Cookie ParameterLocation = "cookie" // Parameter is placed in cookies ) var ( QueryTag = tagDescription{ location: Query, overrideDefault: true, } QsTag = QueryTag HeaderTag = tagDescription{ location: Header, overrideDefault: true, wireFormatter: strings.ToLower, } JSONTag = tagDescription{ location: Body, omitEmptyOption: "omitempty", overrideDefault: false, } CookieTag = tagDescription{ location: Cookie, omitEmptyOption: "omitempty", overrideDefault: true, } ) // authTags is a description of tags used for auth var authTags = map[string]tagDescription{ "query": QueryTag, "header": HeaderTag, "cookie": CookieTag, } // requestTags is a description of tags used for requests var requestTags = map[string]tagDescription{ "query": QueryTag, "qs": QsTag, "header": HeaderTag, "cookie": CookieTag, "json": JSONTag, } // responseTags is a description of tags used for responses var responseTags = map[string]tagDescription{ "header": HeaderTag, "cookie": CookieTag, "json": JSONTag, } // tagDescription is used to map struct field tags to param locations // if overrideDefault is set, tagDescription.location will be used instead of encodingHints.defaultLocation // if the tag matches the paramLocation, the param name will be replaced with the // tag name type tagDescription struct { location ParameterLocation overrideDefault bool omitEmptyOption string wireFormatter func(name string) string } // encodingHints is used to determine the default location and applicable tag overrides for http // request/response encoding type encodingHints struct { defaultLocation ParameterLocation tags map[string]tagDescription options *Options } // RPCEncoding expresses how an RPC should be encoded on the wire for both the request and responses. type RPCEncoding struct { Name string `json:"name"` Doc string `json:"doc"` AccessType string `json:"access_type"` Proto string `json:"proto"` Path *meta.Path `json:"path"` HttpMethods []string `json:"http_methods"` DefaultMethod string `json:"default_method"` // Expresses how the default request encoding and method should be // Note: DefaultRequestEncoding.HTTPMethods will always be a slice with length 1 DefaultRequestEncoding *RequestEncoding `json:"request_encoding"` // Expresses all the different ways the request can be encoded for this RPC RequestEncoding []*RequestEncoding `json:"all_request_encodings"` // Expresses how the response to this RPC will be encoded ResponseEncoding *ResponseEncoding `json:"response_encoding"` } // RequestEncodingForMethod returns the request encoding required for the given HTTP method. // If the method is not supported by the RPC it reports nil. func (e *RPCEncoding) RequestEncodingForMethod(method string) *RequestEncoding { var wildcardOption *RequestEncoding for _, reqEnc := range e.RequestEncoding { for _, m := range reqEnc.HTTPMethods { if strings.EqualFold(m, method) { return reqEnc } if m == "*" { wildcardOption = reqEnc } } } return wildcardOption } // AuthEncoding expresses how a response should be encoded on the wire. type AuthEncoding struct { // LegacyTokenFormat specifies whether the auth encoding uses the legacy format of // "just give us a token as a string". If true, the other parameters are all empty. LegacyTokenFormat bool // Contains metadata about how to marshal an HTTP parameter HeaderParameters []*ParameterEncoding `json:"header_parameters"` QueryParameters []*ParameterEncoding `json:"query_parameters"` CookieParameters []*ParameterEncoding `json:"cookie_parameters"` } // ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName. func (e *AuthEncoding) ParameterEncodingMap() map[string]*ParameterEncoding { return toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.CookieParameters) } // ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name. // Conflicts result in an undefined encoding getting set. func (e *AuthEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding { return toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.CookieParameters) } // ResponseEncoding expresses how a response should be encoded on the wire type ResponseEncoding struct { // Contains metadata about how to marshal an HTTP parameter HeaderParameters []*ParameterEncoding `json:"header_parameters"` CookieParameters []*ParameterEncoding `json:"cookie_parameters"` BodyParameters []*ParameterEncoding `json:"body_parameters"` } // ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName. func (e *ResponseEncoding) ParameterEncodingMap() map[string]*ParameterEncoding { return toEncodingMap(srcNameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters) } // ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name. // Conflicts result in an undefined encoding getting set. func (e *ResponseEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding { return toEncodingMultiMap(nameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters) } // RequestEncoding expresses how a request should be encoded for an explicit set of HTTPMethods type RequestEncoding struct { // The HTTP methods these field configurations can be used for HTTPMethods []string `json:"http_methods"` // Contains metadata about how to marshal an HTTP parameter HeaderParameters []*ParameterEncoding `json:"header_parameters"` QueryParameters []*ParameterEncoding `json:"query_parameters"` CookieParameters []*ParameterEncoding `json:"cookie_parameters"` BodyParameters []*ParameterEncoding `json:"body_parameters"` } // ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName. func (e *RequestEncoding) ParameterEncodingMap() map[string]*ParameterEncoding { return toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters) } // ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name. // Conflicts result in an undefined encoding getting set. func (e *RequestEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding { return toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters) } // ParameterEncoding expresses how a parameter should be encoded on the wire type ParameterEncoding struct { // The location specific name of the parameter (e.g. cheeseEater, cheese-eater, X-Cheese-Eater) Name string `json:"name"` // Location is the location this encoding is for. Location ParameterLocation `json:"location"` // OmitEmpty specifies whether the parameter should be omitted if it's empty. OmitEmpty bool `json:"omit_empty"` // SrcName is the name of the struct field SrcName string `json:"src_name"` // Doc is the documentation of the struct field Doc string `json:"doc"` // Type is the field's type description. Type *schema.Type `json:"type"` // RawTag specifies the raw, unparsed struct tag for the field. RawTag string `json:"raw_tag"` // WireFormat is the wire format of the parameter. WireFormat string `json:"wire_format"` // Optional indicates whether the field is optional. Optional bool `json:"optional"` } type Options struct { // SrcNameTag, if set, specifies which source tag should be used to determine // the value of the SrcName field in the returned parameter descriptions. // // If the given SrcNameTag is not present on the field, SrcName will be set // to the Go field name instead. // // If SrcNameTag is empty, SrcName is set to the Go field name. SrcNameTag string } type APIEncoding struct { Services []*ServiceEncoding `json:"services"` Authorization *AuthEncoding `json:"authorization"` } type ServiceEncoding struct { Name string `json:"name"` Doc string `json:"doc"` RPCs []*RPCEncoding `json:"rpcs"` } func DescribeAPI(meta *meta.Data) *APIEncoding { api := &APIEncoding{Services: make([]*ServiceEncoding, len(meta.Svcs))} for i, s := range meta.Svcs { api.Services[i] = DescribeService(meta, s) } if meta.AuthHandler == nil { return api } var err error api.Authorization, err = DescribeAuth(meta, meta.AuthHandler.Params, nil) if err != nil { panic(fmt.Sprintf("Invalid auth definition: %s: %v", meta.AuthHandler.Name, err)) } return api } func findDoc(relPath string, meta *meta.Data) string { for _, p := range meta.Pkgs { if p.RelPath == relPath { return p.Doc } } return "" } func DescribeService(meta *meta.Data, svc *meta.Service) *ServiceEncoding { service := &ServiceEncoding{Name: svc.Name, Doc: findDoc(svc.RelPath, meta), RPCs: make([]*RPCEncoding, len(svc.Rpcs))} for i, r := range svc.Rpcs { rpc, err := DescribeRPC(meta, r, nil) if err != nil { panic(fmt.Sprintf("invalid rpc: %v", err)) } service.RPCs[i] = rpc } return service } // DescribeRPC expresses how to encode an RPCs request and response objects for the wire. func DescribeRPC(appMetaData *meta.Data, rpc *meta.RPC, options *Options) (*RPCEncoding, error) { encoding := &RPCEncoding{ DefaultMethod: DefaultClientHttpMethod(rpc), Name: rpc.Name, AccessType: rpc.AccessType.String(), Proto: rpc.Proto.String(), Path: rpc.Path, Doc: rpc.GetDoc(), } var err error // Work out the request encoding encoding.RequestEncoding, err = DescribeRequest(appMetaData, rpc.RequestSchema, options, rpc.HttpMethods...) if err != nil { return nil, errors.Wrap(err, "request encoding") } // Work out the response encoding encoding.ResponseEncoding, err = DescribeResponse(appMetaData, rpc.ResponseSchema, options) if err != nil { return nil, errors.Wrap(err, "response encoding") } if encoding.RequestEncoding != nil { // Setup the default request encoding defaultEncoding := encoding.RequestEncodingForMethod(encoding.DefaultMethod) encoding.DefaultRequestEncoding = &RequestEncoding{ HTTPMethods: []string{encoding.DefaultMethod}, HeaderParameters: defaultEncoding.HeaderParameters, BodyParameters: defaultEncoding.BodyParameters, QueryParameters: defaultEncoding.QueryParameters, CookieParameters: defaultEncoding.CookieParameters, } } return encoding, nil } // GetConcreteStructType returns a construct Struct object for the given schema. This means any generic types // in the struct will be resolved to their concrete types and there will be no generic parameters in the struct object. // However, any nested structs may still contain generic types. // // If a nil schema is provided, a nil struct is returned. func GetConcreteStructType(appDecls []*schema.Decl, typ *schema.Type, typeArgs []*schema.Type) (*schema.Struct, error) { // dereference pointers pointer := typ.GetPointer() for pointer != nil { typ = pointer.Base pointer = typ.GetPointer() } typ, err := GetConcreteType(appDecls, typ, typeArgs) if err != nil { return nil, err } struc := typ.GetStruct() if struc == nil { return nil, errors.Newf("unsupported type %+v", reflect.TypeOf(typ.Typ)) } return struc, nil } // GetConcreteType returns a concrete type for the given schema. This means any generic types // in the top level type will be resolved to their concrete types and there will be no generic parameters in returned typ. // However, any nested types may still contain generic types. // // If a nil schema is provided, a nil is returned. func GetConcreteType(appDecls []*schema.Decl, originalType *schema.Type, typeArgs []*schema.Type) (*schema.Type, error) { if originalType == nil { // If there's no schema type, we want to shortcut return nil, nil } switch typ := originalType.Typ.(type) { case *schema.Type_Struct: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct struc, ok := proto.Clone(typ.Struct).(*schema.Struct) if !ok { return nil, errors.New("failed to clone struct") } // replace any type parameters with the type argument for _, field := range struc.Fields { field.Typ = resolveTypeParams(field.Typ, typeArgs) } return &schema.Type{Typ: &schema.Type_Struct{Struct: struc}}, nil case *schema.Type_Map: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct mapType, ok := proto.Clone(typ.Map).(*schema.Map) if !ok { return nil, errors.New("failed to clone map") } return resolveTypeParams(&schema.Type{Typ: &schema.Type_Map{Map: mapType}}, typeArgs), nil case *schema.Type_Union: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } types := make([]*schema.Type, len(typ.Union.Types)) for i, t := range typ.Union.Types { // Deep copy the type cloned := proto.Clone(t).(*schema.Type) types[i] = resolveTypeParams(cloned, typeArgs) } return &schema.Type{Typ: &schema.Type_Union{ Union: &schema.Union{ Types: types, }, }}, nil case *schema.Type_List: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct list, ok := proto.Clone(typ.List).(*schema.List) if !ok { return nil, errors.New("failed to clone list type") } // replace any type parameters with the type argument return resolveTypeParams(&schema.Type{Typ: &schema.Type_List{List: list}}, typeArgs), nil case *schema.Type_Pointer: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct pointer, ok := proto.Clone(typ.Pointer).(*schema.Pointer) if !ok { return nil, errors.New("failed to clone pointer type") } var err error pointer.Base, err = GetConcreteType(appDecls, pointer.Base, typeArgs) if err != nil { return nil, err } // replace any type parameters with the type argument return resolveTypeParams(&schema.Type{Typ: &schema.Type_Pointer{Pointer: pointer}}, typeArgs), nil case *schema.Type_Option: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct option, ok := proto.Clone(typ.Option).(*schema.Option) if !ok { return nil, errors.New("failed to clone option type") } var err error option.Value, err = GetConcreteType(appDecls, option.Value, typeArgs) if err != nil { return nil, err } // replace any type parameters with the type argument return resolveTypeParams(&schema.Type{Typ: &schema.Type_Option{Option: option}}, typeArgs), nil case *schema.Type_Config: // If there are no type arguments, we've got a concrete type if len(typeArgs) == 0 { return originalType, nil } // Deep copy the original struct config, ok := proto.Clone(typ.Config).(*schema.ConfigValue) if !ok { return nil, errors.New("failed to clone config type") } // replace any type parameters with the type argument return resolveTypeParams(&schema.Type{Typ: &schema.Type_Config{Config: config}}, typeArgs), nil case *schema.Type_Named: decl := appDecls[typ.Named.Id] return GetConcreteType(appDecls, decl.Type, typ.Named.TypeArguments) case *schema.Type_Builtin: return originalType, nil case *schema.Type_Literal: return originalType, nil default: return nil, errors.Newf("unsupported type %+v", reflect.TypeOf(typ)) } } // resolveTypeParams resolves any type parameters in the given type to the given type arguments. // only at the top level object - so nested type arguments are not resolved func resolveTypeParams(typ *schema.Type, typeArgs []*schema.Type) *schema.Type { switch t := typ.Typ.(type) { case *schema.Type_TypeParameter: return typeArgs[t.TypeParameter.ParamIdx] case *schema.Type_Struct: for _, field := range t.Struct.Fields { field.Typ = resolveTypeParams(field.Typ, typeArgs) } case *schema.Type_List: t.List.Elem = resolveTypeParams(t.List.Elem, typeArgs) case *schema.Type_Map: t.Map.Key = resolveTypeParams(t.Map.Key, typeArgs) t.Map.Value = resolveTypeParams(t.Map.Value, typeArgs) case *schema.Type_Config: t.Config.Elem = resolveTypeParams(t.Config.Elem, typeArgs) case *schema.Type_Pointer: t.Pointer.Base = resolveTypeParams(t.Pointer.Base, typeArgs) case *schema.Type_Option: t.Option.Value = resolveTypeParams(t.Option.Value, typeArgs) case *schema.Type_Named: for i, param := range t.Named.TypeArguments { t.Named.TypeArguments[i] = resolveTypeParams(param, typeArgs) } } return typ } // DefaultClientHttpMethod works out the default HTTP method a client should use for a given RPC. // When possible we will default to POST either when no method has been specified on the API or when // then is a selection of methods and POST is one of them. If POST is not allowed as a method then // we will use the first specified method. func DefaultClientHttpMethod(rpc *meta.RPC) string { // Default to POST if we have a wildcard method or if POST is one of the allowed methods. if rpc.HttpMethods[0] == "*" || slices.Contains(rpc.HttpMethods, "POST") { return "POST" } return rpc.HttpMethods[0] } // DescribeAuth generates a ParameterEncoding per field of the auth struct and returns it as // the AuthEncoding. If authSchema is nil it returns nil, nil. func DescribeAuth(appMetaData *meta.Data, authSchema *schema.Type, options *Options) (*AuthEncoding, error) { if authSchema == nil { return nil, nil } switch t := authSchema.Typ.(type) { case *schema.Type_Builtin: if t.Builtin != schema.Builtin_STRING { return nil, errors.Newf("unsupported auth parameter %v", errors.Safe(t.Builtin)) } return &AuthEncoding{LegacyTokenFormat: true}, nil case *schema.Type_Named: case *schema.Type_Pointer: default: return nil, errors.Newf("unsupported auth parameter type %T", errors.Safe(t)) } authStruct, err := GetConcreteStructType(appMetaData.Decls, authSchema, nil) if err != nil { return nil, errors.Wrap(err, "auth struct") } fields, err := describeParams(appMetaData.Language, &encodingHints{Undefined, authTags, options}, authStruct) if err != nil { return nil, err } if locationDiff := keyDiff(fields, Header, Query, Cookie); len(locationDiff) > 0 { return nil, errors.Newf("auth must only contain query, header, and cookie parameters. Found: %v", locationDiff) } return &AuthEncoding{ QueryParameters: fields[Query], HeaderParameters: fields[Header], CookieParameters: fields[Cookie], }, nil } // DescribeResponse generates a ParameterEncoding per field of the response struct and returns it as // the ResponseEncoding func DescribeResponse(appMetaData *meta.Data, responseSchema *schema.Type, options *Options) (*ResponseEncoding, error) { if responseSchema == nil { return nil, nil } responseStruct, err := GetConcreteStructType(appMetaData.Decls, responseSchema, nil) if err != nil { return nil, errors.Wrap(err, "response struct") } fields, err := describeParams(appMetaData.Language, &encodingHints{Body, responseTags, options}, responseStruct) if err != nil { return nil, err } if keys := keyDiff(fields, Header, Body, Cookie); len(keys) > 0 { return nil, errors.Newf("response must only contain body, header and cookie parameters. Found: %v", keys) } return &ResponseEncoding{ BodyParameters: fields[Body], HeaderParameters: fields[Header], CookieParameters: fields[Cookie], }, nil } // keyDiff returns the diff between src.keys and keys func keyDiff[T comparable, V any](src map[T]V, keys ...T) (diff []T) { for k := range src { if !slices.Contains(keys, k) { diff = append(diff, k) } } return diff } // DescribeRequest groups the provided httpMethods by default ParameterLocation and returns a RequestEncoding // per ParameterLocation func DescribeRequest(appMetaData *meta.Data, requestSchema *schema.Type, options *Options, httpMethods ...string) ([]*RequestEncoding, error) { methodsByDefaultLocation := make(map[ParameterLocation][]string) for _, m := range httpMethods { switch m { case "GET", "HEAD", "DELETE": methodsByDefaultLocation[Query] = append(methodsByDefaultLocation[Query], m) case "*": methodsByDefaultLocation[Body] = []string{"POST", "PUT", "PATCH"} methodsByDefaultLocation[Query] = []string{"GET", "HEAD", "DELETE"} default: methodsByDefaultLocation[Body] = append(methodsByDefaultLocation[Body], m) } } var requestStruct *schema.Struct var err error if requestSchema != nil { requestStruct, err = GetConcreteStructType(appMetaData.Decls, requestSchema, nil) if err != nil { return nil, errors.Wrap(err, "request struct") } } var reqs []*RequestEncoding for location, methods := range methodsByDefaultLocation { var fields map[ParameterLocation][]*ParameterEncoding if requestStruct != nil { fields, err = describeParams(appMetaData.Language, &encodingHints{location, requestTags, options}, requestStruct) if err != nil { return nil, err } } if keys := keyDiff(fields, Query, Header, Body, Cookie); len(keys) > 0 { return nil, errors.Newf("request must only contain Query, Body, Header and Cookie parameters. Found: %v", keys) } reqs = append(reqs, &RequestEncoding{ HTTPMethods: methods, QueryParameters: fields[Query], HeaderParameters: fields[Header], CookieParameters: fields[Cookie], BodyParameters: fields[Body], }) } // Sort by first method to get a deterministic order (list is randomized by map above) sort.Slice(reqs, func(i, j int) bool { return reqs[i].HTTPMethods[0] < reqs[j].HTTPMethods[0] }) return reqs, nil } // describeParams calls describeParam() for each field in the payload struct func describeParams(lang meta.Lang, encodingHints *encodingHints, payload *schema.Struct) (fields map[ParameterLocation][]*ParameterEncoding, err error) { paramByLocation := make(map[ParameterLocation][]*ParameterEncoding) for _, f := range payload.GetFields() { f, err := describeParam(lang, encodingHints, f) if err != nil { return nil, err } if f != nil { paramByLocation[f.Location] = append(paramByLocation[f.Location], f) } } return paramByLocation, nil } // formatName formats a parameter name with the default formatting for the location (e.g. snakecase for query) func formatName(lang meta.Lang, location ParameterLocation, name string) string { if location == Query && lang == meta.Lang_GO { return idents.Convert(name, idents.SnakeCase) } return name } // IgnoreField returns true if the field name is "-" is any of the valid request or response tags // or if the field is marked with encore:"httpstatus" (which shouldn't appear in client types) func IgnoreField(field *schema.Field) bool { for _, tag := range field.Tags { if _, found := requestTags[tag.Key]; found && tag.Name == "-" { return true } // Skip fields with encore:"httpstatus" tag - they're for internal HTTP status handling only if tag.Key == "encore" && tag.Name == "httpstatus" { return true } } return false } // describeParam returns the ParameterEncoding which uses field tags to describe how the parameter // (e.g. qs, query, header) should be encoded in HTTP (name and location). // // It returns nil, nil if the field is not to be encoded. func describeParam(lang meta.Lang, encodingHints *encodingHints, field *schema.Field) (*ParameterEncoding, error) { location := encodingHints.defaultLocation name := formatName(lang, encodingHints.defaultLocation, field.Name) param := ParameterEncoding{ Name: name, OmitEmpty: false, SrcName: field.Name, Doc: field.Doc, Type: field.Typ, RawTag: field.RawTag, Optional: field.Optional, WireFormat: name, } var usedOverrideTag string for _, tag := range field.Tags { if IgnoreField(field) { return nil, nil } tagHint, ok := encodingHints.tags[tag.Key] if !ok { continue } if tagHint.overrideDefault { if usedOverrideTag != "" { return nil, errors.Newf("tag conflict: %s cannot be combined with %s", usedOverrideTag, tag.Key) } location = tagHint.location usedOverrideTag = tag.Key } if tagHint.location == location { if tag.Name != "" { param.Name = tag.Name if tagHint.wireFormatter != nil { param.WireFormat = tagHint.wireFormatter(tag.Name) } else { param.WireFormat = tag.Name } } } if tagHint.omitEmptyOption != "" { for _, o := range tag.Options { if o == tagHint.omitEmptyOption { param.OmitEmpty = true } } } if encodingHints.options != nil && tag.Key == encodingHints.options.SrcNameTag { param.SrcName = tag.Name } } if param.Name == "-" { return nil, nil } param.Location = location return ¶m, nil } // toEncodingMap returns a map from SrcName to parameter encodings. func toEncodingMap(keyFunc func(e *ParameterEncoding) string, encodings ...[]*ParameterEncoding) map[string]*ParameterEncoding { res := make(map[string]*ParameterEncoding) for _, e := range encodings { for _, param := range e { res[keyFunc(param)] = param } } return res } // toEncodingMultiMap returns a map from a key to the list of parameter encodings // matching that key. func toEncodingMultiMap(keyFunc func(e *ParameterEncoding) string, encodings ...[]*ParameterEncoding) map[string][]*ParameterEncoding { res := make(map[string][]*ParameterEncoding) for _, e := range encodings { for _, param := range e { key := keyFunc(param) res[key] = append(res[key], param) } } return res } func srcNameKey(e *ParameterEncoding) string { return e.SrcName } func nameKey(e *ParameterEncoding) string { return e.Name } ================================================ FILE: pkg/ansi/ansi.go ================================================ // Package ansi provides helper functions for writing ANSI terminal escape codes. package ansi import "fmt" // SetCursorPosition returns the ANSI escape code for setting the cursor position. // The rows and columns are one-based. If <=0 they default to the first row/column. func SetCursorPosition(row, col int) string { if row <= 0 { row = 1 } if col <= 0 { col = 1 } return fmt.Sprintf("\u001b[%d;%dH", row, col) } type ClearScreenMethod int const ( CursorToBottom ClearScreenMethod = 0 CursorToTop ClearScreenMethod = 1 WholeScreen ClearScreenMethod = 2 WholeScreenAndScrollback ClearScreenMethod = 3 ) // ClearScreen clears the screen according to the given method. func ClearScreen(method ClearScreenMethod) string { return fmt.Sprintf("\u001b[%dJ", method) } type ClearLineMethod int const ( CursorToEnd ClearLineMethod = 0 // cursor to end of line CursorToStart ClearLineMethod = 1 // cursor to start of line WholeLine ClearLineMethod = 2 ) // ClearLine clears the current line according to the given method. // The cursor position within the line does not change. func ClearLine(method ClearLineMethod) string { return fmt.Sprintf("\u001b[%dK", method) } const ( // SaveCursorPosition saves the current cursor position. SaveCursorPosition = "\u001b7" // RestoreCursorPosition restores the cursor position to the saved position. RestoreCursorPosition = "\u001b8" ) // MoveCursorLeft moves the cursor left n cells. // If the cursor is already at the edge of the screen it has no effect. // If n is negative it moves to the right instead. func MoveCursorLeft(n int) string { if n < 0 { return MoveCursorRight(-n) } return fmt.Sprintf("\u001b[%dD", n) } // MoveCursorRight moves the cursor right n cells. // If the cursor is already at the edge of the screen it has no effect. // If n is negative it moves to the left instead. func MoveCursorRight(n int) string { if n < 0 { return MoveCursorLeft(-n) } return fmt.Sprintf("\u001b[%dC", n) } ================================================ FILE: pkg/appfile/appfile.go ================================================ // Package appfile reads and writes encore.app files. package appfile import ( "context" "encoding/json" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "github.com/tailscale/hujson" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" "encore.dev/appruntime/exported/experiments" ) // Name is the name of the Encore app file. // It is expected to be located in the root of the Encore app // (which is usually the Git repository root). const Name = "encore.app" type Lang string const ( LangGo Lang = "go" LangTS Lang = "typescript" ) // File is a parsed encore.app file. type File struct { // ID is the encore.dev app id for the app. // It is empty if the app is not linked to encore.dev. ID string `json:"id"` // can be empty // Experiments is a list of values to enable experimental features in Encore. // These are not guaranteed to be stable in either runtime behaviour // or in API design. // // Do not use these features in production without consulting the Encore team. Experiments []experiments.Name `json:"experiments,omitempty"` // Lang is the language the app is written in. // If empty it defaults to Go. Lang Lang `json:"lang"` // Configure global CORS settings for the application which // will be applied to all API gateways into the application. GlobalCORS *CORS `json:"global_cors,omitempty"` // Build contains build settings for the application. Build Build `json:"build,omitempty"` // CgoEnabled enables building with cgo. // // Deprecated: Use build.cgo_enabled instead. CgoEnabled bool `json:"cgo_enabled,omitempty"` // DockerBaseImage changes the docker base image used for building the application // in Encore's CI/CD system. If unspecified it defaults to "scratch". // // Deprecated: Use build.docker.base_image instead. DockerBaseImage string `json:"docker_base_image,omitempty"` // LogLevel is the minimum log level for the app. // If empty it defaults to "trace". LogLevel string `json:"log_level,omitempty"` } type Build struct { // CgoEnabled enables building with cgo. CgoEnabled bool `json:"cgo_enabled,omitempty"` // Docker configures the docker images built // by Encore's CI/CD system. Docker Docker `json:"docker,omitempty"` // WorkerPooling enables worker pooling for Encore.ts. WorkerPooling bool `json:"worker_pooling,omitempty"` // Hooks configures hooks for the build process. Hooks Hooks `json:"hooks,omitempty"` } type Hooks struct { PreBuild Hook `json:"prebuild,omitempty"` PostBuild Hook `json:"postbuild,omitempty"` } // Hook represents a build hook command. // Can be specified as a string or as an object with command and env. type Hook struct { Command string // The command to execute Env map[string]string // Optional environment variables } // IsSet returns true if the hook is configured. func (h Hook) IsSet() bool { return h.Command != "" } // UnmarshalJSON handles both string and object formats. func (h *Hook) UnmarshalJSON(data []byte) error { // Try string format first var cmd string if err := json.Unmarshal(data, &cmd); err == nil { h.Command = cmd return nil } // Try structured format type hookData struct { Command string `json:"command"` Env map[string]string `json:"env"` } var hd hookData if err := json.Unmarshal(data, &hd); err != nil { return err } h.Command = hd.Command h.Env = hd.Env return nil } // Run executes the hook command with shell-style parsing and variable expansion. // Supports shell operators like &&, ||, and pipes. func (h Hook) Run(ctx context.Context, dir string, stdout, stderr io.Writer) error { if h.Command == "" { return nil } // Parse the command as a shell script file, err := syntax.NewParser().Parse(strings.NewReader(h.Command), "") if err != nil { return fmt.Errorf("parse command: %w", err) } // Build environment: system env + custom env vars env := os.Environ() for k, v := range h.Env { env = append(env, k+"="+v) } // Create interpreter with environment and I/O runner, err := interp.New( interp.Env(expand.ListEnviron(env...)), interp.Dir(dir), interp.StdIO(nil, stdout, stderr), ) if err != nil { return fmt.Errorf("create interpreter: %w", err) } return runner.Run(ctx, file) } // String returns the command string. func (h Hook) String() string { return h.Command } type Docker struct { // BaseImage changes the docker base image used for building the application // in Encore's CI/CD system. If unspecified it defaults to "scratch". BaseImage string `json:"base_image,omitempty"` // BundleSource determines whether the source code of the application // should be bundled into the binary, at "/workspace". BundleSource bool `json:"bundle_source,omitempty"` // WorkingDir specifies the working directory to start the docker image in. // If empty it defaults to "/workspace" if the source code is bundled, and to "/" otherwise. WorkingDir string `json:"working_dir,omitempty"` // ProcessPerService specifies whether each service should run in its own process. If false, // all services are run in the same process. ProcessPerService bool `json:"process_per_service,omitempty"` } type CORS struct { // Debug enables CORS debug logging. Debug bool `json:"debug,omitempty"` // AllowHeaders allows an app to specify additional headers that should be // accepted by the app. // // If the list contains "*", then all headers are allowed. AllowHeaders []string `json:"allow_headers"` // ExposeHeaders allows an app to specify additional headers that should be // exposed from the app, beyond the default set always recognized by Encore. // // If the list contains "*", then all headers are exposed. ExposeHeaders []string `json:"expose_headers"` // AllowOriginsWithoutCredentials specifies the allowed origins for requests // that don't include credentials. If nil it defaults to allowing all domains // (equivalent to []string{"*"}). AllowOriginsWithoutCredentials []string `json:"allow_origins_without_credentials,omitempty"` // AllowOriginsWithCredentials specifies the allowed origins for requests // that include credentials. If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). AllowOriginsWithCredentials []string `json:"allow_origins_with_credentials,omitempty"` } // Parse parses the app file data into a File. func Parse(data []byte) (*File, error) { var f File data, err := hujson.Standardize(data) if err == nil { err = json.Unmarshal(data, &f) } if err != nil { return nil, fmt.Errorf("appfile.Parse: %v", err) } switch f.Lang { case LangGo, LangTS: // Do nothing case "": f.Lang = LangGo default: return nil, fmt.Errorf("appfile.Parse: invalid lang %q", f.Lang) } // Parse deprecated fields into the new Build struct. f.Build.CgoEnabled = f.Build.CgoEnabled || f.CgoEnabled if f.Build.Docker.BaseImage == "" { f.Build.Docker.BaseImage = f.DockerBaseImage } return &f, nil } // ParseFile parses the app file located at path. func ParseFile(path string) (*File, error) { data, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return &File{}, nil } else if err != nil { return nil, fmt.Errorf("appfile.ParseFile: %w", err) } return Parse(data) } // ParseFileStrict parses the app file located at path. // Unlike ParseFile, it returns an error if the file does not exist. func ParseFileStrict(path string) (*File, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("appfile.ParseFileStrict: %w", err) } return Parse(data) } // Slug parses the app slug for the encore.app file located at path. // The slug can be empty if the app is not linked to encore.dev. func Slug(appRoot string) (string, error) { f, err := ParseFile(filepath.Join(appRoot, Name)) if err != nil { return "", err } return f.ID, nil } // Experiments returns the experimental feature the app located // at appRoot has opted into. func Experiments(appRoot string) ([]experiments.Name, error) { f, err := ParseFile(filepath.Join(appRoot, Name)) if err != nil { return nil, err } return f.Experiments, nil } // GlobalCORS returns the global CORS settings for the app located func GlobalCORS(appRoot string) (*CORS, error) { f, err := ParseFile(filepath.Join(appRoot, Name)) if err != nil { return nil, err } return f.GlobalCORS, nil } // AppLang returns the language of the app located at appRoot. func AppLang(appRoot string) (Lang, error) { f, err := ParseFile(filepath.Join(appRoot, Name)) if err != nil { return "", err } return f.Lang, nil } ================================================ FILE: pkg/bits/bits.go ================================================ package bits import ( "context" "encoding/json" "io" "net/http" "net/url" "github.com/cockroachdb/errors" ) type Bit struct { Slug string Title string Description string // GitHubTree is a URL to the GitHub tree for this bit, // in the format expected by github.ParseTree. GitHubTree string } // List lists available bits. func List(ctx context.Context) ([]*Bit, error) { resp, err := http.Get("https://automativity.encore.dev/bits") if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { slurp, _ := io.ReadAll(resp.Body) return nil, errors.Newf("got status %d: %s", resp.StatusCode, slurp) } var data struct { Bits []*Bit } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, errors.Wrap(err, "decode json response") } return data.Bits, nil } // ErrNotFound is reported by Get if the bit with the given slug is not found. var ErrNotFound = errors.New("bit not found") // Get retrieves a bit by its slug. func Get(ctx context.Context, slug string) (*Bit, error) { resp, err := http.Get("https://automativity.encore.dev/bits/" + url.PathEscape(slug)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == 404 { return nil, ErrNotFound } else if resp.StatusCode != 200 { slurp, _ := io.ReadAll(resp.Body) return nil, errors.Newf("got status %d: %s", resp.StatusCode, slurp) } var bit Bit if err := json.NewDecoder(resp.Body).Decode(&bit); err != nil { return nil, errors.Wrap(err, "decode json response") } return &bit, nil } ================================================ FILE: pkg/bits/download.go ================================================ package bits import ( "context" "go/token" "runtime" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "encr.dev/pkg/github" "encr.dev/pkg/paths" "encr.dev/v2/app" "encr.dev/v2/internals/parsectx" "encr.dev/v2/internals/perr" "encr.dev/v2/parser" ) // Extract downloads and extracts a bit into a given directory. func Extract(ctx context.Context, b *Bit, dst string) error { tree, err := github.ParseTree(ctx, b.GitHubTree) if err != nil { return err } return github.ExtractTree(ctx, tree, dst) } // Describe describes the contents of the bit extracted in dir. func Describe(ctx context.Context, dir string) (desc *app.Desc, err error) { fs := token.NewFileSet() errs := perr.NewList(ctx, fs) pc := &parsectx.Context{ Ctx: ctx, Log: zerolog.Logger{}, Build: parsectx.BuildInfo{ Experiments: nil, GOARCH: runtime.GOARCH, GOOS: runtime.GOOS, }, MainModuleDir: paths.RootedFSPath(dir, "."), FS: fs, ParseTests: false, Errs: errs, } defer func() { if l, ok := perr.CatchBailout(recover()); ok { err = errors.Newf("parse failure:\n%s", l.FormatErrors()) } else if errs.Len() > 0 { err = errors.Newf("parse failure:\n%s", errs.FormatErrors()) } }() pp := parser.NewParser(pc) res := pp.Parse() return app.ValidateAndDescribe(pc, res), nil } ================================================ FILE: pkg/builder/builder.go ================================================ package builder import ( "context" "errors" "io" "io/fs" "os" pathspkg "path" "runtime" "slices" "github.com/rs/zerolog" "encore.dev/appruntime/exported/experiments" "encr.dev/cli/daemon/apps" "encr.dev/internal/optracker" "encr.dev/internal/version" "encr.dev/pkg/cueutil" "encr.dev/pkg/fns" "encr.dev/pkg/option" "encr.dev/pkg/paths" meta "encr.dev/proto/encore/parser/meta/v1" ) var LocalBuildTags = []string{ "encore_local", "encore_no_gcp", "encore_no_aws", "encore_no_azure", "encore_no_datadog", "encore_no_prometheus", } // DebugMode specifies how to compile the application for debugging. type DebugMode string const ( DebugModeDisabled DebugMode = "disabled" DebugModeEnabled DebugMode = "enabled" DebugModeBreak DebugMode = "break" ) type BuildInfo struct { BuildTags []string CgoEnabled bool StaticLink bool DebugMode DebugMode Environ []string GOOS, GOARCH string KeepOutput bool Revision string UncommittedChanges bool // MainPkg is the path to the existing main package to use, if any. MainPkg option.Option[paths.Pkg] // Overrides to explicitly set the GoRoot and EncoreRuntime paths. // if not set, they will be inferred from the current executable. GoRoot option.Option[paths.FS] EncoreRuntimes option.Option[paths.FS] // UseLocalJSRuntime specifies whether to override the installed Encore version // with the local JS runtime. UseLocalJSRuntime bool // Logger allows a custom logger to be used by the various phases of the builder. Logger option.Option[zerolog.Logger] // DisableSensitiveScrubbing, if true, disables scrubbing of sensitive fields. // Used for local development. DisableSensitiveScrubbing bool } func (b *BuildInfo) IsCrossBuild() bool { return b.GOOS != runtime.GOOS || b.GOARCH != runtime.GOARCH } // DefaultBuildInfo returns a BuildInfo with default values. // It can be modified afterwards. func DefaultBuildInfo() BuildInfo { return BuildInfo{ BuildTags: slices.Clone(LocalBuildTags), CgoEnabled: true, StaticLink: false, DebugMode: DebugModeDisabled, GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, KeepOutput: false, Revision: "", UncommittedChanges: false, // Use the local JS runtime if this is a development build. UseLocalJSRuntime: version.Channel == version.DevBuild, } } type PrepareParams struct { Build BuildInfo App *apps.Instance WorkingDir string Stderr option.Option[io.Writer] } type PrepareResult struct { Data any } type ParseParams struct { Build BuildInfo App *apps.Instance Experiments *experiments.Set WorkingDir string ParseTests bool // Prepare is the result from calling Prepare(). // Required for TypeScript apps, ignored for Go apps. Prepare *PrepareResult // Optional writer to redirect stderr to. Stderr option.Option[io.Writer] } type ParseResult struct { Meta *meta.Data Data any } type CompileParams struct { Build BuildInfo App *apps.Instance Parse *ParseResult OpTracker *optracker.OpTracker Experiments *experiments.Set WorkingDir string // Override to explicitly allow the Encore version to be set. EncoreVersion option.Option[string] Environ []string } type ArtifactString string func (a ArtifactString) Join(strs ...string) ArtifactString { str := pathspkg.Join(strs...) return ArtifactString(pathspkg.Join(string(a), str)) } func (a ArtifactString) Expand(artifactDir paths.FS) string { return os.Expand(string(a), func(key string) string { if key == "ARTIFACT_DIR" { return artifactDir.ToIO() } return "" }) } type ArtifactStrings []ArtifactString func (a ArtifactStrings) Expand(artifactDir paths.FS) []string { return fns.Map(a, func(a ArtifactString) string { return a.Expand(artifactDir) }) } // CmdSpec is a specification for a command to run. // // The fields can refer to file paths within the artifact directory // using the "${ARTIFACT_DIR}" placeholder (substituted with os.ExpandEnv). // This is necessary when building docker images, as otherwise the file paths // will refer to the wrong filesystem location in the built docker image. type CmdSpec struct { // The command to execute. Can either be a filesystem path // or a path to a binary (using "${ARTIFACT_DIR}" as a placeholder). Command ArtifactStrings `json:"command"` // Additional env variables to pass in. Env ArtifactStrings `json:"env"` // PrioritizedFiles are file paths that should be prioritized when // building a streamable docker image. PrioritizedFiles ArtifactStrings `json:"prioritized_files"` } func (s *CmdSpec) Expand(artifactDir paths.FS) Cmd { return Cmd{ Command: s.Command.Expand(artifactDir), Env: s.Env.Expand(artifactDir), } } // Cmd defines a command to run. It's like CmdSpec, but uses expanded paths // instead of ArtifactStrings. A CmdSpec can be turned into a Cmd using Expand. type Cmd struct { // The command to execute, with arguments. Command []string // Additional env variables to pass in. Env []string } type CompileResult struct { OS string Arch string Outputs []BuildOutput } type BuildOutput interface { GetArtifactDir() paths.FS GetEntrypoints() []Entrypoint } type Entrypoint struct { // How to run this entrypoint. Cmd CmdSpec `json:"cmd"` // Services hosted by this entrypoint. Services []string `json:"services"` // Gateways hosted by this entrypoint. Gateways []string `json:"gateways"` // Whether this entrypoint uses the new runtime config. UseRuntimeConfigV2 bool `json:"use_runtime_config_v2"` } type GoBuildOutput struct { // The folder containing the build artifacts. // These artifacts are assumed to be relocatable. ArtifactDir paths.FS `json:"artifact_dir"` // The entrypoints that are part of this build output. Entrypoints []Entrypoint `json:"entrypoints"` } func (o *GoBuildOutput) GetArtifactDir() paths.FS { return o.ArtifactDir } func (o *GoBuildOutput) GetEntrypoints() []Entrypoint { return o.Entrypoints } type JSBuildOutput struct { // The folder containing the build artifacts. // These artifacts are assumed to be relocatable. ArtifactDir paths.FS `json:"artifact_dir"` // The entrypoints that are part of this build output. Entrypoints []Entrypoint `json:"entrypoints"` // Whether the build output uses the local runtime on the builder, // as opposed to installing a published release via e.g. 'npm install'. UsesLocalRuntime bool `json:"uses_local_runtime"` } func (o *JSBuildOutput) GetArtifactDir() paths.FS { return o.ArtifactDir } func (o *JSBuildOutput) GetEntrypoints() []Entrypoint { return o.Entrypoints } type RunTestsParams struct { Spec *TestSpecResult // WorkingDir is the directory to invoke the test command from. WorkingDir paths.FS // Stdout and Stderr are where to redirect the command output. Stdout, Stderr io.Writer } type TestSpecParams struct { Compile CompileParams // Env sets environment variables for "go test". Env []string // Args sets extra arguments for "go test". Args []string } // ErrNoTests is reported by TestSpec when there aren't any tests to run. var ErrNoTests = errors.New("no tests found") type TestSpecResult struct { Command string Args []string Environ []string // For use by the builder when invoking RunTests. BuilderData any } type GenUserFacingParams struct { Build BuildInfo App *apps.Instance Parse *ParseResult } type ServiceConfigsParams struct { Parse *ParseResult CueMeta *cueutil.Meta } type ServiceConfigsResult struct { Configs map[string]string ConfigFiles fs.FS } type Impl interface { Prepare(context.Context, PrepareParams) (*PrepareResult, error) Parse(context.Context, ParseParams) (*ParseResult, error) Compile(context.Context, CompileParams) (*CompileResult, error) TestSpec(context.Context, TestSpecParams) (*TestSpecResult, error) RunTests(context.Context, RunTestsParams) error ServiceConfigs(context.Context, ServiceConfigsParams) (*ServiceConfigsResult, error) GenUserFacing(context.Context, GenUserFacingParams) error UseNewRuntimeConfig() bool NeedsMeta() bool Close() error } ================================================ FILE: pkg/builder/builderimpl/builders.go ================================================ package builderimpl import ( "encore.dev/appruntime/exported/experiments" "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/v2/tsbuilder" "encr.dev/v2/v2builder" ) func Resolve(lang appfile.Lang, expSet *experiments.Set) builder.Impl { if lang == appfile.LangTS || experiments.TypeScript.Enabled(expSet) { return tsbuilder.New() } return v2builder.New() } ================================================ FILE: pkg/clientgen/client.go ================================================ // Package clientgen generates code for use with Encore apps. package clientgen import ( "bytes" "errors" "fmt" "path/filepath" "strings" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/clientgen/openapi" "encr.dev/pkg/errinsrc/srcerrors" meta "encr.dev/proto/encore/parser/meta/v1" ) // Lang represents a programming language or dialect that we support generating code for. type Lang string // These constants represent supported languages. const ( LangUnknown Lang = "" LangTypeScript Lang = "typescript" LangJavascript Lang = "javascript" LangGo Lang = "go" LangOpenAPI Lang = "openapi" ) type generator interface { Generate(p clientgentypes.GenerateParams) error Version() int // The version of the generator. } // ErrUnknownLang is reported by Generate when the language is not known. var ErrUnknownLang = errors.New("unknown language") // Detect attempts to detect the language from the given filename. func Detect(path string) (lang Lang, ok bool) { suffix := strings.ToLower(filepath.Ext(path)) switch suffix { case ".ts": return LangTypeScript, true case ".js": return LangJavascript, true case ".go": return LangGo, true default: return LangUnknown, false } } // Client generates an API client based on the given app metadata. // ServiceNames are the services to include in the output. // If it's nil, all services are included. func Client( lang Lang, appSlug string, md *meta.Data, services clientgentypes.ServiceSet, tags clientgentypes.TagSet, opts clientgentypes.Options, ) (code []byte, err error) { defer func() { if e := recover(); e != nil { err = srcerrors.UnhandledPanic(e) } }() var gen generator switch lang { case LangTypeScript: if opts.TSSharedTypes && md.Language == meta.Lang_TYPESCRIPT { gen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: true, clientTarget: opts.TSClientTarget} } else { gen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: false} } case LangJavascript: gen = &javascript{generatorVersion: javascriptGenLatestVersion} case LangGo: gen = &golang{generatorVersion: goGenLatestVersion} case LangOpenAPI: gen = openapi.New(openapi.LatestVersion) default: return nil, ErrUnknownLang } var buf bytes.Buffer params := clientgentypes.GenerateParams{ Buf: &buf, AppSlug: appSlug, Meta: md, Services: services, Tags: tags, Options: opts, } if err := gen.Generate(params); err != nil { return nil, fmt.Errorf("genclient.Generate %s %s: %v", lang, appSlug, err) } return buf.Bytes(), nil } // getServiceDoc returns the documentation for a service by looking up the // root package matching the service's rel_path func getServiceDoc(md *meta.Data, svc *meta.Service) string { for _, pkg := range md.Pkgs { if pkg.RelPath == svc.RelPath { return pkg.Doc } } return "" } // GetLang returns the language specified by the given string, allowing for case insensitivity and common aliases. func GetLang(lang string) (Lang, error) { switch strings.TrimSpace(strings.ToLower(lang)) { case "typescript", "ts": return LangTypeScript, nil case "javascript", "js": return LangJavascript, nil case "go", "golang": return LangGo, nil case "openapi", "swagger", "oas": return LangOpenAPI, nil default: return LangUnknown, ErrUnknownLang } } ================================================ FILE: pkg/clientgen/client_test.go ================================================ package clientgen import ( "context" "os" "path/filepath" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/rogpeppe/go-internal/txtar" "encr.dev/cli/daemon/apps" "encr.dev/pkg/builder" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/golden" "encr.dev/v2/tsbuilder" "encr.dev/v2/v2builder" ) func TestMain(m *testing.M) { golden.TestMain(m) } func TestClientCodeGenerationFromGoApp(t *testing.T) { t.Helper() c := qt.New(t) tests, err := filepath.Glob("./testdata/goapp/input*.go") if err != nil { t.Fatal(err) } c.Assert(err, qt.IsNil) ctx := context.Background() bld := v2builder.New() for _, path := range tests { path := path c.Run("expected"+strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), ".go"), "input"), func(c *qt.C) { ar, err := txtar.ParseFile(path) c.Assert(err, qt.IsNil) base := t.TempDir() err = txtar.Write(ar, base) c.Assert(err, qt.IsNil) app := apps.NewInstance(base, "app", "") prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) c.Assert(err, qt.IsNil) res, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: nil, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) c.Assert(err, qt.IsNil) files, err := os.ReadDir("./testdata/goapp") c.Assert(err, qt.IsNil) expectedPrefix := "expected" + strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), ".go"), "input") + "_" for _, file := range files { testName := strings.TrimPrefix(file.Name(), expectedPrefix) // Check that the trim prefix removed the expectedPrefix && there are no other underscores in the testName if testName != file.Name() && !strings.Contains(testName, "_") { c.Run(testName, func(c *qt.C) { language, ok := Detect(file.Name()) if strings.Contains(file.Name(), "openapi") { language, ok = LangOpenAPI, true } c.Assert(ok, qt.IsTrue, qt.Commentf("Unable to detect language type for %s", file.Name())) services := clientgentypes.AllServices(res.Meta) generatedClient, err := Client( language, "app", res.Meta, services, clientgentypes.TagSet{}, clientgentypes.Options{}, ) c.Assert(err, qt.IsNil) golden.TestAgainst(c, "goapp/"+file.Name(), string(generatedClient)) }) } } }) } } func TestClientCodeGenerationFromTSApp(t *testing.T) { t.Helper() c := qt.New(t) tests, err := filepath.Glob("./testdata/tsapp/input*.ts") if err != nil { t.Fatal(err) } c.Assert(err, qt.IsNil) ctx := context.Background() bld := tsbuilder.New() for _, path := range tests { path := path c.Run("expected"+strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), ".ts"), "input"), func(c *qt.C) { ar, err := txtar.ParseFile(path) c.Assert(err, qt.IsNil) base := t.TempDir() err = txtar.Write(ar, base) c.Assert(err, qt.IsNil) app := apps.NewInstance(base, "app", "") prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) c.Assert(err, qt.IsNil) res, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: nil, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) c.Assert(err, qt.IsNil) files, err := os.ReadDir("./testdata/tsapp") c.Assert(err, qt.IsNil) expectedPrefix := "expected" + strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), ".ts"), "input") + "_" for _, file := range files { testName := strings.TrimPrefix(file.Name(), expectedPrefix) // Check that the trim prefix removed the expectedPrefix && there are no other underscores in the testName if testName != file.Name() && !strings.Contains(testName, "_") { c.Run(testName, func(c *qt.C) { language, ok := Detect(file.Name()) if strings.Contains(file.Name(), "openapi") { language, ok = LangOpenAPI, true } options := clientgentypes.Options{} if strings.Contains(file.Name(), "shared") { options.TSSharedTypes = true } c.Assert(ok, qt.IsTrue, qt.Commentf("Unable to detect language type for %s", file.Name())) services := clientgentypes.AllServices(res.Meta) generatedClient, err := Client( language, "app", res.Meta, services, clientgentypes.TagSet{}, options, ) c.Assert(err, qt.IsNil) golden.TestAgainst(c, "tsapp/"+file.Name(), string(generatedClient)) }) } } }) } } ================================================ FILE: pkg/clientgen/clientgentypes/clientgentypes.go ================================================ package clientgentypes import ( "bytes" "slices" meta "encr.dev/proto/encore/parser/meta/v1" ) // Options for the client generator. type Options struct { OpenAPIExcludePrivateEndpoints bool TSSharedTypes bool TSClientTarget string } type GenerateParams struct { Buf *bytes.Buffer AppSlug string Meta *meta.Data Services ServiceSet Tags TagSet Options Options } type ServiceSet struct { list []string set map[string]bool } func (s ServiceSet) List() []string { return s.list } func (s ServiceSet) Has(svc string) bool { return s.set[svc] } // NewServiceSet constructs a new service set. // If the list contains "*", include all services in the metadata. // Finally, exclude any services in the exclude list. func NewServiceSet(md *meta.Data, include, exclude []string) ServiceSet { set := make(map[string]bool, len(include)) if slices.Contains(include, "*") { // If the list contains "*", include all services. for _, svc := range md.Svcs { set[svc.Name] = true } } else { for _, svc := range include { set[svc] = true } } // Remove excludes. for _, svc := range exclude { delete(set, svc) } list := make([]string, 0, len(set)) for svc := range set { list = append(list, svc) } slices.Sort(list) return ServiceSet{ list: list, set: set, } } func AllServices(md *meta.Data) ServiceSet { return NewServiceSet(md, []string{"*"}, nil) } type TagSet struct { included map[string]bool excluded map[string]bool } func NewTagSet(tags, excludedTags []string) TagSet { tagSet := TagSet{ included: make(map[string]bool), excluded: make(map[string]bool), } for _, tag := range tags { tagSet.included[tag] = true } for _, tag := range excludedTags { tagSet.excluded[tag] = true } return tagSet } func (t TagSet) IsRPCIncluded(rpc *meta.RPC) bool { // First check if the RPC has any of the excluded tags. for _, selector := range rpc.Tags { if selector.Type != meta.Selector_TAG { continue } if excluded, ok := t.excluded[selector.Value]; ok && excluded { return false } } // If no included tags are specified, all tags are included. if len(t.included) == 0 { return true } // Check if the RPC has any of the included tags. for _, selector := range rpc.Tags { if selector.Type != meta.Selector_TAG { continue } if included, ok := t.included[selector.Value]; ok && included { return true } } // If no included tags are found, the RPC is not included. return false } ================================================ FILE: pkg/clientgen/errors.go ================================================ package clientgen type ErrCode struct { Name string Comment string HttpStatusCode int } var errorCodes []ErrCode = []ErrCode{ {"OK", "OK indicates the operation was successful.", 200}, {"Canceled", "Canceled indicates the operation was canceled (typically by the caller).\n\nEncore will generate this error code when cancellation is requested.", 499}, {"Unknown", "Unknown error. An example of where this error may be returned is\nif a Status value received from another address space belongs to\nan error-space that is not known in this address space. Also\nerrors raised by APIs that do not return enough error information\nmay be converted to this error.\n\nEncore will generate this error code in the above two mentioned cases.", 500}, {"InvalidArgument", "InvalidArgument indicates client specified an invalid argument.\nNote that this differs from FailedPrecondition. It indicates arguments\nthat are problematic regardless of the state of the system\n(e.g., a malformed file name).\n\nThis error code will not be generated by the gRPC framework.", 400}, {"DeadlineExceeded", "DeadlineExceeded means operation expired before completion.\nFor operations that change the state of the system, this error may be\nreturned even if the operation has completed successfully. For\nexample, a successful response from a server could have been delayed\nlong enough for the deadline to expire.\n\nThe gRPC framework will generate this error code when the deadline is\nexceeded.", 504}, {"NotFound", "NotFound means some requested entity (e.g., file or directory) was\nnot found.\n\nThis error code will not be generated by the gRPC framework.", 404}, {"AlreadyExists", "AlreadyExists means an attempt to create an entity failed because one\nalready exists.\n\nThis error code will not be generated by the gRPC framework.", 409}, {"PermissionDenied", "PermissionDenied indicates the caller does not have permission to\nexecute the specified operation. It must not be used for rejections\ncaused by exhausting some resource (use ResourceExhausted\ninstead for those errors). It must not be\nused if the caller cannot be identified (use Unauthenticated\ninstead for those errors).\n\nThis error code will not be generated by the gRPC core framework,\nbut expect authentication middleware to use it.", 403}, {"ResourceExhausted", "ResourceExhausted indicates some resource has been exhausted, perhaps\na per-user quota, or perhaps the entire file system is out of space.\n\nThis error code will be generated by the gRPC framework in\nout-of-memory and server overload situations, or when a message is\nlarger than the configured maximum size.", 429}, {"FailedPrecondition", "FailedPrecondition indicates operation was rejected because the\nsystem is not in a state required for the operation's execution.\nFor example, directory to be deleted may be non-empty, an rmdir\noperation is applied to a non-directory, etc.\n\nA litmus test that may help a service implementor in deciding\nbetween FailedPrecondition, Aborted, and Unavailable:\n (a) Use Unavailable if the client can retry just the failing call.\n (b) Use Aborted if the client should retry at a higher-level\n (e.g., restarting a read-modify-write sequence).\n (c) Use FailedPrecondition if the client should not retry until\n the system state has been explicitly fixed. E.g., if an \"rmdir\"\n fails because the directory is non-empty, FailedPrecondition\n should be returned since the client should not retry unless\n they have first fixed up the directory by deleting files from it.\n (d) Use FailedPrecondition if the client performs conditional\n REST Get/Update/Delete on a resource and the resource on the\n server does not match the condition. E.g., conflicting\n read-modify-write on the same resource.\n\nThis error code will not be generated by the gRPC framework.", 400}, {"Aborted", "Aborted indicates the operation was aborted, typically due to a\nconcurrency issue like sequencer check failures, transaction aborts,\netc.\n\nSee litmus test above for deciding between FailedPrecondition,\nAborted, and Unavailable.", 409}, {"OutOfRange", "OutOfRange means operation was attempted past the valid range.\nE.g., seeking or reading past end of file.\n\nUnlike InvalidArgument, this error indicates a problem that may\nbe fixed if the system state changes. For example, a 32-bit file\nmay be rotated to a 64-bit file without error.\n\nThere is a fair bit of overlap between FailedPrecondition and\nOutOfRange. We recommend using OutOfRange (the more specific\nerror) when it applies so that callers who are iterating through\na space can easily look for an OutOfRange error to detect when\nthey are done.\n\nThis error code will not be generated by the gRPC framework.", 400}, {"Unimplemented", "Unimplemented indicates operation is not implemented or not\nsupported/enabled in this service.\n\nThis is not an error, but a feature not available.\n\nThis error code will not be generated by the gRPC framework.", 501}, {"Internal", "Internal means some invariant expected by the underlying system has\nbeen broken. This is not a per-message error, it is a global\nconditions check.\n\nThis error code will not be generated by the gRPC framework.", 500}, {"Unavailable", "Unavailable indicates the service is currently unavailable.\nThis is most likely a transient condition, which can be corrected by\nretrying with a backoff.\n\nSee litmus test above for deciding between FailedPrecondition,\nAborted, and Unavailable.", 503}, {"DataLoss", "DataLoss indicates unrecoverable data loss or corruption.\n\nThis error code is only defined in the gRPC library, and only for\nunrecoverable data loss (i.e., data loss resulting from errors\nlike hard disk corruption or bandwidth exceeded).\n\nThis error code will not be generated by the gRPC framework.", 500}, {"Unauthenticated", "Unauthenticated indicates the request does not have valid\nauthentication credentials for the operation.\n\nThe gRPC framework will generate this error code when the\nauthentication metadata is invalid or a Credentials callback fails,\nbut also expect authentication middleware to generate it.", 401}, } ================================================ FILE: pkg/clientgen/golang.go ================================================ package clientgen import ( "bytes" "fmt" "regexp" "sort" "strings" "unicode" "github.com/cockroachdb/errors" . "github.com/dave/jennifer/jen" "github.com/fatih/structtag" "encr.dev/internal/gocodegen" "encr.dev/internal/version" "encr.dev/parser/encoding" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/idents" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) // goGenVersion allows us to introduce breaking changes in the generated code but behind a switch // meaning that people with client code reliant on the old behaviour can continue to generate the // old code. type goGenVersion int const ( // GoInitial is the originally released Go client generator GoInitial goGenVersion = iota // GoExperimental can be used to lock experimental or uncompleted features in the generated code // It should always be the last item in the enum GoExperimental ) const goGenLatestVersion = GoExperimental - 1 type golang struct { md *meta.Data enc *gocodegen.MarshallingCodeGenerator generatorVersion goGenVersion skipDocs bool skipPkgTypePrefix bool seenSlicePath bool seenLiteralNull bool } func GenTypes(md *meta.Data, typs ...*schema.Decl) ([]byte, error) { g := &golang{ generatorVersion: goGenLatestVersion, md: md, enc: gocodegen.NewMarshallingCodeGenerator(gocodegen.UnknownPkgPath, "serde", true), skipDocs: true, skipPkgTypePrefix: true, } rtn := &bytes.Buffer{} stmt := Add() g.generateTypeDefinitions(stmt, typs) err := stmt.Render(rtn) return rtn.Bytes(), err } func (g *golang) Generate(p clientgentypes.GenerateParams) (err error) { g.md = p.Meta g.enc = gocodegen.NewMarshallingCodeGenerator(gocodegen.UnknownPkgPath, "serde", true) namedTypes := getNamedTypes(p.Meta, p.Services) // Create a new client file file := NewFile("client") file.HeaderComment(doNotEditHeader()) // Generate the parent Client struct g.generateClient(file, p.AppSlug, p.Services) // Generate the types and service client structs seenNs := make(map[string]bool) for _, service := range p.Meta.Svcs { nsName := service.Name g.generateTypeDefinitions(file, namedTypes.Decls(nsName)) seenNs[nsName] = true if hasPublicRPC(service) && p.Services.Has(service.Name) { if err := g.generateServiceClient(file, service, p.Tags); err != nil { return errors.Wrapf(err, "unable to generate service client for service: %s", service) } } } for _, ns := range namedTypes.Namespaces() { if !seenNs[ns] { g.generateTypeDefinitions(file, namedTypes.Decls(ns)) } } // Generate the base client if err := g.generateBaseClient(file); err != nil { return errors.Wrap(err, "unable to generate base client") } g.writeExtraHelpers(file) // Write the APIError type g.writeErrorType(file) // Generate the serializer g.enc.WriteToFile(file) // Finally, render the client if err := file.Render(p.Buf); err != nil { return errors.Wrap(err, "unable to generate go client") } return nil } func (g *golang) Version() int { return int(g.generatorVersion) } func (g *golang) cleanServiceName(service *meta.Service) string { return strings.Title(strings.ToLower(service.Name)) } // generateClient creates the Client struct, Option type and New function func (g *golang) generateClient(file *File, appSlug string, set clientgentypes.ServiceSet) { // List all services which have public RPCs or types we need fieldDef := make([]Code, 0, len(set.List())) fieldInit := make(Dict) for _, service := range g.md.Svcs { if !hasPublicRPC(service) || !set.Has(service.Name) { continue } name := g.cleanServiceName(service) fieldDef = append(fieldDef, Id(name).Id(fmt.Sprintf("%sClient", name)), ) fieldInit[Id(name)] = Op("&").Id(fmt.Sprintf("%sClient", strings.ToLower(name))).Values(Id("base")) } // The client struct file.Commentf("Client is an API client for the %s Encore application.", appSlug) file.Add( Type().Id("Client").Struct(fieldDef...), Line(), Line(), ) file.Comment("BaseURL is the base URL for calling the Encore application's API.") file.Type().Id("BaseURL").String() file.Line() file.Const().Id("Local").Id("BaseURL").Op("=").Lit("http://localhost:4000") file.Line() file.Comment("Environment returns a BaseURL for calling the cloud environment with the given name.") file.Func().Id("Environment"). Params(Id("name").String()). Id("BaseURL"). Block(Return( Id("BaseURL").Call( Qual("fmt", "Sprintf").Call( Lit(fmt.Sprintf("https://%%s-%s.encr.app", appSlug)), Id("name"), ), ), )) file.Comment("PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.") file.Func().Id("PreviewEnv"). Params(Id("pr").Int()). Id("BaseURL"). Block(Return(Id("Environment").Call( Qual("fmt", "Sprintf").Call(Lit("pr%d"), Id("pr")), ))) // Option type alias file.Comment("Option allows you to customise the baseClient used by the Client") file.Add( Type().Id("Option").Op("="). Func().Params(Id("client").Op("*").Id("baseClient")).Error(), Line(), Line(), ) // New Function file.Comment("New returns a Client for calling the public and authenticated APIs of your Encore application.") file.Comment("You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.") file.Add( Func().Id("New"). Params( Id("target").Id("BaseURL"), Id("options").Id("...Option"), ). Params(Op("*").Id("Client"), Error()). Block( Comment("Parse the base URL where the Encore application is being hosted"), List(Id("baseURL"), Err()). Op(":="). Qual("net/url", "Parse").Call(String().Call(Id("target"))), If(Err().Op("!=").Nil()).Block( Return( Nil(), Qual("fmt", "Errorf").Call(Lit("unable to parse base url: %w"), Err()), ), ), Line(), Comment("Create a client with sensible defaults"), Id("base").Op(":=").Op("&").Id("baseClient").Values(Dict{ Id("baseURL"): Id("baseURL"), Id("httpClient"): Qual("net/http", "DefaultClient"), Id("userAgent"): Lit(fmt.Sprintf("%s-Generated-Go-Client (Encore/%s)", appSlug, version.Version)), }), Line(), Comment("Apply any given options"), For(List(Id("_"), Id("option")).Op(":=").Range().Id("options")).Block( If( Id("err").Op(":=").Id("option").Call(Id("base")), Id("err").Op("!=").Nil(), ).Block( Return( Nil(), Qual("fmt", "Errorf").Call( Lit("unable to apply client option: %w"), Id("err"), ), ), ), ), Line(), Return(Op("&").Id("Client").Values(fieldInit), Nil()), ), ) // Generate the WithHttpClient function g.generateOptionFunc( file, "HTTPClient", `can be used to configure the underlying HTTP client used when making API calls. Defaults to http.DefaultClient`, &Statement{Id("client").Id("HTTPDoer")}, &Statement{ Id("base").Dot("httpClient").Op("=").Id("client"), Return(Nil()), }, ) if g.md.AuthHandler != nil { typ := g.getType(g.md.AuthHandler.Params) rawType := typ funcName := "AuthToken" paramName := "bearerToken" pointer := Id(paramName) comment := "an authentication token to be used for each request.\n\nThis token will be sent as a Bearer token in the Authorization header." if g.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING { funcName = "Auth" paramName = "auth" pointer = Id("auth") typ = rawType comment = "the authentication data to be used with each request" } // Generate the WithAuth function g.generateOptionFunc( file, funcName, "allows you to set "+comment, // Note we take the auth data by value rather than a pointer to ensure it's safe // to use inside multiple goroutines &Statement{Id(paramName).Add(rawType)}, &Statement{ Id("base").Dot("authGenerator").Op("=").Func(). Params(Id("_").Qual("context", "Context")). Params(typ, Error()). Block(Return(pointer, Nil())), Return(Nil()), }, ) g.generateOptionFunc( file, "AuthFunc", "allows you to pass a function which is called for each request to return "+comment, &Statement{ Id("authGenerator").Func(). Params(Id("ctx").Qual("context", "Context")). Params(typ, Error()), }, &Statement{ &Statement{Id("base").Dot("authGenerator").Op("=").Id("authGenerator")}, Return(Nil()), }, ) } } // generateOptionFunc is a helper for reducing the boilerplate we have when creating the option functions func (g *golang) generateOptionFunc(file *File, optionName string, doc string, params *Statement, block *Statement) { for i, line := range strings.Split(doc, "\n") { if i == 0 { file.Commentf("With%s %s", optionName, line) } else { file.Comment(line) } } file.Func(). Id(fmt.Sprintf("With%s", optionName)). Params(*params...). Id("Option"). Block( Return(Func().Params(Id("base").Op("*").Id("baseClient")).Error().Block( *block..., )), ) } func (g *golang) generateServiceClient(file *File, service *meta.Service, tags clientgentypes.TagSet) error { name := g.cleanServiceName(service) interfaceName := fmt.Sprintf("%sClient", name) structName := fmt.Sprintf("%sClient", strings.ToLower(name)) // The interface if doc := getServiceDoc(g.md, service); doc != "" && !g.skipDocs { for _, line := range strings.Split(strings.TrimSpace(doc), "\n") { file.Comment(line) } file.Comment("") } file.Commentf("%s Provides you access to call public and authenticated APIs on %s. The concrete implementation is %s.", interfaceName, service.Name, structName) file.Comment("It is setup as an interface allowing you to use GoMock to create mock implementations during tests.") var interfaceMethods []Code for _, rpc := range service.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) { continue } // streaming endpoints not supported yet if rpc.StreamingRequest || rpc.StreamingResponse { continue } // Add the documentation for the API to the interface method if rpc.Doc != nil && !g.skipDocs { // Add a newline if this is not the first method if len(interfaceMethods) > 0 { interfaceMethods = append(interfaceMethods, Line()) } for _, line := range strings.Split(strings.TrimSpace(*rpc.Doc), "\n") { interfaceMethods = append(interfaceMethods, Comment(line)) } } interfaceMethods = append(interfaceMethods, Id(idents.Convert(rpc.Name, idents.PascalCase)).Add(g.rpcParams(rpc)).Add(g.rpcReturnType(rpc, false)), ) } file.Type().Id(interfaceName).Interface(interfaceMethods...) file.Line() // The struct file.Type().Id(structName).Struct( Id("base").Op("*").Id("baseClient"), ) file.Line() file.Var().Id("_").Id(interfaceName).Op("=").Params(Op("*").Id(structName)).Params(Nil()) // The API functions for _, rpc := range service.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE { continue } // streaming endpoints not supported yet if rpc.StreamingRequest || rpc.StreamingResponse { continue } if rpc.Doc != nil && *rpc.Doc != "" && !g.skipDocs { for _, line := range strings.Split(strings.TrimSpace(*rpc.Doc), "\n") { if line != "" { file.Comment(line) } } } callSite, err := g.rpcCallSite(rpc) if err != nil { return errors.Wrapf(err, "rpc: %s", rpc.Name) } file.Func(). Params(Id("c").Op("*").Id(structName)). Id(idents.Convert(rpc.Name, idents.PascalCase)). Add( g.rpcParams(rpc), g.rpcReturnType(rpc, true), ).Block(callSite...) file.Line() } return nil } func (g *golang) rpcParams(rpc *meta.RPC) Code { params := []Code{ Id("ctx").Qual("context", "Context"), } if rpc.Path != nil && len(rpc.Path.Segments) > 0 { for _, segment := range rpc.Path.Segments { if segment.Type == meta.PathSegment_LITERAL { continue } // We'll default to strings for most things typ := String() switch segment.ValueType { case meta.PathSegment_BOOL: typ = Bool() case meta.PathSegment_INT8: typ = Int8() case meta.PathSegment_INT16: typ = Int16() case meta.PathSegment_INT32: typ = Int32() case meta.PathSegment_INT64: typ = Int64() case meta.PathSegment_INT: typ = Int() case meta.PathSegment_UINT8: typ = Uint8() case meta.PathSegment_UINT16: typ = Uint16() case meta.PathSegment_UINT32: typ = Uint32() case meta.PathSegment_UINT64: typ = Uint64() case meta.PathSegment_UINT: typ = Uint() } if segment.Type == meta.PathSegment_WILDCARD || segment.Type == meta.PathSegment_FALLBACK { typ = Index().Add(typ) } params = append(params, Id(g.nonReservedId(segment.Value)).Add(typ), ) } } if rpc.Proto == meta.RPC_RAW { params = append(params, Id("request").Op("*").Qual("net/http", "Request")) } else { if rpc.RequestSchema != nil { params = append(params, Id("params").Add(g.getType(rpc.RequestSchema))) } } return Params(params...) } // nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Go keyword func (g *golang) nonReservedId(id string) string { switch id { // our reserved keywords (or ID's we use within the generated client functions) case "c", "ctx", "request", "resp", "err", "reqEncoder", "headers", "queryString", "body", "respBody", "respHeaders", "respDecoder": return "_" + id // Go keywords case "break", "default", "func", "interface", "select", "case", "defer", "go", "map", "struct", "chan", "else", "goto", "package", "switch", "const", "fallthrough", "if", "range", "type", "continue", "for", "import", "return", "var": return "_" + id // Go predeclared identifiers case "append", "bool", "byte", "cap", "close", "complex", "complex64", "complex128", "uint16", "copy", "false", "float32", "float64", "imag", "int", "int8", "int16", "uint32", "int32", "int64", "iota", "len", "make", "new", "nil", "panic", "uint64", "print", "println", "real", "recover", "string", "true", "uint", "uint8", "uintptr": return "_" + id default: return id } } func (g *golang) rpcReturnType(rpc *meta.RPC, concreteImpl bool) Code { if rpc.Proto == meta.RPC_RAW { return Params(Op("*").Qual("net/http", "Response"), Error()) } if rpc.ResponseSchema == nil { return Error() } if concreteImpl { // For the concrete implementation we want the response type to be named so we can // refer to it without having to define a variable. return Params(Id("resp").Add(g.getType(rpc.ResponseSchema)), Err().Error()) } else { return Params(g.getType(rpc.ResponseSchema), Error()) } } func (g *golang) rpcCallSite(rpc *meta.RPC) (code []Code, err error) { // Work out how we're going to encode and call this RPC rpcEncoding, err := encoding.DescribeRPC(g.md, rpc, nil) if err != nil { return nil, errors.Wrapf(err, "rpc %s", rpc.Name) } // Raw end points just pass through the request // and need no further code generation if rpc.Proto == meta.RPC_RAW { code = append( code, Id("request").Op("=").Id("request").Dot("WithContext").Call(Id("ctx")), Line(), Comment("Check the request has the method set, as we can't guess what method is required"), If(Id("request").Dot("Method").Op("==").Lit("")).Block( Return( Nil(), Qual("errors", "New").Call(Lit("request.Method must be set")), ), ), Line(), Comment("Set the relative URL for the API call"), List(Id("path"), Err()).Op(":=").Qual("net/url", "Parse").Call(g.createApiPath(rpc, false)), If(Err().Op("!=").Nil()).Block( Return( Nil(), Qual("fmt", "Errorf").Call(Lit("unable to parse api url: %w"), Err()), ), ), If(Id("request").Dot("URL").Op("!=").Nil()).Block( Comment("If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema, "), Comment("host and path to ensure the final URL which hit the right BaseURL"), Id("request").Dot("URL").Dot("Scheme").Op("=").Id("path").Dot("Scheme"), Id("request").Dot("URL").Dot("Host").Op("=").Id("path").Dot("Host"), Id("request").Dot("URL").Dot("Path").Op("=").Id("path").Dot("Path"), ).Else().Block( Id("request").Dot("URL").Op("=").Id("path"), ), Line(), Line(), Return(Id("c").Dot("base").Dot("Do").Call(Id("request"))), ) return } headers := Nil() body := Nil() withQueryString := false // Work out how we encode the Request Schema if rpc.RequestSchema != nil { reqEnc := rpcEncoding.DefaultRequestEncoding if len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 { code = append(code, Comment("Convert our params into the objects we need for the request")) } enc := g.enc.NewPossibleInstance("reqEncoder") // Generate the headers if len(reqEnc.HeaderParameters) > 0 { values := Dict{} for _, field := range reqEnc.HeaderParameters { slice, err := enc.ToStringSlice( field.Type, Id("params").Dot(field.SrcName), ) if err != nil { return nil, errors.Wrapf(err, "unable to encode header %s", field.SrcName) } values[Lit(field.WireFormat)] = slice } headers = Id("headers") enc.Add(Id("headers").Op(":=").Qual("net/http", "Header").Values(values), Line()) } // Generate the query string if len(reqEnc.QueryParameters) > 0 { withQueryString = true values := Dict{} // Check the request schema for fields we can put in the query string for _, field := range reqEnc.QueryParameters { slice, err := enc.ToStringSlice( field.Type, Id("params").Dot(field.SrcName), ) if err != nil { return nil, errors.Wrapf(err, "unable to encode query fields %s", field.SrcName) } values[Lit(field.WireFormat)] = slice } enc.Add(Id("queryString").Op(":=").Qual("net/url", "Values").Values(values), Line()) } if rpc.ResponseSchema != nil { code = append(code, enc.Finalize( Id("err").Op("=").Qual("fmt", "Errorf").Call( Lit("unable to marshal parameters: %w"), enc.LastError(), ), Return(), )...) } else { code = append(code, enc.Finalize( Return(Qual("fmt", "Errorf").Call( Lit("unable to marshal parameters: %w"), enc.LastError(), )), )...) } // Generate the body if len(reqEnc.BodyParameters) > 0 { if len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 { // In the simple case we can just encode the params as the body directly body = Id("params") } else { // Else we need a new struct called "body" body = Id("body") types, err := g.generateAnonStructTypes(reqEnc.BodyParameters, "json") if err != nil { return nil, err } values := Dict{} for _, field := range reqEnc.BodyParameters { values[Id(field.SrcName)] = Id("params").Dot(field.SrcName) } code = append(code, Comment("Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)"), Id("body").Op(":=").Struct(types...).Values(values), Line(), ) } } } // Make the request resp := Nil() apiCallCode := func() Code { return Id("callAPI").Call( Id("ctx"), Id("c").Dot("base"), Lit(rpcEncoding.DefaultMethod), g.createApiPath(rpc, withQueryString), headers, body, resp, ) } // If there's no response schema, we can just return the call to the API directly if rpc.ResponseSchema == nil { code = append(code, List(Id("_"), Err()).Op(":=").Add(apiCallCode()), Return(Err()), ) return } hasAnonResponseStruct := false respEnc := rpcEncoding.ResponseEncoding // If we have a response object, we need if len(respEnc.BodyParameters) > 0 { if len(respEnc.HeaderParameters) == 0 { // If there are no other fields, we can just take the return type and pass it straight through resp = Op("&").Id("resp") } else { hasAnonResponseStruct = true resp = Op("&").Id("respBody") // we need to construct an anonymous struct types, err := g.generateAnonStructTypes(respEnc.BodyParameters, "json") if err != nil { return nil, errors.Wrap(err, "response unmarshal") } code = append(code, Comment("We only want the response body to marshal into these fields and none of the header fields,"), Comment("so we'll construct a new struct with only those fields."), Id("respBody").Op(":=").Struct(types...).Values(), Line(), ) } } // The API Call itself code = append(code, Comment("Now make the actual call to the API")) headersId := "_" if len(respEnc.HeaderParameters) > 0 { headersId = "respHeaders" code = append(code, Var().Id(headersId).Qual("net/http", "Header")) } code = append(code, List(Id(headersId), Err()).Op("=").Add(apiCallCode()), If(Err().Op("!=").Nil()).Block( Return(), ), Line(), ) // In we have an anonymous response struct, we need to copy the results into the full response struct if hasAnonResponseStruct || len(respEnc.HeaderParameters) > 0 { code = append(code, Comment("Copy the unmarshalled response body into our response struct")) enc := g.enc.NewPossibleInstance("respDecoder") for _, field := range respEnc.HeaderParameters { str, err := enc.FromString( field.Type, field.SrcName, Id(headersId).Dot("Get").Call(Lit(field.WireFormat)), Id(headersId).Dot("Values").Call(Lit(field.WireFormat)), true, ) if err != nil { return nil, errors.Wrapf(err, "unable to convert %s to string in response header", field.SrcName) } enc.Add(Id("resp").Dot(field.SrcName).Op("=").Add(str)) } for _, field := range respEnc.BodyParameters { enc.Add(Id("resp").Dot(field.SrcName).Op("=").Id("respBody").Dot(field.SrcName)) } code = append(code, enc.Finalize( Id("err").Op("=").Qual("fmt", "Errorf").Call( Lit("unable to unmarshal headers: %w"), enc.LastError(), ), Return(), )...) code = append(code, Line()) } code = append(code, Return()) return code, err } // goIdentifier converts a string into a valid Go identifier. func goIdentifier(input string) string { if input == "" { return "_" } // Convert string to rune slice for proper handling of Unicode characters runes := []rune(input) // Ensure the first character is a valid Go identifier start (letter or `_`) if !unicode.IsLetter(runes[0]) && runes[0] != '_' { runes[0] = '_' } // Regex to replace invalid characters (anything that isn't a letter, number, or `_`) invalidChars := regexp.MustCompile(`[^\p{L}\p{N}_]`) output := invalidChars.ReplaceAllString(string(runes), "_") return output } func (g *golang) declToID(decl *schema.Decl) *Statement { if g.skipPkgTypePrefix { return Id(goIdentifier(strings.Title(decl.Name))) } else { return Id(goIdentifier(fmt.Sprintf("%s%s", strings.Title(decl.Loc.PkgName), strings.Title(decl.Name)))) } } func (g *golang) getType(typ *schema.Type) Code { switch typ := typ.Typ.(type) { case *schema.Type_Named: decl := g.md.Decls[typ.Named.Id] named := g.declToID(decl) if len(typ.Named.TypeArguments) == 0 { return named } // Add the type arguments types := make([]Code, len(typ.Named.TypeArguments)) for i, t := range typ.Named.TypeArguments { types[i] = g.getType(t) } return named.Types(types...) case *schema.Type_List: return Index().Add(g.getType(typ.List.Elem)) case *schema.Type_Map: return Map(g.getType(typ.Map.Key)).Add(g.getType(typ.Map.Value)) case *schema.Type_Literal: switch typ.Literal.Value.(type) { case *schema.Literal_Str: return String() case *schema.Literal_Boolean: return Bool() case *schema.Literal_Int: return Int() case *schema.Literal_Float: return Float64() case *schema.Literal_Null: // Can't use nil as a type, so use *bool g.seenLiteralNull = true return Id("null") default: return Any() } case *schema.Type_Union: // There's no good way of representing unions in Go. // Use 'any' for now. return Any() case *schema.Type_Builtin: switch typ.Builtin { case schema.Builtin_ANY: return Any() case schema.Builtin_BOOL: return Bool() case schema.Builtin_INT: return Int() case schema.Builtin_INT8: return Int8() case schema.Builtin_INT16: return Int16() case schema.Builtin_INT32: return Int32() case schema.Builtin_INT64: return Int64() case schema.Builtin_UINT: return Uint() case schema.Builtin_UINT8: return Uint8() case schema.Builtin_UINT16: return Uint16() case schema.Builtin_UINT32: return Uint32() case schema.Builtin_UINT64: return Uint64() case schema.Builtin_FLOAT32: return Float32() case schema.Builtin_FLOAT64: return Float64() case schema.Builtin_STRING: return String() case schema.Builtin_BYTES: return Index().Byte() case schema.Builtin_TIME: return Qual("time", "Time") case schema.Builtin_JSON: return Qual("encoding/json", "RawMessage") case schema.Builtin_UUID, schema.Builtin_USER_ID, schema.Builtin_DECIMAL: // we don't want to add any custom deps, so these come in as strings return String() default: return Any() } case *schema.Type_Pointer: return Op("*").Add(g.getType(typ.Pointer.Base)) case *schema.Type_Option: // Avoid the dependency by using a pointer type instead of encore.dev/types/option.Option. value := typ.Option.Value // Avoid double pointer for ptr := value.GetPointer(); ptr != nil; ptr = value.GetPointer() { value = ptr.Base } return Op("*").Add(g.getType(value)) case *schema.Type_Struct: fields := make([]Code, 0, len(typ.Struct.Fields)) for _, field := range typ.Struct.Fields { // Skip over hidden fields if encoding.IgnoreField(field) { continue } // The base field name and type fieldTyp := Id(idents.Convert(field.Name, idents.PascalCase)).Add(g.getType(field.Typ)) // Add the field tags if field.RawTag != "" { tags, err := structtag.Parse(field.RawTag) if err != nil { panic("raw tags failed to parse") // This shouldn't happen at runtime, because the parser should have caught this } tagsForJen := make(map[string]string) for _, tag := range tags.Tags() { tagsForJen[tag.Key] = tag.Value() } fieldTyp = fieldTyp.Tag(tagsForJen) } // Add the docs for the field if field.Doc != "" && !g.skipDocs { lines := strings.Split(strings.TrimSpace(field.Doc), "\n") if len(lines) == 1 { fieldTyp = fieldTyp.Comment(lines[0]) } else { fields = append(fields, Line()) for _, line := range lines { fields = append(fields, Comment(line)) } } } // Finally, add the field to the list of fields on the struct fields = append(fields, fieldTyp) } return Struct(fields...) case *schema.Type_TypeParameter: decl := g.md.Decls[typ.TypeParameter.DeclId] typeParam := decl.TypeParams[typ.TypeParameter.ParamIdx] return Id(typeParam.Name) case *schema.Type_Config: // Config types are invisible outside of the Encore app return g.getType(typ.Config.Elem) default: return Any() } } func (g *golang) createApiPath(rpc *meta.RPC, withQueryString bool) (urlPath *Statement) { var url strings.Builder params := make([]Code, 0) for _, segment := range rpc.Path.Segments { url.WriteByte('/') if segment.Type == meta.PathSegment_LITERAL { url.WriteString(segment.Value) } else { paramID := Id(g.nonReservedId(segment.Value)) switch segment.ValueType { case meta.PathSegment_STRING, meta.PathSegment_UUID: url.WriteString("%s") if segment.Type == meta.PathSegment_WILDCARD || segment.Type == meta.PathSegment_FALLBACK { g.seenSlicePath = true paramID = Id("pathEscapeSlice").Call(paramID) } else { paramID = Qual("net/url", "PathEscape").Call(paramID) } case meta.PathSegment_BOOL: url.WriteString("%t") case meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64, meta.PathSegment_INT, meta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64, meta.PathSegment_UINT: url.WriteString("%d") default: url.WriteString("%v") } params = append(params, paramID) } } // Construct the query string if withQueryString { url.WriteString("?%s") params = append(params, Id("queryString").Dot("Encode").Call()) } if len(params) == 0 { urlPath = Lit(url.String()) } else { // Prepend the string format params = append([]Code{Lit(url.String())}, params...) urlPath = Qual("fmt", "Sprintf").Call(params...) } return urlPath } // FileStatement is an interface implemented by both jen.File and jen.Statement type FileStatement interface { Comment(string) *Statement Line() *Statement Type() *Statement } func (g *golang) generateTypeDefinitions(stmt FileStatement, decls []*schema.Decl) { sort.Slice(decls, func(i, j int) bool { return decls[i].Name < decls[j].Name }) for _, decl := range decls { // Write the docs if decl.Doc != "" && !g.skipDocs { for _, line := range strings.Split(strings.TrimSpace(decl.Doc), "\n") { stmt.Comment(line) } } else { stmt.Line() } // Create the base type definition; `type X[T]` typ := stmt.Type().Add(g.declToID(decl)) if len(decl.TypeParams) > 0 { types := make([]Code, len(decl.TypeParams)) for i, param := range decl.TypeParams { types[i] = Id(param.Name).Any() } typ = typ.Types(types...) } // All types which are not structs should be aliases if decl.Type.GetStruct() == nil && len(decl.TypeParams) == 0 { typ = typ.Op("=") } // Add the type typ.Add(g.getType(decl.Type)) } } func (g *golang) generateAnonStructTypes(fields []*encoding.ParameterEncoding, encodingTag string) (types []Code, err error) { for _, field := range fields { var tagValue strings.Builder tagValue.WriteString(field.Name) // Parse the tags and extract the encoding tag tags, err := structtag.Parse(field.RawTag) if err != nil { return nil, errors.Wrapf(err, "parse tags: %s", field.SrcName) } if tag, err := tags.Get(encodingTag); err == nil { options := strings.Join(tag.Options, ",") if options != "" { tagValue.WriteRune(',') tagValue.WriteString(options) } } types = append( types, Id(idents.Convert(field.SrcName, idents.PascalCase)).Add(g.getType(field.Type)).Tag(map[string]string{encodingTag: tagValue.String()}), ) } return } func (g *golang) generateBaseClient(file *File) (err error) { // Add the interface file.Comment("HTTPDoer is an interface which can be used to swap out the default") file.Comment("HTTP client (http.DefaultClient) with your own custom implementation.") file.Comment("This can be used to inject middleware or mock responses during unit tests.") file.Type().Id("HTTPDoer").Interface( Id("Do"). Params(Id("req").Op("*").Qual("net/http", "Request")). Params(Op("*").Qual("net/http", "Response"), Error()), ) // Add the base client struct file.Line() file.Comment("baseClient holds all the information we need to make requests to an Encore application") file.Type().Id("baseClient").StructFunc(func(grp *Group) { if g.md.AuthHandler != nil { typ := g.getType(g.md.AuthHandler.Params) grp.Id("authGenerator").Func(). Params(Id("ctx").Qual("context", "Context")). Params(typ, Error()). Comment("The function which will add the authentication data to the requests") } grp.Id("httpClient").Id("HTTPDoer"). Comment("The HTTP client which will be used for all API requests") grp.Id("baseURL").Op("*").Qual("net/url", "URL"). Comment("The base URL which API requests will be made against") grp.Id("userAgent").String(). Commentf("What user agent we will use in the API requests") }) // Add the Do method for th base client file.Line() file.Comment("Do sends the req to the Encore application adding the authorization token as required.") file.Func(). Params(Id("b").Op("*").Id("baseClient")). Id("Do"). Params(Id("req").Op("*").Qual("net/http", "Request")). Params(Op("*").Qual("net/http", "Response"), Error()). BlockFunc(func(grp *Group) { grp.Id("req").Dot("Header").Dot("Set").Call( Lit("Content-Type"), Lit("application/json"), ) grp.Id("req").Dot("Header").Dot("Set").Call( Lit("User-Agent"), Id("b").Dot("userAgent"), ) grp.Line() if g.md.AuthHandler != nil { err = g.addAuthData(grp) if err != nil { return } } grp.Comment("Merge the base URL and the API URL") grp.Id("req").Dot("URL").Op("="). Id("b").Dot("baseURL").Dot("ResolveReference").Call(Id("req").Dot("URL")) grp.Id("req").Dot("Host").Op("=").Id("req").Dot("URL").Dot("Host") grp.Line() grp.Comment("Finally, make the request via the configured HTTP Client") grp.Return( Id("b").Dot("httpClient").Dot("Do").Call(Id("req")), ) }) if err != nil { return } // Add the call API function file.Line() file.Comment("callAPI is used by each generated API method to actually make request and decode the responses") file.Func(). Id("callAPI"). Params( Id("ctx").Qual("context", "Context"), Id("client").Op("*").Id("baseClient"), Id("method"), Id("path").String(), Id("headers").Qual("net/http", "Header"), List(Id("body"), Id("resp")).Any(), ). Params(Qual("net/http", "Header"), Error()). Block( Comment("Encode the API body"), Var().Id("bodyReader").Qual("io", "Reader"), If(Id("body").Op("!=").Nil()).Block( List(Id("bodyBytes"), Err()).Op(":="). Qual("encoding/json", "Marshal"). Call(Id("body")), If(Err().Op("!=").Nil()).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("marshal request: %w"), Err())), ), Id("bodyReader").Op("=").Qual("bytes", "NewReader").Call(Id("bodyBytes")), ), Line(), Comment("Create the request"), List(Id("req"), Err()).Op(":="). Qual("net/http", "NewRequestWithContext"). Call( Id("ctx"), Id("method"), Id("path"), Id("bodyReader"), ), If(Err().Op("!=").Nil()).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("create request: %w"), Err())), ), Line(), Comment("Add any headers to the request"), For(List(Id("header"), Id("values")).Op(":=").Range().Id("headers")).Block( For(List(Id("_"), Id("value")).Op(":=").Range().Id("values")).Block( Id("req").Dot("Header").Dot("Add").Call(Id("header"), Id("value")), ), ), Line(), Comment("Make the request via the base client"), List(Id("rawResponse"), Err()).Op(":="). Id("client").Dot("Do").Call(Id("req")), If(Err().Op("!=").Nil()).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("request failed: %w"), Err())), ), Defer().Func().Params().Block( Id("_").Op("=").Id("rawResponse").Dot("Body").Dot("Close").Call(), ).Call(), If(Id("rawResponse").Dot("StatusCode").Op(">=").Lit(400)).Block( Comment("Read the full body sent back"), List(Id("body"), Err()).Op(":=").Qual("io", "ReadAll").Call(Id("rawResponse").Dot("Body")), If(Err().Op("!=").Nil()).Block( Return( Nil(), Op("&").Id("APIError").Values(Dict{ Id("Code"): Id("ErrUnknown"), Id("Message"): Qual("fmt", "Sprintf").Call(Lit("got error response without readable body: %s"), Id("rawResponse").Dot("Status")), }), ), ), Line(), Comment("Attempt to decode the error response as a structured APIError"), Id("apiError").Op(":=").Op("&").Id("APIError").Block(), If( Err().Op(":=").Qual("encoding/json", "Unmarshal"). Call(Id("body"), Id("apiError")), Err().Op("!=").Nil(), ).Block( Comment("If the error is not a parsable as an APIError, then return an error with the raw body"), Return( Nil(), Op("&").Id("APIError").Values(Dict{ Id("Code"): Id("ErrUnknown"), Id("Message"): Qual("fmt", "Sprintf").Call(Lit("got error response: %s"), String().Call(Id("body"))), }), ), ), Return(Nil(), Id("apiError")), ), Line(), Comment("Decode the response"), If( Id("resp").Op("!=").Nil(), ).Block( If( Err().Op(":=").Qual("encoding/json", "NewDecoder"). Call(Id("rawResponse").Dot("Body")). Dot("Decode"). Call(Id("resp")), Err().Op("!=").Nil(), ).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("decode response: %w"), Err())), ), ), Return( Id("rawResponse").Dot("Header"), Nil(), ), ) return nil } func (g *golang) writeErrorType(file *File) { const ErrPrefix = "Err" // Create the error type file.Line() file.Comment("APIError is the error type returned by the API") file.Type().Id("APIError").Struct( Id("Code").Id("ErrCode").Tag(map[string]string{"json": "code"}), Id("Message").String().Tag(map[string]string{"json": "message"}), Id("Details").Any().Tag(map[string]string{"json": "details"}), ) file.Func().Params(Id("e").Op("*").Id("APIError")).Id("Error").Params().String().Block( Return(Qual("fmt", "Sprintf").Call(Lit("%s: %s"), Id("e").Dot("Code"), Id("e").Dot("Message"))), ) file.Line() file.Line() // Create the ErrCode type and list file.Type().Id("ErrCode").Int() errTypes := make([]Code, 0) for i, err := range errorCodes { for _, line := range strings.Split(strings.TrimSpace(err.Comment), "\n") { // Fix the comment with the prefix if strings.HasPrefix(line, err.Name) { line = ErrPrefix + line } errTypes = append(errTypes, Comment(line)) } errTypes = append( errTypes, Id(ErrPrefix+err.Name).Id("ErrCode").Op("=").Lit(i), Line(), ) } file.Const().Defs(errTypes...) // Create the functions to convert an error to string file.Comment("// String returns the string representation of the error code") file.Func().Params(Id("c").Id("ErrCode")).Id("String").Params().String().Block( Switch(Id("c")).BlockFunc(func(g *Group) { for _, err := range errorCodes { g.Case(Id(ErrPrefix + err.Name)).Block( Return(Lit(idents.Convert(err.Name, idents.SnakeCase))), ) } g.Default().Block( Return(Lit("unknown")), ) }), ) file.Line() file.Comment("MarshalJSON converts the error code to a human-readable string") file.Func().Params(Id("c").Id("ErrCode")).Id("MarshalJSON").Params().Params(Index().Byte(), Error()).Block( Return(Index().Byte().Call(Qual("fmt", "Sprintf").Call(Lit("\"%s\""), Id("c"))), Nil()), ) file.Line() file.Comment("UnmarshalJSON converts the human-readable string to an error code") file.Func().Params(Id("c").Op("*").Id("ErrCode")).Id("UnmarshalJSON").Params(Id("b").Index().Byte()).Error().Block( Switch(String().Call(Id("b"))).BlockFunc(func(g *Group) { for _, err := range errorCodes { g.Case(Lit(fmt.Sprintf("\"%s\"", idents.Convert(err.Name, idents.SnakeCase)))).Block( Op("*").Id("c").Op("=").Id(ErrPrefix + err.Name), ) } g.Default().Block( Op("*").Id("c").Op("=").Id(ErrPrefix + "Unknown"), ) }), Return(Nil()), ) } func (g *golang) writeExtraHelpers(file *File) { if g.seenSlicePath { file.Line() file.Comment("// pathEscapeSlice escapes a slice of strings and then joins them into a single string") file.Func().Id("pathEscapeSlice").Params(Id("paths").Index().String()).String().Block( Var().Id("escapedPaths").Qual("strings", "Builder"), For(List(Id("i"), Id("path")).Op(":=").Range().Id("paths")).Block( If(Id("i").Op(">").Lit(0)).Block( Id("escapedPaths").Dot("WriteString").Call(Lit("/")), ), Id("escapedPaths").Dot("WriteString").Call(Qual("net/url", "PathEscape").Call(Id("path"))), ), Return(Id("escapedPaths").Dot("String").Call()), ) } if g.seenLiteralNull { file.Line() file.Comment("null is a helper type to indicate a null value in JSON.") file.Type().Id("null").Op("=").Op("*").Bool() } } func (g *golang) addAuthData(grp *Group) (err error) { grp.Comment("If a authorization data generator is present, call it and add the returned token to the request") // If the auth data is a string, then we want to add it as a bearer token if g.md.AuthHandler.Params.GetBuiltin() == schema.Builtin_STRING { grp.If(Id("b").Dot("authGenerator").Op("!=").Nil()).Block( If( List(Id("token"), Err()).Op(":="). Id("b").Dot("authGenerator").Call( Id("req").Dot("Context").Call(), ), Err().Op("!=").Nil(), ).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("unable to create authorization token for api request: %w"), Err())), ).Else().If(Id("token").Op("!=").Lit("")).Block( Id("req").Dot("Header").Dot("Set").Call( Lit("Authorization"), Qual("fmt", "Sprintf"). Call(Lit("Bearer %s"), Id("token")), ), ), ) grp.Line() return nil } // Otherwise, we need to add the complex data type auth, err := encoding.DescribeAuth(g.md, g.md.AuthHandler.Params, nil) if err != nil { return errors.Wrap(err, "unable to describe auth data") } authGeneratorCodeBlock := If( List(Id("authData"), Err()).Op(":="). Id("b").Dot("authGenerator").Call( Id("req").Dot("Context").Call(), ), Err().Op("!=").Nil(), ).Block( Return(Nil(), Qual("fmt", "Errorf").Call(Lit("unable to create authorization token for api request: %w"), Err())), ).Else() if g.md.AuthHandler.AuthData.GetPointer() != nil { authGeneratorCodeBlock = authGeneratorCodeBlock.If(Id("authData").Op("!=").Nil()) } authGeneratorCodeBlock = authGeneratorCodeBlock.BlockFunc(func(grp *Group) { enc := g.enc.NewPossibleInstance("authEncoder") enc.Add(Line()) if len(auth.QueryParameters) > 0 { enc.Add(Comment("Add the auth fields to the query string"), Line()) enc.Add(Id("query").Op(":=").Id("req").Dot("URL").Dot("Query").Call(), Line()) // Check the request schema for fields we can put in the query string for _, field := range auth.QueryParameters { if field.Type.GetList() != nil { // If we have a slice, we need to encode each bit slice, err := enc.ToStringSlice( field.Type, Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase)), ) if err != nil { err = errors.Wrapf(err, "unable to encode query fields %s", field.SrcName) return } enc.Add(For(List(Id("_"), Id("v")).Op(":=").Range().Add(slice)).Block( Id("query").Dot("Add").Call( Lit(field.WireFormat), Id("v"), ), ), Line()) } else if opt := field.Type.GetOption(); opt != nil { // We encode options as *T, so check if it's non-nil. enc.Add(If( Id("val").Op(":=").Add(Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase))), Id("val").Op("!=").Nil(), ).BlockFunc(func(g *Group) { val, err := enc.ToString( field.Type, Id("val"), ) if err != nil { err = errors.Wrapf(err, "unable to encode query field %s", field.SrcName) return } g.Add(Id("query").Dot("Set").Call( Lit(field.WireFormat), val, ), Line()) })) } else { // Otherwise, we can just append the field val, err := enc.ToString( field.Type, Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase)), ) if err != nil { err = errors.Wrapf(err, "unable to encode query field %s", field.SrcName) return } enc.Add(Id("query").Dot("Set").Call( Lit(field.WireFormat), val, ), Line()) } } enc.Add(Id("req").Dot("URL").Dot("RawQuery").Op("=").Id("query").Dot("Encode").Call(), Line(), Line()) } if len(auth.HeaderParameters) > 0 { enc.Add(Comment("Add the auth fields to the headers"), Line()) // Check the request schema for fields we can put in the query string for _, field := range auth.HeaderParameters { if field.Type.GetList() != nil { // If we have a slice, we need to encode each bit slice, err := enc.ToStringSlice( field.Type, Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase)), ) if err != nil { err = errors.Wrapf(err, "unable to encode header fields %s", field.SrcName) return } enc.Add(For(List(Id("_"), Id("v")).Op(":=").Range().Add(slice)).Block( Id("req").Dot("Header").Dot("Add").Call( Lit(field.WireFormat), Id("v"), ), ), Line()) } else if opt := field.Type.GetOption(); opt != nil { // We encode options as *T, so check if it's non-nil. enc.Add(If( Id("val").Op(":=").Add(Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase))), Id("val").Op("!=").Nil(), ).BlockFunc(func(g *Group) { val, err := enc.ToString( field.Type, Id("val"), ) if err != nil { err = errors.Wrapf(err, "unable to encode header field %s", field.SrcName) return } enc.Add(Id("req").Dot("Header").Dot("Set").Call( Lit(field.WireFormat), val, ), Line()) })) } else { // Otherwise, we can just append the field val, err := enc.ToString( field.Type, Id("authData").Dot(idents.Convert(field.SrcName, idents.PascalCase)), ) if err != nil { err = errors.Wrapf(err, "unable to encode header field %s", field.SrcName) return } enc.Add(Id("req").Dot("Header").Dot("Set").Call( Lit(field.WireFormat), val, ), Line()) } } } grp.Add(enc.Finalize(Return(Nil(), Qual("fmt", "Errorf").Call( Lit("unable to marshal authentication data: %w"), enc.LastError(), )))...) }) grp.If(Id("b").Dot("authGenerator").Op("!=").Nil()).Block( authGeneratorCodeBlock, ) grp.Line() return } ================================================ FILE: pkg/clientgen/javascript.go ================================================ package clientgen import ( "bufio" "bytes" "fmt" "sort" "strings" "unicode" "github.com/cockroachdb/errors" "golang.org/x/text/cases" "golang.org/x/text/language" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/idents" "encr.dev/internal/version" "encr.dev/parser/encoding" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) /* The JavaScript generator generates code that looks like this: class TaskServiceClient { public Add(params) { // ... } } export const task = { ServiceClient: TaskServiceClient } */ // jsGenVersion allows us to introduce breaking changes in the generated code but behind a switch // meaning that people with client code reliant on the old behaviour can continue to generate the // old code. type jsGenVersion int const ( // JsInitial is the originally released javascript generator JsInitial jsGenVersion = iota // JsExperimental can be used to lock experimental or uncompleted features in the generated code // It should always be the last item in the enum JsExperimental ) const javascriptGenLatestVersion = JsExperimental - 1 type javascript struct { *bytes.Buffer md *meta.Data appSlug string typs *typeRegistry currDecl *schema.Decl generatorVersion jsGenVersion seenJSON bool // true if a JSON type was seen seenHeaderResponse bool // true if we've seen a header used in a response object hasAuth bool // true if we've seen an authentication handler authIsComplexType bool // true if the auth type is a complex type } func (js *javascript) Version() int { return int(js.generatorVersion) } func (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) { defer js.handleBailout(&err) js.Buffer = p.Buf js.md = p.Meta js.appSlug = p.AppSlug js.typs = getNamedTypes(p.Meta, p.Services) if js.md.AuthHandler != nil { if !js.isAuthCookiesOnly() { js.hasAuth = true js.authIsComplexType = js.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING } } js.WriteString("// " + doNotEditHeader() + "\n\n") js.WriteString("// Disable eslint, jshint, and jslint for this file.\n") js.WriteString("/* eslint-disable */\n") js.WriteString("/* jshint ignore:start */\n") js.WriteString("/*jslint-disable*/\n") seenNs := make(map[string]bool) js.writeClient(p.Services) for _, svc := range p.Meta.Svcs { if err := js.writeService(svc, p.Services, p.Tags); err != nil { return err } seenNs[svc.Name] = true } js.writeExtraTypes() js.writeStreamClasses() if err := js.writeBaseClient(p.AppSlug); err != nil { return err } js.writeCustomErrorType() return nil } func (js *javascript) getFields(typ *schema.Type) []*schema.Field { if typ == nil { return nil } switch typ.Typ.(type) { case *schema.Type_Struct: return typ.GetStruct().Fields case *schema.Type_Named: decl := js.md.Decls[typ.GetNamed().Id] return js.getFields(decl.Type) default: return nil } } func (js *javascript) isAuthCookiesOnly() bool { if js.md.AuthHandler == nil { return false } fields := js.getFields(js.md.AuthHandler.Params) if fields == nil { return false } for _, field := range fields { if field.Wire.GetCookie() == nil { return false } } return true } func (js *javascript) writeService(svc *meta.Service, set clientgentypes.ServiceSet, tags clientgentypes.TagSet) error { // Determine if we have anything worth exposing. // Either a public RPC or a named type. isIncluded := hasPublicRPC(svc) && set.Has(svc.Name) if !isIncluded { return nil } ns := svc.Name numIndent := 0 indent := func() { js.WriteString(strings.Repeat(" ", numIndent)) } // Service doc string if doc := getServiceDoc(js.md, svc); doc != "" { scanner := bufio.NewScanner(strings.NewReader(doc)) js.WriteString("/**\n") for scanner.Scan() { js.WriteString(" * ") js.WriteString(scanner.Text()) js.WriteByte('\n') } js.WriteString(" */\n") } fmt.Fprintf(js, "class %sServiceClient {\n", cases.Title(language.English, cases.Compact).String(js.typeName(ns))) numIndent++ // Constructor indent() js.WriteString("constructor(baseClient) {\n") numIndent++ indent() js.WriteString("this.baseClient = baseClient\n") for _, rpc := range svc.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) { continue } name := js.memberName(rpc.Name) indent() fmt.Fprintf(js, "this.%s = this.%s.bind(this)\n", name, name) } numIndent-- indent() js.WriteString("}\n") // RPCs for _, rpc := range svc.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) { continue } js.WriteByte('\n') // Doc string if rpc.Doc != nil && *rpc.Doc != "" { scanner := bufio.NewScanner(strings.NewReader(*rpc.Doc)) indent() js.WriteString("/**\n") for scanner.Scan() { indent() js.WriteString(" * ") js.WriteString(scanner.Text()) js.WriteByte('\n') } indent() js.WriteString(" */\n") } // Signature indent() fmt.Fprintf(js, "async %s(", js.memberName(rpc.Name)) if rpc.Proto == meta.RPC_RAW { js.WriteString("method, ") } nParams := 0 var rpcPath strings.Builder for _, s := range rpc.Path.Segments { rpcPath.WriteByte('/') if s.Type != meta.PathSegment_LITERAL { if nParams > 0 { js.WriteString(", ") } js.WriteString(js.nonReservedId(s.Value)) if s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK { rpcPath.WriteString("${" + js.nonReservedId(s.Value) + ".map(encodeURIComponent).join(\"/\")}") } else { rpcPath.WriteString("${encodeURIComponent(" + js.nonReservedId(s.Value) + ")}") } nParams++ } else { rpcPath.WriteString(s.Value) } } var isStream = rpc.StreamingRequest || rpc.StreamingResponse // Avoid a name collision. payloadName := "params" if (!isStream && rpc.RequestSchema != nil) || (isStream && rpc.HandshakeSchema != nil) { if nParams > 0 { js.WriteString(", ") } js.WriteString(payloadName) } else if rpc.Proto == meta.RPC_RAW { if nParams > 0 { js.WriteString(", ") } js.WriteString("body, options") } js.WriteString(") {\n") if isStream { var direction streamDirection if rpc.StreamingRequest && rpc.StreamingResponse { direction = InOut } else if rpc.StreamingRequest { direction = Out } else { direction = In } if err := js.streamCallSite(js.newIdentWriter(numIndent+1), rpc, rpcPath.String(), direction); err != nil { return errors.Wrapf(err, "unable to write streaming RPC call site for %s.%s", rpc.ServiceName, rpc.Name) } } else { if err := js.rpcCallSite(js.newIdentWriter(numIndent+1), rpc, rpcPath.String()); err != nil { return errors.Wrapf(err, "unable to write RPC call site for %s.%s", rpc.ServiceName, rpc.Name) } } indent() js.WriteString("}\n") } numIndent-- indent() js.WriteString("}\n\n") fmt.Fprintf(js, "export const %s = {\n", js.typeName(ns)) numIndent++ indent() fmt.Fprintf(js, "ServiceClient: %sServiceClient\n", cases.Title(language.English, cases.Compact).String(js.typeName(ns))) numIndent-- indent() js.WriteString("}\n\n") return nil } type streamDirection int const ( InOut streamDirection = iota In Out ) func (js *javascript) streamCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string, direction streamDirection) error { headers := "" query := "" if rpc.HandshakeSchema != nil { encs, err := encoding.DescribeRequest(js.md, rpc.HandshakeSchema, &encoding.Options{SrcNameTag: "json"}, "GET") if err != nil { return errors.Wrapf(err, "stream %s", rpc.Name) } handshakeEnc := encs[0] if len(handshakeEnc.HeaderParameters) > 0 || len(handshakeEnc.QueryParameters) > 0 { w.WriteString("// Convert our params into the objects we need for the request\n") } // Generate the headers if len(handshakeEnc.HeaderParameters) > 0 { headers = "headers" dict := make(map[string]string) for _, field := range handshakeEnc.HeaderParameters { ref := js.Dot("params", field.SrcName) dict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("const headers = makeRecord(") js.Values(w, dict) w.WriteString(")\n\n") } // Generate the query string if len(handshakeEnc.QueryParameters) > 0 { query = "query" dict := make(map[string]string) for _, field := range handshakeEnc.QueryParameters { if list := field.Type.GetList(); list != nil { dict[field.WireFormat] = js.Dot("params", field.SrcName) + ".map((v) => " + js.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = js.convertBuiltinToString( field.Type.GetBuiltin(), js.Dot("params", field.SrcName), field.Optional, ) } } w.WriteString("const query = makeRecord(") js.Values(w, dict) w.WriteString(")\n\n") } } // Build the call to createStream var method string switch direction { case InOut: method = "createStreamInOut" case In: method = "createStreamIn" case Out: method = "createStreamOut" } createStream := fmt.Sprintf( "this.baseClient.%s(`%s`", method, rpcPath, ) if headers != "" || query != "" { createStream += ", {" + headers if headers != "" && query != "" { createStream += ", " } if query != "" { createStream += query } createStream += "}" } createStream += ")" w.WriteStringf("return await %s\n", createStream) return nil } func (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string) error { // Work out how we're going to encode and call this RPC rpcEncoding, err := encoding.DescribeRPC(js.md, rpc, &encoding.Options{SrcNameTag: "json"}) if err != nil { return errors.Wrapf(err, "rpc %s", rpc.Name) } // Raw end points just pass through the request // and need no further code generation if rpc.Proto == meta.RPC_RAW { w.WriteStringf( "return this.baseClient.callAPI(method, `%s`, body, options)\n", rpcPath, ) return nil } // Work out how we encode the Request Schema headers := "" query := "" body := "" if rpc.RequestSchema != nil { reqEnc := rpcEncoding.DefaultRequestEncoding if len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 { w.WriteString("// Convert our params into the objects we need for the request\n") } // Generate the headers if len(reqEnc.HeaderParameters) > 0 { headers = "headers" dict := make(map[string]string) for _, field := range reqEnc.HeaderParameters { ref := js.Dot("params", field.SrcName) dict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("const headers = makeRecord(") js.Values(w, dict) w.WriteString(")\n\n") } // Generate the query string if len(reqEnc.QueryParameters) > 0 { query = "query" dict := make(map[string]string) for _, field := range reqEnc.QueryParameters { if list := field.Type.GetList(); list != nil { dict[field.WireFormat] = js.Dot("params", field.SrcName) + ".map((v) => " + js.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = js.convertBuiltinToString( field.Type.GetBuiltin(), js.Dot("params", field.SrcName), field.Optional, ) } } w.WriteString("const query = makeRecord(") js.Values(w, dict) w.WriteString(")\n\n") } // Generate the body if len(reqEnc.BodyParameters) > 0 { if len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 { // In the simple case we can just encode the params as the body directly body = "JSON.stringify(params)" } else { // Else we need a new struct called "body" body = "JSON.stringify(body)" dict := make(map[string]string) for _, field := range reqEnc.BodyParameters { fieldName := field.SrcName dict[fieldName] = js.Dot("params", fieldName) } w.WriteString("// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\nconst body = ") js.Values(w, dict) w.WriteString("\n\n") } } } // Build the call to callTypedAPI callAPI := fmt.Sprintf( "this.baseClient.callTypedAPI(\"%s\", `%s`", rpcEncoding.DefaultMethod, rpcPath, ) if body != "" || headers != "" || query != "" { if body == "" { callAPI += ", undefined" } else { callAPI += ", " + body } if headers != "" || query != "" { callAPI += ", {" + headers if headers != "" && query != "" { callAPI += ", " } if query != "" { callAPI += query } callAPI += "}" } } callAPI += ")" // If there's no response schema, we can just return the call to the API directly if rpc.ResponseSchema == nil { w.WriteStringf("await %s\n", callAPI) return nil } w.WriteStringf("// Now make the actual call to the API\nconst resp = await %s\n", callAPI) respEnc := rpcEncoding.ResponseEncoding // If we don't need to do anything with the body, we can just return the response if len(respEnc.HeaderParameters) == 0 { w.WriteString("return await resp.json()\n") return nil } // Otherwise, we need to add the header fields to the response w.WriteString("\n//Populate the return object from the JSON body and received headers\nconst rtn = await resp.json()\n") for _, headerField := range respEnc.HeaderParameters { isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie" if isSetCookie { w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n") w.WriteString("if (!BROWSER) {\n") w = w.Indent() } js.seenHeaderResponse = true fieldValue := fmt.Sprintf("resp.headers.get(\"%s\")", headerField.WireFormat) if !headerField.Optional { fieldValue = fmt.Sprintf("mustBeSet(\"Header `%s`\", %s)", headerField.WireFormat, fieldValue) } w.WriteStringf("%s = %s\n", js.Dot("rtn", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) if isSetCookie { w = w.Dedent() w.WriteString("}\n") } } w.WriteString("return rtn\n") return nil } // nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Typescript keyword func (js *javascript) nonReservedId(id string) string { switch id { // our reserved keywords (or ID's we use within the generated client functions) case "params", "headers", "query", "body", "resp", "rtn": return "_" + id // Javascript keywords // Based on https://www.w3schools.com/js/js_reserved.asp case "abstract", "arguments", "async", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", "finally", "float", "for", "function", "get", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "let", "long", "native", "new", "null", "of", "package", "private", "protected", "public", "require", "return", "short", "static", "super", "switch", "symbol", "synchronized", "this", "throw", "throws", "transient", "true", "try", "type", "typeof", "var", "void", "volatile", "while", "with", "yield": return "_" + id default: return id } } func (js *javascript) writeClient(set clientgentypes.ServiceSet) { w := js.newIdentWriter(0) w.WriteString(` /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return ` + "`https://${name}-" + js.appSlug + ".encr.app`" + ` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(` + "`pr${pr}`" + `) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the ` + js.appSlug + ` Encore application. */ export default class Client {`) { w := w.Indent() w.WriteString(` /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) {`) { w := w.Indent() if js.hasAuth && !js.authIsComplexType { w.WriteString(` // Convert the old constructor parameters to a BaseURL object and a ClientOptions object if (!target.startsWith("http://") && !target.startsWith("https://")) { target = Environment(target) } if (typeof options === "string") { options = { auth: options } } `) } else { w.WriteString("\n") } w.WriteString("const base = new BaseClient(target, options ?? {})\n") for _, svc := range js.md.Svcs { if hasPublicRPC(svc) && set.Has(svc.Name) { w.WriteStringf("this.%s = new %s.ServiceClient(base)\n", js.memberName(svc.Name), js.typeName(svc.Name)) } } } w.WriteString(" }\n") } w.WriteString("}\n\n") } func (js *javascript) writeStreamClasses() { send := ` async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); }` receive := ` async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } }` js.WriteString(` function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } ` + send + ` ` + receive + ` } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } ` + receive + ` } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } ` + send + ` }`) } func (js *javascript) writeBaseClient(appSlug string) error { userAgent := fmt.Sprintf("%s-Generated-JS-Client (Encore/%s)", appSlug, version.Version) js.WriteString(` const boundFetch = fetch.bind(this) class BaseClient {`) js.WriteString(` constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "` + userAgent + `"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch }`) if js.hasAuth { js.WriteString(` // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } `) } js.WriteString(` } async getAuthData() {`) if js.hasAuth { js.WriteString(` let authData; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data = {}; `) w := js.newIdentWriter(3) if js.authIsComplexType { authData, err := encoding.DescribeAuth(js.md, js.md.AuthHandler.Params, &encoding.Options{SrcNameTag: "json"}) if err != nil { return errors.Wrap(err, "unable to describe auth data") } // Generate the query string if len(authData.QueryParameters) > 0 { dict := make(map[string]string) for _, field := range authData.QueryParameters { if list := field.Type.GetList(); list != nil { dict[field.WireFormat] = js.Dot("authData", field.SrcName) + ".map((v) => " + js.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = js.convertBuiltinToString( field.Type.GetBuiltin(), js.Dot("authData", field.SrcName), field.Optional, ) } } w.WriteString("data.query = makeRecord(") js.Values(w, dict) w.WriteString(");\n") } // Generate the headers if len(authData.HeaderParameters) > 0 { dict := make(map[string]string) for _, field := range authData.HeaderParameters { ref := js.Dot("authData", field.SrcName) dict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("data.headers = makeRecord(") js.Values(w, dict) w.WriteString(")\n") } } else { w.WriteString("data.headers = {};\n") w.WriteString("data.headers[\"Authorization\"] = \"Bearer \" + authData;\n") } w.WriteString("\nreturn data;\n") w.Dedent().WriteString("}\n") } js.WriteString(` return undefined; } `) js.WriteString(` // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: ` + "`request failed: status ${response.status}`" + ` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } }`) return nil } func (js *javascript) writeExtraTypes() { js.WriteString(` function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(` + "`" + `${key}=${encodeURIComponent(v)}` + "`" + `) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } `) if js.seenHeaderResponse { js.WriteString(` // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field, value) { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: ` + "`${field} was unexpectedly ${value}`" + `, // ${value} will create the string "null" or "undefined" }, ) } return value } `) } } func (js *javascript) convertBuiltinToString(typ schema.Builtin, val string, isOptional bool) string { var code string switch typ { case schema.Builtin_STRING: return val case schema.Builtin_JSON: code = fmt.Sprintf("JSON.stringify(%s)", val) default: code = fmt.Sprintf("String(%s)", val) } if isOptional { code = fmt.Sprintf("%s === undefined ? undefined : %s", val, code) } return code } func (js *javascript) convertStringToBuiltin(typ schema.Builtin, val string) string { switch typ { case schema.Builtin_ANY: return val case schema.Builtin_BOOL: return fmt.Sprintf("%s.toLowerCase() === \"true\"", val) case schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64, schema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64: return fmt.Sprintf("parseInt(%s, 10)", val) case schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: return fmt.Sprintf("Number(%s)", val) case schema.Builtin_STRING: return val case schema.Builtin_BYTES: return val case schema.Builtin_TIME: return val case schema.Builtin_JSON: js.seenJSON = true return fmt.Sprintf("JSON.parse(%s)", val) case schema.Builtin_UUID: return val case schema.Builtin_USER_ID: return val case schema.Builtin_DECIMAL: return val default: js.errorf("unknown builtin type %v", typ) return "any" } } func (js *javascript) errorf(format string, args ...interface{}) { panic(bailout{fmt.Errorf(format, args...)}) } func (js *javascript) handleBailout(dst *error) { if err := recover(); err != nil { if bail, ok := err.(bailout); ok { *dst = bail.err } else { panic(err) } } } func (js *javascript) newIdentWriter(indent int) *indentWriter { return &indentWriter{ w: js.Buffer, depth: indent, indent: " ", firstWriteOnLine: true, } } func (js *javascript) Quote(s string) string { return fmt.Sprintf("\"%s\"", strings.Replace(s, "\"", "\\\"", -1)) } func (js *javascript) QuoteIfRequired(s string) string { // If the identifier isn't purely alphanumeric, we need to add quotes. if !stringIsOnly(s, func(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }) { return js.Quote(s) } return s } // Dot allows us to reference a field in a struct by ijs name. func (js *javascript) Dot(structIdent string, fieldIdent string) string { fieldIdent = js.QuoteIfRequired(fieldIdent) if len(fieldIdent) > 0 && fieldIdent[0] == '"' { return fmt.Sprintf("%s[%s]", structIdent, fieldIdent) } else { return fmt.Sprintf("%s.%s", structIdent, fieldIdent) } } func (js *javascript) Values(w *indentWriter, dict map[string]string) { // Work out the largest key length. largestKey := 0 keys := make([]string, 0, len(dict)) for key := range dict { keys = append(keys, key) key = js.QuoteIfRequired(key) if len(key) > largestKey { largestKey = len(key) } } sort.Strings(keys) w.WriteString("{\n") { w := w.Indent() for _, key := range keys { ident := js.QuoteIfRequired(key) w.WriteStringf("%s: %s%s,\n", ident, strings.Repeat(" ", largestKey-len(ident)), dict[key]) } } w.WriteString("}") } func (js *javascript) typeName(identifier string) string { if js.generatorVersion < JsExperimental { return identifier } else { return idents.Convert(identifier, idents.PascalCase) } } func (js *javascript) memberName(identifier string) string { if js.generatorVersion < JsExperimental { return identifier } else { return idents.Convert(identifier, idents.CamelCase) } } func (js *javascript) fieldNameInStruct(field *schema.Field) string { name := field.Name if field.JsonName != "" { name = field.JsonName } return name } func (js *javascript) writeCustomErrorType() { w := js.newIdentWriter(0) w.WriteString(` function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } `) } ================================================ FILE: pkg/clientgen/openapi/openapi.go ================================================ package openapi import ( "encoding/json" "fmt" "go/doc/comment" "strings" "github.com/cockroachdb/errors" "github.com/getkin/kin-openapi/openapi3" "encr.dev/parser/encoding" "encr.dev/pkg/clientgen/clientgentypes" meta "encr.dev/proto/encore/parser/meta/v1" ) type GenVersion int const ( // Initial is the originally released OpenAPI client generator Initial GenVersion = iota // Experimental can be used to lock experimental or uncompleted features in the generated code // It should always be the last item in the enum. Experimental LatestVersion GenVersion = Experimental - 1 ) type Generator struct { ver GenVersion spec *openapi3.T md *meta.Data seenDecls map[string]uint32 } func New(version GenVersion) *Generator { return &Generator{ ver: version, seenDecls: make(map[string]uint32), } } func (g *Generator) Version() int { return int(g.ver) } func (g *Generator) Generate(p clientgentypes.GenerateParams) (err error) { defer func() { if r := recover(); r != nil { if b, ok := r.(bailout); ok { err = b.err } else { panic(r) } } }() g.md = p.Meta g.spec = newSpec(p.AppSlug) for _, svc := range p.Meta.Svcs { if p.Services.Has(svc.Name) { if err := g.addService(svc, p.Tags, p.Options); err != nil { return err } } } out, err := g.spec.MarshalJSON() if err != nil { return errors.Wrap(err, "marshal openapi spec") } // Pretty-print the JSON output return json.Indent(p.Buf, out, "", " ") } func (g *Generator) addService(svc *meta.Service, tags clientgentypes.TagSet, opts clientgentypes.Options) error { for _, rpc := range svc.Rpcs { // streaming endpoints not supported yet if rpc.StreamingRequest || rpc.StreamingResponse { continue } // Skip private endpoints if the flag is set if opts.OpenAPIExcludePrivateEndpoints && rpc.AccessType == meta.RPC_PRIVATE { continue } // Skip RPCs that don't match the tags if !tags.IsRPCIncluded(rpc) { continue } if err := g.addRPC(rpc); err != nil { return err } } return nil } func (g *Generator) addRPC(rpc *meta.RPC) error { item := g.getOrCreatePath(rpc) encodings, err := encoding.DescribeRPC(g.md, rpc, &encoding.Options{}) if err != nil { return errors.Wrapf(err, "describe rpc %s.%s", rpc.ServiceName, rpc.Name) } for _, reqEnc := range encodings.RequestEncoding { for _, m := range reqEnc.HTTPMethods { op, err := g.newOperationForEncoding(rpc, m, reqEnc, encodings.ResponseEncoding) if err != nil { return errors.Wrapf(err, "create operation for rpc %s.%s", rpc.ServiceName, rpc.Name) } item.SetOperation(m, op) } } g.spec.Paths[rpcPath(rpc)] = item return nil } func (g *Generator) getOrCreatePath(rpc *meta.RPC) *openapi3.PathItem { path := rpcPath(rpc) if existing, ok := g.spec.Paths[path]; ok { return existing } item := &openapi3.PathItem{} g.spec.Paths[path] = item return item } func (g *Generator) newOperationForEncoding(rpc *meta.RPC, method string, reqEnc *encoding.RequestEncoding, respEnc *encoding.ResponseEncoding) (*openapi3.Operation, error) { summary, desc := "", "" if rpc.Doc != nil { summary, desc = splitDoc(*rpc.Doc) } op := &openapi3.Operation{ Summary: summary, Description: desc, OperationID: method + ":" + rpc.ServiceName + "." + rpc.Name, Responses: make(openapi3.Responses), } // Add path parameters for _, seg := range rpc.Path.Segments { if seg.Type == meta.PathSegment_LITERAL { continue } op.Parameters = append(op.Parameters, &openapi3.ParameterRef{ Value: &openapi3.Parameter{ Name: seg.Value, In: openapi3.ParameterInPath, Description: "", Style: openapi3.SerializationSimple, Explode: ptr(false), AllowEmptyValue: true, AllowReserved: false, Deprecated: false, Required: true, Schema: g.pathParamType(seg.ValueType).NewRef(), Example: nil, Examples: nil, Content: nil, }, }) } // Add header parameters for _, param := range reqEnc.HeaderParameters { op.Parameters = append(op.Parameters, &openapi3.ParameterRef{ Value: &openapi3.Parameter{ Name: param.WireFormat, In: openapi3.ParameterInHeader, Description: markdownDoc(param.Doc), Style: openapi3.SerializationSimple, Explode: ptr(true), AllowEmptyValue: true, AllowReserved: false, Deprecated: false, Required: !param.Optional, Schema: g.schemaType(param.Type), Example: nil, Examples: nil, Content: nil, }, }) } // Add query parameters for _, param := range reqEnc.QueryParameters { op.Parameters = append(op.Parameters, &openapi3.ParameterRef{ Value: &openapi3.Parameter{ Name: param.WireFormat, In: openapi3.ParameterInQuery, Description: markdownDoc(param.Doc), Style: openapi3.SerializationForm, Explode: ptr(true), AllowEmptyValue: true, AllowReserved: false, Deprecated: false, Required: !param.Optional, Schema: g.schemaType(param.Type), Example: nil, Examples: nil, Content: nil, }, }) } // Add request body if len(reqEnc.BodyParameters) > 0 { op.RequestBody = &openapi3.RequestBodyRef{ Value: &openapi3.RequestBody{ Description: "", Required: false, Content: g.bodyContent(reqEnc.BodyParameters), }, } } // Encode the response { resp := &openapi3.Response{ Headers: make(openapi3.Headers), Links: nil, Description: ptr("Success response"), } if respEnc != nil { for _, param := range respEnc.HeaderParameters { resp.Headers[param.WireFormat] = &openapi3.HeaderRef{ Value: &openapi3.Header{Parameter: openapi3.Parameter{ Description: markdownDoc(param.Doc), Style: openapi3.SerializationSimple, Explode: ptr(true), AllowEmptyValue: true, AllowReserved: false, Deprecated: false, Required: !param.Optional, Schema: g.schemaType(param.Type), Example: nil, Examples: nil, Content: nil, }}, } } if len(respEnc.BodyParameters) > 0 { resp.Content = g.bodyContent(respEnc.BodyParameters) } } op.Responses["200"] = &openapi3.ResponseRef{ Value: resp, } op.Responses["default"] = &openapi3.ResponseRef{ Ref: "#/components/responses/APIError", } } return op, nil } func rpcPath(rpc *meta.RPC) string { var b strings.Builder for _, seg := range rpc.Path.Segments { b.WriteString("/") switch seg.Type { case meta.PathSegment_LITERAL: b.WriteString(seg.Value) default: b.WriteString("{") b.WriteString(seg.Value) b.WriteString("}") } } return b.String() } func splitDoc(doc string) (plaintextSummary, markdownDescription string) { firstLine, remaining := doc, "" if idx := strings.Index(doc, "\n"); idx >= 0 { firstLine = doc[:idx] remaining = doc[idx+1:] } return plaintextDoc(firstLine), markdownDoc(remaining) } func plaintextDoc(doc string) string { var parser comment.Parser var pr comment.Printer d := parser.Parse(doc) return string(pr.Text(d)) } func markdownDoc(doc string) string { var parser comment.Parser var pr comment.Printer d := parser.Parse(doc) return string(pr.Markdown(d)) } func ptr[T any](t T) *T { return &t } func newSpec(appSlug string) *openapi3.T { t := &openapi3.T{ Components: &openapi3.Components{ RequestBodies: make(map[string]*openapi3.RequestBodyRef), Responses: make(map[string]*openapi3.ResponseRef), SecuritySchemes: make(map[string]*openapi3.SecuritySchemeRef), Schemas: make(map[string]*openapi3.SchemaRef), }, Info: &openapi3.Info{ Title: fmt.Sprintf("API for %s", appSlug), Description: "Generated by encore", Version: "1", Extensions: map[string]any{ "x-logo": map[string]string{ "url": "https://encore.dev/assets/branding/logo/logo-black.png", "backgroundColor": "#EEEEE1", "altText": "Encore logo", }, }, }, OpenAPI: "3.0.0", Paths: make(openapi3.Paths), } // Add the local platform server: t.AddServer( &openapi3.Server{ URL: "http://localhost:4000", Description: "Encore local dev environment", }, ) t.Components.Responses["APIError"] = &openapi3.ResponseRef{ Value: &openapi3.Response{ Content: openapi3.Content{ "application/json": &openapi3.MediaType{ Schema: &openapi3.SchemaRef{ Value: &openapi3.Schema{ Type: openapi3.TypeObject, Title: "APIError", ExternalDocs: &openapi3.ExternalDocs{ URL: "https://pkg.go.dev/encore.dev/beta/errs#Error", }, Properties: map[string]*openapi3.SchemaRef{ "code": { Value: &openapi3.Schema{ Description: "Error code", Example: "not_found", Type: openapi3.TypeString, ExternalDocs: &openapi3.ExternalDocs{ URL: "https://pkg.go.dev/encore.dev/beta/errs#ErrCode", }, }, }, "message": { Value: &openapi3.Schema{ Description: "Error message", Type: openapi3.TypeString, }, }, "details": { Value: &openapi3.Schema{ Description: "Error details", Type: openapi3.TypeObject, }, }, }, }, }, }, }, Description: ptr("Error response"), }, } return t } type bailout struct { err error } func doBailout(err error) { panic(bailout{err}) } ================================================ FILE: pkg/clientgen/openapi/schema.go ================================================ package openapi import ( "fmt" "math" "strings" "github.com/cockroachdb/errors" "github.com/getkin/kin-openapi/openapi3" "encr.dev/parser/encoding" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) func (g *Generator) bodyContent(params []*encoding.ParameterEncoding) openapi3.Content { if len(params) == 0 { return nil } required := make([]string, 0, len(params)) props := make(openapi3.Schemas) for _, p := range params { val := g.schemaType(p.Type) if vv := val.Value; vv != nil { vv.Title, vv.Description = splitDoc(p.Doc) } props[p.WireFormat] = val if !p.Optional { required = append(required, p.WireFormat) } } s := openapi3.NewObjectSchema() s.Properties = props s.Required = required return openapi3.Content{ "application/json": &openapi3.MediaType{ Schema: s.NewRef(), Example: nil, Examples: nil, Encoding: nil, }, } } func (g *Generator) schemaType(typ *schema.Type) *openapi3.SchemaRef { switch t := typ.Typ.(type) { // A type switch for all the different schema types we support case *schema.Type_Named: return g.namedSchemaType(t.Named) case *schema.Type_Struct: props := make(openapi3.Schemas) required := make([]string, 0, len(t.Struct.Fields)) for _, f := range t.Struct.Fields { jsonName := f.JsonName if jsonName == "-" { continue } if jsonName == "" { jsonName = f.Name } if !f.Optional { required = append(required, jsonName) } val := g.schemaType(f.Typ) if vv := val.Value; vv != nil { // Direct schema - can set title and description directly vv.Title, vv.Description = splitDoc(f.Doc) } else if val.Ref != "" && f.Doc != "" { // Schema reference with field documentation - use allOf pattern to add description // This is the recommended workaround for OpenAPI 3.0 to add descriptions to $ref // See: https://github.com/OAI/OpenAPI-Specification/issues/2033 // OpenAPI 3.1 supports description alongside $ref directly, but we use a library that doesn't support 3.1 yet title, description := splitDoc(f.Doc) val = &openapi3.SchemaRef{ Value: &openapi3.Schema{ AllOf: []*openapi3.SchemaRef{val}, Title: title, Description: description, }, } } props[jsonName] = val } s := openapi3.NewObjectSchema() s.Properties = props s.Required = required return s.NewRef() case *schema.Type_Map: // TODO non-string keys are not supported s := openapi3.NewObjectSchema() s.AdditionalProperties = openapi3.AdditionalProperties{ Schema: g.schemaType(t.Map.Value), } return s.NewRef() case *schema.Type_List: arr := openapi3.NewArraySchema() arr.Items = g.schemaType(t.List.Elem) return arr.NewRef() case *schema.Type_Pointer: return g.schemaType(t.Pointer.Base) case *schema.Type_Option: return g.schemaType(t.Option.Value) case *schema.Type_Literal: switch tt := t.Literal.Value.(type) { case *schema.Literal_Str: return openapi3.NewStringSchema().WithEnum(tt.Str).NewRef() case *schema.Literal_Boolean: return openapi3.NewBoolSchema().WithEnum(tt.Boolean).NewRef() case *schema.Literal_Int: return openapi3.NewInt64Schema().WithEnum(tt.Int).NewRef() case *schema.Literal_Float: return openapi3.NewFloat64Schema().WithEnum(tt.Float).NewRef() case *schema.Literal_Null: // This shouldn't happen in most situations as we handle literals explicitly. return openapi3.NewBoolSchema().WithNullable().NewRef() default: doBailout(errors.Newf("unknown literal type %T", tt)) return nil // unreachable } case *schema.Type_Union: // First check if all the fields are literals. // If so we can more accurately represent this union as an enum. var ( literals []any literalsType string haveAllLiterals = true haveLiteralNull bool ) for _, tt := range t.Union.Types { lit, ok := tt.Typ.(*schema.Type_Literal) if !ok { // It's not a literal. // Still need to keep going to find any nulls. haveAllLiterals = false continue } var litType string switch tt := lit.Literal.Value.(type) { case *schema.Literal_Str: litType = openapi3.TypeString literals = append(literals, tt.Str) case *schema.Literal_Boolean: litType = openapi3.TypeBoolean literals = append(literals, tt.Boolean) case *schema.Literal_Int: litType = openapi3.TypeInteger literals = append(literals, tt.Int) case *schema.Literal_Float: litType = openapi3.TypeNumber literals = append(literals, tt.Float) case *schema.Literal_Null: haveLiteralNull = true continue default: doBailout(errors.Newf("unknown literal type %T", tt)) } // Set the literals type if we haven't seen it yet. if literalsType == "" { literalsType = litType } else if literalsType != litType { // If we have different types, it can't be represented as an enum. haveAllLiterals = false } } if haveAllLiterals { s := openapi3.NewSchema() s.Type = literalsType s.Nullable = haveLiteralNull return s.WithEnum(literals...).NewRef() } // Otherwise, we have to represent this as an anyOf schema. schemaRefs := make([]*openapi3.SchemaRef, 0, len(t.Union.Types)) for _, tt := range t.Union.Types { schemaRefs = append(schemaRefs, g.schemaType(tt)) } s := openapi3.NewSchema() s.AnyOf = schemaRefs s.Nullable = haveLiteralNull return s.NewRef() case *schema.Type_TypeParameter: return openapi3.NewObjectSchema().NewRef() // unknown case *schema.Type_Config: elem := g.schemaType(t.Config.Elem) if t.Config.IsValuesList { s := openapi3.NewArraySchema() s.Items = elem return s.NewRef() } else { return elem } case *schema.Type_Builtin: return g.builtinSchemaType(t.Builtin).NewRef() default: doBailout(errors.Newf("unknown schema type %T", t)) panic("unreachable") } } func (g *Generator) builtinSchemaType(t schema.Builtin) *openapi3.Schema { switch t { case schema.Builtin_BOOL: return openapi3.NewBoolSchema() case schema.Builtin_INT8: return openapi3.NewInt32Schema().WithMin(math.MinInt8).WithMax(math.MaxInt8) case schema.Builtin_INT16: return openapi3.NewInt32Schema().WithMin(math.MinInt16).WithMax(math.MaxInt16) case schema.Builtin_INT32: return openapi3.NewInt32Schema().WithMin(math.MinInt32).WithMax(math.MaxInt32) case schema.Builtin_INT64, schema.Builtin_INT: return openapi3.NewInt64Schema() case schema.Builtin_UINT8: return openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint8) case schema.Builtin_UINT16: return openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint16) case schema.Builtin_UINT32: return openapi3.NewInt64Schema().WithMin(0).WithMax(math.MaxUint32) case schema.Builtin_UINT64, schema.Builtin_UINT: return openapi3.NewInt64Schema().WithMin(0) case schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: return openapi3.NewFloat64Schema() case schema.Builtin_STRING: return openapi3.NewStringSchema() case schema.Builtin_BYTES: return openapi3.NewStringSchema().WithFormat("byte") case schema.Builtin_TIME: return openapi3.NewStringSchema().WithFormat("date-time") case schema.Builtin_UUID: return openapi3.NewUUIDSchema() case schema.Builtin_JSON: return openapi3.NewObjectSchema() case schema.Builtin_USER_ID: return openapi3.NewStringSchema() case schema.Builtin_DECIMAL: return openapi3.NewStringSchema() default: doBailout(errors.Newf("unknown builtin type %v", t)) panic("unreachable") } } func (g *Generator) namedSchemaType(typ *schema.Named) *openapi3.SchemaRef { namedType := &schema.Type{Typ: &schema.Type_Named{Named: typ}} concrete, err := encoding.GetConcreteType(g.md.Decls, namedType, nil) if err != nil { doBailout(errors.Wrap(err, "get concrete type")) } origCandidate := g.typeToDefinitionName(namedType) // Make sure the candidate name corresponds to this declaration. for idx := 1; ; idx++ { candidate := origCandidate // Add a suffix if this is not the first candidate. if idx > 1 { candidate += fmt.Sprintf("_%d", idx) } if _, ok := g.spec.Components.Schemas[candidate]; ok { // There is already a declaration with that name; make sure it matches if seen, ok := g.seenDecls[candidate]; ok && seen != typ.Id { // Different declaration; try again. continue } } else { // Unused name; allocate it. // Write to the maps before we compute the schema to avoid infinite recursion // in the presence of recursive types. g.seenDecls[candidate] = typ.Id g.spec.Components.Schemas[candidate] = nil // Generate the schema and add the declaration's documentation schemaRef := g.schemaType(concrete) if schemaRef.Value != nil { // Get the declaration to access its documentation if decl := g.md.Decls[typ.Id]; decl != nil && decl.Doc != "" { title, description := splitDoc(decl.Doc) if schemaRef.Value.Title == "" { schemaRef.Value.Title = title } if schemaRef.Value.Description == "" { schemaRef.Value.Description = description } } } g.spec.Components.Schemas[candidate] = schemaRef } return &openapi3.SchemaRef{ Ref: "#/components/schemas/" + candidate, } } } func (g *Generator) typeToDefinitionName(typ *schema.Type) string { switch typ := typ.Typ.(type) { case *schema.Type_Named: var name strings.Builder decl := g.md.Decls[typ.Named.Id] name.WriteString(decl.Loc.PkgName) name.WriteString(".") name.WriteString(decl.Name) for _, typeArg := range typ.Named.TypeArguments { name.WriteString("_") name.WriteString(g.typeToDefinitionName(typeArg)) } return name.String() case *schema.Type_List: return "List_" + g.typeToDefinitionName(typ.List.Elem) case *schema.Type_Map: return "Map_" + g.typeToDefinitionName(typ.Map.Key) + "_" + g.typeToDefinitionName(typ.Map.Value) case *schema.Type_Pointer: return g.typeToDefinitionName(typ.Pointer.Base) case *schema.Type_Option: return "Option_" + g.typeToDefinitionName(typ.Option.Value) case *schema.Type_Config: return g.typeToDefinitionName(typ.Config.Elem) case *schema.Type_Builtin: switch typ.Builtin { case schema.Builtin_ANY: return "any" case schema.Builtin_BOOL: return "bool" case schema.Builtin_INT8: return "int8" case schema.Builtin_INT16: return "int16" case schema.Builtin_INT32: return "int32" case schema.Builtin_INT64: return "int64" case schema.Builtin_UINT8: return "uint8" case schema.Builtin_UINT16: return "uint16" case schema.Builtin_UINT32: return "uint32" case schema.Builtin_UINT64: return "uint64" case schema.Builtin_FLOAT32: return "float32" case schema.Builtin_FLOAT64: return "float64" case schema.Builtin_STRING: return "string" case schema.Builtin_BYTES: return "bytes" case schema.Builtin_TIME: return "string" case schema.Builtin_UUID: return "string" case schema.Builtin_JSON: return "string" case schema.Builtin_USER_ID: return "string" case schema.Builtin_INT: return "int" case schema.Builtin_UINT: return "uint" case schema.Builtin_DECIMAL: return "string" default: return "" } case *schema.Type_Literal: switch typ.Literal.Value.(type) { case *schema.Literal_Boolean: return "bool" case *schema.Literal_Str: return "string" case *schema.Literal_Null: return "null" case *schema.Literal_Int: return "int" case *schema.Literal_Float: return "float64" } } return "" } func (g *Generator) pathParamType(typ meta.PathSegment_ParamType) *openapi3.Schema { switch typ { case meta.PathSegment_BOOL: return openapi3.NewBoolSchema() case meta.PathSegment_INT8: return openapi3.NewInt32Schema().WithMin(math.MinInt8).WithMax(math.MaxInt8) case meta.PathSegment_INT16: return openapi3.NewInt32Schema().WithMin(math.MinInt16).WithMax(math.MaxInt16) case meta.PathSegment_INT32: return openapi3.NewInt32Schema().WithMin(math.MinInt32).WithMax(math.MaxInt32) case meta.PathSegment_INT64, meta.PathSegment_INT: return openapi3.NewInt64Schema() case meta.PathSegment_UINT8: return openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint8) case meta.PathSegment_UINT16: return openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint16) case meta.PathSegment_UINT32: return openapi3.NewInt64Schema().WithMin(0).WithMax(math.MaxUint32) case meta.PathSegment_UINT64, meta.PathSegment_UINT: return openapi3.NewInt64Schema().WithMin(0) case meta.PathSegment_STRING: return openapi3.NewStringSchema() case meta.PathSegment_UUID: return openapi3.NewUUIDSchema() default: doBailout(errors.Newf("unknown path param type: %v")) panic("unreachable") } } ================================================ FILE: pkg/clientgen/testdata/README.md ================================================ # Update Golden Tests Instead of manually updating the golden tests, once you've verified the output of the tests is correct, then you can simply update all the `expected output files` files by running ```bash go test ./pkg/clientgen -golden-update ``` ================================================ FILE: pkg/clientgen/testdata/goapp/expected_baseauth_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } // WithAuthToken allows you to set an authentication token to be used for each request. // // This token will be sent as a Bearer token in the Authorization header. func WithAuthToken(bearerToken string) Option { return func(base *baseClient) error { base.authGenerator = func(_ context.Context) (string, error) { return bearerToken, nil } return nil } } // WithAuthFunc allows you to pass a function which is called for each request to return an authentication token to be used for each request. // // This token will be sent as a Bearer token in the Authorization header. func WithAuthFunc(authGenerator func(ctx context.Context) (string, error)) Option { return func(base *baseClient) error { base.authGenerator = authGenerator return nil } } type SvcRequest struct { Message string } // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { // DummyAPI is a dummy endpoint. DummyAPI(ctx context.Context, params SvcRequest) error // Private is a basic auth endpoint. Private(ctx context.Context, params SvcRequest) error } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) // DummyAPI is a dummy endpoint. func (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error { _, err := callAPI(ctx, c.base, "POST", "/svc.DummyAPI", nil, params, nil) return err } // Private is a basic auth endpoint. func (c *svcClient) Private(ctx context.Context, params SvcRequest) error { _, err := callAPI(ctx, c.base, "POST", "/svc.Private", nil, params, nil) return err } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { authGenerator func(ctx context.Context) (string, error) // The function which will add the authentication data to the requests httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // If a authorization data generator is present, call it and add the returned token to the request if b.authGenerator != nil { if token, err := b.authGenerator(req.Context()); err != nil { return nil, fmt.Errorf("unable to create authorization token for api request: %w", err) } else if token != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } } // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_baseauth_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { // Convert the old constructor parameters to a BaseURL object and a ClientOptions object if (!target.startsWith("http://") && !target.startsWith("https://")) { target = Environment(target) } if (typeof options === "string") { options = { auth: options } } const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.DummyAPI = this.DummyAPI.bind(this) this.Private = this.Private.bind(this) } /** * DummyAPI is a dummy endpoint. */ async DummyAPI(params) { await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(params)) } /** * Private is a basic auth endpoint. */ async Private(params) { await this.baseClient.callTypedAPI("POST", `/svc.Private`, JSON.stringify(params)) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData() { let authData; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data = {}; data.headers = {}; data.headers["Authorization"] = "Bearer " + authData; return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_baseauth_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/svc.DummyAPI": { "post": { "operationId": "POST:svc.DummyAPI", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Message": { "type": "string" } }, "required": [ "Message" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } }, "summary": "DummyAPI is a dummy endpoint.\n" } }, "/svc.Private": { "post": { "operationId": "POST:svc.Private", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Message": { "type": "string" } }, "required": [ "Message" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } }, "summary": "Private is a basic auth endpoint.\n" } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_baseauth_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * @deprecated This constructor is deprecated, and you should move to using BaseURL with an Options object */ constructor(target: string, token?: string) /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) constructor(target: string | BaseURL = "prod", options?: string | ClientOptions) { // Convert the old constructor parameters to a BaseURL object and a ClientOptions object if (!target.startsWith("http://") && !target.startsWith("https://")) { target = Environment(target) } if (typeof options === "string") { options = { auth: options } } this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } /** * Allows you to set the auth token to be used for each request * either by passing in a static token string or by passing in a function * which returns the auth token. * * These tokens will be sent as bearer tokens in the Authorization header. */ auth?: string | AuthDataGenerator } export namespace svc { export interface Request { Message: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.DummyAPI = this.DummyAPI.bind(this) this.Private = this.Private.bind(this) } /** * DummyAPI is a dummy endpoint. */ public async DummyAPI(params: Request): Promise { await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(params)) } /** * Private is a basic auth endpoint. */ public async Private(params: Request): Promise { await this.baseClient.callTypedAPI("POST", `/svc.Private`, JSON.stringify(params)) } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | string | Promise | undefined; // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } readonly authGenerator?: AuthDataGenerator constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData(): Promise { let authData: string | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; data.headers = {}; data.headers["Authorization"] = "Bearer " + authData; return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) // Client is an API client for the app Encore application. type Client struct { Authentication AuthenticationClient Products ProductsClient Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{ Authentication: &authenticationClient{base}, Products: &productsClient{base}, Svc: &svcClient{base}, }, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } // WithAuth allows you to set the authentication data to be used with each request func WithAuth(auth AuthenticationAuthData) Option { return func(base *baseClient) error { base.authGenerator = func(_ context.Context) (AuthenticationAuthData, error) { return auth, nil } return nil } } // WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request func WithAuthFunc(authGenerator func(ctx context.Context) (AuthenticationAuthData, error)) Option { return func(base *baseClient) error { base.authGenerator = authGenerator return nil } } type AuthenticationAuthData struct { APIKey string `header:"X-API-Key"` } // BarType docs type AuthenticationBarType struct { Baz string // Baz docs } // FooType docs type AuthenticationFooType struct { Moo string // Moo docs Bar AuthenticationBarType // Bar docs } type AuthenticationUser struct { ID int `json:"id"` Name string `json:"name"` } // AuthenticationClient Provides you access to call public and authenticated APIs on authentication. The concrete implementation is authenticationClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type AuthenticationClient interface { Docs(ctx context.Context, params AuthenticationFooType) error } type authenticationClient struct { base *baseClient } var _ AuthenticationClient = (*authenticationClient)(nil) func (c *authenticationClient) Docs(ctx context.Context, params AuthenticationFooType) error { _, err := callAPI(ctx, c.base, "POST", "/authentication.Docs", nil, params, nil) return err } type ProductsCreateProductRequest struct { IdempotencyKey string `header:"Idempotency-Key"` Name string `json:"name"` Description string `json:"description,omitempty"` } type ProductsProduct struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` CreatedAt time.Time `json:"created_at"` CreatedBy *AuthenticationUser `json:"created_by"` } type ProductsProductListing struct { Products []*ProductsProduct `json:"products"` PreviousPage struct { Cursor string `json:"cursor,omitempty"` Exists bool `json:"exists"` } `json:"previous"` NextPage struct { Cursor string `json:"cursor,omitempty"` Exists bool `json:"exists"` } `json:"next"` } // ProductsClient Provides you access to call public and authenticated APIs on products. The concrete implementation is productsClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type ProductsClient interface { Create(ctx context.Context, params ProductsCreateProductRequest) (ProductsProduct, error) List(ctx context.Context) (ProductsProductListing, error) } type productsClient struct { base *baseClient } var _ ProductsClient = (*productsClient)(nil) func (c *productsClient) Create(ctx context.Context, params ProductsCreateProductRequest) (resp ProductsProduct, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{"idempotency-key": {reqEncoder.FromString(params.IdempotencyKey)}} if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Name string `json:"name"` Description string `json:"description,omitempty"` }{ Description: params.Description, Name: params.Name, } // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/products.Create", headers, body, &resp) if err != nil { return } return } func (c *productsClient) List(ctx context.Context) (resp ProductsProductListing, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", "/products.List", nil, nil, &resp) if err != nil { return } return } type SvcAllInputTypes[A any] struct { A time.Time `header:"X-Alice"` // Specify this comes from a header field B []int `query:"Bob"` // Specify this comes from a query string C bool `json:"Charlies-Bool,omitempty"` // This can come from anywhere, but if it comes from the payload in JSON it must be called Charile Dave A // This generic type complicates the whole thing 🙈 Optional *A `json:"optional"` // An optional generic type } // DocumentedOrder represents a customer order with references type SvcDocumentedOrder struct { Customer SvcDocumentedUser `json:"customer"` // Customer who placed this order (different from shipping recipient) OrderID string `json:"order_id"` OptionalRef *SvcDocumentedUser `json:"opt_ref"` RequiredRef *SvcDocumentedUser `json:"req_ref"` } // DocumentedUser represents a user in the system with profile information type SvcDocumentedUser struct { Name string `json:"name"` Email string `json:"email"` } // Foo represents a documented integer type type SvcFoo = int type SvcGetRequest struct { Baz int `qs:"boo"` } // HeaderOnlyStruct contains all types we support in headers type SvcHeaderOnlyStruct struct { Boolean bool `header:"x-boolean"` Int int `header:"x-int"` Float float64 `header:"x-float"` String string `header:"x-string"` Bytes []byte `header:"x-bytes"` Time time.Time `header:"x-time"` Json json.RawMessage `header:"x-json"` UUID string `header:"x-uuid"` UserID string `header:"x-user-id"` Optional *string `header:"x-optional"` } type SvcRecursive struct { Optional *SvcRecursive `encore:"optional"` Slice []SvcRecursive SliceOfOptional []*SvcRecursive Map map[string]SvcRecursive MapOfOptional map[string]*SvcRecursive } type SvcRequest struct { Foo SvcFoo `encore:"optional"` // Foo is good Baz string `json:"boo"` // Baz is better QueryFoo bool `encore:"optional" query:"foo"` QueryBar string `encore:"optional" query:"bar"` HeaderBaz string `encore:"optional" header:"baz"` HeaderInt int `encore:"optional" header:"int"` // This is a multiline // comment on the raw message! Raw json.RawMessage } // Tuple is a generic type which allows us to // return two values of two different types type SvcTuple[A any, B any] struct { A A B B } type SvcWithNested struct { Nested *NestedType } type SvcWrappedRequest = SvcWrapper[SvcRequest] type SvcWrapper[T any] struct { Value T } // Svc is a service for testing the client generator. // // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { CreateDocumentedOrder(ctx context.Context, params SvcDocumentedOrder) (SvcDocumentedOrder, error) // DummyAPI is a dummy endpoint. DummyAPI(ctx context.Context, params SvcRequest) error FallbackPath(ctx context.Context, a string, b []string) error Get(ctx context.Context, params SvcGetRequest) error GetRequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[int]) (SvcHeaderOnlyStruct, error) HeaderOnlyRequest(ctx context.Context, params SvcHeaderOnlyStruct) error Nested(ctx context.Context, params SvcWithNested) (SvcWithNested, error) RESTPath(ctx context.Context, a string, b int) error Rec(ctx context.Context, params SvcRecursive) (SvcRecursive, error) RequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[string]) (SvcAllInputTypes[float64], error) // TupleInputOutput tests the usage of generics in the client generator // and this comment is also multiline, so multiline comments get tested as well. TupleInputOutput(ctx context.Context, params SvcTuple[string, SvcWrappedRequest]) (SvcTuple[bool, SvcFoo], error) Webhook(ctx context.Context, a string, b []string, request *http.Request) (*http.Response, error) Webhook2(ctx context.Context, a string, b []string) error } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) func (c *svcClient) CreateDocumentedOrder(ctx context.Context, params SvcDocumentedOrder) (resp SvcDocumentedOrder, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/svc.CreateDocumentedOrder", nil, params, &resp) if err != nil { return } return } // DummyAPI is a dummy endpoint. func (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "baz": {reqEncoder.FromString(params.HeaderBaz)}, "int": {reqEncoder.FromInt(params.HeaderInt)}, } queryString := url.Values{ "bar": {reqEncoder.FromString(params.QueryBar)}, "foo": {reqEncoder.FromBool(params.QueryFoo)}, } if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Foo SvcFoo `json:"Foo"` Baz string `json:"boo"` Raw json.RawMessage `json:"Raw"` }{ Baz: params.Baz, Foo: params.Foo, Raw: params.Raw, } _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/svc.DummyAPI?%s", queryString.Encode()), headers, body, nil) return err } func (c *svcClient) FallbackPath(ctx context.Context, a string, b []string) error { _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/fallbackPath/%s/%s", url.PathEscape(a), pathEscapeSlice(b)), nil, nil, nil) return err } func (c *svcClient) Get(ctx context.Context, params SvcGetRequest) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} queryString := url.Values{"boo": {reqEncoder.FromInt(params.Baz)}} if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } _, err := callAPI(ctx, c.base, "GET", fmt.Sprintf("/svc.Get?%s", queryString.Encode()), nil, nil, nil) return err } func (c *svcClient) GetRequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[int]) (resp SvcHeaderOnlyStruct, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{"x-alice": {reqEncoder.FromTime(params.A)}} queryString := url.Values{ "Bob": reqEncoder.FromIntList(params.B), "c": {reqEncoder.FromBool(params.C)}, "dave": {reqEncoder.FromInt(params.Dave)}, "optional": reqEncoder.FromIntOption(params.Optional), } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "GET", fmt.Sprintf("/svc.GetRequestWithAllInputTypes?%s", queryString.Encode()), headers, nil, nil) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.Boolean = respDecoder.ToBool("Boolean", respHeaders.Get("x-boolean"), true) resp.Int = respDecoder.ToInt("Int", respHeaders.Get("x-int"), true) resp.Float = respDecoder.ToFloat64("Float", respHeaders.Get("x-float"), true) resp.String = respDecoder.ToString("String", respHeaders.Get("x-string"), true) resp.Bytes = respDecoder.ToBytes("Bytes", respHeaders.Get("x-bytes"), true) resp.Time = respDecoder.ToTime("Time", respHeaders.Get("x-time"), true) resp.Json = respDecoder.ToJSON("Json", respHeaders.Get("x-json"), true) resp.UUID = respDecoder.ToString("UUID", respHeaders.Get("x-uuid"), true) resp.UserID = respDecoder.ToString("UserID", respHeaders.Get("x-user-id"), true) resp.Optional = respDecoder.ToStringOption("Optional", respHeaders.Get("x-optional"), false) if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } func (c *svcClient) HeaderOnlyRequest(ctx context.Context, params SvcHeaderOnlyStruct) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "x-boolean": {reqEncoder.FromBool(params.Boolean)}, "x-bytes": {reqEncoder.FromBytes(params.Bytes)}, "x-float": {reqEncoder.FromFloat64(params.Float)}, "x-int": {reqEncoder.FromInt(params.Int)}, "x-json": {reqEncoder.FromJSON(params.Json)}, "x-optional": reqEncoder.FromStringOption(params.Optional), "x-string": {reqEncoder.FromString(params.String)}, "x-time": {reqEncoder.FromTime(params.Time)}, "x-user-id": {reqEncoder.FromString(params.UserID)}, "x-uuid": {reqEncoder.FromString(params.UUID)}, } if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } _, err := callAPI(ctx, c.base, "GET", "/svc.HeaderOnlyRequest", headers, nil, nil) return err } func (c *svcClient) Nested(ctx context.Context, params SvcWithNested) (resp SvcWithNested, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/svc.Nested", nil, params, &resp) if err != nil { return } return } func (c *svcClient) RESTPath(ctx context.Context, a string, b int) error { _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/path/%s/%d", url.PathEscape(a), b), nil, nil, nil) return err } func (c *svcClient) Rec(ctx context.Context, params SvcRecursive) (resp SvcRecursive, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/svc.Rec", nil, params, &resp) if err != nil { return } return } func (c *svcClient) RequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[string]) (resp SvcAllInputTypes[float64], err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{"x-alice": {reqEncoder.FromTime(params.A)}} queryString := url.Values{"Bob": reqEncoder.FromIntList(params.B)} if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { C bool `json:"Charlies-Bool,omitempty"` Dave string `json:"Dave"` Optional *string `json:"optional"` }{ C: params.C, Dave: params.Dave, Optional: params.Optional, } // We only want the response body to marshal into these fields and none of the header fields, // so we'll construct a new struct with only those fields. respBody := struct { B []int `json:"B"` C bool `json:"Charlies-Bool,omitempty"` Dave float64 `json:"Dave"` Optional *float64 `json:"optional"` }{} // Now make the actual call to the API var respHeaders http.Header respHeaders, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/svc.RequestWithAllInputTypes?%s", queryString.Encode()), headers, body, &respBody) if err != nil { return } // Copy the unmarshalled response body into our response struct respDecoder := &serde{} resp.A = respDecoder.ToTime("A", respHeaders.Get("x-alice"), true) resp.B = respBody.B resp.C = respBody.C resp.Dave = respBody.Dave resp.Optional = respBody.Optional if respDecoder.LastError != nil { err = fmt.Errorf("unable to unmarshal headers: %w", respDecoder.LastError) return } return } // TupleInputOutput tests the usage of generics in the client generator // and this comment is also multiline, so multiline comments get tested as well. func (c *svcClient) TupleInputOutput(ctx context.Context, params SvcTuple[string, SvcWrappedRequest]) (resp SvcTuple[bool, SvcFoo], err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/svc.TupleInputOutput", nil, params, &resp) if err != nil { return } return } func (c *svcClient) Webhook(ctx context.Context, a string, b []string, request *http.Request) (*http.Response, error) { request = request.WithContext(ctx) // Check the request has the method set, as we can't guess what method is required if request.Method == "" { return nil, errors.New("request.Method must be set") } // Set the relative URL for the API call path, err := url.Parse(fmt.Sprintf("/webhook/%s/%s", url.PathEscape(a), pathEscapeSlice(b))) if err != nil { return nil, fmt.Errorf("unable to parse api url: %w", err) } if request.URL != nil { // If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema, // host and path to ensure the final URL which hit the right BaseURL request.URL.Scheme = path.Scheme request.URL.Host = path.Host request.URL.Path = path.Path } else { request.URL = path } return c.base.Do(request) } func (c *svcClient) Webhook2(ctx context.Context, a string, b []string) error { _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/webhook2/%s/%s", url.PathEscape(a), pathEscapeSlice(b)), nil, nil, nil) return err } type NestedType struct { Message string } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { authGenerator func(ctx context.Context) (AuthenticationAuthData, error) // The function which will add the authentication data to the requests httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // If a authorization data generator is present, call it and add the returned token to the request if b.authGenerator != nil { if authData, err := b.authGenerator(req.Context()); err != nil { return nil, fmt.Errorf("unable to create authorization token for api request: %w", err) } else { authEncoder := &serde{} // Add the auth fields to the headers req.Header.Set("x-api-key", authEncoder.FromString(authData.APIKey)) if authEncoder.LastError != nil { return nil, fmt.Errorf("unable to marshal authentication data: %w", authEncoder.LastError) } } } // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // pathEscapeSlice escapes a slice of strings and then joins them into a single string func pathEscapeSlice(paths []string) string { var escapedPaths strings.Builder for i, path := range paths { if i > 0 { escapedPaths.WriteString("/") } escapedPaths.WriteString(url.PathEscape(path)) } return escapedPaths.String() } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } // serde is used to serialize request data into strings and deserialize response data from strings type serde struct { LastError error // The last error that occurred NonEmptyValues int // The number of values this decoder has decoded } func (e *serde) FromString(s string) (v string) { e.NonEmptyValues++ return s } func (e *serde) FromInt(s int) (v string) { e.NonEmptyValues++ return strconv.FormatInt(int64(s), 10) } func (e *serde) FromBool(s bool) (v string) { e.NonEmptyValues++ return strconv.FormatBool(s) } func (e *serde) FromTime(s time.Time) (v string) { e.NonEmptyValues++ return s.Format(time.RFC3339) } func (e *serde) FromIntList(s []int) (v []string) { e.NonEmptyValues++ for _, x := range s { v = append(v, e.FromInt(x)) } return v } func (e *serde) FromIntOption(s *int) (v []string) { if s == nil { return nil } e.NonEmptyValues++ return []string{e.FromInt(*s)} } func (e *serde) ToBool(field string, s string, required bool) (v bool) { if !required && s == "" { return } e.NonEmptyValues++ v, err := strconv.ParseBool(s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToInt(field string, s string, required bool) (v int) { if !required && s == "" { return } e.NonEmptyValues++ x, err := strconv.ParseInt(s, 10, 64) e.setErr("invalid parameter", field, err) return int(x) } func (e *serde) ToFloat64(field string, s string, required bool) (v float64) { if !required && s == "" { return } e.NonEmptyValues++ x, err := strconv.ParseFloat(s, 64) e.setErr("invalid parameter", field, err) return x } func (e *serde) ToString(field string, s string, required bool) (v string) { if !required && s == "" { return } e.NonEmptyValues++ return s } func (e *serde) ToBytes(field string, s string, required bool) (v []byte) { if !required && s == "" { return } e.NonEmptyValues++ v, err := base64.URLEncoding.DecodeString(s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToTime(field string, s string, required bool) (v time.Time) { if !required && s == "" { return } e.NonEmptyValues++ v, err := time.Parse(time.RFC3339, s) e.setErr("invalid parameter", field, err) return v } func (e *serde) ToJSON(field string, s string, required bool) (v json.RawMessage) { if !required && s == "" { return } e.NonEmptyValues++ return json.RawMessage(s) } func (e *serde) ToStringOption(field string, s string, required bool) (v *string) { if !required && s == "" { return } e.NonEmptyValues++ val := e.ToString(field, s, required) return &val } func (e *serde) FromFloat64(s float64) (v string) { e.NonEmptyValues++ return strconv.FormatFloat(s, uint8(0x66), -1, 64) } func (e *serde) FromBytes(s []byte) (v string) { e.NonEmptyValues++ return base64.URLEncoding.EncodeToString(s) } func (e *serde) FromJSON(s json.RawMessage) (v string) { e.NonEmptyValues++ return string(s) } func (e *serde) FromStringOption(s *string) (v []string) { if s == nil { return nil } e.NonEmptyValues++ return []string{e.FromString(*s)} } // setErr sets the last error within the object if one is not already set func (e *serde) setErr(msg, field string, err error) { if err != nil && e.LastError == nil { e.LastError = fmt.Errorf("%s: %s: %w", field, msg, err) } } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_httpstatus_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } type SvcResponse struct { Message string } // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { // DummyAPI is a dummy endpoint. DummyAPI(ctx context.Context) (SvcResponse, error) } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) // DummyAPI is a dummy endpoint. func (c *svcClient) DummyAPI(ctx context.Context) (resp SvcResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/svc.DummyAPI", nil, nil, &resp) if err != nil { return } return } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_httpstatus_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Response { Message: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.DummyAPI = this.DummyAPI.bind(this) } /** * DummyAPI is a dummy endpoint. */ public async DummyAPI(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`) return await resp.json() as Response } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.authentication = new authentication.ServiceClient(base) this.products = new products.ServiceClient(base) this.svc = new svc.ServiceClient(base) } } class AuthenticationServiceClient { constructor(baseClient) { this.baseClient = baseClient this.Docs = this.Docs.bind(this) } async Docs(params) { await this.baseClient.callTypedAPI("POST", `/authentication.Docs`, JSON.stringify(params)) } } export const authentication = { ServiceClient: AuthenticationServiceClient } class ProductsServiceClient { constructor(baseClient) { this.baseClient = baseClient this.Create = this.Create.bind(this) this.List = this.List.bind(this) } async Create(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "idempotency-key": params.IdempotencyKey, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { description: params.description, name: params.name, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/products.Create`, JSON.stringify(body), {headers}) return await resp.json() } async List() { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/products.List`) return await resp.json() } } export const products = { ServiceClient: ProductsServiceClient } /** * Svc is a service for testing the client generator. */ class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.CreateDocumentedOrder = this.CreateDocumentedOrder.bind(this) this.DummyAPI = this.DummyAPI.bind(this) this.FallbackPath = this.FallbackPath.bind(this) this.Get = this.Get.bind(this) this.GetRequestWithAllInputTypes = this.GetRequestWithAllInputTypes.bind(this) this.HeaderOnlyRequest = this.HeaderOnlyRequest.bind(this) this.Nested = this.Nested.bind(this) this.RESTPath = this.RESTPath.bind(this) this.Rec = this.Rec.bind(this) this.RequestWithAllInputTypes = this.RequestWithAllInputTypes.bind(this) this.TupleInputOutput = this.TupleInputOutput.bind(this) this.Webhook = this.Webhook.bind(this) this.Webhook2 = this.Webhook2.bind(this) } async CreateDocumentedOrder(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.CreateDocumentedOrder`, JSON.stringify(params)) return await resp.json() } /** * DummyAPI is a dummy endpoint. */ async DummyAPI(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.HeaderBaz, int: params.HeaderInt === undefined ? undefined : String(params.HeaderInt), }) const query = makeRecord({ bar: params.QueryBar, foo: params.QueryFoo === undefined ? undefined : String(params.QueryFoo), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { Foo: params.Foo, Raw: params.Raw, boo: params.boo, } await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(body), {headers, query}) } async FallbackPath(a, b) { await this.baseClient.callTypedAPI("POST", `/fallbackPath/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`) } async Get(params) { // Convert our params into the objects we need for the request const query = makeRecord({ boo: String(params.Baz), }) await this.baseClient.callTypedAPI("GET", `/svc.Get`, undefined, {query}) } async GetRequestWithAllInputTypes(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-alice": String(params.A), }) const query = makeRecord({ Bob: params.B.map((v) => String(v)), c: String(params["Charlies-Bool"]), dave: String(params.Dave), optional: params.optional === undefined ? undefined : String(params.optional), }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/svc.GetRequestWithAllInputTypes`, undefined, {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.Boolean = mustBeSet("Header `x-boolean`", resp.headers.get("x-boolean")).toLowerCase() === "true" rtn.Int = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.Float = Number(mustBeSet("Header `x-float`", resp.headers.get("x-float"))) rtn.String = mustBeSet("Header `x-string`", resp.headers.get("x-string")) rtn.Bytes = mustBeSet("Header `x-bytes`", resp.headers.get("x-bytes")) rtn.Time = mustBeSet("Header `x-time`", resp.headers.get("x-time")) rtn.Json = JSON.parse(mustBeSet("Header `x-json`", resp.headers.get("x-json"))) rtn.UUID = mustBeSet("Header `x-uuid`", resp.headers.get("x-uuid")) rtn.UserID = mustBeSet("Header `x-user-id`", resp.headers.get("x-user-id")) rtn.Optional = resp.headers.get("x-optional") return rtn } async HeaderOnlyRequest(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-boolean": String(params.Boolean), "x-bytes": String(params.Bytes), "x-float": String(params.Float), "x-int": String(params.Int), "x-json": JSON.stringify(params.Json), "x-optional": params.Optional === undefined ? undefined : String(params.Optional), "x-string": params.String, "x-time": String(params.Time), "x-user-id": String(params.UserID), "x-uuid": String(params.UUID), }) await this.baseClient.callTypedAPI("GET", `/svc.HeaderOnlyRequest`, undefined, {headers}) } async Nested(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.Nested`, JSON.stringify(params)) return await resp.json() } async RESTPath(a, b) { await this.baseClient.callTypedAPI("POST", `/path/${encodeURIComponent(a)}/${encodeURIComponent(b)}`) } async Rec(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.Rec`, JSON.stringify(params)) return await resp.json() } async RequestWithAllInputTypes(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-alice": String(params.A), }) const query = makeRecord({ Bob: params.B.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { "Charlies-Bool": params["Charlies-Bool"], Dave: params.Dave, optional: params.optional, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.RequestWithAllInputTypes`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() rtn.A = mustBeSet("Header `x-alice`", resp.headers.get("x-alice")) return rtn } /** * TupleInputOutput tests the usage of generics in the client generator * and this comment is also multiline, so multiline comments get tested as well. */ async TupleInputOutput(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.TupleInputOutput`, JSON.stringify(params)) return await resp.json() } async Webhook(method, a, b, body, options) { return this.baseClient.callAPI(method, `/webhook/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`, body, options) } async Webhook2(a, b) { await this.baseClient.callTypedAPI("POST", `/webhook2/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field, value) { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: `${field} was unexpectedly ${value}`, // ${value} will create the string "null" or "undefined" }, ) } return value } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData() { let authData; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data = {}; data.headers = makeRecord({ "x-api-key": authData.APIKey, }) return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_noauth_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } type SvcRequest struct { Message string } // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { // DummyAPI is a dummy endpoint. DummyAPI(ctx context.Context, params SvcRequest) error } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) // DummyAPI is a dummy endpoint. func (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error { _, err := callAPI(ctx, c.base, "POST", "/svc.DummyAPI", nil, params, nil) return err } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_noauth_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.DummyAPI = this.DummyAPI.bind(this) } /** * DummyAPI is a dummy endpoint. */ async DummyAPI(params) { await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(params)) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData() { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_noauth_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/svc.DummyAPI": { "post": { "operationId": "POST:svc.DummyAPI", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Message": { "type": "string" } }, "required": [ "Message" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } }, "summary": "DummyAPI is a dummy endpoint.\n" } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_noauth_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Request { Message: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.DummyAPI = this.DummyAPI.bind(this) } /** * DummyAPI is a dummy endpoint. */ public async DummyAPI(params: Request): Promise { await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(params)) } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } }, "schemas": { "authentication.BarType": { "properties": { "Baz": { "title": "Baz docs\n", "type": "string" } }, "required": [ "Baz" ], "title": "BarType docs\n", "type": "object" }, "authentication.User": { "properties": { "id": { "format": "int64", "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "type": "object" }, "nested.Type": { "properties": { "Message": { "type": "string" } }, "required": [ "Message" ], "type": "object" }, "products.Product": { "properties": { "created_at": { "format": "date-time", "type": "string" }, "created_by": { "$ref": "#/components/schemas/authentication.User" }, "description": { "type": "string" }, "id": { "format": "uuid", "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name", "description", "created_at", "created_by" ], "type": "object" }, "svc.DocumentedUser": { "properties": { "email": { "type": "string" }, "name": { "type": "string" } }, "required": [ "name", "email" ], "title": "DocumentedUser represents a user in the system with profile information\n", "type": "object" }, "svc.Foo": { "format": "int64", "title": "Foo represents a documented integer type\n", "type": "integer" }, "svc.Recursive": { "properties": { "Map": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "MapOfOptional": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "Optional": { "$ref": "#/components/schemas/svc.Recursive" }, "Slice": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" }, "SliceOfOptional": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" } }, "required": [ "Slice", "SliceOfOptional", "Map", "MapOfOptional" ], "type": "object" }, "svc.Request": { "properties": { "Foo": { "allOf": [ { "$ref": "#/components/schemas/svc.Foo" } ], "title": "Foo is good\n" }, "HeaderBaz": { "type": "string" }, "HeaderInt": { "format": "int64", "type": "integer" }, "QueryBar": { "type": "string" }, "QueryFoo": { "type": "boolean" }, "Raw": { "description": "comment on the raw message!\n", "title": "This is a multiline\n", "type": "object" }, "boo": { "title": "Baz is better\n", "type": "string" } }, "required": [ "boo", "Raw" ], "type": "object" }, "svc.WrappedRequest": { "properties": { "Value": { "$ref": "#/components/schemas/svc.Request" } }, "required": [ "Value" ], "type": "object" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/authentication.Docs": { "post": { "operationId": "POST:authentication.Docs", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Bar": { "$ref": "#/components/schemas/authentication.BarType" }, "Moo": { "title": "Moo docs\n", "type": "string" } }, "required": [ "Moo", "Bar" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/fallbackPath/{a}/{b}": { "get": { "operationId": "GET:svc.FallbackPath", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "post": { "operationId": "POST:svc.FallbackPath", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/path/{a}/{b}": { "get": { "operationId": "GET:svc.RESTPath", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "post": { "operationId": "POST:svc.RESTPath", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/products.Create": { "post": { "operationId": "POST:products.Create", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "idempotency-key", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "description": { "type": "string" }, "name": { "type": "string" } }, "required": [ "name", "description" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "created_at": { "format": "date-time", "type": "string" }, "created_by": { "$ref": "#/components/schemas/authentication.User" }, "description": { "type": "string" }, "id": { "format": "uuid", "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name", "description", "created_at", "created_by" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/products.List": { "get": { "operationId": "GET:products.List", "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "next": { "properties": { "cursor": { "type": "string" }, "exists": { "type": "boolean" } }, "required": [ "cursor", "exists" ], "type": "object" }, "previous": { "properties": { "cursor": { "type": "string" }, "exists": { "type": "boolean" } }, "required": [ "cursor", "exists" ], "type": "object" }, "products": { "items": { "$ref": "#/components/schemas/products.Product" }, "type": "array" } }, "required": [ "products", "previous", "next" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.CreateDocumentedOrder": { "post": { "operationId": "POST:svc.CreateDocumentedOrder", "requestBody": { "content": { "application/json": { "schema": { "properties": { "customer": { "$ref": "#/components/schemas/svc.DocumentedUser" }, "opt_ref": { "$ref": "#/components/schemas/svc.DocumentedUser" }, "order_id": { "type": "string" }, "req_ref": { "$ref": "#/components/schemas/svc.DocumentedUser" } }, "required": [ "customer", "order_id", "req_ref" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "customer": { "$ref": "#/components/schemas/svc.DocumentedUser" }, "opt_ref": { "$ref": "#/components/schemas/svc.DocumentedUser" }, "order_id": { "type": "string" }, "req_ref": { "$ref": "#/components/schemas/svc.DocumentedUser" } }, "required": [ "customer", "order_id", "req_ref" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.DummyAPI": { "post": { "operationId": "POST:svc.DummyAPI", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "baz", "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "int", "schema": { "format": "int64", "type": "integer" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "foo", "schema": { "type": "boolean" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "bar", "schema": { "type": "string" }, "style": "form" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "Foo": { "$ref": "#/components/schemas/svc.Foo" }, "Raw": { "description": "comment on the raw message!\n", "title": "This is a multiline\n", "type": "object" }, "boo": { "title": "Baz is better\n", "type": "string" } }, "required": [ "boo", "Raw" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } }, "summary": "DummyAPI is a dummy endpoint.\n" } }, "/svc.Get": { "get": { "operationId": "GET:svc.Get", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "query", "name": "boo", "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "form" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.GetRequestWithAllInputTypes": { "get": { "operationId": "GET:svc.GetRequestWithAllInputTypes", "parameters": [ { "allowEmptyValue": true, "description": "Specify this comes from a header field\n", "explode": true, "in": "header", "name": "x-alice", "required": true, "schema": { "format": "date-time", "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "description": "Specify this comes from a query string\n", "explode": true, "in": "query", "name": "Bob", "required": true, "schema": { "items": { "format": "int64", "type": "integer" }, "type": "array" }, "style": "form" }, { "allowEmptyValue": true, "description": "This can come from anywhere, but if it comes from the payload in JSON it must be called Charile\n", "explode": true, "in": "query", "name": "c", "required": true, "schema": { "type": "boolean" }, "style": "form" }, { "allowEmptyValue": true, "description": "This generic type complicates the whole thing 🙈\n", "explode": true, "in": "query", "name": "dave", "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "form" }, { "allowEmptyValue": true, "description": "An optional generic type\n", "explode": true, "in": "query", "name": "optional", "schema": { "format": "int64", "type": "integer" }, "style": "form" } ], "responses": { "200": { "description": "Success response", "headers": { "x-boolean": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "type": "boolean" }, "style": "simple" }, "x-bytes": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "format": "byte", "type": "string" }, "style": "simple" }, "x-float": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "type": "number" }, "style": "simple" }, "x-int": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "simple" }, "x-json": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "type": "object" }, "style": "simple" }, "x-optional": { "allowEmptyValue": true, "explode": true, "schema": { "type": "string" }, "style": "simple" }, "x-string": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "type": "string" }, "style": "simple" }, "x-time": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "format": "date-time", "type": "string" }, "style": "simple" }, "x-user-id": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "type": "string" }, "style": "simple" }, "x-uuid": { "allowEmptyValue": true, "explode": true, "required": true, "schema": { "format": "uuid", "type": "string" }, "style": "simple" } } }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.HeaderOnlyRequest": { "get": { "operationId": "GET:svc.HeaderOnlyRequest", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-boolean", "required": true, "schema": { "type": "boolean" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-int", "required": true, "schema": { "format": "int64", "type": "integer" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-float", "required": true, "schema": { "type": "number" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-string", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-bytes", "required": true, "schema": { "format": "byte", "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-time", "required": true, "schema": { "format": "date-time", "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-json", "required": true, "schema": { "type": "object" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-uuid", "required": true, "schema": { "format": "uuid", "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-user-id", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "x-optional", "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.Nested": { "post": { "operationId": "POST:svc.Nested", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Nested": { "$ref": "#/components/schemas/nested.Type" } }, "required": [ "Nested" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "Nested": { "$ref": "#/components/schemas/nested.Type" } }, "required": [ "Nested" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.Rec": { "post": { "operationId": "POST:svc.Rec", "requestBody": { "content": { "application/json": { "schema": { "properties": { "Map": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "MapOfOptional": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "Optional": { "$ref": "#/components/schemas/svc.Recursive" }, "Slice": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" }, "SliceOfOptional": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" } }, "required": [ "Slice", "SliceOfOptional", "Map", "MapOfOptional" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "Map": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "MapOfOptional": { "additionalProperties": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "object" }, "Optional": { "$ref": "#/components/schemas/svc.Recursive" }, "Slice": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" }, "SliceOfOptional": { "items": { "$ref": "#/components/schemas/svc.Recursive" }, "type": "array" } }, "required": [ "Slice", "SliceOfOptional", "Map", "MapOfOptional" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.RequestWithAllInputTypes": { "post": { "operationId": "POST:svc.RequestWithAllInputTypes", "parameters": [ { "allowEmptyValue": true, "description": "Specify this comes from a header field\n", "explode": true, "in": "header", "name": "x-alice", "required": true, "schema": { "format": "date-time", "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "description": "Specify this comes from a query string\n", "explode": true, "in": "query", "name": "Bob", "required": true, "schema": { "items": { "format": "int64", "type": "integer" }, "type": "array" }, "style": "form" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "Charlies-Bool": { "title": "This can come from anywhere, but if it comes from the payload in JSON it must be\ncalled Charile\n", "type": "boolean" }, "Dave": { "title": "This generic type complicates the whole thing 🙈\n", "type": "string" }, "optional": { "title": "An optional generic type\n", "type": "string" } }, "required": [ "Charlies-Bool", "Dave" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "B": { "items": { "format": "int64", "type": "integer" }, "title": "Specify this comes from a query string\n", "type": "array" }, "Charlies-Bool": { "title": "This can come from anywhere, but if it comes from the payload in JSON it must be\ncalled Charile\n", "type": "boolean" }, "Dave": { "title": "This generic type complicates the whole thing 🙈\n", "type": "number" }, "optional": { "title": "An optional generic type\n", "type": "number" } }, "required": [ "B", "Charlies-Bool", "Dave" ], "type": "object" } } }, "description": "Success response", "headers": { "x-alice": { "allowEmptyValue": true, "description": "Specify this comes from a header field\n", "explode": true, "required": true, "schema": { "format": "date-time", "type": "string" }, "style": "simple" } } }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/svc.TupleInputOutput": { "post": { "description": "and this comment is also multiline, so multiline comments get tested as well.\n", "operationId": "POST:svc.TupleInputOutput", "requestBody": { "content": { "application/json": { "schema": { "properties": { "A": { "type": "string" }, "B": { "$ref": "#/components/schemas/svc.WrappedRequest" } }, "required": [ "A", "B" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "A": { "type": "boolean" }, "B": { "$ref": "#/components/schemas/svc.Foo" } }, "required": [ "A", "B" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } }, "summary": "TupleInputOutput tests the usage of generics in the client generator\n" } }, "/webhook/{a}/{b}": { "delete": { "operationId": "DELETE:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "get": { "operationId": "GET:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "head": { "operationId": "HEAD:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "patch": { "operationId": "PATCH:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "post": { "operationId": "POST:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "put": { "operationId": "PUT:svc.Webhook", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/webhook2/{a}/{b}": { "get": { "operationId": "GET:svc.Webhook2", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } }, "post": { "operationId": "POST:svc.Webhook2", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "a", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "b", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/goapp/expected_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly authentication: authentication.ServiceClient public readonly products: products.ServiceClient public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.authentication = new authentication.ServiceClient(base) this.products = new products.ServiceClient(base) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } /** * Allows you to set the authentication data to be used for each * request either by passing in a static object or by passing in * a function which returns a new object for each request. */ auth?: authentication.AuthData | AuthDataGenerator } export namespace authentication { export interface AuthData { APIKey: string } /** * BarType docs */ export interface BarType { /** * Baz docs */ Baz: string } /** * FooType docs */ export interface FooType { /** * Moo docs */ Moo: string /** * Bar docs */ Bar: BarType } export interface User { id: number name: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.Docs = this.Docs.bind(this) } public async Docs(params: FooType): Promise { await this.baseClient.callTypedAPI("POST", `/authentication.Docs`, JSON.stringify(params)) } } } export namespace products { export interface CreateProductRequest { IdempotencyKey: string name: string description: string } export interface Product { id: string name: string description: string "created_at": string "created_by": authentication.User } export interface ProductListing { products: Product[] previous: { cursor: string exists: boolean } next: { cursor: string exists: boolean } } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.Create = this.Create.bind(this) this.List = this.List.bind(this) } public async Create(params: CreateProductRequest): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "idempotency-key": params.IdempotencyKey, }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { description: params.description, name: params.name, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/products.Create`, JSON.stringify(body), {headers}) return await resp.json() as Product } public async List(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/products.List`) return await resp.json() as ProductListing } } } /** * Svc is a service for testing the client generator. */ export namespace svc { export interface AllInputTypes { /** * Specify this comes from a header field */ A: string /** * Specify this comes from a query string */ B: number[] /** * This can come from anywhere, but if it comes from the payload in JSON it must be called Charile */ "Charlies-Bool": boolean /** * This generic type complicates the whole thing 🙈 */ Dave: A /** * An optional generic type */ optional?: A | null } /** * DocumentedOrder represents a customer order with references */ export interface DocumentedOrder { /** * Customer who placed this order (different from shipping recipient) */ customer: DocumentedUser "order_id": string "opt_ref"?: DocumentedUser | null "req_ref": DocumentedUser } /** * DocumentedUser represents a user in the system with profile information */ export interface DocumentedUser { name: string email: string } /** * Foo represents a documented integer type */ export type Foo = number export interface GetRequest { Baz: number } /** * HeaderOnlyStruct contains all types we support in headers */ export interface HeaderOnlyStruct { Boolean: boolean Int: number Float: number String: string Bytes: string Time: string Json: JSONValue UUID: string UserID: string Optional?: string | null } export interface Recursive { Optional?: Recursive Slice: Recursive[] SliceOfOptional: (Recursive | null)[] Map: { [key: string]: Recursive } MapOfOptional: { [key: string]: Recursive | null } } export interface Request { /** * Foo is good */ Foo?: Foo /** * Baz is better */ boo: string QueryFoo?: boolean QueryBar?: string HeaderBaz?: string HeaderInt?: number /** * This is a multiline * comment on the raw message! */ Raw: JSONValue } /** * Tuple is a generic type which allows us to * return two values of two different types */ export interface Tuple { A: A B: B } export interface WithNested { Nested: nested.Type } export type WrappedRequest = Wrapper export interface Wrapper { Value: T } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.CreateDocumentedOrder = this.CreateDocumentedOrder.bind(this) this.DummyAPI = this.DummyAPI.bind(this) this.FallbackPath = this.FallbackPath.bind(this) this.Get = this.Get.bind(this) this.GetRequestWithAllInputTypes = this.GetRequestWithAllInputTypes.bind(this) this.HeaderOnlyRequest = this.HeaderOnlyRequest.bind(this) this.Nested = this.Nested.bind(this) this.RESTPath = this.RESTPath.bind(this) this.Rec = this.Rec.bind(this) this.RequestWithAllInputTypes = this.RequestWithAllInputTypes.bind(this) this.TupleInputOutput = this.TupleInputOutput.bind(this) this.Webhook = this.Webhook.bind(this) this.Webhook2 = this.Webhook2.bind(this) } public async CreateDocumentedOrder(params: DocumentedOrder): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.CreateDocumentedOrder`, JSON.stringify(params)) return await resp.json() as DocumentedOrder } /** * DummyAPI is a dummy endpoint. */ public async DummyAPI(params: Request): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.HeaderBaz, int: params.HeaderInt === undefined ? undefined : String(params.HeaderInt), }) const query = makeRecord({ bar: params.QueryBar, foo: params.QueryFoo === undefined ? undefined : String(params.QueryFoo), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { Foo: params.Foo, Raw: params.Raw, boo: params.boo, } await this.baseClient.callTypedAPI("POST", `/svc.DummyAPI`, JSON.stringify(body), {headers, query}) } public async FallbackPath(a: string, b: string[]): Promise { await this.baseClient.callTypedAPI("POST", `/fallbackPath/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`) } public async Get(params: GetRequest): Promise { // Convert our params into the objects we need for the request const query = makeRecord({ boo: String(params.Baz), }) await this.baseClient.callTypedAPI("GET", `/svc.Get`, undefined, {query}) } public async GetRequestWithAllInputTypes(params: AllInputTypes): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-alice": String(params.A), }) const query = makeRecord({ Bob: params.B.map((v) => String(v)), c: String(params["Charlies-Bool"]), dave: String(params.Dave), optional: params.optional === undefined ? undefined : String(params.optional), }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/svc.GetRequestWithAllInputTypes`, undefined, {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as HeaderOnlyStruct rtn.Boolean = mustBeSet("Header `x-boolean`", resp.headers.get("x-boolean")).toLowerCase() === "true" rtn.Int = parseInt(mustBeSet("Header `x-int`", resp.headers.get("x-int")), 10) rtn.Float = Number(mustBeSet("Header `x-float`", resp.headers.get("x-float"))) rtn.String = mustBeSet("Header `x-string`", resp.headers.get("x-string")) rtn.Bytes = mustBeSet("Header `x-bytes`", resp.headers.get("x-bytes")) rtn.Time = mustBeSet("Header `x-time`", resp.headers.get("x-time")) rtn.Json = JSON.parse(mustBeSet("Header `x-json`", resp.headers.get("x-json"))) rtn.UUID = mustBeSet("Header `x-uuid`", resp.headers.get("x-uuid")) rtn.UserID = mustBeSet("Header `x-user-id`", resp.headers.get("x-user-id")) rtn.Optional = mustBeSet("Header `x-optional`", resp.headers.get("x-optional")) return rtn } public async HeaderOnlyRequest(params: HeaderOnlyStruct): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-boolean": String(params.Boolean), "x-bytes": String(params.Bytes), "x-float": String(params.Float), "x-int": String(params.Int), "x-json": JSON.stringify(params.Json), "x-optional": params.Optional === undefined ? undefined : String(params.Optional), "x-string": params.String, "x-time": String(params.Time), "x-user-id": String(params.UserID), "x-uuid": String(params.UUID), }) await this.baseClient.callTypedAPI("GET", `/svc.HeaderOnlyRequest`, undefined, {headers}) } public async Nested(params: WithNested): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.Nested`, JSON.stringify(params)) return await resp.json() as WithNested } public async RESTPath(a: string, b: number): Promise { await this.baseClient.callTypedAPI("POST", `/path/${encodeURIComponent(a)}/${encodeURIComponent(b)}`) } public async Rec(params: Recursive): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.Rec`, JSON.stringify(params)) return await resp.json() as Recursive } public async RequestWithAllInputTypes(params: AllInputTypes): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "x-alice": String(params.A), }) const query = makeRecord({ Bob: params.B.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { "Charlies-Bool": params["Charlies-Bool"], Dave: params.Dave, optional: params.optional, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.RequestWithAllInputTypes`, JSON.stringify(body), {headers, query}) //Populate the return object from the JSON body and received headers const rtn = await resp.json() as AllInputTypes rtn.A = mustBeSet("Header `x-alice`", resp.headers.get("x-alice")) return rtn } /** * TupleInputOutput tests the usage of generics in the client generator * and this comment is also multiline, so multiline comments get tested as well. */ public async TupleInputOutput(params: Tuple): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/svc.TupleInputOutput`, JSON.stringify(params)) return await resp.json() as Tuple } public async Webhook(method: string, a: string, b: string[], body?: RequestInit["body"], options?: CallParameters): Promise { return this.baseClient.callAPI(method, `/webhook/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`, body, options) } public async Webhook2(a: string, b: string[]): Promise { await this.baseClient.callTypedAPI("POST", `/webhook2/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join("/")}`) } } } export namespace nested { export interface Type { Message: string } } // JSONValue represents an arbitrary JSON value. export type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue} function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field: string, value: A | null | undefined): A { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: `${field} was unexpectedly ${value}`, // ${value} will create the string "null" or "undefined" }, ) } return value } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | authentication.AuthData | Promise | undefined; // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } readonly authGenerator?: AuthDataGenerator constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData(): Promise { let authData: authentication.AuthData | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; data.headers = makeRecord({ "x-api-key": authData.APIKey, }); return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/goapp/input.go ================================================ -- go.mod -- module app require ( encore.dev v1.52.1 ) -- encore.app -- {"id": ""} -- svc/svc.go -- // Svc is a service for testing the client generator. package svc import ( "encoding/json" "time" "encore.dev/beta/auth" "encore.dev/types/uuid" "encore.dev/types/option" ) type UnusedType struct { Foo Foo } type Wrapper[T any] struct { Value T } // Tuple is a generic type which allows us to // return two values of two different types type Tuple[A any, B any] struct { A A B B } type Request struct { Foo Foo `encore:"optional"` // Foo is good Bar string `json:"-"` Baz string `json:"boo"` // Baz is better QueryFoo bool `query:"foo" encore:"optional"` QueryBar string `query:"bar" encore:"optional"` HeaderBaz string `header:"baz" encore:"optional"` HeaderInt int `header:"int" encore:"optional"` // This is a multiline // comment on the raw message! Raw json.RawMessage } type WrappedRequest = Wrapper[Request] type GetRequest struct { Bar string `qs:"-"` Baz int `qs:"boo"` } // Foo represents a documented integer type type Foo int // DocumentedUser represents a user in the system with profile information type DocumentedUser struct { Name string `json:"name"` Email string `json:"email"` } // DocumentedOrder represents a customer order with references type DocumentedOrder struct { // Customer who placed this order (different from shipping recipient) Customer DocumentedUser `json:"customer"` OrderID string `json:"order_id"` OptionalRef option.Option[DocumentedUser] `json:"opt_ref"` RequiredRef *DocumentedUser `json:"req_ref"` } type Nested struct { Value string } type AllInputTypes[A any] struct { A time.Time `header:"X-Alice"` // Specify this comes from a header field B []int `query:"Bob"` // Specify this comes from a query string C bool `json:"Charlies-Bool,omitempty"` // This can come from anywhere, but if it comes from the payload in JSON it must be called Charile Dave A // This generic type complicates the whole thing 🙈 Optional option.Option[A] `json:"optional"` // An optional generic type // Tags named "-" are ignored in schemas Ignore1 string `header:"-"` Ignore2 string `query:"-"` Ignore3 string `json:"-"` // Unexported tags are ignored ignore4 string } // HeaderOnlyStruct contains all types we support in headers type HeaderOnlyStruct struct { Boolean bool `header:"x-boolean"` Int int `header:"x-int"` Float float64 `header:"x-float"` String string `header:"x-string"` Bytes []byte `header:"x-bytes"` Time time.Time `header:"x-time"` Json json.RawMessage `header:"x-json"` UUID uuid.UUID `header:"x-uuid"` UserID auth.UID `header:"x-user-id"` Optional option.Option[string] `header:"x-optional"` } type Recursive struct { Optional *Recursive `encore:"optional"` Slice []Recursive SliceOfOptional []option.Option[*Recursive] Map map[string]Recursive MapOfOptional map[string]option.Option[*Recursive] } -- svc/api.go -- package svc import ( "context" "net/http" "app/svc/nested" ) // DummyAPI is a dummy endpoint. //encore:api public func DummyAPI(ctx context.Context, req *Request) error { return nil } //encore:api public method=GET func Get(ctx context.Context, req *GetRequest) error { return nil } // TupleInputOutput tests the usage of generics in the client generator // and this comment is also multiline, so multiline comments get tested as well. //encore:api public func TupleInputOutput(ctx context.Context, req Tuple[string, WrappedRequest]) (Tuple[bool, Foo], error) { return nil } //encore:api public path=/path/:a/:b func RESTPath(ctx context.Context, a string, b int) error { return nil } //encore:api public raw path=/webhook/:a/*b func Webhook(w http.ResponseWriter, req *http.Request) {} //encore:api public path=/webhook2/:a/*b func Webhook2(ctx context.Context, a string, b string) error { return nil } //encore:api public path=/fallbackPath/:a/!b func FallbackPath(ctx context.Context, a string, b string) error { return nil } //encore:api public method=POST func RequestWithAllInputTypes(ctx context.Context, req *AllInputTypes[string]) (*AllInputTypes[float64], error) { return nil } //encore:api public method=GET func GetRequestWithAllInputTypes(ctx context.Context, req *AllInputTypes[int]) (HeaderOnlyStruct, error) { return nil } //encore:api public method=GET func HeaderOnlyRequest(ctx context.Context, req *HeaderOnlyStruct) error { return nil } type WithNested struct { Nested *nested.Type } //encore:api public method=POST func Nested(ctx context.Context, req *WithNested) (*WithNested, error) { return nil, nil } //encore:api public method=POST func Rec(ctx context.Context, req *Recursive) (*Recursive, error) { return nil, nil } //encore:api public method=POST func CreateDocumentedOrder(ctx context.Context, req *DocumentedOrder) (*DocumentedOrder, error) { return req, nil } -- svc/nested/nested.go -- package nested type Type struct { Message string } -- products/product.go -- package products import ( "context" "time" "encore.dev/types/uuid" "app/authentication" ) type Product struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` CreatedAt time.Time `json:"created_at"` CreatedBy *authentication.User `json:"created_by"` } type ProductListing struct { Products []*Product `json:"products"` PreviousPage struct { Cursor string `json:"cursor,omitempty"` Exists bool `json:"exists"` } `json:"previous"` NextPage struct { Cursor string `json:"cursor,omitempty"` Exists bool `json:"exists"` } `json:"next"` } type CreateProductRequest struct { IdempotencyKey string `header:"Idempotency-Key"` Name string `json:"name"` Description string `json:"description,omitempty"` } //encore:api public method=GET func List(ctx context.Context) (*ProductListing, error) { return nil, nil } //encore:api auth func Create(ctx context.Context, req *CreateProductRequest) (*Product, error) { return nil, nil } -- authentication/auth.go -- package authentication import ( "context" "encore.dev/beta/auth" ) type AuthData struct { APIKey string `header:"X-API-Key"` } type User struct { ID int `json:"id"` Name string `json:"name"` } //encore:authhandler func AuthenticateRequest(ctx context.Context, auth *AuthData) (auth.UID, *User, error) { return "", nil, nil } // FooType docs type FooType struct { Moo string // Moo docs Bar BarType // Bar docs } // BarType docs type BarType struct { Baz string // Baz docs } //encore:api public func Docs(ctx context.Context, req *FooType) error { return nil } ================================================ FILE: pkg/clientgen/testdata/goapp/input_baseauth.go ================================================ -- go.mod -- module app -- encore.app -- {"id": ""} -- svc/svc.go -- package svc type Request struct { Message string } -- svc/api.go -- package svc import ( "context" "net/http" ) // DummyAPI is a dummy endpoint. //encore:api public func DummyAPI(ctx context.Context, req *Request) error { return nil } // Private is a basic auth endpoint. //encore:api auth func Private(ctx context.Context, req *Request) error { return nil } -- authentication/auth.go -- package authentication import ( "context" "encore.dev/beta/auth" ) type User struct { ID int `json:"id"` Name string `json:"name"` } //encore:authhandler func AuthenticateRequest(ctx context.Context, token string) (auth.UID, *User, error) { return "", nil, nil } ================================================ FILE: pkg/clientgen/testdata/goapp/input_httpstatus.go ================================================ -- go.mod -- module app -- encore.app -- {"id": ""} -- svc/svc.go -- package svc type Response struct { Message string Status int `encore:"httpstatus"` } -- svc/api.go -- package svc import ( "context" "net/http" ) // DummyAPI is a dummy endpoint. //encore:api public func DummyAPI(ctx context.Context) (*Response, error) { return nil, nil } ================================================ FILE: pkg/clientgen/testdata/goapp/input_noauth.go ================================================ -- go.mod -- module app -- encore.app -- {"id": ""} -- svc/svc.go -- package svc type Request struct { Message string } -- svc/api.go -- package svc import ( "context" "net/http" ) // DummyAPI is a dummy endpoint. //encore:api public func DummyAPI(ctx context.Context, req *Request) error { return nil } ================================================ FILE: pkg/clientgen/testdata/goapp/tsconfig.json ================================================ // Note: this config is here purely to remove errors about promises not being present in IDE's when viewing the expected_typescript.ts file { "compilerOptions": { "target": "es2021" } } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_decimal_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } type SvcRequest struct { Message string Val string } type SvcResponse struct { Result string } // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { Dummy(ctx context.Context, params SvcRequest) (SvcResponse, error) } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) func (c *svcClient) Dummy(ctx context.Context, params SvcRequest) (resp SvcResponse, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} queryString := url.Values{ "message": {reqEncoder.FromString(params.message)}, "val": {reqEncoder.FromString(params.val)}, } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", fmt.Sprintf("/dummy?%s", queryString.Encode()), nil, nil, &resp) if err != nil { return } return } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } // serde is used to serialize request data into strings and deserialize response data from strings type serde struct { LastError error // The last error that occurred NonEmptyValues int // The number of values this decoder has decoded } func (e *serde) FromString(s string) (v string) { e.NonEmptyValues++ return s } // setErr sets the last error within the object if one is not already set func (e *serde) setErr(msg, field string, err error) { if err != nil && e.LastError == nil { e.LastError = fmt.Errorf("%s: %s: %w", field, msg, err) } } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_decimal_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } async dummy(params) { // Convert our params into the objects we need for the request const query = makeRecord({ message: params.message, val: String(params.val), }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/dummy`, undefined, {query}) return await resp.json() } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData() { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_decimal_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/dummy": { "get": { "operationId": "GET:svc.dummy", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "query", "name": "message", "required": true, "schema": { "type": "string" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "val", "required": true, "schema": { "type": "string" }, "style": "form" } ], "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "result": { "type": "string" } }, "required": [ "result" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_decimal_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Request { message: string val: string } export interface Response { result: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } public async dummy(params: Request): Promise { // Convert our params into the objects we need for the request const query = makeRecord({ message: params.message, val: String(params.val), }) // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/dummy`, undefined, {query}) return await resp.json() as Response } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } // WithAuth allows you to set the authentication data to be used with each request func WithAuth(auth SvcAuthParams) Option { return func(base *baseClient) error { base.authGenerator = func(_ context.Context) (SvcAuthParams, error) { return auth, nil } return nil } } // WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request func WithAuthFunc(authGenerator func(ctx context.Context) (SvcAuthParams, error)) Option { return func(base *baseClient) error { base.authGenerator = authGenerator return nil } } type SvcAuthParams struct { Cookie string `encore:"optional" header:"Cookie,optional"` Token string `encore:"optional" header:"x-api-token,optional"` CookieValue string `cookie:"actual-cookie,optional" encore:"optional"` } type SvcRequest struct { Foo float64 `encore:"optional"` // Foo is good Baz string // Baz is better QueryFoo bool `encore:"optional" query:"foo,optional"` QueryBar string `encore:"optional" query:"bar,optional"` QueryList []bool `encore:"optional" query:"list,optional"` HeaderBaz string `encore:"optional" header:"baz,optional"` HeaderNum float64 `encore:"optional" header:"num,optional"` CookieQux string `cookie:"qux,optional" encore:"optional"` CookieQuux float64 `cookie:"quux,optional" encore:"optional"` } type SvcRequest struct { Foo float64 `encore:"optional"` // Foo is good Baz string // Baz is better QueryFoo bool `encore:"optional" query:"foo,optional"` QueryBar string `encore:"optional" query:"bar,optional"` QueryList []bool `encore:"optional" query:"list,optional"` HeaderBaz string `encore:"optional" header:"baz,optional"` HeaderNum float64 `encore:"optional" header:"num,optional"` CookieQux string `cookie:"qux,optional" encore:"optional"` CookieQuux float64 `cookie:"quux,optional" encore:"optional"` } type SvcRequest struct { Foo float64 `encore:"optional"` // Foo is good Baz string // Baz is better QueryFoo bool `encore:"optional" query:"foo,optional"` QueryBar string `encore:"optional" query:"bar,optional"` QueryList []bool `encore:"optional" query:"list,optional"` HeaderBaz string `encore:"optional" header:"baz,optional"` HeaderNum float64 `encore:"optional" header:"num,optional"` CookieQux string `cookie:"qux,optional" encore:"optional"` CookieQuux float64 `cookie:"quux,optional" encore:"optional"` } // Svc is a service for testing the client generator. // // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { CookieDummy(ctx context.Context, params SvcRequest) (struct { Cookie string `cookie:"cookie"` }, error) CookiesOnly(ctx context.Context, params struct { Field string `cookie:"cookie"` }) (struct { Cookie string `cookie:"cookie"` }, error) Dummy(ctx context.Context, params SvcRequest) error Imported(ctx context.Context, params Common_StuffImportedRequest) (Common_StuffImportedResponse, error) NoTypes(ctx context.Context) error OnlyPathParams(ctx context.Context, pathParam string, pathParam2 string) (Common_StuffImportedResponse, error) Root(ctx context.Context, params SvcRequest) error } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) func (c *svcClient) CookieDummy(ctx context.Context, params SvcRequest) (resp struct { Cookie string `cookie:"cookie"` }, err error) { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "baz": {reqEncoder.FromString(params.headerBaz)}, "num": {reqEncoder.FromFloat64(params.headerNum)}, } queryString := url.Values{ "bar": {reqEncoder.FromString(params.queryBar)}, "foo": {reqEncoder.FromBool(params.queryFoo)}, "list": reqEncoder.FromBoolList(params.queryList), } if reqEncoder.LastError != nil { err = fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) return } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Foo float64 `json:"foo"` Baz string `json:"baz"` }{ baz: params.baz, foo: params.foo, } // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/cookie-dummy?%s", queryString.Encode()), headers, body, nil) if err != nil { return } return } func (c *svcClient) CookiesOnly(ctx context.Context, params struct { Field string `cookie:"cookie"` }) (resp struct { Cookie string `cookie:"cookie"` }, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/cookies-only", nil, nil, nil) if err != nil { return } return } func (c *svcClient) Dummy(ctx context.Context, params SvcRequest) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "baz": {reqEncoder.FromString(params.headerBaz)}, "num": {reqEncoder.FromFloat64(params.headerNum)}, } queryString := url.Values{ "bar": {reqEncoder.FromString(params.queryBar)}, "foo": {reqEncoder.FromBool(params.queryFoo)}, "list": reqEncoder.FromBoolList(params.queryList), } if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Foo float64 `json:"foo"` Baz string `json:"baz"` }{ baz: params.baz, foo: params.foo, } _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/dummy?%s", queryString.Encode()), headers, body, nil) return err } func (c *svcClient) Imported(ctx context.Context, params Common_StuffImportedRequest) (resp Common_StuffImportedResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", "/imported", nil, params, &resp) if err != nil { return } return } func (c *svcClient) NoTypes(ctx context.Context) error { _, err := callAPI(ctx, c.base, "POST", "/type-less", nil, nil, nil) return err } func (c *svcClient) OnlyPathParams(ctx context.Context, pathParam string, pathParam2 string) (resp Common_StuffImportedResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "POST", fmt.Sprintf("/path/%s/%s", url.PathEscape(pathParam), url.PathEscape(pathParam2)), nil, nil, &resp) if err != nil { return } return } func (c *svcClient) Root(ctx context.Context, params SvcRequest) error { // Convert our params into the objects we need for the request reqEncoder := &serde{} headers := http.Header{ "baz": {reqEncoder.FromString(params.headerBaz)}, "num": {reqEncoder.FromFloat64(params.headerNum)}, } queryString := url.Values{ "bar": {reqEncoder.FromString(params.queryBar)}, "foo": {reqEncoder.FromBool(params.queryFoo)}, "list": reqEncoder.FromBoolList(params.queryList), } if reqEncoder.LastError != nil { return fmt.Errorf("unable to marshal parameters: %w", reqEncoder.LastError) } // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) body := struct { Foo float64 `json:"foo"` Baz string `json:"baz"` }{ baz: params.baz, foo: params.foo, } _, err := callAPI(ctx, c.base, "POST", fmt.Sprintf("/?%s", queryString.Encode()), headers, body, nil) return err } type Common_StuffImportedRequest struct { Name string } type Common_StuffImportedResponse struct { Message string } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { authGenerator func(ctx context.Context) (SvcAuthParams, error) // The function which will add the authentication data to the requests httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // If a authorization data generator is present, call it and add the returned token to the request if b.authGenerator != nil { if authData, err := b.authGenerator(req.Context()); err != nil { return nil, fmt.Errorf("unable to create authorization token for api request: %w", err) } else { authEncoder := &serde{} // Add the auth fields to the headers req.Header.Set("cookie", authEncoder.FromString(authData.Cookie)) req.Header.Set("x-api-token", authEncoder.FromString(authData.Token)) if authEncoder.LastError != nil { return nil, fmt.Errorf("unable to marshal authentication data: %w", authEncoder.LastError) } } } // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } // serde is used to serialize request data into strings and deserialize response data from strings type serde struct { LastError error // The last error that occurred NonEmptyValues int // The number of values this decoder has decoded } func (e *serde) FromString(s string) (v string) { e.NonEmptyValues++ return s } func (e *serde) FromFloat64(s float64) (v string) { e.NonEmptyValues++ return strconv.FormatFloat(s, uint8(0x66), -1, 64) } func (e *serde) FromBool(s bool) (v string) { e.NonEmptyValues++ return strconv.FormatBool(s) } func (e *serde) FromBoolList(s []bool) (v []string) { e.NonEmptyValues++ for _, x := range s { v = append(v, e.FromBool(x)) } return v } // setErr sets the last error within the object if one is not already set func (e *serde) setErr(msg, field string, err error) { if err != nil && e.LastError == nil { e.LastError = fmt.Errorf("%s: %s: %w", field, msg, err) } } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_httpstatus_golang.go ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" ) // Client is an API client for the app Encore application. type Client struct { Svc SvcClient } // BaseURL is the base URL for calling the Encore application's API. type BaseURL string const Local BaseURL = "http://localhost:4000" // Environment returns a BaseURL for calling the cloud environment with the given name. func Environment(name string) BaseURL { return BaseURL(fmt.Sprintf("https://%s-app.encr.app", name)) } // PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. func PreviewEnv(pr int) BaseURL { return Environment(fmt.Sprintf("pr%d", pr)) } // Option allows you to customise the baseClient used by the Client type Option = func(client *baseClient) error // New returns a Client for calling the public and authenticated APIs of your Encore application. // You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc. func New(target BaseURL, options ...Option) (*Client, error) { // Parse the base URL where the Encore application is being hosted baseURL, err := url.Parse(string(target)) if err != nil { return nil, fmt.Errorf("unable to parse base url: %w", err) } // Create a client with sensible defaults base := &baseClient{ baseURL: baseURL, httpClient: http.DefaultClient, userAgent: "app-Generated-Go-Client (Encore/v0.0.0-develop)", } // Apply any given options for _, option := range options { if err := option(base); err != nil { return nil, fmt.Errorf("unable to apply client option: %w", err) } } return &Client{Svc: &svcClient{base}}, nil } // WithHTTPClient can be used to configure the underlying HTTP client used when making API calls. // // Defaults to http.DefaultClient func WithHTTPClient(client HTTPDoer) Option { return func(base *baseClient) error { base.httpClient = client return nil } } type SvcResponse struct { Message string } // SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient. // It is setup as an interface allowing you to use GoMock to create mock implementations during tests. type SvcClient interface { Dummy(ctx context.Context) (SvcResponse, error) } type svcClient struct { base *baseClient } var _ SvcClient = (*svcClient)(nil) func (c *svcClient) Dummy(ctx context.Context) (resp SvcResponse, err error) { // Now make the actual call to the API _, err = callAPI(ctx, c.base, "GET", "/dummy", nil, nil, &resp) if err != nil { return } return } // HTTPDoer is an interface which can be used to swap out the default // HTTP client (http.DefaultClient) with your own custom implementation. // This can be used to inject middleware or mock responses during unit tests. type HTTPDoer interface { Do(req *http.Request) (*http.Response, error) } // baseClient holds all the information we need to make requests to an Encore application type baseClient struct { httpClient HTTPDoer // The HTTP client which will be used for all API requests baseURL *url.URL // The base URL which API requests will be made against userAgent string // What user agent we will use in the API requests } // Do sends the req to the Encore application adding the authorization token as required. func (b *baseClient) Do(req *http.Request) (*http.Response, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", b.userAgent) // Merge the base URL and the API URL req.URL = b.baseURL.ResolveReference(req.URL) req.Host = req.URL.Host // Finally, make the request via the configured HTTP Client return b.httpClient.Do(req) } // callAPI is used by each generated API method to actually make request and decode the responses func callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) { // Encode the API body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create the request req, err := http.NewRequestWithContext(ctx, method, path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } // Add any headers to the request for header, values := range headers { for _, value := range values { req.Header.Add(header, value) } } // Make the request via the base client rawResponse, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= 400 { // Read the full body sent back body, err := io.ReadAll(rawResponse.Body) if err != nil { return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response without readable body: %s", rawResponse.Status), } } // Attempt to decode the error response as a structured APIError apiError := &APIError{} if err := json.Unmarshal(body, apiError); err != nil { // If the error is not a parsable as an APIError, then return an error with the raw body return nil, &APIError{ Code: ErrUnknown, Message: fmt.Sprintf("got error response: %s", string(body)), } } return nil, apiError } // Decode the response if resp != nil { if err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } } return rawResponse.Header, nil } // APIError is the error type returned by the API type APIError struct { Code ErrCode `json:"code"` Message string `json:"message"` Details any `json:"details"` } func (e *APIError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } type ErrCode int const ( // ErrOK indicates the operation was successful. ErrOK ErrCode = 0 // ErrCanceled indicates the operation was canceled (typically by the caller). // // Encore will generate this error code when cancellation is requested. ErrCanceled ErrCode = 1 // ErrUnknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // Encore will generate this error code in the above two mentioned cases. ErrUnknown ErrCode = 2 // ErrInvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. ErrInvalidArgument ErrCode = 3 // ErrDeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. // // The gRPC framework will generate this error code when the deadline is // exceeded. ErrDeadlineExceeded ErrCode = 4 // ErrNotFound means some requested entity (e.g., file or directory) was // not found. // // This error code will not be generated by the gRPC framework. ErrNotFound ErrCode = 5 // ErrAlreadyExists means an attempt to create an entity failed because one // already exists. // // This error code will not be generated by the gRPC framework. ErrAlreadyExists ErrCode = 6 // ErrPermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted // instead for those errors). It must not be // used if the caller cannot be identified (use Unauthenticated // instead for those errors). // // This error code will not be generated by the gRPC core framework, // but expect authentication middleware to use it. ErrPermissionDenied ErrCode = 7 // ErrResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. // // This error code will be generated by the gRPC framework in // out-of-memory and server overload situations, or when a message is // larger than the configured maximum size. ErrResourceExhausted ErrCode = 8 // ErrFailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition, Aborted, and Unavailable: // // (a) Use Unavailable if the client can retry just the failing call. // (b) Use Aborted if the client should retry at a higher-level // (e.g., restarting a read-modify-write sequence). // (c) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (d) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. // // This error code will not be generated by the gRPC framework. ErrFailedPrecondition ErrCode = 9 // ErrAborted indicates the operation was aborted, typically due to a // concurrency issue like sequencer check failures, transaction aborts, // etc. // // See litmus test above for deciding between FailedPrecondition, // ErrAborted, and Unavailable. ErrAborted ErrCode = 10 // ErrOutOfRange means operation was attempted past the valid range. // E.g., seeking or reading past end of file. // // Unlike InvalidArgument, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // may be rotated to a 64-bit file without error. // // There is a fair bit of overlap between FailedPrecondition and // ErrOutOfRange. We recommend using OutOfRange (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an OutOfRange error to detect when // they are done. // // This error code will not be generated by the gRPC framework. ErrOutOfRange ErrCode = 11 // ErrUnimplemented indicates operation is not implemented or not // supported/enabled in this service. // // This is not an error, but a feature not available. // // This error code will not be generated by the gRPC framework. ErrUnimplemented ErrCode = 12 // ErrInternal means some invariant expected by the underlying system has // been broken. This is not a per-message error, it is a global // conditions check. // // This error code will not be generated by the gRPC framework. ErrInternal ErrCode = 13 // ErrUnavailable indicates the service is currently unavailable. // This is most likely a transient condition, which can be corrected by // retrying with a backoff. // // See litmus test above for deciding between FailedPrecondition, // Aborted, and Unavailable. ErrUnavailable ErrCode = 14 // ErrDataLoss indicates unrecoverable data loss or corruption. // // This error code is only defined in the gRPC library, and only for // unrecoverable data loss (i.e., data loss resulting from errors // like hard disk corruption or bandwidth exceeded). // // This error code will not be generated by the gRPC framework. ErrDataLoss ErrCode = 15 // ErrUnauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. ErrUnauthenticated ErrCode = 16 ) // String returns the string representation of the error code func (c ErrCode) String() string { switch c { case ErrOK: return "ok" case ErrCanceled: return "canceled" case ErrUnknown: return "unknown" case ErrInvalidArgument: return "invalid_argument" case ErrDeadlineExceeded: return "deadline_exceeded" case ErrNotFound: return "not_found" case ErrAlreadyExists: return "already_exists" case ErrPermissionDenied: return "permission_denied" case ErrResourceExhausted: return "resource_exhausted" case ErrFailedPrecondition: return "failed_precondition" case ErrAborted: return "aborted" case ErrOutOfRange: return "out_of_range" case ErrUnimplemented: return "unimplemented" case ErrInternal: return "internal" case ErrUnavailable: return "unavailable" case ErrDataLoss: return "data_loss" case ErrUnauthenticated: return "unauthenticated" default: return "unknown" } } // MarshalJSON converts the error code to a human-readable string func (c ErrCode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%s\"", c)), nil } // UnmarshalJSON converts the human-readable string to an error code func (c *ErrCode) UnmarshalJSON(b []byte) error { switch string(b) { case "\"ok\"": *c = ErrOK case "\"canceled\"": *c = ErrCanceled case "\"unknown\"": *c = ErrUnknown case "\"invalid_argument\"": *c = ErrInvalidArgument case "\"deadline_exceeded\"": *c = ErrDeadlineExceeded case "\"not_found\"": *c = ErrNotFound case "\"already_exists\"": *c = ErrAlreadyExists case "\"permission_denied\"": *c = ErrPermissionDenied case "\"resource_exhausted\"": *c = ErrResourceExhausted case "\"failed_precondition\"": *c = ErrFailedPrecondition case "\"aborted\"": *c = ErrAborted case "\"out_of_range\"": *c = ErrOutOfRange case "\"unimplemented\"": *c = ErrUnimplemented case "\"internal\"": *c = ErrInternal case "\"unavailable\"": *c = ErrUnavailable case "\"data_loss\"": *c = ErrDataLoss case "\"unauthenticated\"": *c = ErrUnauthenticated default: *c = ErrUnknown } return nil } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_httpstatus_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Response { message: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } public async dummy(): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("GET", `/dummy`) return await resp.json() as Response } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } /** * Svc is a service for testing the client generator. */ class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.cookieDummy = this.cookieDummy.bind(this) this.cookiesOnly = this.cookiesOnly.bind(this) this.dummy = this.dummy.bind(this) this.imported = this.imported.bind(this) this.noTypes = this.noTypes.bind(this) this.onlyPathParams = this.onlyPathParams.bind(this) this.root = this.root.bind(this) } async cookieDummy(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { baz: params.baz, foo: params.foo, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cookie-dummy`, JSON.stringify(body), {headers, query}) return await resp.json() } async cookiesOnly(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cookies-only`) return await resp.json() } async dummy(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI("POST", `/dummy`, JSON.stringify(body), {headers, query}) } async imported(params) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/imported`, JSON.stringify(params)) return await resp.json() } async noTypes() { await this.baseClient.callTypedAPI("POST", `/type-less`) } async onlyPathParams(pathParam, pathParam2) { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/path/${encodeURIComponent(pathParam)}/${encodeURIComponent(pathParam2)}`) return await resp.json() } async root(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI("POST", `/`, JSON.stringify(body), {headers, query}) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData() { let authData; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data = {}; data.headers = makeRecord({ cookie: authData.cookie, "x-api-token": authData.token, }) return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_list_of_union_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } async dummy(params) { // Convert our params into the objects we need for the request const query = makeRecord({ listOfUnion: params.listOfUnion.map((v) => String(v)), }) await this.baseClient.callTypedAPI("GET", `/dummy`, undefined, {query}) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData() { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_list_of_union_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/dummy": { "get": { "operationId": "GET:svc.dummy", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "query", "name": "listOfUnion", "required": true, "schema": { "items": { "enum": [ "a", "b" ], "type": "string" }, "type": "array" }, "style": "form" } ], "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_list_of_union_shared.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ import type { CookieWithOptions } from "encore.dev/api"; /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } /** * Import the endpoint handlers to derive the types for the client. */ import { dummy as api_svc_svc_dummy } from "~backend/svc/svc"; export namespace svc { export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } public async dummy(params: RequestType): Promise { // Convert our params into the objects we need for the request const query = makeRecord({ listOfUnion: params.listOfUnion.map((v) => String(v)), }) await this.baseClient.callTypedAPI(`/dummy`, {query, method: "GET", body: undefined}) } } } type PickMethods = Omit & { method?: Type }; // Helper type to omit all fields that are cookies. type OmitCookie = { [K in keyof T as T[K] extends CookieWithOptions ? never : K]: T[K]; }; type RequestType any> = Parameters extends [infer H, ...any[]] ? OmitCookie : void; type ResponseType any> = OmitCookie>>; function dateReviver(key: string, value: any): any { if ( typeof value === "string" && value.length >= 10 && value.charCodeAt(0) >= 48 && // '0' value.charCodeAt(0) <= 57 // '9' ) { const parsedDate = new Date(value); if (!isNaN(parsedDate.getTime())) { return parsedDate; } } return value; } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } import { StreamInOutHandlerFn, StreamInHandlerFn, StreamOutHandlerFn, } from "encore.dev/api"; type StreamRequest = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Req : never; type StreamResponse = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Resp : never; function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data, dateReviver)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(path: string, params?: CallParameters): Promise { return this.callAPI(path, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(path: string, params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_list_of_union_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Request { listOfUnion: ("a" | "b")[] } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.dummy = this.dummy.bind(this) } public async dummy(params: Request): Promise { // Convert our params into the objects we need for the request const query = makeRecord({ listOfUnion: params.listOfUnion.map((v) => String(v)), }) await this.baseClient.callTypedAPI("GET", `/dummy`, undefined, {query}) } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_openapi.json ================================================ { "components": { "responses": { "APIError": { "content": { "application/json": { "schema": { "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#Error" }, "properties": { "code": { "description": "Error code", "example": "not_found", "externalDocs": { "url": "https://pkg.go.dev/encore.dev/beta/errs#ErrCode" }, "type": "string" }, "details": { "description": "Error details", "type": "object" }, "message": { "description": "Error message", "type": "string" } }, "title": "APIError", "type": "object" } } }, "description": "Error response" } } }, "info": { "description": "Generated by encore", "title": "API for app", "version": "1", "x-logo": { "altText": "Encore logo", "backgroundColor": "#EEEEE1", "url": "https://encore.dev/assets/branding/logo/logo-black.png" } }, "openapi": "3.0.0", "paths": { "/": { "post": { "operationId": "POST:svc.root", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "baz", "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "num", "schema": { "type": "number" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "foo", "schema": { "type": "boolean" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "bar", "schema": { "type": "string" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "list", "schema": { "items": { "type": "boolean" }, "type": "array" }, "style": "form" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "baz": { "title": "Baz is better\n", "type": "string" }, "foo": { "title": "Foo is good\n", "type": "number" } }, "required": [ "baz" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/cookie-dummy": { "post": { "operationId": "POST:svc.cookieDummy", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "baz", "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "num", "schema": { "type": "number" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "foo", "schema": { "type": "boolean" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "bar", "schema": { "type": "string" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "list", "schema": { "items": { "type": "boolean" }, "type": "array" }, "style": "form" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "baz": { "title": "Baz is better\n", "type": "string" }, "foo": { "title": "Foo is good\n", "type": "number" } }, "required": [ "baz" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/cookies-only": { "post": { "operationId": "POST:svc.cookiesOnly", "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/dummy": { "post": { "operationId": "POST:svc.dummy", "parameters": [ { "allowEmptyValue": true, "explode": true, "in": "header", "name": "baz", "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "header", "name": "num", "schema": { "type": "number" }, "style": "simple" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "foo", "schema": { "type": "boolean" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "bar", "schema": { "type": "string" }, "style": "form" }, { "allowEmptyValue": true, "explode": true, "in": "query", "name": "list", "schema": { "items": { "type": "boolean" }, "type": "array" }, "style": "form" } ], "requestBody": { "content": { "application/json": { "schema": { "properties": { "baz": { "title": "Baz is better\n", "type": "string" }, "foo": { "title": "Foo is good\n", "type": "number" } }, "required": [ "baz" ], "type": "object" } } } }, "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/imported": { "post": { "operationId": "POST:svc.imported", "requestBody": { "content": { "application/json": { "schema": { "properties": { "name": { "type": "string" } }, "required": [ "name" ], "type": "object" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "message": { "type": "string" } }, "required": [ "message" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/path/{pathParam}/{pathParam2}": { "post": { "operationId": "POST:svc.onlyPathParams", "parameters": [ { "allowEmptyValue": true, "explode": false, "in": "path", "name": "pathParam", "required": true, "schema": { "type": "string" }, "style": "simple" }, { "allowEmptyValue": true, "explode": false, "in": "path", "name": "pathParam2", "required": true, "schema": { "type": "string" }, "style": "simple" } ], "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "message": { "type": "string" } }, "required": [ "message" ], "type": "object" } } }, "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } }, "/type-less": { "post": { "operationId": "POST:svc.noTypes", "responses": { "200": { "description": "Success response" }, "default": { "$ref": "#/components/responses/APIError" } } } } }, "servers": [ { "description": "Encore local dev environment", "url": "http://localhost:4000" } ] } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_shared.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ import type { CookieWithOptions } from "encore.dev/api"; /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * Import the auth handler to be able to derive the auth type */ import type { auth as auth_auth } from "~backend/svc/svc"; /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } /** * Allows you to set the authentication data to be used for each * request either by passing in a static object or by passing in * a function which returns a new object for each request. */ auth?: RequestType | AuthDataGenerator } /** * Import the endpoint handlers to derive the types for the client. */ import { cookieDummy as api_svc_svc_cookieDummy, cookiesOnly as api_svc_svc_cookiesOnly, dummy as api_svc_svc_dummy, imported as api_svc_svc_imported, onlyPathParams as api_svc_svc_onlyPathParams, root as api_svc_svc_root } from "~backend/svc/svc"; /** * Svc is a service for testing the client generator. */ export namespace svc { export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.cookieDummy = this.cookieDummy.bind(this) this.cookiesOnly = this.cookiesOnly.bind(this) this.dummy = this.dummy.bind(this) this.imported = this.imported.bind(this) this.noTypes = this.noTypes.bind(this) this.onlyPathParams = this.onlyPathParams.bind(this) this.root = this.root.bind(this) } public async cookieDummy(params: RequestType): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI(`/cookie-dummy`, {headers, query, method: "POST", body: JSON.stringify(body)}) return JSON.parse(await resp.text(), dateReviver) as ResponseType } public async cookiesOnly(params: RequestType): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI(`/cookies-only`, {method: "POST", body: undefined}) return JSON.parse(await resp.text(), dateReviver) as ResponseType } public async dummy(params: RequestType): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI(`/dummy`, {headers, query, method: "POST", body: JSON.stringify(body)}) } public async imported(params: RequestType): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI(`/imported`, {method: "POST", body: JSON.stringify(params)}) return JSON.parse(await resp.text(), dateReviver) as ResponseType } public async noTypes(): Promise { await this.baseClient.callTypedAPI(`/type-less`, {method: "POST", body: undefined}) } public async onlyPathParams(params: { pathParam: string, pathParam2: string }): Promise> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI(`/path/${encodeURIComponent(params.pathParam)}/${encodeURIComponent(params.pathParam2)}`, {method: "POST", body: undefined}) return JSON.parse(await resp.text(), dateReviver) as ResponseType } public async root(params: RequestType): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI(`/`, {headers, query, method: "POST", body: JSON.stringify(body)}) } } } type PickMethods = Omit & { method?: Type }; // Helper type to omit all fields that are cookies. type OmitCookie = { [K in keyof T as T[K] extends CookieWithOptions ? never : K]: T[K]; }; type RequestType any> = Parameters extends [infer H, ...any[]] ? OmitCookie : void; type ResponseType any> = OmitCookie>>; function dateReviver(key: string, value: any): any { if ( typeof value === "string" && value.length >= 10 && value.charCodeAt(0) >= 48 && // '0' value.charCodeAt(0) <= 57 // '9' ) { const parsedDate = new Date(value); if (!isNaN(parsedDate.getTime())) { return parsedDate; } } return value; } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } import { StreamInOutHandlerFn, StreamInHandlerFn, StreamOutHandlerFn, } from "encore.dev/api"; type StreamRequest = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Req : never; type StreamResponse = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Resp : never; function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data, dateReviver)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | RequestType | Promise | undefined> | undefined; // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } readonly authGenerator?: AuthDataGenerator constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData(): Promise { let authData: RequestType | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; data.headers = makeRecord({ cookie: authData.cookie, "x-api-token": authData.token, }); return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(path: string, params?: CallParameters): Promise { return this.callAPI(path, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(path: string, params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_stream_javascript.js ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * Local is the base URL for calling the Encore application's API. */ export const Local = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name) { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr) { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target = "prod", options = undefined) { const base = new BaseClient(target, options ?? {}) this.svc = new svc.ServiceClient(base) } } class SvcServiceClient { constructor(baseClient) { this.baseClient = baseClient this.inOutWithHandshake = this.inOutWithHandshake.bind(this) this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this) this.inWithHandshake = this.inWithHandshake.bind(this) this.inWithResponse = this.inWithResponse.bind(this) this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this) this.inWithoutHandshake = this.inWithoutHandshake.bind(this) this.outWithHandshake = this.outWithHandshake.bind(this) this.outWithoutHandshake = this.outWithoutHandshake.bind(this) } /** * InOut stream type variants */ async inOutWithHandshake(pathParam, params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(pathParam)}`, {headers, query}) } async inOutWithoutHandshake() { return await this.baseClient.createStreamInOut(`/inout/noHandshake`) } /** * In stream type variants */ async inWithHandshake(pathParam, params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(pathParam)}`, {headers, query}) } async inWithResponse() { return await this.baseClient.createStreamOut(`/in/withResponse`) } async inWithResponseAndHandshake(params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ pathParam: params.pathParam, "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query}) } async inWithoutHandshake() { return await this.baseClient.createStreamOut(`/in/noHandshake`) } /** * Out stream type variants */ async outWithHandshake(pathParam, params) { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(pathParam)}`, {headers, query}) } async outWithoutHandshake() { return await this.baseClient.createStreamIn(`/out/noHandshake`) } } export const svc = { ServiceClient: SvcServiceClient } function encodeQuery(parts) { const pairs = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. function makeRecord(record) { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record } function encodeWebSocketHeaders(headers) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { hasUpdateHandlers = []; constructor(url, headers) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)); } this.ws = new WebSocket(url, protocols); this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type, handler) { this.ws.addEventListener(type, handler); } off(type, handler) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamIn { buffer = []; constructor(url, headers) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next() { for await (const next of this) return next; } async *[Symbol.asyncIterator]() { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift(); } else { if (this.socket.ws.readyState === WebSocket.CLOSED) break; await this.socket.hasUpdate(); } } } } export class StreamOut { constructor(url, headers) { let responseResolver; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event) => { responseResolver(JSON.parse(event.data)) }); } async response() { return this.responseValue; } close() { this.socket.close(); } async send(msg) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } const boundFetch = fetch.bind(this) class BaseClient { constructor(baseURL, options) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-JS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {} // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData() { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : ''; return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path, params) { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" async callTypedAPI(method, path, body, params) { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request async callAPI(method, path, body, params) { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } function isAPIErrorResponse(err) { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code) { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { constructor(status, response) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if (Object.setPrototypeOf == undefined) { this.__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if (Error.captureStackTrace !== undefined) { Error.captureStackTrace(this, this.constructor); } /** * The HTTP status code associated with the error. */ this.status = status /** * The Encore error code */ this.code = response.code /** * The error details */ this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err) { return err instanceof APIError; } export const ErrCode = { /** * OK indicates the operation was successful. */ OK: "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled: "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown: "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument: "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded: "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound: "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists: "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied: "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted: "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition: "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted: "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange: "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented: "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal: "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable: "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss: "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated: "unauthenticated" } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_stream_shared.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ import type { CookieWithOptions } from "encore.dev/api"; /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } /** * Import the endpoint handlers to derive the types for the client. */ import { inOutWithHandshake as api_svc_svc_inOutWithHandshake, inOutWithoutHandshake as api_svc_svc_inOutWithoutHandshake, inWithHandshake as api_svc_svc_inWithHandshake, inWithResponse as api_svc_svc_inWithResponse, inWithResponseAndHandshake as api_svc_svc_inWithResponseAndHandshake, inWithoutHandshake as api_svc_svc_inWithoutHandshake, outWithHandshake as api_svc_svc_outWithHandshake, outWithoutHandshake as api_svc_svc_outWithoutHandshake } from "~backend/svc/svc"; export namespace svc { export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.inOutWithHandshake = this.inOutWithHandshake.bind(this) this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this) this.inWithHandshake = this.inWithHandshake.bind(this) this.inWithResponse = this.inWithResponse.bind(this) this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this) this.inWithoutHandshake = this.inWithoutHandshake.bind(this) this.outWithHandshake = this.outWithHandshake.bind(this) this.outWithoutHandshake = this.outWithoutHandshake.bind(this) } /** * InOut stream type variants */ public async inOutWithHandshake(params: RequestType): Promise, StreamResponse>> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(params.pathParam)}`, {headers, query}) } public async inOutWithoutHandshake(): Promise, StreamResponse>> { return await this.baseClient.createStreamInOut(`/inout/noHandshake`) } /** * In stream type variants */ public async inWithHandshake(params: RequestType): Promise, void>> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(params.pathParam)}`, {headers, query}) } public async inWithResponse(): Promise, StreamResponse>> { return await this.baseClient.createStreamOut(`/in/withResponse`) } public async inWithResponseAndHandshake(params: RequestType): Promise, StreamResponse>> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ pathParam: params.pathParam, "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query}) } public async inWithoutHandshake(): Promise, void>> { return await this.baseClient.createStreamOut(`/in/noHandshake`) } /** * Out stream type variants */ public async outWithHandshake(params: RequestType): Promise>> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(params.pathParam)}`, {headers, query}) } public async outWithoutHandshake(): Promise>> { return await this.baseClient.createStreamIn(`/out/noHandshake`) } } } type PickMethods = Omit & { method?: Type }; // Helper type to omit all fields that are cookies. type OmitCookie = { [K in keyof T as T[K] extends CookieWithOptions ? never : K]: T[K]; }; type RequestType any> = Parameters extends [infer H, ...any[]] ? OmitCookie : void; type ResponseType any> = OmitCookie>>; function dateReviver(key: string, value: any): any { if ( typeof value === "string" && value.length >= 10 && value.charCodeAt(0) >= 48 && // '0' value.charCodeAt(0) <= 57 // '9' ) { const parsedDate = new Date(value); if (!isNaN(parsedDate.getTime())) { return parsedDate; } } return value; } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } import { StreamInOutHandlerFn, StreamInHandlerFn, StreamOutHandlerFn, } from "encore.dev/api"; type StreamRequest = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Req : never; type StreamResponse = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Resp : never; function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data, dateReviver)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data, dateReviver)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(path: string, params?: CallParameters): Promise { return this.callAPI(path, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(path: string, params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_stream_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } } export namespace svc { export interface Handshake { headerValue: string queryValue: string } export interface Handshake { headerValue: string queryValue: string } export interface Handshake { headerValue: string queryValue: string pathParam: string } export interface Handshake { headerValue: string queryValue: string } export interface InMsg { data: string } export interface InMsg { data: string } export interface InMsg { data: string } export interface InMsg { data: string } export interface InMsg { data: string } export interface InMsg { data: string } export interface OutMsg { user: number msg: string } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.inOutWithHandshake = this.inOutWithHandshake.bind(this) this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this) this.inWithHandshake = this.inWithHandshake.bind(this) this.inWithResponse = this.inWithResponse.bind(this) this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this) this.inWithoutHandshake = this.inWithoutHandshake.bind(this) this.outWithHandshake = this.outWithHandshake.bind(this) this.outWithoutHandshake = this.outWithoutHandshake.bind(this) } /** * InOut stream type variants */ public async inOutWithHandshake(pathParam: string, params: Handshake): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(pathParam)}`, {headers, query}) } public async inOutWithoutHandshake(): Promise> { return await this.baseClient.createStreamInOut(`/inout/noHandshake`) } /** * In stream type variants */ public async inWithHandshake(pathParam: string, params: Handshake): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(pathParam)}`, {headers, query}) } public async inWithResponse(): Promise> { return await this.baseClient.createStreamOut(`/in/withResponse`) } public async inWithResponseAndHandshake(params: Handshake): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ pathParam: params.pathParam, "some-query": params.queryValue, }) return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query}) } public async inWithoutHandshake(): Promise> { return await this.baseClient.createStreamOut(`/in/noHandshake`) } /** * Out stream type variants */ public async outWithHandshake(pathParam: string, params: Handshake): Promise> { // Convert our params into the objects we need for the request const headers = makeRecord({ "some-header": params.headerValue, }) const query = makeRecord({ "some-query": params.queryValue, }) return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(pathParam)}`, {headers, query}) } public async outWithoutHandshake(): Promise> { return await this.baseClient.createStreamIn(`/out/noHandshake`) } } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } } async getAuthData(): Promise { return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/expected_typescript.ts ================================================ // Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT. // Disable eslint, jshint, and jslint for this file. /* eslint-disable */ /* jshint ignore:start */ /*jslint-disable*/ /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `https://${name}-app.encr.app` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`pr${pr}`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the app Encore application. */ export default class Client { public readonly svc: svc.ServiceClient private readonly options: ClientOptions private readonly target: string /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions) { this.target = target this.options = options ?? {} const base = new BaseClient(this.target, this.options) this.svc = new svc.ServiceClient(base) } /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } } /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } /** * Allows you to set the authentication data to be used for each * request either by passing in a static object or by passing in * a function which returns a new object for each request. */ auth?: svc.AuthParams | AuthDataGenerator } /** * Svc is a service for testing the client generator. */ export namespace svc { export interface AuthParams { cookie?: string token?: string } export interface Request { /** * Foo is good */ foo?: number /** * Baz is better */ baz: string queryFoo?: boolean queryBar?: string queryList?: boolean[] headerBaz?: string headerNum?: number } export interface Request { /** * Foo is good */ foo?: number /** * Baz is better */ baz: string queryFoo?: boolean queryBar?: string queryList?: boolean[] headerBaz?: string headerNum?: number } export interface Request { /** * Foo is good */ foo?: number /** * Baz is better */ baz: string queryFoo?: boolean queryBar?: string queryList?: boolean[] headerBaz?: string headerNum?: number } export class ServiceClient { private baseClient: BaseClient constructor(baseClient: BaseClient) { this.baseClient = baseClient this.cookieDummy = this.cookieDummy.bind(this) this.cookiesOnly = this.cookiesOnly.bind(this) this.dummy = this.dummy.bind(this) this.imported = this.imported.bind(this) this.noTypes = this.noTypes.bind(this) this.onlyPathParams = this.onlyPathParams.bind(this) this.root = this.root.bind(this) } public async cookieDummy(params: Request): Promise<{ }> { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cookie-dummy`, JSON.stringify(body), {headers, query}) return await resp.json() as { } } public async cookiesOnly(params: { }): Promise<{ }> { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/cookies-only`) return await resp.json() as { } } public async dummy(params: Request): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI("POST", `/dummy`, JSON.stringify(body), {headers, query}) } public async imported(params: common_stuff.ImportedRequest): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/imported`, JSON.stringify(params)) return await resp.json() as common_stuff.ImportedResponse } public async noTypes(): Promise { await this.baseClient.callTypedAPI("POST", `/type-less`) } public async onlyPathParams(pathParam: string, pathParam2: string): Promise { // Now make the actual call to the API const resp = await this.baseClient.callTypedAPI("POST", `/path/${encodeURIComponent(pathParam)}/${encodeURIComponent(pathParam2)}`) return await resp.json() as common_stuff.ImportedResponse } public async root(params: Request): Promise { // Convert our params into the objects we need for the request const headers = makeRecord({ baz: params.headerBaz, num: params.headerNum === undefined ? undefined : String(params.headerNum), }) const query = makeRecord({ bar: params.queryBar, foo: params.queryFoo === undefined ? undefined : String(params.queryFoo), list: params.queryList?.map((v) => String(v)), }) // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields) const body: Record = { baz: params.baz, foo: params.foo, } await this.baseClient.callTypedAPI("POST", `/`, JSON.stringify(body), {headers, query}) } } } export namespace common_stuff { export interface ImportedRequest { name: string } export interface ImportedResponse { message: string } } function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(`${key}=${encodeURIComponent(v)}`) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(JSON.parse(event.data)); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } } } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(JSON.parse(event.data)) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); } } // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | svc.AuthParams | Promise | undefined; // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record } readonly authGenerator?: AuthDataGenerator constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "app-Generated-TS-Client (Encore/v0.0.0-develop)"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch } // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } } } async getAuthData(): Promise { let authData: svc.AuthParams | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; data.headers = makeRecord({ cookie: authData.cookie, "x-api-token": authData.token, }); return data; } return undefined; } // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { return this.callAPI(method, path, body, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } // callAPI is used by each generated API method to actually make the request public async callAPI(method: string, path: string, body?: RequestInit["body"], params?: CallParameters): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest, method, body: body ?? null, } // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } } /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } ================================================ FILE: pkg/clientgen/testdata/tsapp/input.ts ================================================ -- encore.app -- {"id": ""} -- package.json -- {"name": "ts-test-app"} -- common-stuff/types.ts -- export interface ImportedRequest { name: string; } export interface ImportedResponse { message: string; } -- svc/encore.service.ts -- import { Service } from "encore.dev/service"; // Svc is a service for testing the client generator. export default new Service("svc"); -- svc/svc.ts -- import { Header, Query, api, Gateway, Cookie } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import type { ImportedRequest, ImportedResponse } from "../common-stuff/types"; interface UnusedType { foo: Foo; } export const root = api( { expose: true, method: "POST", path: "/" }, async (req: Request) => { }, ); export const imported = api( { expose: true, method: "POST", path: "/imported" }, async (req: ImportedRequest) : Promise => { }, ); export const onlyPathParams = api( { expose: true, method: "POST", path: "/path/:pathParam/:pathParam2" }, async (req: { pathParam: string, pathParam2: string }) : Promise => { }, ); export const dummy = api( { expose: true, method: "POST", path: "/dummy" }, async (req: Request) => { }, ); export const noTypes = api( { expose: true, method: "POST", path: "/type-less" }, async () => { }, ) export const cookiesOnly = api( { expose: true, method: "POST", path: "/cookies-only" }, async (req: { field: Cookie<'cookie'> }): Promise<{ cookie: Cookie<'cookie'> }> => { return { cookie: { value: "value" } } }, ) export const cookieDummy = api( { expose: true, method: "POST", path: "/cookie-dummy" }, async (req: Request): Promise<{ cookie: Cookie<'cookie'> }> => { return { cookie: { value: "value" } } }, ); export interface AuthParams { cookie?: Header<'Cookie'> token?: Header<'x-api-token'> cookieValue?: Cookie<'actual-cookie'> } export interface AuthData { userID: string; } export const auth = authHandler( async (params) => { return { userID: "my-user-id" }; } ) export const gw = new Gateway({ authHandler: auth, }) interface Request { // Foo is good foo?: number; // Baz is better baz: string; queryFoo?: Query; queryBar?: Query<"bar">; queryList?: Query headerBaz?: Header<"baz">; headerNum?: Header; cookieQux?: Cookie<"qux">; cookieQuux?: Cookie; } ================================================ FILE: pkg/clientgen/testdata/tsapp/input_decimal.ts ================================================ -- encore.app -- {"id": ""} -- package.json -- {"name": "ts-test-app"} -- svc/svc.ts -- import { api } from "encore.dev/api"; import { Decimal } from "encore.dev/types"; export const dummy = api( { expose: true, method: "GET", path: "/dummy" }, async (req: Request): Promise => {}, ); interface Request { message: string, val: Decimal, } interface Response { result: Decimal } ================================================ FILE: pkg/clientgen/testdata/tsapp/input_httpstatus.ts ================================================ -- encore.app -- {"id": ""} -- package.json -- {"name": "ts-test-app"} -- svc/svc.ts -- import { api, HttpStatus } from "encore.dev/api"; export const dummy = api( { expose: true, method: "GET", path: "/dummy" }, async (): Promise => {}, ); interface Response { message: string, status: HttpStatus, } ================================================ FILE: pkg/clientgen/testdata/tsapp/input_list_of_union.ts ================================================ -- encore.app -- {"id": ""} -- package.json -- {"name": "ts-test-app"} -- svc/svc.ts -- import { api } from "encore.dev/api"; export const dummy = api( { expose: true, method: "GET", path: "/dummy" }, async (req: Request) => {}, ); interface Request { listOfUnion: ("a" | "b")[] } ================================================ FILE: pkg/clientgen/testdata/tsapp/input_stream.ts ================================================ -- encore.app -- {"id": ""} -- package.json -- {"name": "ts-test-app"} -- svc/svc.ts -- import { api, Header, Query } from "encore.dev/api"; interface Handshake { headerValue: Header<"some-header">; queryValue: Query<"some-query">; pathParam: string; } interface InMsg { data: string; } interface OutMsg { user: number; msg: string; } // InOut stream type variants export const inOutWithHandshake = api.streamInOut( { expose: true, path: "/inout/:pathParam" }, async (handshake: Handshake, stream) => {}, ); export const inOutWithoutHandshake = api.streamInOut( { expose: true, path: "/inout/noHandshake" }, async (stream) => {}, ); // Out stream type variants export const outWithHandshake = api.streamOut( { expose: true, path: "/out/:pathParam" }, async (handshake: Handshake, stream) => {}, ); export const outWithoutHandshake = api.streamOut( { expose: true, path: "/out/noHandshake" }, async (stream) => {}, ); // In stream type variants export const inWithHandshake = api.streamIn( { expose: true, path: "/in/:pathParam" }, async (handshake: Handshake, stream) => {}, ); export const inWithoutHandshake = api.streamIn( { expose: true, path: "/in/noHandshake" }, async (stream) => {}, ); export const inWithResponse = api.streamIn( { expose: true, path: "/in/withResponse" }, async (stream) => {}, ); export const inWithResponseAndHandshake = api.streamIn( { expose: true, path: "/in/withResponseAndHandshake" }, async (handshake: Handshake, stream) => {}, ); ================================================ FILE: pkg/clientgen/testdata/tsapp/tsconfig.json ================================================ // Note: this config is here purely to remove errors about promises not being present in IDE's when viewing the expected_typescript.ts file { "compilerOptions": { "target": "es2022", "paths": { "~backend/*": ["./*"] }, } } ================================================ FILE: pkg/clientgen/types.go ================================================ package clientgen import ( "fmt" "reflect" "sort" "encr.dev/pkg/clientgen/clientgentypes" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) func getNamedTypes(md *meta.Data, set clientgentypes.ServiceSet) *typeRegistry { r := &typeRegistry{ md: md, namespaces: make(map[string][]*schema.Decl), seenDecls: make(map[uint32]bool), declRefs: make(map[uint32]map[uint32]bool), } for _, svc := range md.Svcs { if !set.Has(svc.Name) { continue } for _, rpc := range svc.Rpcs { if rpc.AccessType != meta.RPC_PRIVATE { r.Visit(rpc.HandshakeSchema) r.Visit(rpc.RequestSchema) r.Visit(rpc.ResponseSchema) } } } if md.AuthHandler != nil && md.AuthHandler.Params != nil { r.Visit(md.AuthHandler.Params) } return r } // typeRegistry computes the visible set of type declarations // and how to group them into namespaces. type typeRegistry struct { md *meta.Data namespaces map[string][]*schema.Decl seenDecls map[uint32]bool declRefs map[uint32]map[uint32]bool // tracks which decls reference which other decls currDecl *schema.Decl // may be nil } func (v *typeRegistry) Decls(name string) []*schema.Decl { return v.namespaces[name] } func (v *typeRegistry) Namespaces() []string { nss := make([]string, 0, len(v.namespaces)) for ns := range v.namespaces { nss = append(nss, ns) } sort.Strings(nss) return nss } func (v *typeRegistry) Visit(typ *schema.Type) { if typ == nil { return } switch t := typ.Typ.(type) { case *schema.Type_Named: v.visitNamed(t.Named) case *schema.Type_List: v.Visit(t.List.Elem) case *schema.Type_Map: v.Visit(t.Map.Key) v.Visit(t.Map.Value) case *schema.Type_Struct: for _, f := range t.Struct.Fields { v.Visit(f.Typ) } case *schema.Type_Builtin, *schema.Type_TypeParameter, *schema.Type_Literal: // do nothing case *schema.Type_Pointer: v.Visit(t.Pointer.Base) case *schema.Type_Option: v.Visit(t.Option.Value) case *schema.Type_Config: v.Visit(t.Config.Elem) case *schema.Type_Union: for _, tt := range t.Union.Types { v.Visit(tt) } default: panic(fmt.Sprintf("unhandled type: %+v", reflect.TypeOf(typ.Typ))) } } func (v *typeRegistry) visitDecl(decl *schema.Decl) { if decl == nil { return } if !v.seenDecls[decl.Id] { v.seenDecls[decl.Id] = true ns := decl.Loc.PkgName v.namespaces[ns] = append(v.namespaces[ns], decl) // Set currDecl when processing this and then reset it prev := v.currDecl v.currDecl = decl v.Visit(decl.Type) v.currDecl = prev } } func (v *typeRegistry) visitNamed(n *schema.Named) { to := n.Id curr := v.currDecl if curr != nil { from := curr.Id if _, ok := v.declRefs[from]; !ok { v.declRefs[from] = make(map[uint32]bool) } v.declRefs[from][to] = true } decl := v.md.Decls[to] v.visitDecl(decl) // Add transitive refs if curr != nil { from := curr.Id for to2 := range v.declRefs[to] { v.declRefs[from][to2] = true } } for _, typeArg := range n.TypeArguments { v.Visit(typeArg) } } func (v *typeRegistry) IsRecursiveRef(from, to uint32) bool { return v.declRefs[from][to] && v.declRefs[to][from] } ================================================ FILE: pkg/clientgen/typescript.go ================================================ package clientgen import ( "bufio" "bytes" "fmt" "maps" "path/filepath" "reflect" "regexp" "slices" "sort" "strconv" "strings" "unicode" "github.com/cockroachdb/errors" "encr.dev/internal/version" "encr.dev/parser/encoding" "encr.dev/pkg/clientgen/clientgentypes" "encr.dev/pkg/fns" "encr.dev/pkg/idents" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) /* The TypeScript generator generates code that looks like this: export namespace task { export interface AddParams { description: string } export class ServiceClient { public Add(params: task_AddParams): Promise { // ... } } } */ // tsGenVersion allows us to introduce breaking changes in the generated code but behind a switch // meaning that people with client code reliant on the old behaviour can continue to generate the // old code. type tsGenVersion int const ( // TsInitial is the originally released typescript generator TsInitial tsGenVersion = iota // TsExperimental can be used to lock experimental or uncompleted features in the generated code // It should always be the last item in the enum TsExperimental ) const typescriptGenLatestVersion = TsExperimental - 1 type typescript struct { *bytes.Buffer md *meta.Data appSlug string typs *typeRegistry currDecl *schema.Decl generatorVersion tsGenVersion sharedTypes bool clientTarget string seenJSON bool // true if a JSON type was seen seenStream bool // true if a stream endpoint was seen seenHeaderResponse bool // true if we've seen a header used in a response object hasAuth bool // true if we've seen an authentication handler authIsComplexType bool // true if the auth type is a complex type } func (ts *typescript) Version() int { return int(ts.generatorVersion) } func (ts *typescript) Generate(p clientgentypes.GenerateParams) (err error) { defer ts.handleBailout(&err) ts.Buffer = p.Buf ts.md = p.Meta ts.appSlug = p.AppSlug ts.typs = getNamedTypes(p.Meta, p.Services) if ts.md.AuthHandler != nil { if !ts.isAuthCookieOnly() { ts.hasAuth = true ts.authIsComplexType = ts.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING } } ts.WriteString("// " + doNotEditHeader() + "\n\n") ts.WriteString("// Disable eslint, jshint, and jslint for this file.\n") ts.WriteString("/* eslint-disable */\n") ts.WriteString("/* jshint ignore:start */\n") ts.WriteString("/*jslint-disable*/\n") if ts.sharedTypes { ts.WriteString("import type { CookieWithOptions } from \"encore.dev/api\";\n") } nss := ts.typs.Namespaces() seenNs := make(map[string]bool) ts.writeClient(p.Services) for _, svc := range p.Meta.Svcs { if err := ts.writeService(svc, p.Services, p.Tags); err != nil { return err } seenNs[svc.Name] = true } if !ts.sharedTypes { for _, ns := range nss { if !seenNs[ns] { ts.writeNamespace(ns) } } } ts.writeExtraTypes() ts.writeStreamClasses() if err := ts.writeBaseClient(p.AppSlug); err != nil { return err } ts.writeCustomErrorType() if ts.clientTarget != "" { fmt.Fprintf(ts, ` export default new Client(%s, { requestInit: { credentials: "include" } }); `, ts.clientTarget) } return nil } func (ts *typescript) getFields(typ *schema.Type) []*schema.Field { if typ == nil { return nil } switch typ.Typ.(type) { case *schema.Type_Struct: return typ.GetStruct().Fields case *schema.Type_Named: decl := ts.md.Decls[typ.GetNamed().Id] return ts.getFields(decl.Type) default: return nil } } func (ts *typescript) isAuthCookieOnly() bool { if ts.md.AuthHandler == nil { return false } fields := ts.getFields(ts.md.AuthHandler.Params) if fields == nil { return false } for _, field := range fields { if field.Wire.GetCookie() == nil { return false } } return true } func hasPathParams(rpc *meta.RPC) bool { return fns.Any(rpc.Path.Segments, func(s *meta.PathSegment) bool { return s.Type != meta.PathSegment_LITERAL }) } func (ts *typescript) authImportName() string { return fmt.Sprintf("auth_%s", validTSIdentifier(ts.md.AuthHandler.Name)) } func (ts *typescript) writeAuthType() { if ts.sharedTypes { fmt.Fprintf(ts, "RequestType", ts.authImportName()) } else { ts.writeTyp("", ts.md.AuthHandler.Params, 2) } } func rpcImportName(rpc *meta.RPC) string { fileName := strings.TrimSuffix(rpc.Loc.Filename, filepath.Ext(rpc.Loc.Filename)) return fmt.Sprintf("api_%s_%s_%s", validTSIdentifier(rpc.ServiceName), validTSIdentifier(fileName), validTSIdentifier(rpc.Name)) } func getMethodType(rpc *meta.RPC) string { ts := strings.Builder{} for i, method := range rpc.HttpMethods { if i > 0 { ts.WriteString(" | ") } if method == "*" { ts.WriteString("string") } else { ts.WriteString("\"" + method + "\"") } } return ts.String() } func (ts *typescript) writeService(svc *meta.Service, p clientgentypes.ServiceSet, tags clientgentypes.TagSet) error { // Determine if we have anything worth exposing. // Either a public RPC or a named type. isIncluded := hasPublicRPC(svc) && p.Has(svc.Name) decls := ts.typs.Decls(svc.Name) if !isIncluded && len(decls) == 0 { return nil } if ts.sharedTypes { importsByPath := make(map[string][]string) for _, rpc := range svc.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) || rpc.Proto == meta.RPC_RAW || (rpc.ResponseSchema == nil && rpc.RequestSchema == nil && !hasPathParams(rpc)) { continue } path := fmt.Sprintf("~backend/%s/%s", rpc.Loc.PkgPath, strings.TrimSuffix(rpc.Loc.Filename, filepath.Ext(rpc.Loc.Filename))) importsByPath[path] = append(importsByPath[path], fmt.Sprintf("%s as %s", rpc.Name, rpcImportName(rpc))) } if len(importsByPath) > 0 { ts.WriteString(`/** * Import the endpoint handlers to derive the types for the client. */ `) } for _, path := range slices.Sorted(maps.Keys(importsByPath)) { imps := fmt.Sprintf(" %s ", importsByPath[path][0]) if len(importsByPath[path]) > 1 { imps = "\n " + strings.Join(importsByPath[path], ",\n ") + "\n" } fmt.Fprintf(ts, "import {%s} from \"%s\";\n", imps, path) } ts.WriteString("\n") } ns := svc.Name // Service doc string if doc := getServiceDoc(ts.md, svc); doc != "" { scanner := bufio.NewScanner(strings.NewReader(doc)) ts.WriteString("/**\n") for scanner.Scan() { ts.WriteString(" * ") ts.WriteString(scanner.Text()) ts.WriteByte('\n') } ts.WriteString(" */\n") } fmt.Fprintf(ts, "export namespace %s {\n", ts.typeName(ns)) sort.Slice(decls, func(i, j int) bool { return decls[i].Name < decls[j].Name }) if !ts.sharedTypes { for i, d := range decls { if i > 0 { ts.WriteString("\n") } ts.writeDeclDef(ns, d) } } if !isIncluded { ts.WriteString("}\n\n") return nil } ts.WriteString("\n") numIndent := 1 indent := func() { ts.WriteString(strings.Repeat(" ", numIndent)) } indent() fmt.Fprint(ts, "export class ServiceClient {\n") numIndent++ // Constructor indent() ts.WriteString("private baseClient: BaseClient\n\n") indent() ts.WriteString("constructor(baseClient: BaseClient) {\n") numIndent++ indent() ts.WriteString("this.baseClient = baseClient\n") for _, rpc := range svc.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) { continue } name := ts.memberName(rpc.Name) indent() fmt.Fprintf(ts, "this.%s = this.%s.bind(this)\n", name, name) } numIndent-- indent() ts.WriteString("}\n") // RPCs for _, rpc := range svc.Rpcs { if rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) { continue } ts.WriteByte('\n') // Doc string if rpc.Doc != nil && *rpc.Doc != "" { scanner := bufio.NewScanner(strings.NewReader(*rpc.Doc)) indent() ts.WriteString("/**\n") for scanner.Scan() { indent() ts.WriteString(" * ") ts.WriteString(scanner.Text()) ts.WriteByte('\n') } indent() ts.WriteString(" */\n") } // Signature indent() fmt.Fprintf(ts, "public async %s(", ts.memberName(rpc.Name)) isRaw := rpc.Proto == meta.RPC_RAW if isRaw && !ts.sharedTypes { fmt.Fprintf(ts, "method: %s, ", getMethodType(rpc)) } nParams := 0 // Avoid a name collision. payloadName := "params" segmentPrefix := "" if ts.sharedTypes { segmentPrefix = payloadName + "." } var isStream = rpc.StreamingRequest || rpc.StreamingResponse var hasHandshake = rpc.HandshakeSchema != nil var inlinePathParams = (isRaw || (rpc.RequestSchema == nil && !hasHandshake)) && hasPathParams(rpc) && ts.sharedTypes if inlinePathParams { ts.WriteString(payloadName + ": { ") } var rpcPath strings.Builder for _, s := range rpc.Path.Segments { rpcPath.WriteByte('/') if s.Type != meta.PathSegment_LITERAL { if !ts.sharedTypes || inlinePathParams { if nParams > 0 { ts.WriteString(", ") } ts.WriteString(ts.nonReservedId(s.Value)) ts.WriteString(": ") switch s.ValueType { case meta.PathSegment_STRING, meta.PathSegment_UUID: ts.WriteString("string") case meta.PathSegment_BOOL: ts.WriteString("boolean") case meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64, meta.PathSegment_INT, meta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64, meta.PathSegment_UINT: ts.WriteString("number") default: panic(fmt.Sprintf("unhandled PathSegment type %s", s.ValueType)) } if s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK { ts.WriteString("[]") } } if s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK { rpcPath.WriteString("${" + segmentPrefix + ts.nonReservedId(s.Value) + ".map(encodeURIComponent).join(\"/\")}") } else { rpcPath.WriteString("${encodeURIComponent(" + segmentPrefix + ts.nonReservedId(s.Value) + ")}") } nParams++ } else { rpcPath.WriteString(s.Value) } } if inlinePathParams { ts.WriteString(" }") } if (!isStream && rpc.RequestSchema != nil) || (isStream && hasHandshake) { if !ts.sharedTypes && nParams > 0 { ts.WriteString(", ") } ts.WriteString(payloadName + ": ") if ts.sharedTypes { fmt.Fprintf(ts, "RequestType", rpcImportName(rpc)) } else if isStream { ts.writeTyp(ns, rpc.HandshakeSchema, 0) } else { ts.writeTyp(ns, rpc.RequestSchema, 0) } } else if isRaw { if nParams > 0 { ts.WriteString(", ") } if !ts.sharedTypes { ts.WriteString("body?: RequestInit[\"body\"], ") } if ts.sharedTypes { fmt.Fprintf(ts, "options: PickMethods<%s> = {}", getMethodType(rpc)) } else { ts.WriteString("options?: CallParameters") } } var direction streamDirection if rpc.StreamingRequest && rpc.StreamingResponse { direction = InOut } else if rpc.StreamingRequest { direction = Out } else { direction = In } writeStreamRequest := func(ns string, numIndents int) { if rpc.RequestSchema == nil { ts.WriteString("void") } else if ts.sharedTypes { fmt.Fprintf(ts, "StreamRequest", rpcImportName(rpc)) } else { ts.writeTyp(ns, rpc.RequestSchema, numIndents) } } writeStreamResponse := func(ns string, numIndents int) { if rpc.ResponseSchema == nil { ts.WriteString("void") } else if ts.sharedTypes { ts.seenStream = true fmt.Fprintf(ts, "StreamResponse", rpcImportName(rpc)) } else { ts.writeTyp(ns, rpc.ResponseSchema, numIndents) } } ts.WriteString("): Promise<") if isStream { ts.seenStream = true switch direction { case InOut: ts.WriteString("StreamInOut<") writeStreamRequest(ns, 0) ts.WriteString(", ") writeStreamResponse(ns, 0) ts.WriteString(">") case In: ts.WriteString("StreamIn<") writeStreamResponse(ns, 0) ts.WriteString(">") case Out: ts.WriteString("StreamOut<") writeStreamRequest(ns, 0) ts.WriteString(", ") writeStreamResponse(ns, 0) ts.WriteString(">") } } else if rpc.ResponseSchema != nil { if ts.sharedTypes { fmt.Fprintf(ts, "ResponseType", rpcImportName(rpc)) } else { ts.writeTyp(ns, rpc.ResponseSchema, 0) } } else if isRaw { ts.WriteString("globalThis.Response") } else { ts.WriteString("void") } ts.WriteString("> {\n") if isStream { if err := ts.streamCallSite(ts.newIdentWriter(numIndent+1), rpc, rpcPath.String(), direction); err != nil { return errors.Wrapf(err, "unable to write streaming RPC call site for %ss.%s", rpc.ServiceName, rpc.Name) } } else { if err := ts.rpcCallSite(ns, ts.newIdentWriter(numIndent+1), rpc, rpcPath.String()); err != nil { return errors.Wrapf(err, "unable to write RPC call site for %s.%s", rpc.ServiceName, rpc.Name) } } indent() ts.WriteString("}\n") } numIndent-- indent() ts.WriteString("}\n}\n\n") return nil } func (ts *typescript) streamCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string, direction streamDirection) error { headers := "" query := "" if rpc.HandshakeSchema != nil { encs, err := encoding.DescribeRequest(ts.md, rpc.HandshakeSchema, &encoding.Options{SrcNameTag: "json"}, "GET") if err != nil { return errors.Wrapf(err, "stream %s", rpc.Name) } handshakeEnc := encs[0] if len(handshakeEnc.HeaderParameters) > 0 || len(handshakeEnc.QueryParameters) > 0 { w.WriteString("// Convert our params into the objects we need for the request\n") } // Generate the headers if len(handshakeEnc.HeaderParameters) > 0 { headers = "headers" dict := make(map[string]string) for _, field := range handshakeEnc.HeaderParameters { ref := ts.Dot("params", field.SrcName) dict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("const headers = makeRecord(") ts.Values(w, dict) w.WriteString(")\n\n") } // Generate the query string if len(handshakeEnc.QueryParameters) > 0 { query = "query" dict := make(map[string]string) for _, field := range handshakeEnc.QueryParameters { if list := field.Type.GetList(); list != nil { dot := ts.Dot("params", field.SrcName) if field.Optional || ts.isRecursive(field.Type) { dot += "?" } dict[field.WireFormat] = dot + ".map((v) => " + ts.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = ts.convertBuiltinToString( field.Type.GetBuiltin(), ts.Dot("params", field.SrcName), field.Optional, ) } } w.WriteString("const query = makeRecord(") ts.Values(w, dict) w.WriteString(")\n\n") } } // Build the call to createStream var method string switch direction { case InOut: method = "createStreamInOut" case In: method = "createStreamIn" case Out: method = "createStreamOut" } createStream := fmt.Sprintf( "this.baseClient.%s(`%s`", method, rpcPath, ) if headers != "" || query != "" { createStream += ", {" + headers if headers != "" && query != "" { createStream += ", " } if query != "" { createStream += query } createStream += "}" } createStream += ")" w.WriteStringf("return await %s\n", createStream) return nil } func (ts *typescript) rpcCallSite(ns string, w *indentWriter, rpc *meta.RPC, rpcPath string) error { // Work out how we're going to encode and call this RPC rpcEncoding, err := encoding.DescribeRPC(ts.md, rpc, &encoding.Options{SrcNameTag: "json"}) if err != nil { return errors.Wrapf(err, "rpc %s", rpc.Name) } // Raw end points just pass through the request // and need no further code generation if rpc.Proto == meta.RPC_RAW { if ts.sharedTypes { w.WriteStringf("options.method ||= \"%s\";\n", rpcEncoding.DefaultMethod) } w.WriteString("return this.baseClient.callAPI(") if ts.sharedTypes { w.WriteStringf( "`%s`, options", rpcPath, ) } else { w.WriteStringf( "method, `%s`, body, options", rpcPath, ) } w.WriteString(")\n") return nil } // Work out how we encode the Request Schema headers := "" query := "" body := "" if rpc.RequestSchema != nil { reqEnc := rpcEncoding.DefaultRequestEncoding if len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 { w.WriteString("// Convert our params into the objects we need for the request\n") } // Generate the headers if len(reqEnc.HeaderParameters) > 0 { headers = "headers" dict := make(map[string]string) for _, field := range reqEnc.HeaderParameters { ref := ts.Dot("params", field.SrcName) dict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("const headers = makeRecord(") ts.Values(w, dict) w.WriteString(")\n\n") } // Generate the query string if len(reqEnc.QueryParameters) > 0 { query = "query" dict := make(map[string]string) for _, field := range reqEnc.QueryParameters { if list := field.Type.GetList(); list != nil { dot := ts.Dot("params", field.SrcName) if field.Optional || ts.isRecursive(field.Type) { dot += "?" } dict[field.WireFormat] = dot + ".map((v) => " + ts.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = ts.convertBuiltinToString( field.Type.GetBuiltin(), ts.Dot("params", field.SrcName), field.Optional, ) } } w.WriteString("const query = makeRecord(") ts.Values(w, dict) w.WriteString(")\n\n") } // Generate the body if len(reqEnc.BodyParameters) > 0 { if len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 && (!ts.sharedTypes || !hasPathParams(rpc)) { // In the simple case we can just encode the params as the body directly body = "JSON.stringify(params)" } else { // Else we need a new struct called "body" body = "JSON.stringify(body)" dict := make(map[string]string) for _, field := range reqEnc.BodyParameters { dict[field.WireFormat] = ts.Dot("params", field.SrcName) } w.WriteString("// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\nconst body: Record = ") ts.Values(w, dict) w.WriteString("\n\n") } } } // Build the call to callTypedAPI callAPI := "this.baseClient.callTypedAPI(" if !ts.sharedTypes { callAPI += fmt.Sprintf("\"%s\", ", rpcEncoding.DefaultMethod) } callAPI += fmt.Sprintf("`%s`", rpcPath) if body != "" || headers != "" || query != "" || ts.sharedTypes { if body == "" { body = "undefined" } if !ts.sharedTypes { callAPI += ", " + body } if headers != "" || query != "" || ts.sharedTypes { callAPI += ", {" + headers if headers != "" && query != "" { callAPI += ", " } if query != "" { callAPI += query } if ts.sharedTypes { if headers != "" || query != "" { callAPI += ", " } callAPI += fmt.Sprintf(`method: "%s", body: %s`, rpcEncoding.DefaultMethod, body) } callAPI += "}" } } callAPI += ")" // If there's no response schema, we can just return the call to the API directly if rpc.ResponseSchema == nil { w.WriteStringf("await %s\n", callAPI) return nil } w.WriteStringf("// Now make the actual call to the API\nconst resp = await %s\n", callAPI) respEnc := rpcEncoding.ResponseEncoding // If we don't need to do anything with the body, we can just return the response if len(respEnc.HeaderParameters) == 0 { if ts.sharedTypes { w.WriteString("return JSON.parse(await resp.text(), dateReviver) as ") fmt.Fprintf(ts, "ResponseType", rpcImportName(rpc)) } else { w.WriteString("return await resp.json() as ") ts.writeTyp(ns, rpc.ResponseSchema, 0) } w.WriteString("\n") return nil } // Otherwise, we need to add the header fields to the response w.WriteString("\n//Populate the return object from the JSON body and received headers\n") if ts.sharedTypes { w.WriteStringf("const rtn = JSON.parse(await resp.text(), dateReviver) as ResponseType", rpcImportName(rpc)) } else { w.WriteString("const rtn = await resp.json() as ") ts.writeTyp(ns, rpc.ResponseSchema, 0) } w.WriteString("\n") for _, headerField := range respEnc.HeaderParameters { isSetCookie := strings.ToLower(headerField.WireFormat) == "set-cookie" if isSetCookie { w.WriteString("// Skip set-cookie header in browser context as browsers doesn't have access to read it\n") w.WriteString("if (!BROWSER) {\n") w = w.Indent() } ts.seenHeaderResponse = true fieldValue := fmt.Sprintf("mustBeSet(\"Header `%s`\", resp.headers.get(\"%s\"))", headerField.WireFormat, headerField.WireFormat) w.WriteStringf("%s = %s\n", ts.Dot("rtn", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue)) if isSetCookie { w = w.Dedent() w.WriteString("}\n") } } w.WriteString("return rtn\n") return nil } // nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Typescript keyword func (ts *typescript) nonReservedId(id string) string { switch id { // our reserved keywords (or ID's we use within the generated client functions) case "params", "headers", "query", "body", "resp", "rtn": return "_" + id // Typescript & Javascript keywords case "abstract", "any", "arguments", "as", "async", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "constructor", "continue", "debugger", "declare", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", "finally", "float", "for", "from", "function", "get", "goto", "if", "implements", "import", "in", "instanceof", "interface", "let", "long", "module", "namespace", "native", "new", "null", "number", "of", "package", "private", "protected", "public", "require", "return", "set", "short", "static", "string", "super", "switch", "symbol", "synchronized", "this", "throw", "throws", "transient", "true", "try", "type", "typeof", "var", "void", "volatile", "while", "with", "yield": return "_" + id default: return id } } func (ts *typescript) writeNamespace(ns string) { decls := ts.typs.Decls(ns) if len(decls) == 0 { return } fmt.Fprintf(ts, "export namespace %s {\n", ts.typeName(ns)) sort.Slice(decls, func(i, j int) bool { return decls[i].Name < decls[j].Name }) for i, d := range decls { if i > 0 { ts.WriteString("\n") } ts.writeDeclDef(ns, d) } ts.WriteString("}\n\n") } func (ts *typescript) writeDeclDef(ns string, decl *schema.Decl) { if decl.Doc != "" { scanner := bufio.NewScanner(strings.NewReader(decl.Doc)) ts.WriteString(" /**\n") for scanner.Scan() { ts.WriteString(" * ") ts.WriteString(scanner.Text()) ts.WriteByte('\n') } ts.WriteString(" */\n") } var typeParams strings.Builder if len(decl.TypeParams) > 0 { typeParams.WriteRune('<') for i, typeParam := range decl.TypeParams { if i > 0 { typeParams.WriteString(", ") } typeParams.WriteString(typeParam.Name) } typeParams.WriteRune('>') } // If it's a struct type, expose it as an interface; // other types should be type aliases. if st := decl.Type.GetStruct(); st != nil { fmt.Fprintf(ts, " export interface %s%s ", ts.typeName(decl.Name), typeParams.String()) } else { fmt.Fprintf(ts, " export type %s%s = ", ts.typeName(decl.Name), typeParams.String()) } prev := ts.currDecl ts.currDecl = decl ts.writeTyp(ns, decl.Type, 1) ts.currDecl = prev ts.WriteString("\n") } func (ts *typescript) writeStreamClasses() { if ts.sharedTypes { ts.WriteString(` import { StreamInOutHandlerFn, StreamInHandlerFn, StreamOutHandlerFn, } from "encore.dev/api"; type StreamRequest = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Req : never; type StreamResponse = Type extends | StreamInOutHandlerFn | StreamInHandlerFn | StreamOutHandlerFn ? Resp : never; `) } parse := "JSON.parse(event.data)" if ts.sharedTypes { parse = "JSON.parse(event.data, dateReviver)" } send := ` async send(msg: Request) { if (this.socket.ws.readyState === WebSocket.CONNECTING) { // await that the socket is opened await new Promise((resolve) => { this.socket.ws.addEventListener("open", resolve, { once: true }); }); } return this.socket.ws.send(JSON.stringify(msg)); }` receive := ` async next(): Promise { for await (const next of this) return next; return undefined; } async *[Symbol.asyncIterator](): AsyncGenerator { while (true) { if (this.buffer.length > 0) { yield this.buffer.shift() as Response; } else { if (this.socket.ws.readyState === WebSocket.CLOSED) return; await this.socket.hasUpdate(); } } }` ts.WriteString(` function encodeWebSocketHeaders(headers: Record) { // url safe, no pad const base64encoded = btoa(JSON.stringify(headers)) .replaceAll("=", "") .replaceAll("+", "-") .replaceAll("/", "_"); return "encore.dev.headers." + base64encoded; } class WebSocketConnection { public ws: WebSocket; private hasUpdateHandlers: (() => void)[] = []; constructor(url: string, headers?: Record) { let protocols = ["encore-ws"]; if (headers) { protocols.push(encodeWebSocketHeaders(headers)) } this.ws = new WebSocket(url, protocols) this.on("error", () => { this.resolveHasUpdateHandlers(); }); this.on("close", () => { this.resolveHasUpdateHandlers(); }); } resolveHasUpdateHandlers() { const handlers = this.hasUpdateHandlers; this.hasUpdateHandlers = []; for (const handler of handlers) { handler() } } async hasUpdate() { // await until a new message have been received, or the socket is closed await new Promise((resolve) => { this.hasUpdateHandlers.push(() => resolve(null)) }); } on(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.addEventListener(type, handler); } off(type: "error" | "close" | "message" | "open", handler: (event: any) => void) { this.ws.removeEventListener(type, handler); } close() { this.ws.close(); } } export class StreamInOut { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(` + parse + `); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } ` + send + ` ` + receive + ` } export class StreamIn { public socket: WebSocketConnection; private buffer: Response[] = []; constructor(url: string, headers?: Record) { this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { this.buffer.push(` + parse + `); this.socket.resolveHasUpdateHandlers(); }); } close() { this.socket.close(); } ` + receive + ` } export class StreamOut { public socket: WebSocketConnection; private responseValue: Promise; constructor(url: string, headers?: Record) { let responseResolver: (_: any) => void; this.responseValue = new Promise((resolve) => responseResolver = resolve); this.socket = new WebSocketConnection(url, headers); this.socket.on("message", (event: any) => { responseResolver(` + parse + `) }); } async response(): Promise { return this.responseValue; } close() { this.socket.close(); } ` + send + ` }`) } func (ts *typescript) writeClient(set clientgentypes.ServiceSet) { w := ts.newIdentWriter(0) w.WriteStringf(` /** * BaseURL is the base URL for calling the Encore application's API. */ export type BaseURL = string export const Local: BaseURL = "http://localhost:4000" /** * Environment returns a BaseURL for calling the cloud environment with the given name. */ export function Environment(name: string): BaseURL { return `+"`https://${name}-"+ts.appSlug+".encr.app`"+` } /** * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number. */ export function PreviewEnv(pr: number | string): BaseURL { return Environment(`+"`pr${pr}`"+`) } const BROWSER = typeof globalThis === "object" && ("window" in globalThis); /** * Client is an API client for the `+ts.appSlug+` Encore application. */ export %sclass Client { `, func() string { if ts.clientTarget != "" { return "" } return "default " }()) { w := w.Indent() for _, svc := range ts.md.Svcs { if hasPublicRPC(svc) && set.Has(svc.Name) { w.WriteStringf("public readonly %s: %s.ServiceClient\n", ts.memberName(svc.Name), ts.typeName(svc.Name)) } } w.WriteString("private readonly options: ClientOptions\n") w.WriteString("private readonly target: string\n") w.WriteString("\n") // Only include the deprecated constructor if bearer token authentication is being used if ts.hasAuth && !ts.authIsComplexType { w.WriteString(` /** * @deprecated This constructor is deprecated, and you should move to using BaseURL with an Options object */ constructor(target: string, token?: string) `) } w.WriteString(` /** * Creates a Client for calling the public and authenticated APIs of your Encore application. * * @param target The target which the client should be configured to use. See Local and Environment for options. * @param options Options for the client */ constructor(target: BaseURL, options?: ClientOptions)`) if ts.hasAuth && !ts.authIsComplexType { w.WriteString("\nconstructor(target: string | BaseURL = \"prod\", options?: string | ClientOptions) {\n") { w := w.Indent() w.WriteString(` // Convert the old constructor parameters to a BaseURL object and a ClientOptions object if (!target.startsWith("http://") && !target.startsWith("https://")) { target = Environment(target) } if (typeof options === "string") { options = { auth: options } } `) } } else { w.WriteString(" {\n") } { w := w.Indent() w.WriteString("this.target = target\n") w.WriteString("this.options = options ?? {}\n") w.WriteString("const base = new BaseClient(this.target, this.options)\n") for _, svc := range ts.md.Svcs { if hasPublicRPC(svc) && set.Has(svc.Name) { w.WriteStringf("this.%s = new %s.ServiceClient(base)\n", ts.memberName(svc.Name), ts.typeName(svc.Name)) } } } w.WriteString("}\n") } w.WriteString(` /** * Creates a new Encore client with the given client options set. * * @param options Client options to set. They are merged with existing options. **/ public with(options: ClientOptions): Client { return new Client(this.target, { ...this.options, ...options, }) } `) w.WriteString("}\n") handler := ts.md.AuthHandler if ts.hasAuth && ts.sharedTypes && ts.authIsComplexType { ts.WriteString(` /** * Import the auth handler to be able to derive the auth type */ `) fmt.Fprintf(ts, `import type { %s as %s } from "~backend/%s/%s";`, handler.Name, ts.authImportName(), handler.Loc.PkgName, strings.TrimSuffix(handler.Loc.Filename, filepath.Ext(handler.Loc.Filename))) ts.WriteString("\n") } w.WriteString(` /** * ClientOptions allows you to override any default behaviour within the generated Encore client. */ export interface ClientOptions { /** * By default the client will use the inbuilt fetch function for making the API requests. * however you can override it with your own implementation here if you want to run custom * code on each API request made or response received. */ fetcher?: Fetcher /** Default RequestInit to be used for the client */ requestInit?: Omit & { headers?: Record } `) if ts.hasAuth { if !ts.authIsComplexType { w.WriteString(` /** * Allows you to set the auth token to be used for each request * either by passing in a static token string or by passing in a function * which returns the auth token. * * These tokens will be sent as bearer tokens in the Authorization header. */ `) } else { w.WriteString(` /** * Allows you to set the authentication data to be used for each * request either by passing in a static object or by passing in * a function which returns a new object for each request. */ `) } w.WriteString(" auth?: ") ts.writeAuthType() w.WriteString(" | AuthDataGenerator\n") } w.WriteString(`} `) } func (ts *typescript) writeBaseClient(appSlug string) error { userAgent := fmt.Sprintf("%s-Generated-TS-Client (Encore/%s)", appSlug, version.Version) reqOmit := `"method" | "body" | "headers"` if ts.sharedTypes { reqOmit = `"headers"` } fmt.Fprintf(ts, ` // CallParameters is the type of the parameters to a method call, but require headers to be a Record type type CallParameters = Omit & { /** Headers to be sent with the request */ headers?: Record /** Query parameters to be sent with the request */ query?: Record } `, reqOmit) if ts.hasAuth { ts.WriteString(` // AuthDataGenerator is a function that returns a new instance of the authentication data required by this API export type AuthDataGenerator = () => | `) ts.writeAuthType() ts.WriteString(` | Promise<`) ts.writeAuthType() ts.WriteString(` | undefined> | undefined;`) } ts.WriteString(` // A fetcher is the prototype for the inbuilt Fetch function export type Fetcher = typeof fetch; const boundFetch = fetch.bind(this); class BaseClient { readonly baseURL: string readonly fetcher: Fetcher readonly headers: Record readonly requestInit: Omit & { headers?: Record }`) if ts.hasAuth { ts.WriteString("\n readonly authGenerator?: AuthDataGenerator") } ts.WriteString(` constructor(baseURL: string, options: ClientOptions) { this.baseURL = baseURL this.headers = {} // Add User-Agent header if the script is running in the server // because browsers do not allow setting User-Agent headers to requests if (!BROWSER) { this.headers["User-Agent"] = "` + userAgent + `"; } this.requestInit = options.requestInit ?? {}; // Setup what fetch function we'll be using in the base client if (options.fetcher !== undefined) { this.fetcher = options.fetcher } else { this.fetcher = boundFetch }`) if ts.hasAuth { ts.WriteString(` // Setup an authentication data generator using the auth data token option if (options.auth !== undefined) { const auth = options.auth if (typeof auth === "function") { this.authGenerator = auth } else { this.authGenerator = () => auth } }`) } ts.WriteString(` } async getAuthData(): Promise {`) if ts.hasAuth { ts.WriteString(` let authData: `) ts.writeAuthType() ts.WriteString(` | undefined; // If authorization data generator is present, call it and add the returned data to the request if (this.authGenerator) { const mayBePromise = this.authGenerator(); if (mayBePromise instanceof Promise) { authData = await mayBePromise; } else { authData = mayBePromise; } } if (authData) { const data: CallParameters = {}; `) w := ts.newIdentWriter(3) if ts.authIsComplexType { authData, err := encoding.DescribeAuth(ts.md, ts.md.AuthHandler.Params, &encoding.Options{SrcNameTag: "json"}) if err != nil { return errors.Wrap(err, "unable to describe auth data") } // Generate the query string if len(authData.QueryParameters) > 0 { dict := make(map[string]string) for _, field := range authData.QueryParameters { if list := field.Type.GetList(); list != nil { dot := ts.Dot("authData", field.SrcName) if field.Optional || ts.isRecursive(field.Type) { dot += "?" } dict[field.WireFormat] = dot + ".map((v) => " + ts.convertBuiltinToString(list.Elem.GetBuiltin(), "v", false) + ")" } else { dict[field.WireFormat] = ts.convertBuiltinToString( field.Type.GetBuiltin(), ts.Dot("authData", field.SrcName), field.Optional, ) } } w.WriteString("data.query = makeRecord(") ts.Values(w, dict) w.WriteString(");\n") } // Generate the headers if len(authData.HeaderParameters) > 0 { dict := make(map[string]string) for _, field := range authData.HeaderParameters { ref := ts.Dot("authData", field.SrcName) dict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional) } w.WriteString("data.headers = makeRecord(") ts.Values(w, dict) w.WriteString(");\n") } } else { w.WriteString("data.headers = {};\n") w.WriteString("data.headers[\"Authorization\"] = \"Bearer \" + authData;\n") } w.WriteString("\nreturn data;\n") w.Dedent().WriteString("}\n") } ts.WriteString(` return undefined; } `) ts.WriteString(` // createStreamInOut sets up a stream to a streaming API endpoint. async createStreamInOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamInOut(this.baseURL + path + queryString, headers); } // createStreamIn sets up a stream to a streaming API endpoint. async createStreamIn(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamIn(this.baseURL + path + queryString, headers); } // createStreamOut sets up a stream to a streaming API endpoint. async createStreamOut(path: string, params?: CallParameters): Promise> { let { query, headers } = params ?? {}; // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { headers = {...headers, ...authData.headers}; } } const queryString = query ? '?' + encodeQuery(query) : '' return new StreamOut(this.baseURL + path + queryString, headers); } `) callParams := "method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters" callAPIParams := "method, path, body" initParams := ` method, body: body ?? null,` if ts.sharedTypes { callParams = "path: string, params?: CallParameters" callAPIParams = "path" initParams = "" } fmt.Fprintf(ts, ` // callTypedAPI makes an API call, defaulting content type to "application/json" public async callTypedAPI(%s): Promise { return this.callAPI(%s, { ...params, headers: { "Content-Type": "application/json", ...params?.headers } }); } `, callParams, callAPIParams) fmt.Fprintf(ts, ` // callAPI is used by each generated API method to actually make the request public async callAPI(%s): Promise { let { query, headers, ...rest } = params ?? {} const init = { ...this.requestInit, ...rest,%s } `, callParams, initParams) ts.WriteString(` // Merge our headers with any predefined headers init.headers = {...this.headers, ...init.headers, ...headers} // Fetch auth data if there is any const authData = await this.getAuthData(); // If we now have authentication data, add it to the request if (authData) { if (authData.query) { query = {...query, ...authData.query}; } if (authData.headers) { init.headers = {...init.headers, ...authData.headers}; } } // Make the actual request const queryString = query ? '?' + encodeQuery(query) : '' const response = await this.fetcher(this.baseURL+path+queryString, init) // handle any error responses if (!response.ok) { // try and get the error message from the response body let body: APIErrorResponse = { code: ErrCode.Unknown, message: ` + "`request failed: status ${response.status}`" + ` } // if we can get the structured error we should, otherwise give a best effort try { const text = await response.text() try { const jsonBody = JSON.parse(text) if (isAPIErrorResponse(jsonBody)) { body = jsonBody } else { body.message += ": " + JSON.stringify(jsonBody) } } catch { body.message += ": " + text } } catch (e) { // otherwise we just append the text to the error message body.message += ": " + String(e) } throw new APIError(response.status, body) } return response } }`) return nil } func (ts *typescript) writeExtraTypes() { if ts.seenJSON { ts.WriteString(`// JSONValue represents an arbitrary JSON value. export type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue} `) } if ts.sharedTypes { ts.WriteString(` type PickMethods = Omit & { method?: Type }; // Helper type to omit all fields that are cookies. type OmitCookie = { [K in keyof T as T[K] extends CookieWithOptions ? never : K]: T[K]; }; type RequestType any> = Parameters extends [infer H, ...any[]] ? OmitCookie : void; type ResponseType any> = OmitCookie>>; function dateReviver(key: string, value: any): any { if ( typeof value === "string" && value.length >= 10 && value.charCodeAt(0) >= 48 && // '0' value.charCodeAt(0) <= 57 // '9' ) { const parsedDate = new Date(value); if (!isNaN(parsedDate.getTime())) { return parsedDate; } } return value; } `) } ts.WriteString(` function encodeQuery(parts: Record): string { const pairs: string[] = [] for (const key in parts) { const val = (Array.isArray(parts[key]) ? parts[key] : [parts[key]]) as string[] for (const v of val) { pairs.push(` + "`" + `${key}=${encodeURIComponent(v)}` + "`" + `) } } return pairs.join("&") } // makeRecord takes a record and strips any undefined values from it, // and returns the same record with a narrower type. // @ts-ignore - TS ignore because makeRecord is not always used function makeRecord(record: Record): Record { for (const key in record) { if (record[key] === undefined) { delete record[key] } } return record as Record } `) if ts.seenHeaderResponse { ts.WriteString(` // mustBeSet will throw an APIError with the Data Loss code if value is null or undefined function mustBeSet(field: string, value: A | null | undefined): A { if (value === null || value === undefined) { throw new APIError( 500, { code: ErrCode.DataLoss, message: ` + "`${field} was unexpectedly ${value}`" + `, // ${value} will create the string "null" or "undefined" }, ) } return value } `) } } func (ts *typescript) writeDecl(ns string, decl *schema.Decl) { if decl.Loc.PkgName != ns { ts.WriteString(ts.typeName(decl.Loc.PkgName) + ".") } ts.WriteString(ts.typeName(decl.Name)) } func (ts *typescript) writeDecl2(buf *bytes.Buffer, ns string, decl *schema.Decl) { if decl.Loc.PkgName != ns { buf.WriteString(ts.typeName(decl.Loc.PkgName) + ".") } buf.WriteString(ts.typeName(decl.Name)) } func (ts *typescript) builtinType(typ schema.Builtin) string { switch typ { case schema.Builtin_ANY: return "any" case schema.Builtin_BOOL: return "boolean" case schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64, schema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64, schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: return "number" case schema.Builtin_STRING: return "string" case schema.Builtin_BYTES: return "string" // TODO case schema.Builtin_TIME: return "string" // TODO case schema.Builtin_JSON: ts.seenJSON = true return "JSONValue" case schema.Builtin_UUID: return "string" case schema.Builtin_USER_ID: return "string" case schema.Builtin_DECIMAL: return "string" default: ts.errorf("unknown builtin type %v", typ) return "any" } } func (ts *typescript) convertBuiltinToString(typ schema.Builtin, val string, isOptional bool) string { var code string switch typ { case schema.Builtin_STRING: return val case schema.Builtin_JSON: code = fmt.Sprintf("JSON.stringify(%s)", val) case schema.Builtin_TIME: if ts.sharedTypes { // If we're using shared types then this will actually be a Date object. // Otherwise it will be a string. code = fmt.Sprintf("%s.toISOString()", val) } else { code = fmt.Sprintf("String(%s)", val) } default: code = fmt.Sprintf("String(%s)", val) } if isOptional { code = fmt.Sprintf("%s === undefined ? undefined : %s", val, code) } return code } func (ts *typescript) convertStringToBuiltin(typ schema.Builtin, val string) string { switch typ { case schema.Builtin_ANY: return val case schema.Builtin_BOOL: return fmt.Sprintf("%s.toLowerCase() === \"true\"", val) case schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64, schema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64: return fmt.Sprintf("parseInt(%s, 10)", val) case schema.Builtin_FLOAT32, schema.Builtin_FLOAT64: return fmt.Sprintf("Number(%s)", val) case schema.Builtin_STRING: return val case schema.Builtin_BYTES: return val case schema.Builtin_TIME: return val case schema.Builtin_JSON: ts.seenJSON = true return fmt.Sprintf("JSON.parse(%s)", val) case schema.Builtin_UUID: return val case schema.Builtin_USER_ID: return val default: ts.errorf("unknown builtin type %v", typ) return "any" } } func (ts *typescript) writeTyp(ns string, typ *schema.Type, numIndents int) { var buf strings.Builder ts.renderTyp(ts.Buffer, ns, typ, numIndents) ts.WriteString(buf.String()) } func (ts *typescript) renderTyp(buf *bytes.Buffer, ns string, tt *schema.Type, numIndents int) { switch typ := tt.Typ.(type) { case *schema.Type_Named: decl := ts.md.Decls[typ.Named.Id] ts.writeDecl2(buf, ns, decl) // Write the type arguments if len(typ.Named.TypeArguments) > 0 { buf.WriteRune('<') for i, typeArg := range typ.Named.TypeArguments { if i > 0 { buf.WriteString(", ") } ts.renderTyp(buf, ns, typeArg, 0) } buf.WriteRune('>') } case *schema.Type_List: // Determine if we need parens by counting the number of union elements. var unionBuf bytes.Buffer numCases := ts.renderUnionTypes(&unionBuf, ns, typ.List.Elem, numIndents) paren := numCases > 1 if paren { buf.WriteString("(") } buf.Write(unionBuf.Bytes()) if paren { buf.WriteString(")") } buf.WriteString("[]") case *schema.Type_Map: buf.WriteString("{ [key: ") ts.renderTyp(buf, ns, typ.Map.Key, numIndents) buf.WriteString("]: ") ts.renderTyp(buf, ns, typ.Map.Value, numIndents) buf.WriteString(" }") case *schema.Type_Builtin: buf.WriteString(ts.builtinType(typ.Builtin)) case *schema.Type_Literal: switch lit := typ.Literal.Value.(type) { case *schema.Literal_Str: buf.WriteString(ts.Quote(lit.Str)) case *schema.Literal_Int: buf.WriteString(strconv.FormatInt(lit.Int, 10)) case *schema.Literal_Float: buf.WriteString(strconv.FormatFloat(lit.Float, 'f', -1, 64)) case *schema.Literal_Boolean: buf.WriteString(strconv.FormatBool(lit.Boolean)) case *schema.Literal_Null: buf.WriteString("null") default: ts.errorf("unknown literal type %T", lit) } case *schema.Type_Pointer: ts.renderUnionTypes(buf, ns, tt, numIndents) case *schema.Type_Option: ts.renderUnionTypes(buf, ns, tt, numIndents) case *schema.Type_Union: ts.renderUnionTypes(buf, ns, tt, numIndents) case *schema.Type_Struct: indent := func() { buf.WriteString(strings.Repeat(" ", numIndents+1)) } // Filter the fields to print based on struct tags. fields := make([]*schema.Field, 0, len(typ.Struct.Fields)) for _, f := range typ.Struct.Fields { // skip cookie fields as they are handled by the browser if f.Wire.GetCookie() != nil { continue } if encoding.IgnoreField(f) { continue } fields = append(fields, f) } buf.WriteString("{\n") for i, field := range fields { if field.Doc != "" { scanner := bufio.NewScanner(strings.NewReader(field.Doc)) indent() buf.WriteString("/**\n") for scanner.Scan() { indent() buf.WriteString(" * ") buf.WriteString(scanner.Text()) buf.WriteByte('\n') } indent() buf.WriteString(" */\n") } indent() buf.WriteString(ts.QuoteIfRequired(ts.fieldNameInStruct(field))) if field.Optional || ts.isRecursive(field.Typ) { buf.WriteString("?") } buf.WriteString(": ") ts.renderTyp(buf, ns, field.Typ, numIndents+1) buf.WriteString("\n") // Add another empty line if we have a doc comment // and this was not the last field. if field.Doc != "" && i < len(fields)-1 { buf.WriteByte('\n') } } buf.WriteString(strings.Repeat(" ", numIndents)) buf.WriteByte('}') case *schema.Type_TypeParameter: decl := ts.md.Decls[typ.TypeParameter.DeclId] typeParam := decl.TypeParams[typ.TypeParameter.ParamIdx] buf.WriteString(typeParam.Name) case *schema.Type_Config: // Config type is transparent ts.renderTyp(buf, ns, typ.Config.Elem, numIndents) default: ts.errorf("unknown type %+v", reflect.TypeOf(typ)) } } func (ts *typescript) renderUnionTypes(buf *bytes.Buffer, ns string, typ *schema.Type, numIndents int) (renderedCases int) { cases := ts.getUnionCases(typ) seenCases := make(map[string]bool) for i, caseType := range cases { var caseBuf bytes.Buffer ts.renderTyp(&caseBuf, ns, caseType, numIndents) caseStr := caseBuf.String() if seenCases[caseStr] { continue } seenCases[caseStr] = true renderedCases++ if i > 0 { buf.WriteString(" | ") } buf.WriteString(caseStr) } return renderedCases } func (ts *typescript) getUnionCases(typ *schema.Type) []*schema.Type { null := &schema.Type{ Typ: &schema.Type_Literal{ Literal: &schema.Literal{ Value: &schema.Literal_Null{Null: true}, }, }, } switch tt := typ.Typ.(type) { case *schema.Type_Union: return slices.Clone(tt.Union.Types) case *schema.Type_Option: return append(ts.getUnionCases(tt.Option.Value), null) case *schema.Type_Pointer: // We do not treat pointers as nullable, as we have the Option type for that. // This makes a lot of APIs nicer to use. return ts.getUnionCases(tt.Pointer.Base) default: return []*schema.Type{typ} } } type bailout struct{ err error } func (ts *typescript) errorf(format string, args ...interface{}) { panic(bailout{fmt.Errorf(format, args...)}) } func (ts *typescript) handleBailout(dst *error) { if err := recover(); err != nil { if bail, ok := err.(bailout); ok { *dst = bail.err } else { panic(err) } } } func (ts *typescript) newIdentWriter(indent int) *indentWriter { return &indentWriter{ w: ts.Buffer, depth: indent, indent: " ", firstWriteOnLine: true, } } func (ts *typescript) Quote(s string) string { return fmt.Sprintf("\"%s\"", strings.Replace(s, "\"", "\\\"", -1)) } func (ts *typescript) QuoteIfRequired(s string) string { // If the identifier isn't purely alphanumeric, we need to add quotes. if !stringIsOnly(s, func(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }) { return ts.Quote(s) } return s } // Dot allows us to reference a field in a struct by its name. func (ts *typescript) Dot(structIdent string, fieldIdent string) string { fieldIdent = ts.QuoteIfRequired(fieldIdent) if len(fieldIdent) > 0 && fieldIdent[0] == '"' { return fmt.Sprintf("%s[%s]", structIdent, fieldIdent) } else { return fmt.Sprintf("%s.%s", structIdent, fieldIdent) } } func (ts *typescript) Values(w *indentWriter, dict map[string]string) { // Work out the largest key length. largestKey := 0 keys := make([]string, 0, len(dict)) for key := range dict { keys = append(keys, key) key = ts.QuoteIfRequired(key) if len(key) > largestKey { largestKey = len(key) } } sort.Strings(keys) w.WriteString("{\n") { w := w.Indent() for _, key := range keys { ident := ts.QuoteIfRequired(key) w.WriteStringf("%s: %s%s,\n", ident, strings.Repeat(" ", largestKey-len(ident)), dict[key]) } } w.WriteString("}") } // Regex to replace invalid characters (anything that isn't a letter, number, `_`, or `$`) var invalidChars = regexp.MustCompile(`[^\p{L}\p{N}$_]`) // validTSIdentifier converts a string into a valid TypeScript identifier. func validTSIdentifier(input string) string { if input == "" { return "_" } runes := []rune(input) // Ensure the first character is a valid TS identifier start (letter, `_`, or `$`) if !unicode.IsLetter(runes[0]) && runes[0] != '_' && runes[0] != '$' { runes[0] = '_' } output := invalidChars.ReplaceAllString(string(runes), "_") return output } func (ts *typescript) typeName(identifier string) string { if ts.generatorVersion < TsExperimental { return validTSIdentifier(identifier) } else { return idents.Convert(identifier, idents.PascalCase) } } func (ts *typescript) memberName(identifier string) string { if ts.generatorVersion < TsExperimental { return validTSIdentifier(identifier) } else { return idents.Convert(identifier, idents.CamelCase) } } func (ts *typescript) fieldNameInStruct(field *schema.Field) string { name := field.Name if field.JsonName != "" { name = field.JsonName } return name } func (ts *typescript) isRecursive(typ *schema.Type) bool { if ts.currDecl == nil { return false } // Treat recursively seen types as if they are optional recursiveType := false if n := typ.GetNamed(); n != nil { recursiveType = ts.typs.IsRecursiveRef(ts.currDecl.Id, n.Id) } return recursiveType } func (ts *typescript) writeCustomErrorType() { w := ts.newIdentWriter(0) w.WriteString(` /** * APIErrorDetails represents the response from an Encore API in the case of an error */ interface APIErrorResponse { code: ErrCode message: string details?: any } function isAPIErrorResponse(err: any): err is APIErrorResponse { return ( err !== undefined && err !== null && isErrCode(err.code) && typeof(err.message) === "string" && (err.details === undefined || err.details === null || typeof(err.details) === "object") ) } function isErrCode(code: any): code is ErrCode { return code !== undefined && Object.values(ErrCode).includes(code) } /** * APIError represents a structured error as returned from an Encore application. */ export class APIError extends Error { /** * The HTTP status code associated with the error. */ public readonly status: number /** * The Encore error code */ public readonly code: ErrCode /** * The error details */ public readonly details?: any constructor(status: number, response: APIErrorResponse) { // extending errors causes issues after you construct them, unless you apply the following fixes super(response.message); // set error name as constructor name, make it not enumerable to keep native Error behavior // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors Object.defineProperty(this, 'name', { value: 'APIError', enumerable: false, configurable: true, }) // fix the prototype chain if ((Object as any).setPrototypeOf == undefined) { (this as any).__proto__ = APIError.prototype } else { Object.setPrototypeOf(this, APIError.prototype); } // capture a stack trace if ((Error as any).captureStackTrace !== undefined) { (Error as any).captureStackTrace(this, this.constructor); } this.status = status this.code = response.code this.details = response.details } } /** * Typeguard allowing use of an APIError's fields' */ export function isAPIError(err: any): err is APIError { return err instanceof APIError; } export enum ErrCode { /** * OK indicates the operation was successful. */ OK = "ok", /** * Canceled indicates the operation was canceled (typically by the caller). * * Encore will generate this error code when cancellation is requested. */ Canceled = "canceled", /** * Unknown error. An example of where this error may be returned is * if a Status value received from another address space belongs to * an error-space that is not known in this address space. Also * errors raised by APIs that do not return enough error information * may be converted to this error. * * Encore will generate this error code in the above two mentioned cases. */ Unknown = "unknown", /** * InvalidArgument indicates client specified an invalid argument. * Note that this differs from FailedPrecondition. It indicates arguments * that are problematic regardless of the state of the system * (e.g., a malformed file name). * * This error code will not be generated by the gRPC framework. */ InvalidArgument = "invalid_argument", /** * DeadlineExceeded means operation expired before completion. * For operations that change the state of the system, this error may be * returned even if the operation has completed successfully. For * example, a successful response from a server could have been delayed * long enough for the deadline to expire. * * The gRPC framework will generate this error code when the deadline is * exceeded. */ DeadlineExceeded = "deadline_exceeded", /** * NotFound means some requested entity (e.g., file or directory) was * not found. * * This error code will not be generated by the gRPC framework. */ NotFound = "not_found", /** * AlreadyExists means an attempt to create an entity failed because one * already exists. * * This error code will not be generated by the gRPC framework. */ AlreadyExists = "already_exists", /** * PermissionDenied indicates the caller does not have permission to * execute the specified operation. It must not be used for rejections * caused by exhausting some resource (use ResourceExhausted * instead for those errors). It must not be * used if the caller cannot be identified (use Unauthenticated * instead for those errors). * * This error code will not be generated by the gRPC core framework, * but expect authentication middleware to use it. */ PermissionDenied = "permission_denied", /** * ResourceExhausted indicates some resource has been exhausted, perhaps * a per-user quota, or perhaps the entire file system is out of space. * * This error code will be generated by the gRPC framework in * out-of-memory and server overload situations, or when a message is * larger than the configured maximum size. */ ResourceExhausted = "resource_exhausted", /** * FailedPrecondition indicates operation was rejected because the * system is not in a state required for the operation's execution. * For example, directory to be deleted may be non-empty, an rmdir * operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FailedPrecondition, Aborted, and Unavailable: * (a) Use Unavailable if the client can retry just the failing call. * (b) Use Aborted if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FailedPrecondition if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FailedPrecondition * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FailedPrecondition if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. * * This error code will not be generated by the gRPC framework. */ FailedPrecondition = "failed_precondition", /** * Aborted indicates the operation was aborted, typically due to a * concurrency issue like sequencer check failures, transaction aborts, * etc. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. */ Aborted = "aborted", /** * OutOfRange means operation was attempted past the valid range. * E.g., seeking or reading past end of file. * * Unlike InvalidArgument, this error indicates a problem that may * be fixed if the system state changes. For example, a 32-bit file * system will generate InvalidArgument if asked to read at an * offset that is not in the range [0,2^32-1], but it will generate * OutOfRange if asked to read from an offset past the current * file size. * * There is a fair bit of overlap between FailedPrecondition and * OutOfRange. We recommend using OutOfRange (the more specific * error) when it applies so that callers who are iterating through * a space can easily look for an OutOfRange error to detect when * they are done. * * This error code will not be generated by the gRPC framework. */ OutOfRange = "out_of_range", /** * Unimplemented indicates operation is not implemented or not * supported/enabled in this service. * * This error code will be generated by the gRPC framework. Most * commonly, you will see this error code when a method implementation * is missing on the server. It can also be generated for unknown * compression algorithms or a disagreement as to whether an RPC should * be streaming. */ Unimplemented = "unimplemented", /** * Internal errors. Means some invariants expected by underlying * system has been broken. If you see one of these errors, * something is very broken. * * This error code will be generated by the gRPC framework in several * internal error conditions. */ Internal = "internal", /** * Unavailable indicates the service is currently unavailable. * This is a most likely a transient condition and may be corrected * by retrying with a backoff. Note that it is not always safe to retry * non-idempotent operations. * * See litmus test above for deciding between FailedPrecondition, * Aborted, and Unavailable. * * This error code will be generated by the gRPC framework during * abrupt shutdown of a server process or network connection. */ Unavailable = "unavailable", /** * DataLoss indicates unrecoverable data loss or corruption. * * This error code will not be generated by the gRPC framework. */ DataLoss = "data_loss", /** * Unauthenticated indicates the request does not have valid * authentication credentials for the operation. * * The gRPC framework will generate this error code when the * authentication metadata is invalid or a Credentials callback fails, * but also expect authentication middleware to generate it. */ Unauthenticated = "unauthenticated", } `) } func stringIsOnly(str string, predicate func(r rune) bool) bool { for _, r := range str { if !predicate(r) { return false } } return true } ================================================ FILE: pkg/clientgen/utils.go ================================================ package clientgen import ( "bytes" "fmt" "strings" "encr.dev/internal/version" meta "encr.dev/proto/encore/parser/meta/v1" ) func doNotEditHeader() string { return fmt.Sprintf("Code generated by the Encore %s client generator. DO NOT EDIT.", version.Version) } func hasPublicRPC(svc *meta.Service) bool { for _, rpc := range svc.Rpcs { if rpc.AccessType != meta.RPC_PRIVATE { return true } } return false } type indentWriter struct { w *bytes.Buffer depth int indent string firstWriteOnLine bool } func (w *indentWriter) Indent() *indentWriter { return &indentWriter{ w: w.w, depth: w.depth + 1, indent: w.indent, firstWriteOnLine: true, } } func (w *indentWriter) Dedent() *indentWriter { return &indentWriter{ w: w.w, depth: max(w.depth-1, 0), indent: w.indent, firstWriteOnLine: true, } } func (w *indentWriter) WriteString(s string) { parts := strings.Split(s, "\n") for i, part := range parts { if i > 0 { w.w.WriteString("\n") w.firstWriteOnLine = true } if part == "" { continue } if w.firstWriteOnLine { w.w.WriteString(strings.Repeat(w.indent, w.depth)) w.firstWriteOnLine = false } w.w.WriteString(part) } } func (w *indentWriter) WriteStringf(s string, args ...interface{}) { w.WriteString(fmt.Sprintf(s, args...)) } ================================================ FILE: pkg/cueutil/build.go ================================================ package cueutil import ( "bytes" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/build" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/load" "cuelang.org/go/cue/parser" "cuelang.org/go/cue/token" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" "encr.dev/pkg/eerror" "encr.dev/pkg/errinsrc/srcerrors" "encr.dev/pkg/fns" ) // LoadFromFS takes a given filesystem object and the app-relative path to the service's root package // and loads the full configuration needed for that service. func LoadFromFS(filesys fs.FS, serviceRelPath string, meta *Meta) (cue.Value, error) { // Work out of a temporary directory tmpPath, err := os.MkdirTemp("", "encr-cfg-") if err != nil { return cue.Value{}, eerror.Wrap(err, "config", "unable to create temporary directory", nil) } defer func() { _ = os.RemoveAll(tmpPath) }() // Write the FS to the file system err = writeFSToPath(filesys, tmpPath) if err != nil { return cue.Value{}, err } // Find all config files for the service configFilesForService, err := allFilesUnder(filesys, serviceRelPath) if err != nil { return cue.Value{}, eerror.Wrap(err, "config", "unable to list all config files for service", map[string]any{"path": serviceRelPath}) } // If there are no config files, return an empty value if len(configFilesForService) == 0 { return cue.Value{}, nil } // Tell CUE to load all the files loaderCfg := &load.Config{ Dir: tmpPath, Tools: true, Tags: meta.ToTags(), } pkgs := load.Instances(configFilesForService, loaderCfg) for _, pkg := range pkgs { if pkg.Err != nil { return cue.Value{}, srcerrors.UnableToLoadCUEInstances(pkg.Err, tmpPath) } // Non CUE files may be orphaned (JSON/YAML), so need to be parsed into the CUE AST and added to the package. if err := addOrphanedFiles(pkg); err != nil { return cue.Value{}, srcerrors.UnableToAddOrphanedCUEFiles(err, tmpPath) } } // Build the CUE values ctx := cuecontext.New() values, err := ctx.BuildInstances(pkgs) if err != nil { return cue.Value{}, srcerrors.UnableToLoadCUEInstances(err, tmpPath) } if len(values) == 0 { return cue.Value{}, eerror.New("config", "no values generated from config", nil) } // Unify all returned values into a single value // Note; to get all errors in the CUE files, we want to wait until // the validate output to check for errors rtnValue := values[0] for _, value := range values { rtnValue = rtnValue.Unify(value) } // Validate the unified value is concrete if err := rtnValue.Validate(cue.Concrete(true)); err != nil { return cue.Value{}, srcerrors.CUEEvaluationFailed(err, tmpPath) } return rtnValue, nil } // allFilesUnder returns all files under the given path in the given filesystem. func allFilesUnder(filesys fs.FS, path string) ([]string, error) { var files []string // Check if the path exists // and if it doesn't that means there zero CUE files for this service // this can happen when a Config struct has zero fields if _, err := fs.Stat(filesys, path); err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, nil } return nil, err } err := fs.WalkDir(filesys, path, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } if !info.IsDir() { files = append(files, path) } return nil }) if err != nil { return nil, err } return files, nil } // writeFSToPath writes the contents of the given filesystem to a temporary directory on the local filesystem. func writeFSToPath(fsys fs.FS, targetPath string) error { // Copy the files into the temporary directory err := fs.WalkDir(fsys, ".", func(path string, info fs.DirEntry, err error) error { if err != nil { return eerror.Wrap(err, "config", "unable to walk VFS", nil) } // Convert the io/fs slash-based path to a filepath. osPath := filepath.FromSlash(path) if !info.IsDir() { // Open the source file from our filesystem srcFile, err := fsys.Open(path) if err != nil { return eerror.Wrap(err, "config", "unable to open src file", nil) } defer fns.CloseIgnore(srcFile) dstFile, err := os.OpenFile(filepath.Join(targetPath, osPath), os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return eerror.Wrap(err, "config", "unable to open dst file", nil) } _, err = io.Copy(dstFile, srcFile) if err2 := dstFile.Close(); err == nil { err = err2 } if err != nil { return eerror.Wrap(err, "config", "unable to copy file", nil) } } else { if err := os.Mkdir(filepath.Join(targetPath, osPath), 0755); err != nil && !errors.Is(err, os.ErrExist) { return eerror.Wrap(err, "config", "unable to make dir", nil) } } return nil }) if err != nil { return eerror.Wrap(err, "config", "unable to write config files to temporary directory", map[string]any{"path": targetPath}) } return nil } // addOrphanedFiles adds any orphaned files outside the package to the build instance. This could be CUE, YAML or JSON files // // The majority of the code in the function is taken directly from the CUE source code as the code is currently only accessible // from internal paths - they are planning to move this out of the internal path so non-CUE code can directly call it // as library functions. (Src: cue/internal/encoding/encoding.go : NewDecoder()) func addOrphanedFiles(i *build.Instance) (err error) { for _, f := range i.OrphanedFiles { var file ast.Node rc, err := reader(f) if err != nil { return err } //goland:noinspection GoDeferInLoop defer func() { _ = rc.Close() }() t := unicode.BOMOverride(unicode.UTF8.NewDecoder()) r := transform.NewReader(rc, t) switch f.Encoding { case build.CUE: file, err = parser.ParseFile(f.Filename, r, parser.ParseComments) if err != nil { return err } default: return errors.New(fmt.Sprintf("unsupported encoding: %s", f.Encoding)) } if err != nil { return err } if err := i.AddSyntax(toFile(file)); err != nil { return err } } return nil } // toFile converts an ast.Node to a *ast.File. (from the CUE source code) func toFile(n ast.Node) *ast.File { switch x := n.(type) { case nil: return nil case *ast.StructLit: return &ast.File{Decls: x.Elts} case ast.Expr: ast.SetRelPos(x, token.NoSpace) return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}} case *ast.File: return x default: panic(fmt.Sprintf("Unsupported node type %T", x)) } } // reader returns a reader for the given file. (from the CUE source code) func reader(f *build.File) (io.ReadCloser, error) { switch s := f.Source.(type) { case nil: // Use the file name. case string: return io.NopCloser(strings.NewReader(s)), nil case []byte: return io.NopCloser(bytes.NewReader(s)), nil case *bytes.Buffer: // is io.Reader, but it needs to be readable repeatedly if s != nil { return io.NopCloser(bytes.NewReader(s.Bytes())), nil } default: return nil, fmt.Errorf("invalid source type %T", f.Source) } return os.Open(f.Filename) } ================================================ FILE: pkg/cueutil/types.go ================================================ package cueutil type EnvType string const ( EnvType_Production EnvType = "production" EnvType_Development EnvType = "development" EnvType_Ephemeral EnvType = "ephemeral" EnvType_Test EnvType = "test" ) type CloudType string const ( CloudType_AWS CloudType = "aws" CloudType_Azure CloudType = "azure" CloudType_GCP CloudType = "gcp" CloudType_Encore CloudType = "encore" CloudType_Local CloudType = "local" ) type Meta struct { APIBaseURL string EnvName string EnvType EnvType CloudType CloudType } func (m *Meta) ToTags() []string { if m == nil { return nil } return []string{ tag("APIBaseURL", m.APIBaseURL), tag("EnvName", m.EnvName), tag("EnvType", m.EnvType), tag("CloudType", m.CloudType), } } func tag[T ~string](name string, value T) string { return name + "=" + string(value) } ================================================ FILE: pkg/dockerbuild/dockerbuild.go ================================================ package dockerbuild import ( "context" "encoding/json" "io" "net/http" "os" "path/filepath" "slices" "strings" "time" "github.com/cockroachdb/errors" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/rs/zerolog/log" "encr.dev/pkg/option" ) // DefaultCACertsPath is the default path for where to write CA Certs. // From https://go.dev/src/crypto/x509/root_linux.go. const DefaultCACertsPath ImagePath = "/etc/ssl/certs/ca-certificates.crt" type ImageBuildConfig struct { // The time to use when recording times in the image. BuildTime time.Time // AddCACerts, if set, specifies where in the image to mount the CA certificates. // If set to Some(""), defaults to DefaultCACertsPath. AddCACerts option.Option[ImagePath] // SupervisorPath is the path to the supervisor binary to use. // It must be set if the image includes the supervisor. SupervisorPath option.Option[HostPath] // BaseImageOverride overrides the base image to use. // If None it resolves the image from the spec using ResolveRemoteImage. BaseImageOverride option.Option[v1.Image] // A URL to a http proxy used to fetch images DockerOptions []remote.Option } // BuildImage builds a docker image from the given spec. func BuildImage(ctx context.Context, spec *ImageSpec, cfg ImageBuildConfig) (v1.Image, error) { options := append(cfg.DockerOptions, remote.WithPlatform(v1.Platform{ OS: spec.OS, Architecture: spec.Arch, }), ) baseImg, err := resolveBaseImage(ctx, spec.DockerBaseImage, cfg.BaseImageOverride, options...) if err != nil { return nil, errors.Wrap(err, "resolve base image") } opener, err := buildImageFilesystem(ctx, spec, &cfg) if err != nil { return nil, errors.Wrap(err, "build image fs") } layer, err := tarball.LayerFromOpener(opener, tarball.WithCompressionLevel(5), // balance speed and compression ) if err != nil { return nil, errors.Wrap(err, "create tarball layer") } log.Info().Msg("adding layer to base image") img, err := mutate.Append(baseImg, mutate.Addendum{ Layer: layer, History: v1.History{ Author: "encore-app", Created: v1.Time{Time: cfg.BuildTime}, CreatedBy: "encore.dev", Comment: "Built with encore.dev, the backend development engine", }, }) if err != nil { return nil, errors.Wrap(err, "add layer") } // Copy the base image's environment variables. baseCfg, err := baseImg.ConfigFile() if err != nil { return nil, errors.Wrap(err, "get base image config") } envs := newEnvMap(baseCfg.Config.Env) imgCfg, err := img.ConfigFile() if err != nil { return nil, errors.Wrap(err, "get image config") } imgCfg = imgCfg.DeepCopy() // Add the image spec's environment. envs.Update(spec.Env) imgCfg.Config.Entrypoint = spec.Entrypoint imgCfg.Config.Cmd = nil imgCfg.Config.Env = envs.ToSlice() imgCfg.Config.WorkingDir = string(spec.WorkingDir) imgCfg.Author = "encore.dev" imgCfg.Created = v1.Time{Time: cfg.BuildTime} imgCfg.Architecture = spec.Arch imgCfg.OS = spec.OS img, err = mutate.ConfigFile(img, imgCfg) if err != nil { return nil, errors.Wrap(err, "add config") } return img, nil } // ResolveRemoteImage resolves the base image with the given reference. // If imageRef is the empty string or "scratch" it resolves to the empty image. func ResolveRemoteImage(ctx context.Context, imageRef string, options ...remote.Option) (v1.Image, error) { if imageRef == "" || imageRef == "scratch" { return empty.Image, nil } // Try to get it from the daemon if it exists. baseImgRef, err := name.ParseReference(imageRef) if err != nil { return nil, errors.Wrap(err, "parse image ref") } img, err := remote.Image(baseImgRef, append(options, remote.WithContext(ctx))...) if err != nil { return nil, errors.Wrap(err, "fetch image") } return img, nil } func resolveBaseImage(ctx context.Context, baseImgTag string, overrideBaseImage option.Option[v1.Image], options ...remote.Option) (v1.Image, error) { if override, ok := overrideBaseImage.Get(); ok { return override, nil } return ResolveRemoteImage(ctx, baseImgTag, options...) } func buildImageFilesystem(ctx context.Context, spec *ImageSpec, cfg *ImageBuildConfig) (opener tarball.Opener, err error) { tc := newTarCopier(setFileTimes(cfg.BuildTime)) // Bundle the source code, if requested. if bundle, ok := spec.BundleSource.Get(); ok { if err := bundleSource(tc, spec, &bundle); err != nil { return nil, err } } // Copy data into the image. if err := tc.CopyData(spec); err != nil { return nil, err } // Copy Encore binaries into the image. if err := setupSupervisor(tc, spec, cfg); err != nil { return nil, err } // Add build information. if err := writeBuildInfo(tc, spec.BuildInfo); err != nil { return nil, err } // Write additional files if err := writeExtraFiles(tc, spec.WriteFiles); err != nil { return nil, err } // Add CA certificates, if requested. if caCertsDest, ok := cfg.AddCACerts.Get(); ok { if caCertsDest == "" { caCertsDest = DefaultCACertsPath } if err := addCACerts(ctx, tc, caCertsDest); err != nil { return nil, errors.Wrap(err, "add ca certs") } } return tc.Opener(), nil } func writeExtraFiles(tc *tarCopier, files map[ImagePath][]byte) error { for path, data := range files { if err := tc.WriteFile(path, 0644, data); err != nil { return errors.Wrap(err, "write image data") } } return nil } func setupSupervisor(tc *tarCopier, spec *ImageSpec, cfg *ImageBuildConfig) error { super, ok := spec.Supervisor.Get() if !ok { return nil } // Add the supervisor binary. { hostPath, ok := cfg.SupervisorPath.Get() if !ok { return errors.New("supervisor requested, but not provided") } fi, err := os.Stat(string(hostPath)) if err != nil { return errors.Wrap(err, "stat supervisor") } if err := tc.MkdirAll(super.MountPath.Dir(), 0755); err != nil { return errors.Wrap(err, "create supervisor dir") } if err := tc.CopyFile(super.MountPath, hostPath, fi, ""); err != nil { return errors.Wrap(err, "copy supervisor") } } // Write the supervisor configuration. { data, err := json.MarshalIndent(super.Config, "", " ") if err != nil { return errors.Wrap(err, "marshal supervisor config") } if err := tc.WriteFile(super.ConfigPath, 0644, data); err != nil { return errors.Wrap(err, "write supervisor config") } } return nil } func bundleSource(tc *tarCopier, spec *ImageSpec, bundle *BundleSourceSpec) error { includes := []HostPath{bundle.Source.Join(filepath.FromSlash(string(bundle.AppRootRelpath)))} for _, ex := range bundle.IncludeSource { includes = append(includes, bundle.Source.Join(string(ex))) } excludes := make(map[HostPath]bool, len(bundle.ExcludeSource)) for _, ex := range bundle.ExcludeSource { absPath := bundle.Source.Join(string(ex)) excludes[absPath] = true } err := tc.CopyDir(&dirCopyDesc{ Spec: spec, SrcPath: bundle.Source, DstPath: bundle.Dest, ExcludeSrcPaths: excludes, IncludeSrcPaths: includes, }) return errors.Wrap(err, "bundle source") } func writeBuildInfo(tc *tarCopier, spec BuildInfoSpec) error { info, err := json.MarshalIndent(spec.Info, "", " ") if err != nil { return errors.Wrap(err, "marshal build info") } err = tc.WriteFile(spec.InfoPath, 0644, info) return errors.Wrap(err, "write build info") } func tryFetch(ctx context.Context, url string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, errors.Wrap(err, "create request") } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, errors.Wrap(err, "get root certs") } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() return nil, errors.Newf("root cert url returned status code: %s", resp.Status) } return resp, nil } func addCACerts(ctx context.Context, tc *tarCopier, dest ImagePath) error { const ( encoreCachedRootCerts = "https://api.encore.cloud/artifacts/build/root-certs" curlCACertStore = "https://curl.se/ca/cacert.pem" ) var ( resp *http.Response err error ) for _, url := range []string{encoreCachedRootCerts, curlCACertStore} { resp, err = tryFetch(ctx, url) if err == nil { break } log.Warn().Err(err).Msgf("failed to fetch root certs from: %s", url) } if err != nil { return errors.Wrap(err, "failed to fetch cert file") } defer func() { _ = resp.Body.Close() }() data, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "read cert data") } // Add the file err = tc.WriteFile(dest, 0644, data) return err } type envMap map[string]string func (m envMap) Update(envs []string) { for _, e := range envs { key, value, _ := strings.Cut(e, "=") m[key] = value } } func (m envMap) ToSlice() []string { envs := make([]string, 0, len(m)) for k, v := range m { envs = append(envs, k+"="+v) } slices.Sort(envs) return envs } func newEnvMap(envs []string) envMap { m := make(envMap, len(envs)) for _, e := range envs { key, value, _ := strings.Cut(e, "=") m[key] = value } return m } ================================================ FILE: pkg/dockerbuild/dockerbuild_test.go ================================================ package dockerbuild import ( "context" "os" "path/filepath" "testing" "time" qt "github.com/frankban/quicktest" "encr.dev/pkg/builder" "encr.dev/pkg/option" "encr.dev/pkg/paths" meta "encr.dev/proto/encore/parser/meta/v1" ) func TestBuildImage(t *testing.T) { c := qt.New(t) artifacts := paths.FS(c.TempDir()) writeFiles(c, artifacts, map[string]string{ "entrypoint": "echo hello", "package.json": `{"name": "package/name"}`, "node_modules/foo": "foo", }) runtimes := paths.FS(c.TempDir()) writeFiles(c, runtimes, map[string]string{ "js/encore-runtime.node": "node runtime", "js/encore.dev/package.json": `{"name": "encore.dev"}`, }) cfg := DescribeConfig{ Meta: &meta.Data{}, Runtimes: HostPath(runtimes), BundleSource: option.Some(BundleSourceSpec{ Source: HostPath(artifacts), Dest: "/workspace", AppRootRelpath: ".", }), Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.JSBuildOutput{ ArtifactDir: artifacts, Entrypoints: []builder.Entrypoint{{ Cmd: builder.CmdSpec{ Command: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, PrioritizedFiles: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, }, Services: []string{"foo", "bar"}, Gateways: []string{"baz", "qux"}, UseRuntimeConfigV2: true, }}, }, }}, } spec, err := describe(cfg) c.Assert(err, qt.IsNil) encoreBinaries := paths.FS(c.TempDir()) writeFiles(c, encoreBinaries, map[string]string{ "supervisor.bin": "supervisor", }) ctx := context.Background() buildTime := time.Unix(1234567890, 0) img, err := BuildImage(ctx, spec, ImageBuildConfig{ BuildTime: buildTime, SupervisorPath: option.Some(HostPath(encoreBinaries.Join("supervisor.bin"))), }) c.Assert(err, qt.IsNil) _, err = img.Digest() c.Assert(err, qt.IsNil) // Note: this digest changes depending on the machine it's being built on // c.Assert(digest.String(), qt.Equals, "sha256:6e0032a1560c506901bbc1bb291d7655639d242f5ca09d5e119876830e34813d") } func writeFiles(c *qt.C, dir paths.FS, files map[string]string) { for name, content := range files { c.Assert(filepath.IsLocal(name), qt.IsTrue) path := string(dir.Join(name)) err := os.MkdirAll(filepath.Dir(path), 0755) c.Assert(err, qt.IsNil) err = os.WriteFile(path, []byte(content), 0755) c.Assert(err, qt.IsNil) } } ================================================ FILE: pkg/dockerbuild/features.go ================================================ package dockerbuild type FeatureFlag string const ( NewRuntimeConfig FeatureFlag = "new_runtime_config" ) ================================================ FILE: pkg/dockerbuild/manifest.go ================================================ package dockerbuild type ImageManifestFile struct { Manifests []*ImageManifest } type ImageManifest struct { // The Docker Image reference, e.g. "gcr.io/my-project/my-image:sha256:abcdef..." Reference string // Services and gateways bundled in this image. BundledServices []string BundledGateways []string // FeatureFlags captures feature flags enabled for this image. FeatureFlags map[FeatureFlag]bool } ================================================ FILE: pkg/dockerbuild/spec.go ================================================ package dockerbuild import ( "fmt" "os" pathpkg "path" "path/filepath" "runtime" "slices" strconv "strconv" "strings" "github.com/cockroachdb/errors" "github.com/golang/protobuf/proto" "github.com/rs/xid" "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/pkg/noopgateway" "encr.dev/pkg/noopgwdesc" "encr.dev/pkg/option" "encr.dev/pkg/paths" "encr.dev/pkg/supervisor" meta "encr.dev/proto/encore/parser/meta/v1" ) type ImageSpecFile struct { Images []*ImageSpec } // ImageSpec is a specification for how to build a docker image. type ImageSpec struct { // The operating system to use for the image. OS string // The architecture to use for the image. Arch string // The entrypoint to use for the image. It must be non-empty. // The first entry is the executable path, and the rest are the arguments. Entrypoint []string // Environment variables to set for the entrypoint. Env []string // The working dir to use for executing the entrypoint. WorkingDir ImagePath // BuildInfo contains information about the build. BuildInfo BuildInfoSpec // A map from the builder filesystem paths to the destination path in the image. // If the source is a directory, it will be copied recursively. CopyData map[ImagePath]HostPath // A set of files which should be written to the image. WriteFiles map[ImagePath][]byte // Whether to bundle source into the image. // It's handled separately from CopyData since we apply some filtering // on what's copied, like excluding .git directories and other build artifacts. BundleSource option.Option[BundleSourceSpec] // Supervisor specifies the supervisor configuration. Supervisor option.Option[SupervisorSpec] // The names of services bundled in this image. BundledServices []string // The names of gateways bundled in this image. BundledGateways []string // The docker base image to use. If None it defaults to the empty scratch image. DockerBaseImage string // StargzPrioritizedFiles are file paths in the image that should be prioritized for // stargz compression, allowing for faster streaming of those files. StargzPrioritizedFiles []ImagePath // FeatureFlags specifies feature flags enabled for this image. FeatureFlags map[FeatureFlag]bool } type BuildInfoSpec struct { // The build info to include in the image. Info BuildInfo // The path in the image where the build info is written, as a JSON file. InfoPath ImagePath } type BuildInfo struct { // The version of Encore with which the app was compiled. // This is string is for informational use only, and its format should not be relied on. EncoreCompiler string // AppCommit describes the commit of the app. AppCommit CommitInfo } type CommitInfo struct { Revision string Uncommitted bool } type BundleSourceSpec struct { Source HostPath Dest ImagePath // Source paths to exclude from copying, relative to Source. ExcludeSource []RelPath // Path to app root, will be included, relative to Source. AppRootRelpath RelPath // Extra source paths to include when bundling, relative to Source. IncludeSource []RelPath } type SupervisorSpec struct { // Where to mount the supervisor binary in the image. MountPath ImagePath // Where to write the supervisor configuration in the image. ConfigPath ImagePath // The config to pass to the supervisor. Config *supervisor.Config } type DescribeConfig struct { // The parsed metadata. Meta *meta.Data // The compile result. Compile *builder.CompileResult // The directory containing the runtimes. Runtimes HostPath // The path to the node runtime, if any. NodeRuntime option.Option[HostPath] // The docker base image to use, if any. If None it defaults to the empty scratch image. DockerBaseImage option.Option[string] // BundleSource specifies whether to bundle source into the image, // and where the source is located on the host filesystem. BundleSource option.Option[BundleSourceSpec] // WorkingDir specifies the working directory to start the docker image in. WorkingDir option.Option[ImagePath] // BuildInfo contains information about the build. BuildInfo BuildInfo // ProcessPerService specifies whether to run each service in a separate process. ProcessPerService bool } type ( // HostPath is a path on the host filesystem. HostPath string // ImagePath is a path in the docker image. ImagePath string // RelPath is a relative path. RelPath string ) func (i ImagePath) Dir() ImagePath { return ImagePath(pathpkg.Dir(string(i))) } func (i ImagePath) Clean() ImagePath { return ImagePath(pathpkg.Clean(string(i))) } func (i ImagePath) String() string { return string(i) } func (i ImagePath) Join(p ...string) ImagePath { return ImagePath(pathpkg.Join(string(i), pathpkg.Join(p...))) } func (i ImagePath) JoinImage(p ImagePath) ImagePath { return i.Join(string(p)) } func (h HostPath) Dir() HostPath { return HostPath(filepath.Dir(string(h))) } func (h HostPath) Join(p ...string) HostPath { return HostPath(filepath.Join(string(h), filepath.Join(p...))) } func (h HostPath) JoinHost(p HostPath) HostPath { return h.Join(string(p)) } func (h HostPath) ToImage() ImagePath { return ImagePath(string(h.ToUnix())) } func (h HostPath) ToUnix() HostPath { if runtime.GOOS == "windows" { // convert windows path with volume to a unix path, i.e c:\some\path -> /c/some/path volume := filepath.VolumeName(string(h)) if len(volume) == 2 && volume[1] == ':' { return HostPath("/" + string(volume[0]) + filepath.ToSlash(string(h[2:]))) } } return HostPath(filepath.ToSlash(string(h))) } func (h HostPath) String() string { return string(h) } func (h HostPath) Rel(target HostPath) (HostPath, error) { rel, err := filepath.Rel(string(h), string(target)) return HostPath(rel), err } func (h HostPath) IsAbs() bool { return filepath.IsAbs(h.String()) } // Describe describes the docker image to build. func Describe(cfg DescribeConfig) (*ImageSpec, error) { return newImageSpecBuilder().Describe(cfg) } func newImageSpecBuilder() *imageSpecBuilder { return &imageSpecBuilder{ procIDGen: randomProcID, spec: &ImageSpec{ CopyData: make(map[ImagePath]HostPath), WriteFiles: map[ImagePath][]byte{}, FeatureFlags: make(map[FeatureFlag]bool), BundledGateways: []string{}, BundledServices: []string{}, }, seenArtifactDirs: make(map[HostPath]*imageArtifactDir), seenPrioFiles: make(map[ImagePath]bool), } } type imageArtifactDir struct { Base ImagePath BuildArtifacts ImagePath } type imageSpecBuilder struct { spec *ImageSpec // procIDGen generates a random id for each process. // Defaults to randomProcID. procIDGen func() string // The artifact dirs we've already seen, to avoid // duplicate copies into the image. seenArtifactDirs map[HostPath]*imageArtifactDir seenPrioFiles map[ImagePath]bool } const ( // defaultSupervisorMountPath is the path in the image where the supervisor is mounted. defaultSupervisorMountPath ImagePath = "/encore/bin/supervisor" // defaultSupervisorConfigPath is the path in the image where the supervisor config is located. defaultSupervisorConfigPath ImagePath = "/encore/supervisor.config.json" // defaultBuildInfoPath is the path in the image where the build information is located. defaultBuildInfoPath ImagePath = "/encore/build-info.json" // defaultMetaPath is the path in the image where the application metadata is located. defaultMetaPath ImagePath = "/encore/meta" ) func (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) { // Allocate artifact directories for each output. for _, out := range cfg.Compile.Outputs { b.allocArtifactDir(cfg, out) } // Determine if we should use the supervisor. // We must use the supervisor if we have more than one service or gateway. useSupervisor := cfg.ProcessPerService || len(cfg.Compile.Outputs) > 1 || len(cfg.Compile.Outputs[0].GetEntrypoints()) > 1 if !useSupervisor { ep := cfg.Compile.Outputs[0].GetEntrypoints()[0] out := cfg.Compile.Outputs[0] imageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())] if !ok { return nil, errors.Errorf("missing image artifact dir for %q", out.GetArtifactDir()) } cmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts)) b.spec.Entrypoint = cmd.Command b.spec.Env = cmd.Env } else { config := &supervisor.Config{ NoopGateways: make(map[string]*noopgateway.Description), } super := SupervisorSpec{ MountPath: defaultSupervisorMountPath, ConfigPath: defaultSupervisorConfigPath, Config: config, } seenGateways := make(map[string]bool) for _, out := range cfg.Compile.Outputs { imageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())] if !ok { return nil, errors.Errorf("missing image artifact dir for %q", out.GetArtifactDir()) } for _, ep := range out.GetEntrypoints() { cmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts)) proc := supervisor.Proc{ ID: b.procIDGen(), Command: cmd.Command, Env: cmd.Env, Services: slices.Clone(ep.Services), Gateways: slices.Clone(ep.Gateways), } slices.Sort(proc.Services) slices.Sort(proc.Gateways) for _, gw := range ep.Gateways { seenGateways[gw] = true } config.Procs = append(config.Procs, proc) } } // We need all gateways to be provided by some docker image. But for now, since we only support // a single docker image, we need all gateways to be provided by this image. // Each gateway that's not hosted by this image should be provided by a noop-gateway. if cfg.Meta != nil { // nil check for backwards compatibility for _, gw := range cfg.Meta.Gateways { if !seenGateways[gw.EncoreName] { config.NoopGateways[gw.EncoreName] = noopgwdesc.Describe(cfg.Meta, nil) } } } b.addPrio(super.MountPath) b.spec.Supervisor = option.Some(super) b.spec.Entrypoint = []string{string(super.MountPath), "-c", string(super.ConfigPath)} b.spec.Env = nil // not needed by supervisor } // TS apps use runtime config v2. if cfg.Meta.Language == meta.Lang_TYPESCRIPT { b.spec.FeatureFlags[NewRuntimeConfig] = true } // Compute bundled services and gateways. { for _, out := range cfg.Compile.Outputs { for _, ep := range out.GetEntrypoints() { b.spec.BundledServices = append(b.spec.BundledServices, ep.Services...) b.spec.BundledGateways = append(b.spec.BundledGateways, ep.Gateways...) } } // If we have any noop-gateways, consider them bundled, too. if super, ok := b.spec.Supervisor.Get(); ok { for name := range super.Config.NoopGateways { b.spec.BundledGateways = append(b.spec.BundledGateways, name) } } // Sort and deduplicate. slices.Sort(b.spec.BundledServices) b.spec.BundledServices = slices.Compact(b.spec.BundledServices) slices.Sort(b.spec.BundledGateways) b.spec.BundledGateways = slices.Compact(b.spec.BundledGateways) } // Add entrypoint files to prioritized files. for _, out := range cfg.Compile.Outputs { hostArtifacts := HostPath(out.GetArtifactDir()) imageArtifacts, ok := b.seenArtifactDirs[hostArtifacts] if !ok { return nil, errors.Errorf("missing image artifact dir for %q", hostArtifacts) } for _, ep := range out.GetEntrypoints() { // For each entrypoint, add prioritized files. files := ep.Cmd.PrioritizedFiles.Expand(paths.FS(imageArtifacts.BuildArtifacts)) for _, file := range files { b.addPrio(ImagePath(file)) } } } // If we have any JS outputs that need the local runtime, copy it into the image. { for _, out := range cfg.Compile.Outputs { if _, ok := out.(*builder.JSBuildOutput); ok { if nativeRuntimeHost, ok := cfg.NodeRuntime.Get(); ok { // Add the encore-runtime.node file, and set the environment variable to point to it. nativeRuntimeImg := ImagePath("/encore/runtimes/js/encore-runtime.node") b.spec.CopyData[nativeRuntimeImg] = nativeRuntimeHost b.spec.Env = append(b.spec.Env, fmt.Sprintf("ENCORE_RUNTIME_LIB=%s", nativeRuntimeImg)) b.addPrio(nativeRuntimeImg) // Copy the encore.dev package. nativePackageHost := cfg.Runtimes.Join("js", "encore.dev") nativePackageImg := ImagePath("/encore/runtimes/js/encore.dev") b.spec.CopyData[nativePackageImg] = nativePackageHost } else { // Copy the whole js runtime. runtimeHost := cfg.Runtimes.Join("js") runtimeImg := ImagePath("/encore/runtimes/js") b.spec.CopyData[runtimeImg] = runtimeHost nativeRuntimeImg := runtimeImg.Join("encore-runtime.node") b.spec.Env = append(b.spec.Env, fmt.Sprintf("ENCORE_RUNTIME_LIB=%s", nativeRuntimeImg)) b.addPrio(nativeRuntimeImg) } break } } } b.spec.DockerBaseImage = cfg.DockerBaseImage.GetOrElse("scratch") b.spec.BundleSource = cfg.BundleSource b.spec.WorkingDir = cfg.WorkingDir.GetOrElse("/") b.spec.OS = cfg.Compile.OS b.spec.Arch = cfg.Compile.Arch // Include build information. b.spec.BuildInfo = BuildInfoSpec{ Info: cfg.BuildInfo, InfoPath: defaultBuildInfoPath, } // Include the app metadata. { md, err := proto.Marshal(cfg.Meta) if err != nil { return nil, errors.Wrap(err, "marshal meta") } b.spec.WriteFiles[defaultMetaPath] = md } return b.spec, nil } func (b *imageSpecBuilder) addPrio(path ImagePath) { if !b.seenPrioFiles[path] { b.seenPrioFiles[path] = true b.spec.StargzPrioritizedFiles = append(b.spec.StargzPrioritizedFiles, path) } } func (b *imageSpecBuilder) allocArtifactDir(cfg DescribeConfig, out builder.BuildOutput) *imageArtifactDir { hostArtifacts := HostPath(out.GetArtifactDir()) if s := b.seenArtifactDirs[hostArtifacts]; s != nil { // Already copied this artifact dir. return s } bundle, hasBundle := cfg.BundleSource.Get() _, isJSOutput := out.(*builder.JSBuildOutput) // For TS apps the artifacts will be bundled with the source if isJSOutput && hasBundle { // If hostArtifacts are within the the bundled source, we dont need to copy them. if strings.HasPrefix(string(hostArtifacts), string(bundle.Source)) { relpath, err := filepath.Rel(string(bundle.Source), string(hostArtifacts)) if err != nil { panic(fmt.Sprintf("failed to calculate relative path from %q to %q: %v", bundle.Source, hostArtifacts, err)) } imageBuildArtifactsDir := bundle.Dest.Join(string(filepath.ToSlash(relpath))) artifactDir := &imageArtifactDir{ Base: imageBuildArtifactsDir.Dir(), BuildArtifacts: imageBuildArtifactsDir, } b.seenArtifactDirs[hostArtifacts] = artifactDir return artifactDir } } // This artifact directory has not been copied yet. // Determine a reasonable name for it. basePath := "/artifacts" for i := 0; ; i++ { candidatePath := ImagePath(pathpkg.Join(basePath, strconv.Itoa(i))) candidate := &imageArtifactDir{ Base: candidatePath, BuildArtifacts: candidatePath.Join("build"), } if b.spec.CopyData[candidate.Base] == "" && b.spec.CopyData[candidate.BuildArtifacts] == "" { // This name is available. b.spec.CopyData[candidate.BuildArtifacts] = hostArtifacts b.seenArtifactDirs[hostArtifacts] = candidate return candidate } // This path already exists. Keep trying. } } func randomProcID() string { return fmt.Sprintf("proc_%s", xid.New()) } // DetermineIncludes determines what paths within the workspace should be included in the image. func DetermineIncludes(appLang appfile.Lang, bundleSource bool, workspaceRoot string, appRoot string) ([]RelPath, error) { // if the actual setting is set, include all files from the workspace if bundleSource { return []RelPath{"."}, nil } // app root is always included if workspaceRoot == appRoot { return nil, nil } var extraIncludePaths []RelPath // Include node_modules and package.json from the workspace root if the app is a TypeScript app. if appLang == appfile.LangTS { dir := filepath.Dir(appRoot) for { relPath, err := filepath.Rel(workspaceRoot, dir) if err != nil { return nil, errors.Wrap(err, "relative path from workspace root") } pathsToCheck := []string{"node_modules", "package.json"} for _, path := range pathsToCheck { if _, err := os.Stat(filepath.Join(dir, path)); err == nil { extraIncludePaths = append(extraIncludePaths, RelPath(filepath.Join(relPath, path))) } } if dir == workspaceRoot { break } dir = filepath.Dir(dir) } } // Walk all files and folders in includedPaths and find any symlinks. // Add the symlink target to includedPaths if it is within the workspace root. for _, path := range extraIncludePaths { absPath := filepath.Join(workspaceRoot, string(path)) filepath.Walk(absPath, func(path string, fi os.FileInfo, err error) error { if err != nil { return err } if fi.Mode()&os.ModeSymlink != 0 { target, err := os.Readlink(path) if err != nil { return nil } absTarget := filepath.Join(filepath.Dir(path), filepath.Clean(target)) relTarget, err := filepath.Rel(workspaceRoot, absTarget) if err != nil { return nil } if strings.HasPrefix(absTarget, workspaceRoot) { extraIncludePaths = append(extraIncludePaths, RelPath(relTarget)) } } return nil }) } return extraIncludePaths, nil } ================================================ FILE: pkg/dockerbuild/spec_test.go ================================================ package dockerbuild import ( "testing" qt "github.com/frankban/quicktest" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "encr.dev/pkg/builder" "encr.dev/pkg/option" "encr.dev/pkg/supervisor" meta "encr.dev/proto/encore/parser/meta/v1" ) func TestBuild_Node(t *testing.T) { c := qt.New(t) cfg := DescribeConfig{ Meta: &meta.Data{Language: meta.Lang_TYPESCRIPT}, Runtimes: "/host/runtimes", BundleSource: option.Some(BundleSourceSpec{ Source: "/host", Dest: "/image", AppRootRelpath: ".", }), Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.JSBuildOutput{ ArtifactDir: "/host/artifacts", Entrypoints: []builder.Entrypoint{{ Cmd: builder.CmdSpec{ Command: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, PrioritizedFiles: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, }, Services: []string{"foo", "bar"}, Gateways: []string{"baz", "qux"}, }}, }, }}, } spec, err := describe(cfg) c.Assert(err, qt.IsNil) meta, err := proto.Marshal(cfg.Meta) c.Assert(err, qt.IsNil) opts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...) c.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{ Entrypoint: []string{"/image/artifacts/entrypoint"}, Env: []string{ "ENCORE_RUNTIME_LIB=/encore/runtimes/js/encore-runtime.node", }, WorkingDir: "/", BuildInfo: BuildInfoSpec{InfoPath: defaultBuildInfoPath}, WriteFiles: map[ImagePath][]byte{defaultMetaPath: meta}, CopyData: map[ImagePath]HostPath{ "/encore/runtimes/js": "/host/runtimes/js", }, BundleSource: option.Some(BundleSourceSpec{ Source: "/host", Dest: "/image", AppRootRelpath: ".", ExcludeSource: []RelPath{}, IncludeSource: []RelPath{}, }), Supervisor: option.None[SupervisorSpec](), BundledServices: []string{"bar", "foo"}, BundledGateways: []string{"baz", "qux"}, DockerBaseImage: "scratch", FeatureFlags: map[FeatureFlag]bool{NewRuntimeConfig: true}, StargzPrioritizedFiles: []ImagePath{ "/image/artifacts/entrypoint", "/encore/runtimes/js/encore-runtime.node", }, }) } func TestBuild_Go_SingleBinary(t *testing.T) { c := qt.New(t) cfg := DescribeConfig{ Meta: &meta.Data{}, Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.GoBuildOutput{ ArtifactDir: "/host/artifacts", Entrypoints: []builder.Entrypoint{ { Cmd: builder.CmdSpec{ Command: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, PrioritizedFiles: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, }, Services: []string{"foo", "bar"}, }, }, }, }}, } spec, err := describe(cfg) c.Assert(err, qt.IsNil) opts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...) c.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{ Entrypoint: []string{"/artifacts/0/build/entrypoint"}, Env: nil, WorkingDir: "/", BuildInfo: BuildInfoSpec{InfoPath: defaultBuildInfoPath}, CopyData: map[ImagePath]HostPath{ "/artifacts/0/build": "/host/artifacts", }, BundledServices: []string{"bar", "foo"}, BundleSource: option.Option[BundleSourceSpec]{}, Supervisor: option.None[SupervisorSpec](), DockerBaseImage: "scratch", FeatureFlags: map[FeatureFlag]bool{}, StargzPrioritizedFiles: []ImagePath{ "/artifacts/0/build/entrypoint", }, WriteFiles: map[ImagePath][]byte{ defaultMetaPath: {}, }, }) } func TestBuild_Go_MultiProc(t *testing.T) { c := qt.New(t) cfg := DescribeConfig{ Meta: &meta.Data{Language: meta.Lang_TYPESCRIPT}, Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.GoBuildOutput{ ArtifactDir: "/host/artifacts", Entrypoints: []builder.Entrypoint{ { Cmd: builder.CmdSpec{ Command: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, PrioritizedFiles: builder.ArtifactStrings{"${ARTIFACT_DIR}/entrypoint"}, }, Services: []string{"foo"}, }, { Cmd: builder.CmdSpec{ Command: builder.ArtifactStrings{"${ARTIFACT_DIR}/other-entrypoint"}, PrioritizedFiles: builder.ArtifactStrings{"${ARTIFACT_DIR}/other-entrypoint"}, }, Services: []string{"bar"}, }, }, }, }}, } spec, err := describe(cfg) c.Assert(err, qt.IsNil) meta, err := proto.Marshal(cfg.Meta) c.Assert(err, qt.IsNil) opts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...) c.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{ Entrypoint: []string{"/encore/bin/supervisor", "-c", string(defaultSupervisorConfigPath)}, Env: nil, WorkingDir: "/", BuildInfo: BuildInfoSpec{InfoPath: defaultBuildInfoPath}, CopyData: map[ImagePath]HostPath{ "/artifacts/0/build": "/host/artifacts", }, BundledServices: []string{"bar", "foo"}, BundleSource: option.Option[BundleSourceSpec]{}, Supervisor: option.Some(SupervisorSpec{ MountPath: "/encore/bin/supervisor", ConfigPath: defaultSupervisorConfigPath, Config: &supervisor.Config{ Procs: []supervisor.Proc{ { ID: "proc-id", Command: []string{"/artifacts/0/build/entrypoint"}, Services: []string{"foo"}, Gateways: []string{}, }, { ID: "proc-id", Command: []string{"/artifacts/0/build/other-entrypoint"}, Services: []string{"bar"}, Gateways: []string{}, }, }, }, }), DockerBaseImage: "scratch", FeatureFlags: map[FeatureFlag]bool{NewRuntimeConfig: true}, StargzPrioritizedFiles: []ImagePath{ "/encore/bin/supervisor", "/artifacts/0/build/entrypoint", "/artifacts/0/build/other-entrypoint", }, WriteFiles: map[ImagePath][]byte{ defaultMetaPath: meta, }, }) } // describe is like Describe but mocks the proc id generation // for reproducible tests. func describe(cfg DescribeConfig) (*ImageSpec, error) { b := newImageSpecBuilder() b.procIDGen = func() string { return "proc-id" } return b.Describe(cfg) } ================================================ FILE: pkg/dockerbuild/tarcopy.go ================================================ package dockerbuild import ( "archive/tar" "bytes" "fmt" "io" "io/fs" "os" "path/filepath" "runtime" "sort" "strings" "time" "github.com/cockroachdb/errors" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/rs/zerolog/log" "encr.dev/pkg/option" "encr.dev/pkg/tarstream" "encr.dev/pkg/xos" "encr.dev/v2/compiler/build" ) type tarCopier struct { fileTimes *time.Time entries []*tarEntry seenDirs map[ImagePath]bool } func newTarCopier(opts ...tarCopyOption) *tarCopier { tc := &tarCopier{ seenDirs: make(map[ImagePath]bool), } for _, opt := range opts { opt(tc) } return tc } type tarCopyOption func(*tarCopier) func setFileTimes(t time.Time) tarCopyOption { return func(tc *tarCopier) { tc.fileTimes = &t } } // dirCopyDesc describes how to copy a directory to the tar. type dirCopyDesc struct { Spec *ImageSpec SrcPath HostPath DstPath ImagePath // Src paths to exclude. ExcludeSrcPaths map[HostPath]bool // Src paths to include. IncludeSrcPaths []HostPath } func (tc *tarCopier) CopyData(spec *ImageSpec) error { // Sort the paths by the destination path so that the tar file is deterministic. type pathPair struct { Src HostPath Dest ImagePath } var paths []pathPair for dest, src := range spec.CopyData { paths = append(paths, pathPair{Src: src, Dest: dest}) } sort.Slice(paths, func(i, j int) bool { return paths[i].Dest < paths[j].Dest }) for _, p := range paths { fi, err := os.Stat(string(p.Src)) if err != nil { return errors.Wrap(err, "stat source file") } if err := tc.MkdirAll(p.Dest.Dir(), 0755); err != nil { return errors.Wrap(err, "create dirs") } if fi.IsDir() { err = tc.CopyDir(&dirCopyDesc{ Spec: spec, SrcPath: p.Src, DstPath: p.Dest, ExcludeSrcPaths: nil, IncludeSrcPaths: []HostPath{p.Src}, }) } else { err = tc.CopyFile(p.Dest, p.Src, fi, "") } if err != nil { return errors.Wrap(err, "copy path") } } return nil } // shouldInclude returns true if the path should be included in the tar. func shouldInclude(desc *dirCopyDesc, path HostPath) bool { for _, include := range desc.IncludeSrcPaths { if string(path) == string(include) { return true } if strings.HasPrefix(string(path), string(include)) { return true } if strings.HasPrefix(string(include), string(path)) { return true } } return false } func (tc *tarCopier) CopyDir(desc *dirCopyDesc) error { err := filepath.WalkDir(string(desc.SrcPath), func(pathStr string, d fs.DirEntry, err error) error { if err != nil { return err } path := HostPath(pathStr) // Should we keep this path? if !shouldInclude(desc, path) { if d.IsDir() { return filepath.SkipDir } else { return nil } } // Should we skip this path? if desc.ExcludeSrcPaths[path] { if d.IsDir() { return filepath.SkipDir } else { return nil } } relPath, err := desc.SrcPath.Rel(path) if err != nil { return errors.WithStack(err) } dstPath := desc.DstPath.Join(string(relPath.ToImage())) // If this is a symlink, compute the link target relative to DstPath. var link ImagePath isSymlink := d.Type()&fs.ModeSymlink != 0 if !isSymlink && runtime.GOOS == "windows" { // Check if the file is a junction point on Windows. if isJunction, _ := xos.IsWindowsJunctionPoint(pathStr); isJunction { return errors.Newf("%q is a windows junction point and cannot be copied to a docker image. Use symlinks instead.", pathStr) } } if isSymlink { target, err := os.Readlink(string(path)) if err != nil { return errors.WithStack(err) } link, err = tc.rewriteSymlink(desc, path, HostPath(target)) if err != nil { return errors.WithStack(err) } else if link == "" { // Drop the symlink return nil } } fi, err := d.Info() if err != nil { return errors.WithStack(err) } err = tc.CopyFile(dstPath, path, fi, link) return errors.Wrap(err, "add file") }) return errors.WithStack(err) } // rewriteSymlink rewrites the symlink to the target filesystem. func (tc *tarCopier) rewriteSymlink(desc *dirCopyDesc, path HostPath, linkTarget HostPath) (newTarget ImagePath, err error) { var ( absTarget HostPath relFromSrcPath HostPath ) if linkTarget.IsAbs() { // It's a link to an absolute destination. // Determine its relative path, and see if that lives within the desc.SrcPath. absTarget = linkTarget // On Windows, we can only make a relative link if the source and target are on the same volume. if runtime.GOOS != "windows" || filepath.VolumeName(desc.SrcPath.String()) == filepath.VolumeName(absTarget.String()) { relFromSrcPath, err = desc.SrcPath.Rel(absTarget) if err != nil { return "", err } // If the relative path is local to the SrcPath, allow it. if filepath.IsLocal(relFromSrcPath.String()) { return desc.DstPath.JoinImage(relFromSrcPath.ToImage()), nil } } } else { // We have a relative link target. Determine its absolute destination. // Use path.Dir() since the symlink is relative to its directory, not relative to itself. absTarget = path.Dir().JoinHost(linkTarget) // Determine its relative path, and see if that lives within the desc.SrcPath. relFromSrcPath, err = desc.SrcPath.Rel(absTarget) if err != nil { return "", err } // If the relative path is local to the SrcPath, allow it. if filepath.IsLocal(relFromSrcPath.String()) { return desc.DstPath.JoinImage(relFromSrcPath.ToImage()), nil } } // Otherwise, determine if the absTarget is within some other path being copied. absTargetStr := absTarget.String() for dst, src := range desc.Spec.CopyData { srcStr := src.String() stcStrSep := srcStr + string(filepath.Separator) if absTargetStr == srcStr { return dst, nil } else if suffix, found := strings.CutPrefix(absTargetStr, stcStrSep); found { // It lives within the target. Compute the new target path. return dst.Join(suffix), nil } } log.Debug(). Str("target", linkTarget.String()). Str("rel_target", relFromSrcPath.String()). Str("abs_target", absTarget.String()). Msg("dropping escaping symlink") return "", nil } func (tc *tarCopier) MkdirAll(dstPath ImagePath, mode fs.FileMode) (err error) { dstPath = ImagePath(filepath.ToSlash(dstPath.String())) dstPath = dstPath.Clean() for dstPath != "." && dstPath != "/" { if !tc.seenDirs[dstPath] { modTime := time.Time{} if tc.fileTimes != nil { modTime = *tc.fileTimes } header := &tar.Header{ Typeflag: tar.TypeDir, ModTime: modTime, Name: tarHeaderName(dstPath, true), Mode: int64(mode.Perm()), } tc.entries = append(tc.entries, &tarEntry{ header: header, }) tc.seenDirs[dstPath] = true } dstPath = dstPath.Dir() } return nil } func (tc *tarCopier) CopyFile(dstPath ImagePath, srcPath HostPath, fi fs.FileInfo, linkTarget ImagePath) (err error) { header, err := tar.FileInfoHeader(fi, linkTarget.String()) if err != nil { return err } if tc.fileTimes != nil { t := *tc.fileTimes header.ModTime = t header.AccessTime = t header.ChangeTime = t } // HACK: make the linux binary executable when cross compiling from windows as the unix permissions gets lost. if runtime.GOOS == "windows" && fi.Name() == build.BinaryName { header.Mode = 0755 } header.Name = tarHeaderName(dstPath, fi.IsDir()) entry := &tarEntry{header: header} tc.entries = append(tc.entries, entry) if fi.IsDir() { tc.seenDirs[dstPath] = true return nil } // If this is not a symlink, write the file. if (fi.Mode() & fs.ModeSymlink) != fs.ModeSymlink { entry.hostPath = option.Some(srcPath) } return nil } func (tc *tarCopier) WriteFile(dstPath ImagePath, mode fs.FileMode, data []byte) (err error) { header := &tar.Header{ Name: tarHeaderName(dstPath, false), Typeflag: tar.TypeReg, Mode: int64(mode.Perm()), Size: int64(len(data)), } if tc.fileTimes != nil { t := *tc.fileTimes header.ModTime = t header.AccessTime = t header.ChangeTime = t } tc.entries = append(tc.entries, &tarEntry{ header: header, data: option.Some(data), }) return nil } type tarEntry struct { header *tar.Header data option.Option[[]byte] hostPath option.Option[HostPath] } func (tc *tarCopier) Opener() tarball.Opener { errThunk := func(err error) tarball.Opener { return func() (io.ReadCloser, error) { return nil, err } } var dvecs []tarstream.Datavec for _, e := range tc.entries { // create buffer to write tar header to buf := new(bytes.Buffer) tw := tar.NewWriter(buf) // write tar header to buffer if err := tw.WriteHeader(e.header); err != nil { return errThunk(errors.Wrap(err, fmt.Sprintf("writing header %v", e))) } memv := tarstream.MemVec{ Data: buf.Bytes(), } // add the tar header mem buffer to the tarvec dvecs = append(dvecs, memv) var dataEntry tarstream.Datavec if hostPath, ok := e.hostPath.Get(); ok { fi := e.header.FileInfo() dataEntry = &tarstream.PathVec{ Path: hostPath.String(), Info: fi, } } else if data, ok := e.data.Get(); ok { dataEntry = tarstream.MemVec{Data: data} } if dataEntry != nil { // add the file path info to the tarvec dvecs = append(dvecs, dataEntry) // tar requires file entries to be padded out to 512 bytes. if !e.header.FileInfo().IsDir() { if size := dataEntry.GetSize(); size%512 != 0 { padv := tarstream.PadVec{ Size: 512 - (size % 512), } dvecs = append(dvecs, padv) } } } } tv := tarstream.NewTarVec(dvecs) return func() (io.ReadCloser, error) { tv2 := tv.Clone() return tv2, nil } } func tarHeaderName(p ImagePath, isDir bool) string { name := strings.TrimPrefix(filepath.ToSlash(p.String()), "/") if isDir { name += "/" } return name } ================================================ FILE: pkg/editors/LICENSE ================================================ Original Copyright (c) GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: pkg/editors/doc.go ================================================ // Package editors is a Go Port of [GitHub Desktop's editor code](https://github.com/desktop/desktop/tree/development/app/src/lib/editors) // // It was taken at this commit [a1ece18](https://github.com/desktop/desktop/tree/a1ece186bae3a742f206c5edeb0762e78fba2f7c/app/src/lib/editors) package editors ================================================ FILE: pkg/editors/encore_names.go ================================================ package editors /* This file was added by Encore and is not part of the original GitHub Desktop codebase */ // EditorName represents the name of an editor we display to a user // // This file contains the full list of editors we support type EditorName string const ( AndroidStudio EditorName = "Android Studio" AptanaStudio EditorName = "Aptana Studio" Atom EditorName = "Atom" AtomBeta EditorName = "Atom (Beta)" AtomNightly EditorName = "Atom (Nightly)" BBEdit EditorName = "BBEdit" Brackets EditorName = "Brackets" Code EditorName = "Code" CodeRunner EditorName = "CodeRunner" ColdFusionBuilder EditorName = "ColdFusion Builder" Emacs EditorName = "Emacs" Geany EditorName = "Geany" GEdit EditorName = "GEdit" GnomeBuilder EditorName = "GNOME Builder" GnomeTextEditor EditorName = "GNOME Text Editor" GVim EditorName = "gVim" JetbrainsCLion EditorName = "CLion" JetbrainsDataSpell EditorName = "DataSpell" JetbrainsFleet EditorName = "Fleet" JetbrainsGoLand EditorName = "GoLand" JetbrainsIntelliJ EditorName = "IntelliJ" JetbrainsIntelliJCE EditorName = "IntelliJ CE" JetbrainsPhpStorm EditorName = "PhpStorm" JetbrainsPyCharm EditorName = "PyCharm" JetbrainsPyCharmCE EditorName = "PyCharm CE" JetbrainsRider EditorName = "Rider" JetbrainsRubyMine EditorName = "RubyMine" JetbrainsWebStorm EditorName = "WebStorm" Kate EditorName = "Kate" LiteXL EditorName = "Lite XL" MacVim EditorName = "MacVim" Mousepad EditorName = "Mousepad" Neovim EditorName = "Neovim" NeovimQt EditorName = "Neovim-Qt" Neovide EditorName = "Neovide" NotePadPlusPlus EditorName = "Notepad++" Notepadqq EditorName = "Notepadqq" Nova EditorName = "Nova" Pulsar EditorName = "Pulsar" RStudio EditorName = "RStudio" SlickEdit EditorName = "SlickEdit" Studio EditorName = "Studio" SublimeText EditorName = "Sublime Text" TextMate EditorName = "TextMate" Typora EditorName = "Typora" VimR EditorName = "VimR" VSCode EditorName = "VSCode" VSCodeInsiders EditorName = "VSCode (Insiders)" VSCodium EditorName = "VSCodium" VSCodiumInsiders EditorName = "VSCodium (Insiders)" XCode EditorName = "XCode" Zed EditorName = "Zed" ZedPreview EditorName = "Zed (Preview)" Cursor EditorName = "Cursor" ) ================================================ FILE: pkg/editors/encore_urls.go ================================================ package editors /* This file was added by Encore and is not part of the original GitHub Desktop codebase */ import ( "fmt" "net/url" ) // convertFilePathToURLScheme converts a file path to a URL scheme that can be used to open to a specific // line and column number. // // If the returned string should be executed as a URL, true is returned as the second argument. If the returned string // should be executed as a normal argument against the editor, then false is returned // // If no URL scheme exists for that editor and empty string and false is returned. func convertFilePathToURLScheme(editorName EditorName, fullPath string, startLine int, startCol int) (openArg string, executeAsURL bool) { switch editorName { case VSCode, VSCodeInsiders: if startLine > 0 { fullPath = fmt.Sprintf("%s:%d", fullPath, startLine) } return toURLScheme("vscode", "file", fullPath, "", "", 0, 0), true case Cursor: if startLine > 0 { fullPath = fmt.Sprintf("%s:%d", fullPath, startLine) } return toURLScheme("cursor", "file", fullPath, "", "", 0, 0), true case JetbrainsGoLand: return toJetBrainsScheme("goland", fullPath, startLine, startCol), true case JetbrainsPhpStorm: return toJetBrainsScheme("phpstorm", fullPath, startLine, startCol), true case JetbrainsPyCharm, JetbrainsPyCharmCE: return toJetBrainsScheme("pycharm", fullPath, startLine, startCol), true case JetbrainsRubyMine: return toJetBrainsScheme("rubymine", fullPath, startLine, startCol), true case JetbrainsWebStorm: return toJetBrainsScheme("webstorm", fullPath, startLine, startCol), true case JetbrainsIntelliJ, JetbrainsIntelliJCE: return toJetBrainsScheme("idea", fullPath, startLine, startCol), true case JetbrainsCLion: return toJetBrainsScheme("clion", fullPath, startLine, startCol), true case TextMate: return toOpenURLScheme("txmt", "", fullPath, startLine, startCol), true case BBEdit: return toOpenURLScheme("bbedit", "", fullPath, startLine, startCol), true default: return "", false } } func toJetBrainsScheme(scheme string, file string, line int, col int) string { return toURLScheme(scheme, "open", "", "file", file, line, col) } func toOpenURLScheme(scheme string, basePath string, file string, line int, col int) string { return toURLScheme(scheme, "open", basePath, "url", fmt.Sprintf("file://%s", file), line, col) } func toURLScheme(scheme string, host string, basePath string, fileKey string, file string, line int, col int) string { u := &url.URL{ Scheme: scheme, Host: host, Path: basePath, } q := u.Query() if fileKey != "" && file != "" { q.Set(fileKey, file) } if line > 0 { q.Set("line", fmt.Sprintf("%d", line)) if col > 0 { q.Set("col", fmt.Sprintf("%d", col)) } } u.RawQuery = q.Encode() return u.String() } ================================================ FILE: pkg/editors/launch.go ================================================ package editors import ( "io/fs" "os" "os/exec" "runtime" "github.com/cockroachdb/errors" "github.com/pkg/browser" "github.com/rs/zerolog/log" "encr.dev/pkg/xos" ) // LaunchExternalEditor opens a given file or folder in the desired external editor. func LaunchExternalEditor(fullPath string, startLine int, startCol int, editor FoundEditor) error { _, err := os.Stat(editor.Path) if errors.Is(err, fs.ErrNotExist) { return errors.Wrapf(err, "editor %s not found", editor.Editor) } // Encore patch to allow opening to a specific line and column in the file // if supported by the IDE toExecute, executeAsURL := convertFilePathToURLScheme(editor.Editor, fullPath, startLine, startCol) if executeAsURL { log.Info().Str("full_path", fullPath).Str("editor", string(editor.Editor)).Str("url", toExecute).Msg("attempting to open file via URL") if err := browser.OpenURL(toExecute); err == nil { return nil } else { log.Warn().Err(err).Str("url", toExecute).Msg("failed to open URL, falling back to file appraoch") // If the URL scheme failed to open, then we'll just open the file normally toExecute = fullPath } } else if toExecute == "" { toExecute = fullPath } var cmd *exec.Cmd //goland:noinspection GoBoolExpressions if editor.UsesShell { if runtime.GOOS == "windows" { // nosemgrep cmd = exec.Command("cmd.exe", "/c", editor.Path, toExecute) } else { // nosemgrep cmd = exec.Command("sh", "-c", editor.Path+" "+toExecute) } } else if runtime.GOOS == "darwin" { // nosemgrep cmd = exec.Command("open", "-a", editor.Path, toExecute) } else { // nosemgrep cmd = exec.Command(editor.Path, toExecute) } // Make sure the editor processes are detached from the Encore daemon. // Otherwise, some editors (like Notepad++) will be killed when the // Encore daemon shutsdown. cmd.SysProcAttr = xos.CreateNewProcessGroup() log.Info().Str("full_path", fullPath).Str("editor", string(editor.Editor)).Str("cmd", cmd.String()).Msg("attempting to open file") return errors.Wrap(cmd.Start(), "failed to start editor") } ================================================ FILE: pkg/editors/lookup.go ================================================ package editors import ( "context" goerrors "errors" "sort" "github.com/cockroachdb/errors" "go4.org/syncutil" ) // FoundEditor is a found external editor on the user's machine type FoundEditor struct { // The friendly name of the editor, to be used in labels Editor EditorName `json:"editor"` // The executable associated with the editor to launch Path string `json:"path"` // The editor requires a shell to spawn UsesShell bool `json:"usesShell,omitempty"` } var ( editorCacheOnce syncutil.Once // editorCache is a cache of the available editors on the user's machine editorCache []FoundEditor // ErrEditorNotFound is returned when an editor is not found when called from // the Find function ErrEditorNotFound = goerrors.New("editor not found") ) // Resolve a list of installed editors on the user's machine, using the known // install identifiers that each OS supports. func Resolve(ctx context.Context) ([]FoundEditor, error) { err := editorCacheOnce.Do(func() error { var err error editorCache, err = getAvailableEditors(ctx) if err != nil { return errors.Wrap(err, "unable to get available editors") } sort.Slice(editorCache, func(i, j int) bool { return editorCache[i].Editor < editorCache[j].Editor }) return nil }) return editorCache, err } // Find searches to an editor by name, returning the editor if found, or // [ErrEditorNotFound] if not found func Find(ctx context.Context, name EditorName) (FoundEditor, error) { editors, err := Resolve(ctx) if err != nil { return FoundEditor{}, err } for _, editor := range editors { if editor.Editor == name { return editor, nil } } return FoundEditor{}, errors.WithStack(ErrEditorNotFound) } ================================================ FILE: pkg/editors/lookup_darwin.go ================================================ //go:build darwin package editors import ( "bytes" "context" "strings" "github.com/cockroachdb/errors" "golang.org/x/sync/errgroup" exec "golang.org/x/sys/execabs" ) // DarwinExternalEditor represents an external editor on macOS type DarwinExternalEditor struct { // Name of the editor. It will be used both as identifier and user-facing. Name EditorName // List of bundle identifiers that are used by the app in its multiple versions. BundleIdentifiers []string } // This list contains all the external editors supported on macOS. Add a new // entry here to add support for your favorite editor. var editors = []DarwinExternalEditor{ { Name: Atom, BundleIdentifiers: []string{"com.github.atom"}, }, { Name: AptanaStudio, BundleIdentifiers: []string{"aptana.studio"}, }, { Name: MacVim, BundleIdentifiers: []string{"org.vim.MacVim"}, }, { Name: Neovide, BundleIdentifiers: []string{"com.neovide.neovide"}, }, { Name: VimR, BundleIdentifiers: []string{"com.qvacua.VimR"}, }, { Name: VSCode, BundleIdentifiers: []string{"com.microsoft.VSCode"}, }, { Name: VSCodeInsiders, BundleIdentifiers: []string{"com.microsoft.VSCodeInsiders"}, }, { Name: VSCodium, BundleIdentifiers: []string{"com.visualstudio.code.oss", "com.vscodium"}, }, { Name: SublimeText, BundleIdentifiers: []string{ "com.sublimetext.4", "com.sublimetext.3", "com.sublimetext.2", }, }, { Name: BBEdit, BundleIdentifiers: []string{"com.barebones.bbedit"}, }, { Name: JetbrainsPhpStorm, BundleIdentifiers: []string{"com.jetbrains.PhpStorm"}, }, { Name: JetbrainsPyCharm, BundleIdentifiers: []string{"com.jetbrains.PyCharm"}, }, { Name: JetbrainsPyCharmCE, BundleIdentifiers: []string{"com.jetbrains.pycharm.ce"}, }, { Name: JetbrainsDataSpell, BundleIdentifiers: []string{"com.jetbrains.DataSpell"}, }, { Name: JetbrainsRubyMine, BundleIdentifiers: []string{"com.jetbrains.RubyMine"}, }, { Name: RStudio, BundleIdentifiers: []string{"org.rstudio.RStudio", "com.rstudio.desktop"}, }, { Name: TextMate, BundleIdentifiers: []string{"com.macromates.TextMate"}, }, { Name: Brackets, BundleIdentifiers: []string{"io.brackets.appshell"}, }, { Name: JetbrainsWebStorm, BundleIdentifiers: []string{"com.jetbrains.WebStorm"}, }, { Name: JetbrainsCLion, BundleIdentifiers: []string{"com.jetbrains.CLion"}, }, { Name: Typora, BundleIdentifiers: []string{"abnerworks.Typora"}, }, { Name: CodeRunner, BundleIdentifiers: []string{"com.krill.CodeRunner"}, }, { Name: SlickEdit, BundleIdentifiers: []string{ "com.slickedit.SlickEditPro2018", "com.slickedit.SlickEditPro2017", "com.slickedit.SlickEditPro2016", "com.slickedit.SlickEditPro2015", }, }, { Name: JetbrainsIntelliJ, BundleIdentifiers: []string{"com.jetbrains.intellij"}, }, { Name: JetbrainsIntelliJCE, BundleIdentifiers: []string{"com.jetbrains.intellij.ce"}, }, { Name: XCode, BundleIdentifiers: []string{"com.apple.dt.Xcode"}, }, { Name: JetbrainsGoLand, BundleIdentifiers: []string{"com.jetbrains.goland"}, }, { Name: AndroidStudio, BundleIdentifiers: []string{"com.google.android.studio"}, }, { Name: JetbrainsRider, BundleIdentifiers: []string{"com.jetbrains.rider"}, }, { Name: Nova, BundleIdentifiers: []string{"com.panic.Nova"}, }, { Name: Emacs, BundleIdentifiers: []string{"org.gnu.Emacs"}, }, { Name: LiteXL, BundleIdentifiers: []string{"com.lite-xl"}, }, { Name: JetbrainsFleet, BundleIdentifiers: []string{"Fleet.app"}, }, { Name: Pulsar, BundleIdentifiers: []string{"dev.pulsar-edit.pulsar"}, }, { Name: Zed, BundleIdentifiers: []string{"dev.zed.Zed"}, }, { Name: ZedPreview, BundleIdentifiers: []string{"dev.zed.Zed-Preview"}, }, { Name: Cursor, BundleIdentifiers: []string{"com.todesktop.230313mzl4w4u92"}, }, } func findApplication(ctx context.Context, editor DarwinExternalEditor, foundEditors chan FoundEditor) error { for _, bundleIdentifier := range editor.BundleIdentifiers { path, err := getAppLocationByBundleID(ctx, bundleIdentifier) switch { case err != nil: return errors.WithStack(err) case path != "": foundEditors <- FoundEditor{ Editor: editor.Name, Path: path, } } } return nil } // getAppLocationByBundleID returns the location of the app with the given bundle identifier. func getAppLocationByBundleID(ctx context.Context, bundleID string) (string, error) { cmd := exec.CommandContext(ctx, "mdfind", "kMDItemCFBundleIdentifier == '"+bundleID+"'") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return "", err } // mdfind can return multiple results, so we'll just take the first one. results := strings.Split(out.String(), "\n") if len(results) > 0 { return results[0], nil } return "", nil } // Resolve a list of installed editors on the user's machine, using the known // install identifiers that each OS supports. func getAvailableEditors(ctx context.Context) ([]FoundEditor, error) { results := make([]FoundEditor, 0) grp, ctx := errgroup.WithContext(ctx) foundEditors := make(chan FoundEditor) errs := make(chan error, 1) for _, editor := range editors { editor := editor grp.Go(func() error { return findApplication(ctx, editor, foundEditors) }) } go func() { errs <- grp.Wait() close(foundEditors) }() // Collect results and the error from the group for editor := range foundEditors { results = append(results, editor) } if err := <-errs; err != nil { return nil, errors.WithStack(err) } return results, nil } ================================================ FILE: pkg/editors/lookup_linux.go ================================================ //go:build linux package editors import ( "context" ) // LinuxExternalEditor represents an external editor on Linux type LinuxExternalEditor struct { // Name of the editor. It will be used both as identifier and user-facing. Name EditorName // List of possible paths where the editor's executable might be located. Paths []string } // This list contains all the external editors supported on Linux. Add a new // entry here to add support for your favorite editor. var editors = []LinuxExternalEditor{ { Name: Atom, Paths: []string{"/snap/bin/atom", "/usr/bin/atom"}, }, { Name: Neovim, Paths: []string{"/usr/bin/nvim"}, }, { Name: NeovimQt, Paths: []string{"/usr/bin/nvim-qt"}, }, { Name: Neovide, Paths: []string{"/usr/bin/neovide"}, }, { Name: GVim, Paths: []string{"/usr/bin/gvim"}, }, { Name: VSCode, Paths: []string{ "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code", "/mnt/c/Program Files/Microsoft VS Code/bin/code", }, }, { Name: VSCodeInsiders, Paths: []string{"/snap/bin/code-insiders", "/usr/bin/code-insiders"}, }, { Name: VSCodium, Paths: []string{ "/usr/bin/codium", "/var/lib/flatpak/app/com.vscodium.codium", "/usr/share/vscodium-bin/bin/codium", }, }, { Name: SublimeText, Paths: []string{"/usr/bin/subl"}, }, { Name: Typora, Paths: []string{"/usr/bin/typora"}, }, { Name: SlickEdit, Paths: []string{ "/opt/slickedit-pro2018/bin/vs", "/opt/slickedit-pro2017/bin/vs", "/opt/slickedit-pro2016/bin/vs", "/opt/slickedit-pro2015/bin/vs", }, }, { // Code editor for elementary OS // https://github.com/elementary/code Name: Code, Paths: []string{"/usr/bin/io.elementary.code"}, }, { Name: LiteXL, Paths: []string{"/usr/bin/lite-xl"}, }, { Name: JetbrainsPhpStorm, Paths: []string{ "/snap/bin/phpstorm", ".local/share/JetBrains/Toolbox/scripts/phpstorm", }, }, { Name: JetbrainsGoLand, Paths: []string{ "/snap/bin/goland", ".local/share/JetBrains/Toolbox/scripts/goland", }, }, { Name: JetbrainsWebStorm, Paths: []string{ "/snap/bin/webstorm", ".local/share/JetBrains/Toolbox/scripts/webstorm", }, }, { Name: JetbrainsIntelliJ, Paths: []string{"/snap/bin/idea", ".local/share/JetBrains/Toolbox/scripts/idea"}, }, { Name: JetbrainsPyCharm, Paths: []string{ "/snap/bin/pycharm", ".local/share/JetBrains/Toolbox/scripts/pycharm", }, }, { Name: Studio, Paths: []string{ "/snap/bin/studio", ".local/share/JetBrains/Toolbox/scripts/studio", }, }, { Name: Emacs, Paths: []string{"/snap/bin/emacs", "/usr/local/bin/emacs", "/usr/bin/emacs"}, }, { Name: Kate, Paths: []string{"/usr/bin/kate"}, }, { Name: GEdit, Paths: []string{"/usr/bin/gedit"}, }, { Name: GnomeTextEditor, Paths: []string{"/usr/bin/gnome-text-editor"}, }, { Name: GnomeBuilder, Paths: []string{"/usr/bin/gnome-builder"}, }, { Name: Notepadqq, Paths: []string{"/usr/bin/notepadqq"}, }, { Name: Geany, Paths: []string{"/usr/bin/geany"}, }, { Name: Mousepad, Paths: []string{"/usr/bin/mousepad"}, }, { Name: Cursor, Paths: []string{ "/usr/bin/cursor", "/usr/share/cursor/cursor", "/snap/bin/cursor", "/opt/Cursor/cursor", }, }, } // Returns the first available path from the provided list. func getAvailablePath(paths []string) string { for _, path := range paths { if pathExists(path) { return path } } return "" } // Returns a list of available editors with their paths. func getAvailableEditors(_ context.Context) ([]FoundEditor, error) { var results []FoundEditor for _, editor := range editors { path := getAvailablePath(editor.Paths) // Assuming the editor struct has a Paths field if path != "" { results = append(results, FoundEditor{Editor: editor.Name, Path: path}) } } return results, nil } ================================================ FILE: pkg/editors/lookup_test.go ================================================ package editors import ( "context" "fmt" "testing" qt "github.com/frankban/quicktest" ) func TestResolve(t *testing.T) { c := qt.New(t) editors, err := Resolve(context.Background()) c.Assert(err, qt.IsNil) fmt.Printf("Found editors:\n%+v\n", editors) } ================================================ FILE: pkg/editors/lookup_unsupported.go ================================================ //go:build !darwin && !linux && !windows package editors import ( "context" ) // Returns no editors as we don't know how to find them on this platform. func getAvailableEditors(ctx context.Context) ([]FoundEditor, error) { return []FoundEditor{}, nil } ================================================ FILE: pkg/editors/lookup_windows.go ================================================ //go:build windows package editors import ( "context" "fmt" "path" "path/filepath" "slices" "strings" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows/registry" "encr.dev/pkg/fns" ) type WindowsExternalEditor struct { Name EditorName RegistryKeys []RegistryKey DisplayNamePrefixes []string Publishers []string JetBrainsToolboxScriptName string InstallLocationRegistryKey string ExecutableShimPaths []string } type RegistryKey struct { Key registry.Key SubKey string } type RegistryValue struct { Name string Type uint32 StringValue string } var editors = []WindowsExternalEditor{ { Name: Atom, RegistryKeys: []RegistryKey{CurrentUserUninstallKey("atom")}, ExecutableShimPaths: []string{"bin/atom.cmd"}, DisplayNamePrefixes: []string{"Atom"}, Publishers: []string{"GitHub Inc."}, }, { Name: AtomBeta, RegistryKeys: []RegistryKey{CurrentUserUninstallKey("atom-beta")}, ExecutableShimPaths: []string{"bin/atom-beta.cmd"}, DisplayNamePrefixes: []string{"Atom Beta"}, Publishers: []string{"GitHub Inc."}, }, { Name: AtomNightly, RegistryKeys: []RegistryKey{CurrentUserUninstallKey("atom-nightly")}, ExecutableShimPaths: []string{"bin/atom-nightly.cmd"}, DisplayNamePrefixes: []string{"Atom Nightly"}, Publishers: []string{"GitHub Inc."}, }, { Name: VSCode, RegistryKeys: []RegistryKey{ // 64-bit version of VSCode (user) - provided by default in 64-bit Windows CurrentUserUninstallKey("{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1"), // 32-bit version of VSCode (user) CurrentUserUninstallKey("{D628A17A-9713-46BF-8D57-E671B46A741E}_is1"), // ARM64 version of VSCode (user) CurrentUserUninstallKey("{D9E514E7-1A56-452D-9337-2990C0DC4310}_is1"), // 64-bit version of VSCode (system) - was default before user scope installation LocalMachineUninstallKey("{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"), // 32-bit version of VSCode (system) Wow64LocalMachineUninstallKey("{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1"), // ARM64 version of VSCode (system) LocalMachineUninstallKey("{A5270FC5-65AD-483E-AC30-2C276B63D0AC}_is1"), }, ExecutableShimPaths: []string{"bin/code.cmd"}, DisplayNamePrefixes: []string{"Microsoft Visual Studio Code"}, Publishers: []string{"Microsoft Corporation"}, }, { Name: VSCodeInsiders, RegistryKeys: []RegistryKey{ // 64-bit version of VSCode (user) - provided by default in 64-bit Windows CurrentUserUninstallKey("{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1"), // 32-bit version of VSCode (user) CurrentUserUninstallKey("{26F4A15E-E392-4887-8C09-7BC55712FD5B}_is1"), // ARM64 version of VSCode (user) CurrentUserUninstallKey("{69BD8F7B-65EB-4C6F-A14E-44CFA83712C0}_is1"), // 64-bit version of VSCode (system) - was default before user scope installation LocalMachineUninstallKey("{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"), // 32-bit version of VSCode (system) Wow64LocalMachineUninstallKey("{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1"), // ARM64 version of VSCode (system) LocalMachineUninstallKey("{0AEDB616-9614-463B-97D7-119DD86CCA64}_is1"), }, ExecutableShimPaths: []string{"bin/code-insiders.cmd"}, DisplayNamePrefixes: []string{"Microsoft Visual Studio Code Insiders"}, Publishers: []string{"Microsoft Corporation"}, }, { Name: VSCodium, RegistryKeys: []RegistryKey{ // 64-bit version of VSCodium (user) CurrentUserUninstallKey("{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1"), // 32-bit version of VSCodium (user) - new key CurrentUserUninstallKey("{0FD05EB4-651E-4E78-A062-515204B47A3A}_is1"), // ARM64 version of VSCodium (user) - new key CurrentUserUninstallKey("{57FD70A5-1B8D-4875-9F40-C5553F094828}_is1"), // 64-bit version of VSCodium (system) - new key LocalMachineUninstallKey("{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1"), // 32-bit version of VSCodium (system) - new key Wow64LocalMachineUninstallKey("{763CBF88-25C6-4B10-952F-326AE657F16B}_is1"), // ARM64 version of VSCodium (system) - new key LocalMachineUninstallKey("{67DEE444-3D04-4258-B92A-BC1F0FF2CAE4}_is1"), // 32-bit version of VSCodium (user) - old key CurrentUserUninstallKey("{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1"), // ARM64 version of VSCodium (user) - old key CurrentUserUninstallKey("{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}_is1"), // 64-bit version of VSCodium (system) - old key LocalMachineUninstallKey("{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1"), // 32-bit version of VSCodium (system) - old key Wow64LocalMachineUninstallKey("{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1"), // ARM64 version of VSCodium (system) - old key LocalMachineUninstallKey("{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1"), }, ExecutableShimPaths: []string{"bin/codium.cmd"}, DisplayNamePrefixes: []string{"VSCodium"}, Publishers: []string{"VSCodium", "Microsoft Corporation"}, }, { Name: VSCodiumInsiders, RegistryKeys: []RegistryKey{ // 64-bit version of VSCodium - Insiders (user) CurrentUserUninstallKey("{20F79D0D-A9AC-4220-9A81-CE675FFB6B41}_is1"), // 32-bit version of VSCodium - Insiders (user) CurrentUserUninstallKey("{ED2E5618-3E7E-4888-BF3C-A6CCC84F586F}_is1"), // ARM64 version of VSCodium - Insiders (user) CurrentUserUninstallKey("{2E362F92-14EA-455A-9ABD-3E656BBBFE71}_is1"), // 64-bit version of VSCodium - Insiders (system) LocalMachineUninstallKey("{B2E0DDB2-120E-4D34-9F7E-8C688FF839A2}_is1"), // 32-bit version of VSCodium - Insiders (system) Wow64LocalMachineUninstallKey("{EF35BB36-FA7E-4BB9-B7DA-D1E09F2DA9C9}_is1"), // ARM64 version of VSCodium - Insiders (system) LocalMachineUninstallKey("{44721278-64C6-4513-BC45-D48E07830599}_is1"), }, ExecutableShimPaths: []string{"bin/codium-insiders.cmd"}, DisplayNamePrefixes: []string{"VSCodium Insiders", "VSCodium (Insiders)"}, Publishers: []string{"VSCodium"}, }, { Name: SublimeText, RegistryKeys: []RegistryKey{ // Sublime Text 4 (and newer?) LocalMachineUninstallKey("Sublime Text_is1"), // Sublime Text 3 LocalMachineUninstallKey("Sublime Text 3_is1"), }, ExecutableShimPaths: []string{"subl.exe"}, DisplayNamePrefixes: []string{"Sublime Text"}, Publishers: []string{"Sublime HQ Pty Ltd"}, }, { Name: Brackets, RegistryKeys: []RegistryKey{ Wow64LocalMachineUninstallKey("{4F3B6E8C-401B-4EDE-A423-6481C239D6FF}"), }, ExecutableShimPaths: []string{"Brackets.exe"}, DisplayNamePrefixes: []string{"Brackets"}, Publishers: []string{"brackets.io"}, }, { Name: ColdFusionBuilder, RegistryKeys: []RegistryKey{ // 64-bit version of ColdFusionBuilder3 LocalMachineUninstallKey("Adobe ColdFusion Builder 3_is1"), // 64-bit version of ColdFusionBuilder2016 LocalMachineUninstallKey("Adobe ColdFusion Builder 2016"), }, ExecutableShimPaths: []string{"CFBuilder.exe"}, DisplayNamePrefixes: []string{"Adobe ColdFusion Builder"}, Publishers: []string{"Adobe Systems Incorporated"}, }, { Name: Typora, RegistryKeys: []RegistryKey{ // 64-bit version of Typora LocalMachineUninstallKey("{37771A20-7167-44C0-B322-FD3E54C56156}_is1"), // 32-bit version of Typora Wow64LocalMachineUninstallKey("{37771A20-7167-44C0-B322-FD3E54C56156}_is1"), }, ExecutableShimPaths: []string{"typora.exe"}, DisplayNamePrefixes: []string{"Typora"}, Publishers: []string{"typora.io"}, }, { Name: SlickEdit, RegistryKeys: []RegistryKey{ // 64-bit version of SlickEdit Pro 2018 LocalMachineUninstallKey("{18406187-F49E-4822-CAF2-1D25C0C83BA2}"), // 32-bit version of SlickEdit Pro 2018 Wow64LocalMachineUninstallKey("{18006187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Standard 2018 LocalMachineUninstallKey("{18606187-F49E-4822-CAF2-1D25C0C83BA2}"), // 32-bit version of SlickEdit Standard 2018 Wow64LocalMachineUninstallKey("{18206187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2017 LocalMachineUninstallKey("{15406187-F49E-4822-CAF2-1D25C0C83BA2}"), // 32-bit version of SlickEdit Pro 2017 Wow64LocalMachineUninstallKey("{15006187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2016 (21.0.1) LocalMachineUninstallKey("{10C06187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2016 (21.0.0) LocalMachineUninstallKey("{10406187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2015 (20.0.3) LocalMachineUninstallKey("{0DC06187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2015 (20.0.2) LocalMachineUninstallKey("{0D406187-F49E-4822-CAF2-1D25C0C83BA2}"), // 64-bit version of SlickEdit Pro 2014 (19.0.2) LocalMachineUninstallKey("{7CC0E567-ACD6-41E8-95DA-154CEEDB0A18}"), }, ExecutableShimPaths: []string{"win/vs.exe"}, DisplayNamePrefixes: []string{"SlickEdit"}, Publishers: []string{"SlickEdit Inc."}, }, { Name: AptanaStudio, RegistryKeys: []RegistryKey{ Wow64LocalMachineUninstallKey("{2D6C1116-78C6-469C-9923-3E549218773F}"), }, ExecutableShimPaths: []string{"AptanaStudio3.exe"}, DisplayNamePrefixes: []string{"Aptana Studio"}, Publishers: []string{"Appcelerator"}, }, { Name: JetbrainsWebStorm, RegistryKeys: registryKeysForJetBrainsIDE("WebStorm"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("webstorm"), JetBrainsToolboxScriptName: "webstorm", DisplayNamePrefixes: []string{"WebStorm"}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsPhpStorm, RegistryKeys: registryKeysForJetBrainsIDE("PhpStorm"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("phpstorm"), JetBrainsToolboxScriptName: "phpstorm", DisplayNamePrefixes: []string{"PhpStorm"}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: AndroidStudio, RegistryKeys: []RegistryKey{LocalMachineUninstallKey("Android Studio")}, InstallLocationRegistryKey: "UninstallString", JetBrainsToolboxScriptName: "studio", ExecutableShimPaths: []string{ "../bin/studio64.exe", "../bin/studio.exe", }, DisplayNamePrefixes: []string{"Android Studio"}, Publishers: []string{"Google LLC"}, }, { Name: NotePadPlusPlus, RegistryKeys: []RegistryKey{ // 64-bit version of Notepad++ LocalMachineUninstallKey("Notepad++"), // 32-bit version of Notepad++ Wow64LocalMachineUninstallKey("Notepad++"), }, InstallLocationRegistryKey: "DisplayIcon", DisplayNamePrefixes: []string{"Notepad++"}, Publishers: []string{"Notepad++ Team"}, }, { Name: JetbrainsRider, RegistryKeys: registryKeysForJetBrainsIDE("JetBrains Rider"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("rider"), JetBrainsToolboxScriptName: "rider", DisplayNamePrefixes: []string{"JetBrains Rider"}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: RStudio, RegistryKeys: []RegistryKey{Wow64LocalMachineUninstallKey("RStudio")}, InstallLocationRegistryKey: "DisplayIcon", DisplayNamePrefixes: []string{"RStudio"}, Publishers: []string{"RStudio", "Posit Software"}, }, { Name: JetbrainsIntelliJ, RegistryKeys: registryKeysForJetBrainsIDE("IntelliJ IDEA"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("idea"), JetBrainsToolboxScriptName: "idea", DisplayNamePrefixes: []string{"IntelliJ IDEA "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsIntelliJCE, RegistryKeys: registryKeysForJetBrainsIDE("IntelliJ IDEA Community Edition"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("idea"), DisplayNamePrefixes: []string{"IntelliJ IDEA Community Edition "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsPyCharm, RegistryKeys: registryKeysForJetBrainsIDE("PyCharm"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("pycharm"), JetBrainsToolboxScriptName: "pycharm", DisplayNamePrefixes: []string{"PyCharm "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsPyCharmCE, RegistryKeys: registryKeysForJetBrainsIDE("PyCharm Community Edition"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("pycharm"), DisplayNamePrefixes: []string{"PyCharm Community Edition"}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsCLion, RegistryKeys: registryKeysForJetBrainsIDE("CLion"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("clion"), JetBrainsToolboxScriptName: "clion", DisplayNamePrefixes: []string{"CLion "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsRubyMine, RegistryKeys: registryKeysForJetBrainsIDE("RubyMine"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("rubymine"), JetBrainsToolboxScriptName: "rubymine", DisplayNamePrefixes: []string{"RubyMine "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsGoLand, RegistryKeys: registryKeysForJetBrainsIDE("GoLand"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("goland"), JetBrainsToolboxScriptName: "goland", DisplayNamePrefixes: []string{"GoLand "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsFleet, RegistryKeys: []RegistryKey{LocalMachineUninstallKey("Fleet")}, JetBrainsToolboxScriptName: "fleet", InstallLocationRegistryKey: "DisplayIcon", DisplayNamePrefixes: []string{"Fleet "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: JetbrainsDataSpell, RegistryKeys: registryKeysForJetBrainsIDE("DataSpell"), ExecutableShimPaths: executableShimPathsForJetBrainsIDE("dataspell"), JetBrainsToolboxScriptName: "dataspell", DisplayNamePrefixes: []string{"DataSpell "}, Publishers: []string{"JetBrains s.r.o."}, }, { Name: Pulsar, RegistryKeys: []RegistryKey{ CurrentUserUninstallKey("0949b555-c22c-56b7-873a-a960bdefa81f"), LocalMachineUninstallKey("0949b555-c22c-56b7-873a-a960bdefa81f"), }, ExecutableShimPaths: []string{"../pulsar/Pulsar.exe"}, DisplayNamePrefixes: []string{"Pulsar"}, Publishers: []string{"Pulsar-Edit"}, }, { Name: Cursor, RegistryKeys: []RegistryKey{ // 64-bit version of Cursor (user) CurrentUserUninstallKey("{4F4FB13E-18F1-5261-B8EB-6E696B5F2C7E}_is1"), }, ExecutableShimPaths: []string{"bin/cursor.cmd"}, DisplayNamePrefixes: []string{"Cursor"}, Publishers: []string{"Anysphere Inc.", "Anysphere"}, }, } func registryKey(key registry.Key, subKey string) RegistryKey { return RegistryKey{Key: key, SubKey: subKey} } func CurrentUserUninstallKey(subKey string) RegistryKey { return registryKey(registry.CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"+subKey) } func LocalMachineUninstallKey(subKey string) RegistryKey { return registryKey(registry.LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"+subKey) } func Wow64LocalMachineUninstallKey(subKey string) RegistryKey { return registryKey(registry.LOCAL_MACHINE, "Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"+subKey) } // This function generates registry keys for a given JetBrains product for the // last 2 years, assuming JetBrains make no more than 5 major releases and // no more than 5 minor releases per year. func registryKeysForJetBrainsIDE(product string) []RegistryKey { const maxMajorReleasesPerYear = 5 const maxMinorReleasesPerYear = 5 var lastYear = time.Now().Year() var firstYear = lastYear - 2 var result []RegistryKey for year := firstYear; year <= lastYear; year++ { for majorRelease := 1; majorRelease <= maxMajorReleasesPerYear; majorRelease++ { for minorRelease := 0; minorRelease <= maxMinorReleasesPerYear; minorRelease++ { key := fmt.Sprintf("%s %d.%d", product, year, majorRelease) if minorRelease > 0 { key = fmt.Sprintf("%s.%d", key, minorRelease) } result = append(result, Wow64LocalMachineUninstallKey(key), CurrentUserUninstallKey(key), ) } } } // Return in reverse order to prioritize newer versions slices.Reverse(result) return result } // JetBrains IDE's might have 64 and/or 32 bit executables, so let's add both func executableShimPathsForJetBrainsIDE(baseName string) []string { return []string{ fmt.Sprintf("bin/%s64.exe", baseName), fmt.Sprintf("bin/%s.exe", baseName), } } func getKeyOrEmpty(keys []RegistryValue, valueName string) string { for _, key := range keys { if key.Name == valueName { if key.Type == registry.SZ { return key.StringValue } } } return "" } func getAppInfo(editor WindowsExternalEditor, keys []RegistryValue) (displayName, publisher, installLocation string) { displayName = getKeyOrEmpty(keys, "DisplayName") publisher = getKeyOrEmpty(keys, "Publisher") loc := editor.InstallLocationRegistryKey if loc == "" { loc = "InstallLocation" } installLocation = getKeyOrEmpty(keys, loc) return } func findApplication(ctx context.Context, editor WindowsExternalEditor, foundEditors chan FoundEditor) error { for _, registryKey := range editor.RegistryKeys { values, err := enumerateValues(registryKey.Key, registryKey.SubKey) if err != nil { return errors.Wrap(err, "failed to enumerate registry values") } if len(values) == 0 { continue } displayName, publisher, installLocation := getAppInfo(editor, values) if !validateStartsWith(displayName, editor.DisplayNamePrefixes) || !stringInSlice(publisher, editor.Publishers) { log.Warn().Str("editor", string(editor.Name)).Str("publisher", publisher).Str("display_name", displayName).Msg("unexpected registry entry") continue } var executableShimPaths []string if editor.InstallLocationRegistryKey == "DisplayIcon" { executableShimPaths = []string{installLocation} } else { for _, shim := range editor.ExecutableShimPaths { executableShimPaths = append(executableShimPaths, filepath.Join(installLocation, shim)) } } for _, exePath := range executableShimPaths { if pathExists(exePath) { foundEditors <- FoundEditor{ Editor: editor.Name, Path: exePath, UsesShell: strings.HasSuffix(exePath, ".cmd"), } return nil } else { log.Debug().Str("editor", string(editor.Name)).Str("path", exePath).Msg("executable not found") } } } return findJetBrainsToolboxApplication(ctx, editor, foundEditors) } // Find JetBrain products installed through JetBrains Toolbox func findJetBrainsToolboxApplication(_ context.Context, editor WindowsExternalEditor, foundEditors chan FoundEditor) error { if editor.JetBrainsToolboxScriptName == "" { return nil } var toolboxRegistryReference = []RegistryKey{ CurrentUserUninstallKey("toolbox"), Wow64LocalMachineUninstallKey("toolbox"), } for _, registryKey := range toolboxRegistryReference { keys, err := enumerateValues(registryKey.Key, registryKey.SubKey) if err != nil { return errors.Wrap(err, "failed to enumerate registry values") } if len(keys) > 0 { editorPathInToolbox := path.Join( getKeyOrEmpty(keys, "UninstallString"), "..", "..", "scripts", fmt.Sprintf("%s.cmd", editor.JetBrainsToolboxScriptName), ) if pathExists(editorPathInToolbox) { foundEditors <- FoundEditor{ Editor: editor.Name, Path: editorPathInToolbox, UsesShell: true, } } } } return nil } func validateStartsWith(registryVal string, definedVal []string) bool { for _, val := range definedVal { if strings.HasPrefix(registryVal, val) { return true } } return false } func enumerateValues(key registry.Key, subKey string) ([]RegistryValue, error) { k, err := registry.OpenKey(key, subKey, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) if err != nil { return nil, err } defer fns.CloseIgnore(k) valueNames, err := k.ReadValueNames(-1) // read all value names if err != nil { return nil, err } var values []RegistryValue for _, valueName := range valueNames { value, valueType, err := k.GetStringValue(valueName) if err != nil { if !errors.Is(err, registry.ErrUnexpectedType) && !errors.Is(err, registry.ErrNotExist) { return nil, err } } else { values = append(values, RegistryValue{Name: valueName, Type: valueType, StringValue: value}) } } return values, nil } func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } // Resolve a list of installed editors on the user's machine, using known // install and uninstall path registry keys func getAvailableEditors(ctx context.Context) ([]FoundEditor, error) { results := make([]FoundEditor, 0) grp, ctx := errgroup.WithContext(ctx) foundEditors := make(chan FoundEditor) errs := make(chan error, 1) for _, editor := range editors { editor := editor grp.Go(func() error { return findApplication(ctx, editor, foundEditors) }) } go func() { errs <- grp.Wait() close(foundEditors) }() // Collect results and the error from the group for editor := range foundEditors { results = append(results, editor) } if err := <-errs; err != nil { return nil, errors.WithStack(err) } return results, nil } ================================================ FILE: pkg/editors/utils.go ================================================ package editors import ( "errors" "io/fs" "os" ) // Checks if the given path exists. func pathExists(path string) bool { _, err := os.Stat(path) return !errors.Is(err, fs.ErrNotExist) } ================================================ FILE: pkg/eerror/error.go ================================================ // Package eerror stands for Encore Error and is used to provide // a little more information about the underlying error's metadata. // // It also provides helper methods for working with zerolog's context package eerror import ( "fmt" "strings" "github.com/cockroachdb/errors/errbase" "github.com/pkg/errors" "github.com/rs/zerolog" ) type Error struct { Module string `json:"module"` // The module the error was raised in (normally the package name, but could be a larger "module" name) Message string `json:"message"` // The message of the error, it should be human-readable and should be low entropy (i.e. so multiple errors of the same can be grouped) Meta map[string]any `json:"meta"` // Metadata about the error, this can be high entropy as it isn't used to group errors Stack []*StackFrame `json:"stack"` // The stack trace of the error cause error `json:"-"` // The underlying error, this is not serialized } var _ error = (*Error)(nil) var _ errbase.StackTraceProvider = (*Error)(nil) // New creates a new error with the given error func New(module string, msg string, meta map[string]any) error { return &Error{ Module: module, Message: msg, Meta: meta, Stack: getStack(), } } // Wrap wraps the cause error with the given message and meta. // If cause is nil, wrap returns nil func Wrap(cause error, module string, msg string, meta map[string]any) error { if cause == nil { return nil } return &Error{ Module: module, Message: msg, Meta: meta, Stack: getStack(), cause: cause, } } func WithMeta(err error, meta map[string]any) error { loopErr := err for loopErr != nil { if e, ok := loopErr.(*Error); ok { for key, value := range meta { e.Meta[key] = value } return loopErr } switch e := loopErr.(type) { case interface{ Unwrap() error }: loopErr = e.Unwrap() case interface{ Cause() error }: loopErr = e.Cause() default: loopErr = nil } } // if here we didn't find an *Error to add the metadata to, so we'll put on in return &Error{ Module: "", Message: err.Error(), Meta: meta, Stack: getStack(), cause: err, } } // Error returns a simple string of the error func (e *Error) Error() string { if e.cause != nil { cause := e.cause.Error() // Remove the module prefix if it's the same cause = strings.TrimPrefix(cause, "["+e.Module+"]: ") return fmt.Sprintf("[%s]: %s: %s", e.Module, e.Message, cause) } return fmt.Sprintf("[%s]: %s", e.Module, e.Message) } // Cause implements Causer for some libraries and returns the underlying cause func (e *Error) Cause() error { return e.cause } // Unwrap implements the Go 2 unwrap interface used by xerrors and errors func (e *Error) Unwrap() error { return e.cause } // StackTrace implements the StackTraceProvider interface for some libraries // including ZeroLog, xerrors and Sentry func (e *Error) StackTrace() errors.StackTrace { frames := make([]errors.Frame, len(e.Stack)) for i, frame := range e.Stack { // Note: for historic reasons the PC is off by 1 in github.com/pkg/errors frames[i] = errors.Frame(frame.PC + 1) } return frames } // MarshalZerologObject provides a strongly-typed and encoding-agnostic interface // to be implemented by types used with Event/Context's Object methods. func (e *Error) MarshalZerologObject(evt *zerolog.Event) { LogWithMeta(evt, e) } // BottomStackTraceFrom returns the deepest stack trace from the given error func BottomStackTraceFrom(err error) (rtn errors.StackTrace) { count := 0 for err != nil && count < 100 { count++ // If we're an error set our return data if e, ok := err.(interface{ StackTrace() errors.StackTrace }); ok { rtn = e.StackTrace() } // Recurse switch typed := err.(type) { case interface{ Unwrap() error }: err = typed.Unwrap() case interface{ Unwrap() []error }: errs := typed.Unwrap() if len(errs) > 0 { err = errs[0] } else { err = nil } case interface{ Cause() error }: err = typed.Cause() } } return } // MetaFrom will return the merged metadata from any eerror.Error objects in the errors // given. It will unwrap errors as it descends func MetaFrom(err error) map[string]any { meta := make(map[string]any) mergeMeta(err, meta) return meta } func mergeMeta(err error, meta map[string]any) { if err == nil { return } // Merge in the data from the deepest error first switch err := err.(type) { case interface{ Unwrap() error }: mergeMeta(err.Unwrap(), meta) case interface{ Cause() error }: mergeMeta(err.Cause(), meta) } // Then merge in our data if e, ok := err.(*Error); ok { for key, value := range e.Meta { meta[key] = value } } } ================================================ FILE: pkg/eerror/stack.go ================================================ package eerror import ( "fmt" "os" "path/filepath" "runtime" "strings" ) type StackFrame struct { // PC is the program counter for the frame (needed for things like sentry reporting) PC uintptr `json:"pc"` // Human-readable fields Function string `json:"function"` File string `json:"file"` Line int `json:"line"` } var projectSourcePath = getProjectSrcPath() // getProjectSrcPath returns the path to this repo on the local system func getProjectSrcPath() string { _, file, _, ok := runtime.Caller(0) if !ok { return "" } eerrorPath := filepath.Dir(file) pkgPath := filepath.Dir(eerrorPath) encoreProjectPath := filepath.Dir(pkgPath) return fmt.Sprintf("%s%c", encoreProjectPath, os.PathSeparator) } // getStack returns a human read able stack trace func getStack() []*StackFrame { ret := make([]uintptr, 100) index := runtime.Callers(1, ret) if index == 0 { return nil } cf := runtime.CallersFrames(ret[:index]) frame, more := cf.Next() // Skip over the "eerror" package files for strings.Contains(frame.File, "eerror") { if !more { return nil } frame, more = cf.Next() } var frames []*StackFrame for { frames = append(frames, &StackFrame{ PC: frame.PC, Function: strings.TrimPrefix(frame.Function, "encr.dev/"), File: strings.TrimPrefix(frame.File, projectSourcePath), Line: frame.Line, }) if !more { return frames } frame, more = cf.Next() } } ================================================ FILE: pkg/eerror/zerolog.go ================================================ package eerror import ( "bytes" "encoding/json" "fmt" "net" "runtime" "sort" "strconv" "strings" "time" "github.com/rs/zerolog" ) type errorMeta struct { Frames []uintptr `json:"frames"` Meta map[string]any `json:"meta"` } // ZeroLogStackMarshaller will encode the following in the Zerolog error stack field: // - the deepest error stack trace // - the error meta data collected from all errors // // This can then be extracted by ZeroLogConsoleExtraFormatter func ZeroLogStackMarshaller(err error) interface{} { var frames []uintptr for _, frame := range BottomStackTraceFrom(err) { // Account for the stack trace being 1 frame deeper than the error frames = append(frames, uintptr(frame-1)) } frames = filterFrames(frames) return &errorMeta{ Frames: frames, Meta: MetaFrom(err), } } // ZeroLogConsoleExtraFormatter extracts the extra fields from the error and formats them for console output // // This field can be passed to a zerolog.ConsoleWriter as it's ExtraFieldFormatter func ZeroLogConsoleExtraFormatter(event map[string]any, buf *bytes.Buffer) error { stackFieldValue, found := event[zerolog.ErrorStackFieldName] if !found || stackFieldValue == nil { return nil } // Marshal the stack field to our error meta var errorMeta errorMeta jsonBytes, err := json.Marshal(stackFieldValue) if err != nil { return err } if err := json.Unmarshal(jsonBytes, &errorMeta); err != nil { return err } // Log any additional meta data if len(errorMeta.Meta) > 0 { // Get the fields not already set in the log, and then sort them fields := make([]string, 0, len(errorMeta.Meta)) for field := range errorMeta.Meta { if _, alreadySet := event[field]; !alreadySet { fields = append(fields, field) } } sort.Strings(fields) // Then log the additional fields for _, field := range fields { if buf.Len() > 0 { buf.WriteByte(' ') } fieldName := fmt.Sprintf("\x1b[%dm%v\x1b[0m=", 36, field) buf.WriteString(fieldName) switch fValue := errorMeta.Meta[field].(type) { case string: if needsQuote(fValue) { buf.WriteString(strconv.Quote(fValue)) } else { buf.WriteString(fValue) } case json.Number: buf.WriteString(fValue.String()) default: b, err := zerolog.InterfaceMarshalFunc(fValue) if err != nil { buf.WriteString(fmt.Sprintf("[error: \x1b[%dm%v\x1b[0m=]", 31, err)) } else { buf.WriteString(string(b)) } } } } // Now print a stack trace if we have it if len(errorMeta.Frames) > 0 { // Find the longest function name so we can align them longestFunc := 0 for _, frame := range errorMeta.Frames { module, function := frameToModuleFunc(frame) fName := fmt.Sprintf("%s.%s", module, function) if len(fName) > longestFunc { longestFunc = len(fName) } } // Print the stack trace for i, frame := range errorMeta.Frames { module, function := frameToModuleFunc(frame) filename, line := frameToFileLine(frame) buf.WriteString(fmt.Sprintf( "\n\tat %s.%s%s %s:%d", fmt.Sprintf("\x1b[%dm%v\x1b[0m", 90, module), fmt.Sprintf("\x1b[%dm%v\x1b[0m", 35, function), strings.Repeat(" ", longestFunc-(len(function)+len(module)+1)), filename, line, )) if i > 5 { buf.WriteString(fmt.Sprintf("\n\t... remaining %d frames omitted ...", len(errorMeta.Frames)-(i+1))) break } } } return nil } func frameToModuleFunc(frame uintptr) (module string, name string) { fn := runtime.FuncForPC(frame) if fn == nil { return "", "unknown" } name = fn.Name() if idx := strings.LastIndex(name, "."); idx != -1 { module = name[:idx] name = name[idx+1:] } module = strings.TrimPrefix(module, "encr.dev/") name = strings.Replace(name, "·", ".", -1) return } func frameToFileLine(frame uintptr) (file string, line int) { fn := runtime.FuncForPC(frame) if fn == nil { return "unknown", 0 } filename, line := fn.FileLine(frame) filename = strings.TrimPrefix(filename, projectSourcePath) return filename, line } // filterFrames filters out stack frames from zerolog and this package. func filterFrames(frames []uintptr) []uintptr { if len(frames) == 0 { return nil } filteredFrames := make([]uintptr, 0, len(frames)) for _, frame := range frames { module, _ := frameToModuleFunc(frame) if strings.HasPrefix(module, "github.com/rs/zerolog") { continue } else if strings.HasPrefix(module, "github.com/spf13/cobra") { continue } filteredFrames = append(filteredFrames, frame) } return filteredFrames } // needsQuote returns true when the string s should be quoted in output. func needsQuote(s string) bool { for i := range s { if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' { return true } } return false } // LogWithMeta merges in the metadata from the errors into the log context func LogWithMeta(evt *zerolog.Event, err error) *zerolog.Event { if err == nil { return evt } meta := MetaFrom(err) for key, value := range meta { switch value := value.(type) { case json.RawMessage: evt = evt.RawJSON(key, value) case error: evt = evt.AnErr(key, value) case time.Time: evt = evt.Time(key, value) case time.Duration: evt = evt.Dur(key, value) case net.IP: evt = evt.IPAddr(key, value) case net.IPNet: evt = evt.IPPrefix(key, value) case net.HardwareAddr: evt = evt.MACAddr(key, value) case string: evt = evt.Str(key, value) case int: evt = evt.Int(key, value) case int8: evt = evt.Int8(key, value) case int16: evt = evt.Int16(key, value) case int32: evt = evt.Int32(key, value) case int64: evt = evt.Int64(key, value) case uint: evt = evt.Uint(key, value) case uint8: evt = evt.Uint8(key, value) case uint16: evt = evt.Uint16(key, value) case uint32: evt = evt.Uint32(key, value) case uint64: evt = evt.Uint64(key, value) case float32: evt = evt.Float32(key, value) case float64: evt = evt.Float64(key, value) case bool: evt = evt.Bool(key, value) case []error: evt = evt.Errs(key, value) case []time.Time: evt = evt.Times(key, value) case []time.Duration: evt = evt.Durs(key, value) case []string: evt = evt.Strs(key, value) case []int: evt = evt.Ints(key, value) case []int8: evt = evt.Ints8(key, value) case []int16: evt = evt.Ints16(key, value) case []int32: evt = evt.Ints32(key, value) case []int64: evt = evt.Ints64(key, value) case []uint: evt = evt.Uints(key, value) case []byte: // uint8 / byte are the same thing so we'll default to bytes evt = evt.Bytes(key, value) case []uint16: evt = evt.Uints16(key, value) case []uint32: evt = evt.Uints32(key, value) case []uint64: evt = evt.Uints64(key, value) case []float32: evt = evt.Floats32(key, value) case []float64: evt = evt.Floats64(key, value) case []bool: evt = evt.Bools(key, value) default: evt = evt.Interface(key, value) } } return evt } ================================================ FILE: pkg/emulators/storage/LICENSE ================================================ MIT License Copyright (c) 2021 Engineering at Fullstory Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: pkg/emulators/storage/gcsemu/batch.go ================================================ package gcsemu import ( "bufio" "bytes" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "net/textproto" ) // BatchHandler handles emulated GCS http requests for "storage.googleapis.com/batch/storage/v1". func (g *GcsEmu) BatchHandler(w http.ResponseWriter, r *http.Request) { // First parse the entire incoming message. reader, err := r.MultipartReader() if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } var reqs []*http.Request var contentIds []string for i := 0; true; i++ { part, err := reader.NextPart() if err == io.EOF { break // done } else if err != nil { g.gapiError(w, http.StatusBadRequest, err.Error()) return } if ct := part.Header.Get("Content-Type"); ct != "application/http" { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("Content-Type: want=application/http, got=%s", ct)) return } contentId := part.Header.Get("Content-ID") content, err := io.ReadAll(part) _ = part.Close() if err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("part=%d, Content-ID=%s: read error %v", i, contentId, err)) return } newReader := bufio.NewReader(bytes.NewReader(content)) req, err := http.ReadRequest(newReader) if err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("part=%d, Content-ID=%s: unable to parse request %v", i, contentId, err)) return } // Any remaining bytes are the body. rem, _ := io.ReadAll(newReader) if len(rem) > 0 { req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(rem)), nil } req.Body, _ = req.GetBody() } if cte := part.Header.Get("Content-Transfer-Encoding"); cte != "" { req.Header.Set("Transfer-Encoding", cte) } // encoded requests don't include a host, so patch it up from the incoming request req.Host = r.Host reqs = append(reqs, req) contentIds = append(contentIds, contentId) } // At this point, we can respond with a 200. mw := multipart.NewWriter(w) w.Header().Set("Content-Type", "multipart/mixed; boundary="+mw.Boundary()) w.WriteHeader(http.StatusOK) // run each request for i := range reqs { req, contentId := reqs[i], contentIds[i] rw := httptest.NewRecorder() g.Handler(rw, req) rsp := rw.Result() rsp.ContentLength = int64(rw.Body.Len()) partHeaders := textproto.MIMEHeader{} partHeaders.Set("Content-Type", "application/http") if contentId != "" { if contentId[0] == '<' { contentId = "" } return fmt.Sprintf("http error %d: %s", err.code, err.cause) } ================================================ FILE: pkg/emulators/storage/gcsemu/filestore.go ================================================ package gcsemu import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" cloudstorage "cloud.google.com/go/storage" "google.golang.org/api/storage/v1" ) const ( metaExtention = ".emumeta" ) type filestore struct { gcsDir string } var _ Store = (*filestore)(nil) // NewFileStore returns a new Store that writes to the given directory. func NewFileStore(gcsDir string) *filestore { return &filestore{gcsDir: gcsDir} } type composeObj struct { filename string conds cloudstorage.Conditions } func (fs *filestore) CreateBucket(bucket string) error { bucketDir := filepath.Join(fs.gcsDir, bucket) return os.MkdirAll(bucketDir, 0777) } func (fs *filestore) GetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error) { f := fs.filename(bucket, "") fInfo, err := os.Stat(f) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("stating %s: %w", f, err) } obj := BucketMeta(baseUrl, bucket) obj.Updated = fInfo.ModTime().UTC().Format(time.RFC3339Nano) return obj, nil } func (fs *filestore) Get(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error) { obj, err := fs.GetMeta(baseUrl, bucket, filename) if err != nil { return nil, nil, err } if obj == nil { return nil, nil, nil } f := fs.filename(bucket, filename) contents, err := os.ReadFile(f) if err != nil { return nil, nil, fmt.Errorf("reading %s: %w", f, err) } return obj, contents, nil } func (fs *filestore) GetMeta(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, error) { f := fs.filename(bucket, filename) fInfo, err := os.Stat(f) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("stating %s: %w", f, err) } return fs.ReadMeta(baseUrl, bucket, filename, fInfo) } func (fs *filestore) Add(bucket string, filename string, contents []byte, meta *storage.Object) error { f := fs.filename(bucket, filename) if err := os.MkdirAll(filepath.Dir(f), 0777); err != nil { return fmt.Errorf("could not create dirs for: %s: %w", f, err) } if err := os.WriteFile(f, contents, 0666); err != nil { return fmt.Errorf("could not write: %s: %w", f, err) } // Force a new modification time, since this is what Generation is based on. now := time.Now().UTC() _ = os.Chtimes(f, now, now) InitScrubbedMeta(meta, filename) meta.Metageneration = 1 meta.Generation = now.UnixNano() if meta.TimeCreated == "" { meta.TimeCreated = now.UTC().Format(time.RFC3339Nano) } meta.Id = fmt.Sprintf("%s/%s/%d", bucket, filename, meta.Generation) meta.Etag = fmt.Sprintf("%d", meta.Generation) fMeta := metaFilename(f) if err := os.WriteFile(fMeta, mustJson(meta), 0666); err != nil { return fmt.Errorf("could not write metadata file: %s: %w", fMeta, err) } return nil } func (fs *filestore) UpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error { InitScrubbedMeta(meta, filename) meta.Metageneration = metagen fMeta := metaFilename(fs.filename(bucket, filename)) if err := os.WriteFile(fMeta, mustJson(meta), 0666); err != nil { return fmt.Errorf("could not write metadata file: %s: %w", fMeta, err) } return nil } func (fs *filestore) Copy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error) { // Make sure it's there meta, err := fs.GetMeta(dontNeedUrls, srcBucket, srcFile) if err != nil { return false, err } // Handle object-not-found if meta == nil { return false, nil } // Copy with metadata f1 := fs.filename(srcBucket, srcFile) contents, err := os.ReadFile(f1) if err != nil { return false, err } meta.TimeCreated = "" // reset creation time on the dest file err = fs.Add(dstBucket, dstFile, contents, meta) if err != nil { return false, err } return true, nil } func (fs *filestore) Delete(bucket string, filename string) error { f := fs.filename(bucket, filename) err := func() error { // Check if the bucket exists if _, err := os.Stat(f); os.IsNotExist(err) { return os.ErrNotExist } // Remove the bucket if filename == "" { return os.RemoveAll(f) } // Remove just the file and the associated metadata file if err := os.Remove(f); err != nil { return err } err := os.Remove(metaFilename(f)) if os.IsNotExist(err) { // Legacy files do not have an accompanying metadata file. return nil } return err }() if err != nil { if os.IsNotExist(err) { return err } return fmt.Errorf("could not delete %s: %w", f, err) } // Try to delete empty directories for fp := filepath.Dir(f); len(fp) > len(fs.filename(bucket, "")); fp = filepath.Dir(fp) { files, err := os.ReadDir(fp) if err != nil || len(files) > 0 { // Quit trying to delete the directory break } if err := os.Remove(fp); err != nil { // If removing fails, quit trying break } } return nil } func (fs *filestore) ReadMeta(baseUrl HttpBaseUrl, bucket string, filename string, fInfo os.FileInfo) (*storage.Object, error) { if fInfo.IsDir() { return nil, nil } f := fs.filename(bucket, filename) obj := &storage.Object{} fMeta := metaFilename(f) buf, err := os.ReadFile(fMeta) if err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("could not read metadata file %s: %w", fMeta, err) } } if len(buf) != 0 { if err := json.NewDecoder(bytes.NewReader(buf)).Decode(obj); err != nil { return nil, fmt.Errorf("could not parse file attributes %q for %s: %w", buf, f, err) } } InitMetaWithUrls(baseUrl, obj, bucket, filename, uint64(fInfo.Size())) // obj.Generation = fInfo.ModTime().UnixNano() // use the mod time as the generation number obj.Updated = fInfo.ModTime().UTC().Format(time.RFC3339Nano) return obj, nil } func (fs *filestore) filename(bucket string, filename string) string { if filename == "" { return filepath.Join(fs.gcsDir, bucket) } return filepath.Join(fs.gcsDir, bucket, filename) } func metaFilename(filename string) string { return filename + metaExtention } func (fs *filestore) Walk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error { root := filepath.Join(fs.gcsDir, bucket) return filepath.Walk(root, func(path string, fInfo os.FileInfo, err error) error { if strings.HasSuffix(path, metaExtention) { // Ignore metadata files return nil } filename := strings.TrimPrefix(path, root) filename = strings.TrimPrefix(filename, string(os.PathSeparator)) if err != nil { if os.IsNotExist(err) { return err } return fmt.Errorf("walk error at %s: %w", filename, err) } if err := cb(ctx, filename, fInfo); err != nil { return err } return nil }) } ================================================ FILE: pkg/emulators/storage/gcsemu/filestore_test.go ================================================ package gcsemu import ( "context" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "gotest.tools/v3/assert" ) func TestFileStore(t *testing.T) { // Setup an on-disk emulator. gcsDir := filepath.Join(os.TempDir(), fmt.Sprintf("gcsemu-test-%d", time.Now().Unix())) gcsEmu := NewGcsEmu(Options{ Store: NewFileStore(gcsDir), Verbose: true, Log: func(err error, fmt string, args ...interface{}) { t.Helper() if err != nil { fmt = "ERROR: " + fmt + ": %s" args = append(args, err) } t.Logf(fmt, args...) }, }) mux := http.NewServeMux() gcsEmu.Register(mux) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("about to method=%s host=%s u=%s", r.Method, r.Host, r.URL) mux.ServeHTTP(w, r) })) t.Cleanup(svr.Close) gcsClient, err := NewTestClientWithHost(context.Background(), svr.URL) assert.NilError(t, err) t.Cleanup(func() { _ = gcsClient.Close() }) bh := BucketHandle{ Name: "file-bucket", BucketHandle: gcsClient.Bucket("file-bucket"), } initBucket(t, bh) attrs, err := bh.Attrs(context.Background()) assert.NilError(t, err) assert.Equal(t, bh.Name, attrs.Name) t.Parallel() for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() tc.f(t, bh) }) } t.Run("RawHttp", func(t *testing.T) { t.Parallel() testRawHttp(t, bh, http.DefaultClient, svr.URL) }) } ================================================ FILE: pkg/emulators/storage/gcsemu/gcsemu.go ================================================ // Package gcsemu implements a Google Cloud Storage emulator for development. package gcsemu import ( "bytes" "compress/gzip" "context" "crypto/md5" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "sync/atomic" cloudstorage "cloud.google.com/go/storage" "encr.dev/pkg/emulators/storage/gcsutil" "github.com/bluele/gcache" "google.golang.org/api/storage/v1" ) const maybeNotImplementedErrorMsg = "This may be a valid request, but we haven't implemented it in gcsemu yet." // Options configure the emulator. type Options struct { // A storage layer to use; if nil, defaults to in-mem storage. Store Store // If true, log verbosely. Verbose bool // Optional log function. `err` will be `nil` for informational/debug messages. Log func(err error, fmt string, args ...interface{}) } // GcsEmu is a Google Cloud Storage emulator for development. type GcsEmu struct { // The directory which contains gcs emulation. store Store locks *gcsutil.TransientLockMap uploadIds gcache.Cache idCounter int32 verbose bool log func(err error, fmt string, args ...interface{}) } // NewGcsEmu creates a new Google Cloud Storage emulator. func NewGcsEmu(opts Options) *GcsEmu { if opts.Store == nil { opts.Store = NewMemStore() } if opts.Log == nil { opts.Log = func(_ error, _ string, _ ...interface{}) {} } return &GcsEmu{ store: opts.Store, locks: gcsutil.NewTransientLockMap(), uploadIds: gcache.New(1024).LRU().Build(), verbose: opts.Verbose, log: opts.Log, } } func lockName(bucket string, filename string) string { return bucket + "/" + filename } // Register the emulator's HTTP handlers on the given mux. func (g *GcsEmu) Register(mux *http.ServeMux) { mux.HandleFunc("/", DrainRequestHandler(GzipRequestHandler(g.Handler))) mux.HandleFunc("/batch/storage/v1", DrainRequestHandler(GzipRequestHandler(g.BatchHandler))) } // Handler handles emulated GCS http requests for "storage.googleapis.com". func (g *GcsEmu) Handler(w http.ResponseWriter, r *http.Request) { baseUrl := dontNeedUrls { host := requestHost(r) if host != "" { // Prepend the proto. if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { baseUrl = HttpBaseUrl("https://" + host + "/") } else { baseUrl = HttpBaseUrl("http://" + host + "/") } } } ctx := r.Context() p, ok := ParseGcsUrl(r.URL) if !ok { g.gapiError(w, http.StatusBadRequest, "unrecognized request") return } object := p.Object bucket := p.Bucket if err := r.ParseForm(); err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("failed to parse form: %s", err)) return } conds, err := parseConds(r.Form) if err != nil { g.gapiError(w, http.StatusBadRequest, err.Error()) return } if g.verbose { if object == "" { g.log(nil, "%s request for bucket %q", r.Method, bucket) } else { g.log(nil, "%s request for bucket %q, object %q", r.Method, bucket, object) } } switch r.Method { case "DELETE": g.handleGcsDelete(ctx, w, bucket, object, conds) case "GET": if object == "" { if strings.HasSuffix(r.URL.Path, "/o") { g.handleGcsListBucket(ctx, baseUrl, w, r.URL.Query(), bucket) } else { g.handleGcsMetadataRequest(baseUrl, w, bucket, object) } } else { alt := r.URL.Query().Get("alt") if alt == "media" || (p.IsPublic && alt == "") { g.handleGcsMediaRequest(baseUrl, w, r.Header.Get("Accept-Encoding"), bucket, object) } else if alt == "json" || (!p.IsPublic && alt == "") { g.handleGcsMetadataRequest(baseUrl, w, bucket, object) } else { // should never happen? g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("unsupported value for alt param to GET: %q\n%s", alt, maybeNotImplementedErrorMsg)) } } case "PATCH": alt := r.URL.Query().Get("alt") if alt == "json" || r.Header.Get("Content-Type") == "application/json" { g.handleGcsUpdateMetadataRequest(ctx, baseUrl, w, r, bucket, object, conds) } else { // should never happen? g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("unsupported value for alt param to PATCH: %q\n%s", alt, maybeNotImplementedErrorMsg)) } case "POST": if bucket == "" { g.handleGcsNewBucket(ctx, w, r, conds) } else if object == "" { g.handleGcsNewObject(ctx, baseUrl, w, r, bucket, conds) } else if strings.Contains(object, "/compose") { // TODO: enforce other conditions outside of generation g.handleGcsCompose(ctx, baseUrl, w, r, bucket, object, conds) } else if strings.Contains(object, "/rewriteTo/") { g.handleGcsCopy(ctx, baseUrl, w, bucket, object) } else if r.Form.Get("upload_id") != "" { g.handleGcsNewObjectResume(ctx, baseUrl, w, r, r.Form.Get("upload_id")) } else { // unsupported method, or maybe should never happen g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("unsupported POST request: %v\n%s", r.URL, maybeNotImplementedErrorMsg)) } case "PUT": if r.Form.Get("upload_id") != "" { g.handleGcsNewObjectResume(ctx, baseUrl, w, r, r.Form.Get("upload_id")) } else { // unsupported method, or maybe should never happen g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("unsupported PUT request: %v\n%s", r.URL, maybeNotImplementedErrorMsg)) } default: g.gapiError(w, http.StatusMethodNotAllowed, "") } } func (g *GcsEmu) handleGcsCompose(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket, object string, conds cloudstorage.Conditions) { var req storage.ComposeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { g.gapiError(w, http.StatusBadRequest, "bad compose request") return } // Get the composed object name from the path parts := strings.Split(object, "/compose") if len(parts) != 2 { g.gapiError(w, http.StatusBadRequest, "bad compose request") return } dst := composeObj{ filename: parts[0], conds: conds, } srcs := make([]composeObj, len(req.SourceObjects)) for i, sObj := range req.SourceObjects { var generationMatch int64 if sObj.ObjectPreconditions != nil { generationMatch = sObj.ObjectPreconditions.IfGenerationMatch } srcs[i] = composeObj{ filename: sObj.Name, conds: cloudstorage.Conditions{ GenerationMatch: generationMatch, }, } } var obj *storage.Object if err := g.locks.Run(ctx, lockName(bucket, dst.filename), func(_ context.Context) error { var err error obj, err = g.finishCompose(baseUrl, bucket, dst, srcs, req.Destination) return err }); err != nil { g.gapiError(w, httpStatusCodeOf(err), fmt.Sprintf("failed to compose objects: %s", err)) return } g.jsonRespond(w, &obj) } func (g *GcsEmu) handleGcsListBucket(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, params url.Values, bucket string) { delimiter := params.Get("delimiter") prefix := params.Get("prefix") pageToken := params.Get("pageToken") var cursor string if pageToken != "" { lastFilename, err := gcsutil.DecodePageToken(pageToken) if err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("invalid pageToken parameter (failed to decode) %s: %s", pageToken, err)) return } cursor = lastFilename } maxResults := 1000 maxResultsStr := params.Get("maxResults") if maxResultsStr != "" { var err error maxResults, err = strconv.Atoi(maxResultsStr) if err != nil || maxResults < 1 { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("invalid maxResults parameter: %s", maxResultsStr)) return } } g.makeBucketListResults(ctx, baseUrl, w, delimiter, cursor, prefix, bucket, maxResults) } func (g *GcsEmu) handleGcsDelete(ctx context.Context, w http.ResponseWriter, bucket string, filename string, conds cloudstorage.Conditions) { err := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error { // Find the existing file / meta. obj, err := g.store.GetMeta(dontNeedUrls, bucket, filename) if err != nil { return fmt.Errorf("failed to check existence of %s/%s: %w", bucket, filename, err) } if err := validateConds(obj, conds); err != nil { return err } if err := g.store.Delete(bucket, filename); err != nil { if os.IsNotExist(err) { return fmtErrorfCode(http.StatusNotFound, "%s/%s not found", bucket, filename) } return fmt.Errorf("failed to delete %s/%s: %w", bucket, filename, err) } return nil }) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } w.WriteHeader(http.StatusNoContent) } func (g *GcsEmu) handleGcsMediaRequest(baseUrl HttpBaseUrl, w http.ResponseWriter, acceptEncoding, bucket, filename string) { obj, contents, err := g.store.Get(baseUrl, bucket, filename) if err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to check existence of %s/%s: %s", bucket, filename, err)) return } if obj == nil { g.gapiError(w, http.StatusNotFound, fmt.Sprintf("%s/%s not found", bucket, filename)) return } w.Header().Set("Content-Type", obj.ContentType) w.Header().Set("X-Goog-Generation", strconv.FormatInt(obj.Generation, 10)) w.Header().Set("X-Goog-Metageneration", strconv.FormatInt(obj.Metageneration, 10)) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration") w.Header().Set("Content-Disposition", obj.ContentDisposition) if obj.ContentEncoding == "gzip" { if strings.Contains(acceptEncoding, "gzip") { w.Header().Set("Content-Encoding", "gzip") } else { // Uncompress on behalf of the client. buf := bytes.NewBuffer(contents) gzipReader, err := gzip.NewReader(buf) if err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to gunzip from %s/%s: %s", bucket, filename, err)) } if _, err := io.Copy(w, gzipReader); err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to copy+gunzip from %s/%s: %s", bucket, filename, err)) } if err := gzipReader.Close(); err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to copy+gunzip from %s/%s: %s", bucket, filename, err)) } return } } // Just write the contents w.Header().Set("Content-Length", strconv.Itoa(len(contents))) if _, err := w.Write(contents); err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to copy from %s/%s: %s", bucket, filename, err)) } } func (g *GcsEmu) handleGcsMetadataRequest(baseUrl HttpBaseUrl, w http.ResponseWriter, bucket string, filename string) { var obj interface{} var err error if filename == "" { var b *storage.Bucket b, err = g.store.GetBucketMeta(baseUrl, bucket) if b != nil { obj = b } } else { var o *storage.Object o, err = g.store.GetMeta(baseUrl, bucket, filename) if o != nil { obj = o } } if err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get meta for %s/%s: %s", bucket, filename, err)) return } if obj == nil { g.gapiError(w, http.StatusNotFound, fmt.Sprintf("%s/%s not found", bucket, filename)) return } g.jsonRespond(w, obj) } func (g *GcsEmu) handleGcsUpdateMetadataRequest(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket, filename string, conds cloudstorage.Conditions) { var obj *storage.Object err := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error { // Find the existing file / meta. var err error obj, err = g.store.GetMeta(baseUrl, bucket, filename) if err != nil { return fmt.Errorf("failed to check existence of %s/%s: %w", bucket, filename, err) } if obj == nil { return nil } if err := validateConds(obj, conds); err != nil { return err } // Update via json decode. metagen := obj.Metageneration err = json.NewDecoder(r.Body).Decode(&obj) if err != nil { return fmtErrorfCode(http.StatusBadRequest, "failed to parse request: %w", err) } if err := g.store.UpdateMeta(bucket, filename, obj, metagen+1); err != nil { return fmt.Errorf("failed to update attrs of %s/%s: %w", bucket, filename, err) } return nil }) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } if obj == nil { g.gapiError(w, http.StatusNotFound, fmt.Sprintf("%s/%s not found", bucket, filename)) return } // Respond with the updated metadata. obj, err = g.store.GetMeta(baseUrl, bucket, filename) if err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get meta for %s/%s: %s", bucket, filename, err)) return } g.jsonRespond(w, obj) } func (g *GcsEmu) handleGcsCopy(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, b1 string, objectPaths string) { // TODO(dk): this operation supports conditionals and metadata rewriting, but the emulator implementation currently does not. // See https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite parts := strings.Split(objectPaths, "/rewriteTo/b/") // Copy is implemented using the Rewrite API, with object strings of format /o/sourceObject/rewriteTo/b/destinationBucket/o/destinationObject if len(parts) != 2 { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("Bad rewrite request format: %s", objectPaths)) return } f1 := parts[0] destParts := strings.Split(parts[1], "/o/") if len(parts) != 2 { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("Bad rewrite request, expected object/file split: %s", parts[1])) return } b2 := destParts[0] f2 := destParts[1] // Must lock the destination object. var obj *storage.Object err := g.locks.Run(ctx, lockName(b2, f2), func(ctx context.Context) error { if ok, err := g.store.Copy(b1, f1, b2, f2); err != nil { return err } else if !ok { return nil // file missing } else { obj, err = g.store.GetMeta(baseUrl, b2, f2) return err } }) if err != nil { g.gapiError(w, httpStatusCodeOf(err), fmt.Sprintf("failed to copy: %s", err)) return } if obj == nil { g.gapiError(w, http.StatusNotFound, fmt.Sprintf("%s not found", b1+"/"+f1)) return } rr := storage.RewriteResponse{ Kind: "storage#rewriteResponse", TotalBytesRewritten: int64(obj.Size), ObjectSize: int64(obj.Size), Done: true, RewriteToken: "-not-implemented-", Resource: obj, } g.jsonRespond(w, &rr) } type uploadData struct { Object storage.Object Conds cloudstorage.Conditions data []byte } func (g *GcsEmu) handleGcsNewBucket(ctx context.Context, w http.ResponseWriter, r *http.Request, _ cloudstorage.Conditions) { var bucket storage.Bucket if err := json.NewDecoder(r.Body).Decode(&bucket); err != nil { g.gapiError(w, http.StatusBadRequest, "failed to parse body as json") return } bucketName := bucket.Name err := g.locks.Run(ctx, lockName(bucketName, ""), func(ctx context.Context) error { if err := g.store.CreateBucket(bucketName); err != nil { return fmt.Errorf("could not create bucket %s: %w", bucketName, err) } return nil }) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } g.jsonRespond(w, bucket) } func (g *GcsEmu) handleGcsNewObject(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket string, conds cloudstorage.Conditions) { switch r.Form.Get("uploadType") { case "media": // simple upload name := r.Form.Get("name") if name == "" { g.gapiError(w, http.StatusBadRequest, "missing object name") return } contents, err := io.ReadAll(r.Body) if err != nil { g.gapiError(w, http.StatusBadRequest, "failed to read body") return } obj := &storage.Object{ Bucket: bucket, ContentType: r.Header.Get("Content-Type"), Name: name, Size: uint64(len(contents)), } meta, err := g.finishUpload(ctx, baseUrl, obj, contents, bucket, conds) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } w.Header().Set("x-goog-generation", strconv.FormatInt(meta.Generation, 10)) w.Header().Set("X-Goog-Metageneration", strconv.FormatInt(meta.Metageneration, 10)) g.jsonRespond(w, meta) return case "resumable": var obj storage.Object if err := json.NewDecoder(r.Body).Decode(&obj); err != nil { g.gapiError(w, http.StatusBadRequest, "failed to parse body as json") return } obj.Bucket = bucket nextId := atomic.AddInt32(&g.idCounter, 1) id := strconv.Itoa(int(nextId)) _ = g.uploadIds.Set(id, &uploadData{ Object: obj, Conds: conds, }) w.Header().Set("Location", ObjectUrl(baseUrl, bucket, obj.Name)+"?upload_id="+id) w.Header().Set("Content-Type", obj.ContentType) w.WriteHeader(http.StatusCreated) return case "multipart": obj, contents, err := readMultipartInsert(r) if err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("failed to parse request: %s", err)) return } meta, err := g.finishUpload(ctx, baseUrl, obj, contents, bucket, conds) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } w.Header().Set("x-goog-generation", strconv.FormatInt(meta.Generation, 10)) w.Header().Set("X-Goog-Metageneration", strconv.FormatInt(meta.Metageneration, 10)) g.jsonRespond(w, meta) return default: // TODO g.gapiError(w, http.StatusNotImplemented, "not yet implemented") return } } func (g *GcsEmu) handleGcsNewObjectResume(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, id string) { found, err := g.uploadIds.GetIFPresent(id) if err != nil { g.gapiError(w, http.StatusInternalServerError, fmt.Sprintf("unexpected error: %s", err)) return } if found == nil { g.gapiError(w, http.StatusNotFound, "no such id") return } u := found.(*uploadData) contents, err := io.ReadAll(r.Body) if err != nil { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("failed to ready body: %s", err)) return } contentRange := r.Header.Get("Content-Range") if contentRange == "" { g.gapiError(w, http.StatusBadRequest, "expected Content-Range") return } // Parse the content range byteRange := parseByteRange(contentRange) if byteRange == nil { g.gapiError(w, http.StatusBadRequest, "malformed Content-Range header") return } if byteRange.lo == -1 && len(contents) != 0 || byteRange.lo != -1 && len(contents) != int(byteRange.hi+1-byteRange.lo) { g.gapiError(w, http.StatusBadRequest, fmt.Sprintf("Content-Range does not match content size: range=%v, len=%v", contentRange, len(contents))) return } if len(u.data) < int(byteRange.lo) { g.gapiError(w, http.StatusBadRequest, "missing content") return } // Apply the content to our stored data. if byteRange.lo != -1 { u.data = u.data[:byteRange.lo] // truncate a previous write if we've seen this range before } u.data = append(u.data, contents...) // Are we done? if byteRange.sz < 0 || len(u.data) < int(byteRange.sz) { // Not finished; save the contents and tell the client to resume. w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", len(u.data)-1)) w.Header().Set("Content-Type", u.Object.ContentType) if r.Header.Get("X-Guploader-No-308") == "yes" { w.Header().Set("X-Http-Status-Code-Override", "308") w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusPermanentRedirect) } return } // Done meta, err := g.finishUpload(ctx, baseUrl, &u.Object, u.data, u.Object.Bucket, u.Conds) if err != nil { g.gapiError(w, httpStatusCodeOf(err), err.Error()) return } g.uploadIds.Remove(id) w.Header().Set("x-goog-generation", strconv.FormatInt(meta.Generation, 10)) w.Header().Set("X-Goog-Metageneration", strconv.FormatInt(meta.Metageneration, 10)) g.jsonRespond(w, meta) } func (g *GcsEmu) finishUpload(ctx context.Context, baseUrl HttpBaseUrl, obj *storage.Object, contents []byte, bucket string, conds cloudstorage.Conditions) (*storage.Object, error) { filename := obj.Name bHash := md5.Sum(contents) contentHash := bHash[:] md5Hash := base64.StdEncoding.EncodeToString(contentHash) if obj.Md5Hash != "" { h, err := base64.StdEncoding.DecodeString(obj.Md5Hash) if err != nil { return nil, fmtErrorfCode(http.StatusBadRequest, "not a valid md5 hash: %w", err) } if !bytes.Equal(contentHash, h) { return nil, fmtErrorfCode(http.StatusBadRequest, "md5 hash %s != expected %s", obj.Md5Hash, md5Hash) } } obj.Md5Hash = md5Hash obj.Etag = strconv.Quote(md5Hash) err := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error { // Find the existing file / meta. existing, err := g.store.GetMeta(baseUrl, bucket, filename) if err != nil { return fmt.Errorf("failed to check existence of %s/%s: %w", bucket, filename, err) } if err := validateConds(existing, conds); err != nil { return err } if existing != nil { obj.TimeCreated = existing.TimeCreated } if err := g.store.Add(bucket, filename, contents, obj); err != nil { return fmt.Errorf("failed to create %s/%s: %w", bucket, filename, err) } return nil }) if err != nil { return nil, err } // respond with object metadata meta, err := g.store.GetMeta(baseUrl, bucket, filename) if err != nil { return nil, fmt.Errorf("failed to get meta for %s/%s: %w", bucket, filename, err) } meta.Id = fmt.Sprintf("%s/%s/%d", bucket, filename, meta.Generation) return meta, nil } // Returns true if item is strictly greater than anything that begins with prefix func greaterThanPrefix(item string, prefix string) bool { if len(item) < len(prefix) { return item > prefix } return item[:len(prefix)] > prefix } // Returns true if item is strictly less than anything that begins with prefix func lessThanPrefix(item string, prefix string) bool { if len(item) < len(prefix) { return item < prefix[:len(item)] } return item < prefix } var ( emptyConds = cloudstorage.Conditions{} doesNotExistConds = cloudstorage.Conditions{DoesNotExist: true} ) func validateConds(obj *storage.Object, cond cloudstorage.Conditions) error { if obj == nil { // The only way a nil object can succeed is if the conds are exactly equal to empty or doesNotExist if cond == emptyConds || cond == doesNotExistConds { return nil } return fmtErrorfCode(http.StatusPreconditionFailed, "precondition failed") } // obj != nil from here on if cond.DoesNotExist { return fmtErrorfCode(http.StatusPreconditionFailed, "precondition failed") } if cond.GenerationMatch != 0 && obj.Generation != cond.GenerationMatch { return fmtErrorfCode(http.StatusPreconditionFailed, "precondition failed") } if cond.GenerationNotMatch != 0 && obj.Generation == cond.GenerationNotMatch { // not-match failures use a different code return fmtErrorfCode(http.StatusNotModified, "precondition failed") } if cond.MetagenerationMatch != 0 && obj.Metageneration != cond.MetagenerationMatch { return fmtErrorfCode(http.StatusPreconditionFailed, "precondition failed") } if cond.MetagenerationNotMatch != 0 && obj.Metageneration == cond.MetagenerationNotMatch { // not-match failures use a different code return fmtErrorfCode(http.StatusNotModified, "precondition failed") } return nil } func parseConds(vals url.Values) (cloudstorage.Conditions, error) { var ret cloudstorage.Conditions for i, e := range []struct { paramName string ref *int64 }{ {"ifGenerationMatch", &ret.GenerationMatch}, {"ifGenerationNotMatch", &ret.GenerationNotMatch}, {"ifMetagenerationMatch", &ret.MetagenerationMatch}, {"ifMetagenerationNotMatch", &ret.MetagenerationNotMatch}, } { v := vals.Get(e.paramName) if v == "" { continue } val, err := strconv.ParseInt(v, 10, 64) if err != nil { return ret, fmt.Errorf("failed to parse %s=%s: %w", e.paramName, v, err) } *e.ref = val if i == 0 { // Special case ret.DoesNotExist = val == 0 } } return ret, nil } const ( gcsMaxComposeSources = 32 ) func (g *GcsEmu) finishCompose(baseUrl HttpBaseUrl, bucket string, dst composeObj, srcs []composeObj, meta *storage.Object) (*storage.Object, error) { if len(srcs) > gcsMaxComposeSources { return nil, fmtErrorfCode(http.StatusBadRequest, "too many sources") } // TODO: consider moving this to disk to handle very large compose operations var data []byte metas := make([]*storage.Object, len(srcs)) for i, src := range srcs { meta, contents, err := g.store.Get(baseUrl, bucket, src.filename) if err != nil { return nil, fmt.Errorf("failed to get object %s: %w", src.filename, err) } if meta == nil { return nil, fmtErrorfCode(http.StatusNotFound, "no such source object %s", src.filename) } if err := validateConds(meta, src.conds); err != nil { return nil, err } data = append(data, contents...) metas[i] = meta } for _, m := range metas { meta.ComponentCount += m.ComponentCount } // composite objects do not have an MD5 hash (https://cloud.google.com/storage/docs/composite-objects) meta.Md5Hash = "" dstMeta, err := g.store.GetMeta(baseUrl, bucket, dst.filename) if err != nil { return nil, fmt.Errorf("failed to get object %s: %w", dst.filename, err) } if err := validateConds(dstMeta, dst.conds); err != nil { return nil, err } if dstMeta != nil { meta.TimeCreated = dstMeta.TimeCreated } if err := g.store.Add(bucket, dst.filename, data, meta); err != nil { return nil, fmt.Errorf("failed to add new file: %w", err) } return g.store.GetMeta(baseUrl, bucket, dst.filename) } // InitBucket creates the given bucket directly. func (g *GcsEmu) InitBucket(bucketName string) error { return g.locks.Run(context.Background(), lockName(bucketName, ""), func(ctx context.Context) error { if err := g.store.CreateBucket(bucketName); err != nil { return fmt.Errorf("could not create bucket: %s: %w", bucketName, err) } return nil }) } ================================================ FILE: pkg/emulators/storage/gcsemu/gcsemu_test.go ================================================ package gcsemu import ( "context" "crypto/md5" "fmt" "io" "net/http" "strings" "testing" "time" "cloud.google.com/go/storage" "google.golang.org/api/googleapi" "google.golang.org/api/iterator" "gotest.tools/v3/assert" ) const ( invalidBucketName = "fullstory-non-existant-bucket" ) var ( testCases = []struct { name string f func(t *testing.T, bh BucketHandle) }{ {"Basics", testBasics}, {"MultipleFiles", testMultipleFiles}, {"HugeFile", testHugeFile}, {"HugeFile_MultipleOfChunkSize", testHugeFileMultipleOfChunkSize}, {"HugeFileWithConditional", testHugeFileWithConditional}, {"ConditionalUpdates", testConditionalUpdates}, {"GenNotMatchDoesntExist", testGenNotMatchDoesntExist}, {"CopyBasics", testCopyBasics}, {"Compose", testCompose}, {"CopyMetadata", testCopyMetadata}, {"CopyConditionals", testCopyConditionals}, } ) const ( v1 = `This file is for gcsemu_intg_test.go, please ignore (v1)` v2 = `This file is for gcsemu_intg_test.go, please ignore (this is version 2)` source1 = `This is source file number 1` source2 = `This is source file number 2` ) type BucketHandle struct { Name string *storage.BucketHandle } func initBucket(t *testing.T, bh BucketHandle) { ctx := context.Background() _ = bh.Delete(ctx) err := bh.Create(ctx, "dev", &storage.BucketAttrs{}) assert.NilError(t, err, "failed") attrs, err := bh.Attrs(ctx) assert.NilError(t, err, "failed") assert.Equal(t, bh.Name, attrs.Name, "wrong") } func testBasics(t *testing.T, bh BucketHandle) { const name = "gscemu-test/1.txt" ctx := context.Background() oh := bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. err := oh.Delete(ctx) if err != nil { assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } // Should not exist. _, err = oh.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Checker funcs checkAttrs := func(attrs *storage.ObjectAttrs, content string, metagen int64) { assert.Equal(t, name, attrs.Name, "wrong") assert.Equal(t, bh.Name, attrs.Bucket, "wrong") assert.Equal(t, int64(len(content)), attrs.Size, "wrong") assert.Equal(t, metagen, attrs.Metageneration, "wrong") checkSum := md5.Sum([]byte(content)) assert.DeepEqual(t, checkSum[:], attrs.MD5) } checkObject := func(content string, metagen int64) *storage.ObjectAttrs { attrs, err := oh.Attrs(ctx) assert.NilError(t, err, "failed") checkAttrs(attrs, content, metagen) r, err := oh.NewReader(ctx) assert.NilError(t, err, "failed") data, err := io.ReadAll(r) assert.NilError(t, err, "failed") assert.NilError(t, r.Close(), "failed") assert.Equal(t, content, string(data), "wrong data") return attrs } // Create the object. w := oh.NewWriter(ctx) assert.NilError(t, write(w, v1), "failed") checkAttrs(w.Attrs(), v1, 1) // Read the object. attrs := checkObject(v1, 1) assert.Assert(t, attrs.Generation != 0, "expected non-zero") gen := attrs.Generation // Update the object to version 2. Also test MD5 setting. w = oh.NewWriter(ctx) checkSum := md5.Sum([]byte(v2)) w.MD5 = checkSum[:] assert.NilError(t, write(w, v2), "failed") checkAttrs(w.Attrs(), v2, 1) assert.Assert(t, gen != w.Attrs().Generation, "expected different gen") gen = w.Attrs().Generation // Read the object again. attrs = checkObject(v2, 1) assert.Equal(t, gen, attrs.Generation, "expected same gen") // Update the attrs. attrs, err = oh.Update(ctx, storage.ObjectAttrsToUpdate{ ContentType: "text/plain", }) assert.NilError(t, err, "failed") checkAttrs(attrs, v2, 2) assert.Equal(t, "text/plain", attrs.ContentType, "wrong") assert.Equal(t, gen, attrs.Generation, "expected same gen") // Delete the object. assert.NilError(t, oh.Delete(ctx), "failed") // Should not exist. _, err = oh.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Should not be able to update attrs. _, err = oh.Update(ctx, storage.ObjectAttrsToUpdate{ ContentType: "text/plain", }) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } func testMultipleFiles(t *testing.T, bh BucketHandle) { dir := "multi-test/" ctx := context.Background() files := []string{"file1", "file2", "file3"} for _, f := range files { oh := bh.Object(dir + f) w := oh.NewWriter(ctx) assert.NilError(t, write(w, v1), "failed to write file %s", dir+f) } iter := bh.Objects(ctx, &storage.Query{Prefix: dir}) for _, f := range files { obj, err := iter.Next() assert.NilError(t, err, "failed to fetch next object") assert.Equal(t, dir+f, obj.Name, "wrong filename") } // No more objects should exist _, err := iter.Next() assert.Equal(t, iterator.Done, err, "iteration not finished or failed after first bucket object") } // Tests resumable GCS uploads. func testHugeFile(t *testing.T, bh BucketHandle) { doHugeFile(t, bh, "gscemu-test/huge.txt", googleapi.DefaultUploadChunkSize+4*1024*1024) } func testHugeFileMultipleOfChunkSize(t *testing.T, bh BucketHandle) { doHugeFile(t, bh, "gscemu-test/huge2.txt", googleapi.DefaultUploadChunkSize*4) } func doHugeFile(t *testing.T, bh BucketHandle, name string, size int) { ctx := context.Background() oh := bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. err := oh.Delete(ctx) if err != nil { assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } // Should not exist. _, err = oh.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Create the object. w := oh.NewWriter(ctx) hash, err := writeHugeObject(t, w, size) assert.NilError(t, err, "failed") attrs, err := oh.Attrs(ctx) assert.NilError(t, err, "failed") assert.Equal(t, size, int(attrs.Size), "wrong") assert.DeepEqual(t, hash, attrs.MD5) } func writeHugeObject(t *testing.T, w *storage.Writer, sz int) ([]byte, error) { data := []byte(`0123456789ABCDEF`) hash := md5.New() for i := 0; i < sz/len(data); i++ { n, err := w.Write(data) _, _ = hash.Write(data) assert.NilError(t, err, "failed") assert.Equal(t, n, len(data), "short write") } return hash.Sum(nil), w.Close() } // Tests resumable GCS uploads. func testHugeFileWithConditional(t *testing.T, bh BucketHandle) { const name = "gscemu-test/huge2.txt" const size = googleapi.DefaultUploadChunkSize*2 + 1024 ctx := context.Background() oh := bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. err := oh.Delete(ctx) if err != nil { assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } // Should not exist. _, err = oh.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Create the object. w := oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) hash, err := writeHugeObject(t, w, size) assert.NilError(t, err, "failed") attrs, err := oh.Attrs(ctx) assert.NilError(t, err, "failed") assert.Equal(t, size, int(attrs.Size), "wrong") assert.DeepEqual(t, hash, attrs.MD5) // Should fail this time. w = oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) _, err = writeHugeObject(t, w, size) assert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), "wrong error %T: %s", err, err) } func testConditionalUpdates(t *testing.T, bh BucketHandle) { const name = "gscemu-test/2.txt" ctx := context.Background() oh := bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. err := oh.Delete(ctx) if err != nil { assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } // Ensure write fails w := oh.If(storage.Conditions{GenerationMatch: 1}).NewWriter(ctx) err = write(w, "bogus") assert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), "wrong error %T: %s", err, err) // Now actually write it. w = oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) assert.NilError(t, write(w, v1), "failed") attrs := w.Attrs() t.Logf("attrs.Generation=%d attrs.Metageneration=%d", attrs.Generation, attrs.Metageneration) expectFailConds := func(expectCode int, conds storage.Conditions) { // Ensure attr update fails. _, err = oh.If(conds).Update(ctx, storage.ObjectAttrsToUpdate{ContentType: "text/plain"}) assert.Equal(t, expectCode, httpStatusCodeOf(err), "wrong error %T: %s", err, err) // Ensure write fails w := oh.If(conds).NewWriter(ctx) err = write(w, "bogus") assert.Equal(t, expectCode, httpStatusCodeOf(err), "wrong error %T: %s", err, err) // Ensure delete fails err = oh.If(conds).Delete(ctx) assert.Equal(t, expectCode, httpStatusCodeOf(err), "wrong error %T: %s", err, err) } for i, conds := range []storage.Conditions{ { DoesNotExist: true, }, { GenerationMatch: attrs.Generation + 1, }, { MetagenerationMatch: attrs.Metageneration + 1, }, { GenerationMatch: attrs.Generation, MetagenerationMatch: attrs.Metageneration + 1, }, { GenerationMatch: attrs.Generation + 1, MetagenerationMatch: attrs.Metageneration, }, { GenerationNotMatch: attrs.Generation, }, { MetagenerationNotMatch: attrs.Metageneration, }, { GenerationNotMatch: attrs.Generation, MetagenerationMatch: attrs.Metageneration, }, { GenerationMatch: attrs.Generation, MetagenerationNotMatch: attrs.Metageneration, }, } { t.Logf("case %d", i) expectCode := http.StatusPreconditionFailed if i >= 5 { // For some reason, "not match" cases return 304 rather than 412. expectCode = http.StatusNotModified } expectFailConds(expectCode, conds) } // Actually update the attrs. attrs, err = oh.If(storage.Conditions{ GenerationMatch: attrs.Generation, MetagenerationMatch: attrs.Metageneration, }).Update(ctx, storage.ObjectAttrsToUpdate{ ContentType: "text/plain", }) assert.NilError(t, err, "failed") // Actually update the content. w = oh.If(storage.Conditions{ GenerationMatch: attrs.Generation, MetagenerationMatch: attrs.Metageneration, }).NewWriter(ctx) assert.NilError(t, write(w, v2), "failed") attrs = w.Attrs() // Actually delete. err = oh.If(storage.Conditions{ GenerationMatch: attrs.Generation, MetagenerationMatch: attrs.Metageneration, }).Delete(ctx) assert.NilError(t, err, "failed") // Should not exist. _, err = oh.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } func testGenNotMatchDoesntExist(t *testing.T, bh BucketHandle) { // How does generation not match interact with a non-existent file? const name = "gscemu-test-gen-not-match.txt" ctx := context.Background() oh := bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. err := oh.Delete(ctx) if err != nil { assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") } // Write should fail on non-existent object, even though the generation doesn't match. w := oh.If(storage.Conditions{GenerationNotMatch: 1}).NewWriter(ctx) err = write(w, "bogus") assert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), "wrong error %T: %s", err, err) } func testCopyBasics(t *testing.T, bh BucketHandle) { ctx := context.Background() file1 := "file-1" file2 := "file-1-again" src := bh.Object(file1) dest := bh.Object(file2) // Forcibly delete the object at the start, make sure it doesn't exist. _ = src.Delete(ctx) _ = dest.Delete(ctx) // Should not exist. _, err := src.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Create the object. w := src.NewWriter(ctx) n, err := io.Copy(w, strings.NewReader(v1)) assert.NilError(t, err, "failed") assert.Equal(t, n, int64(len(v1)), "wrong length") assert.NilError(t, w.Close(), "failed") // Wait a ms to ensure different timestamps. time.Sleep(time.Millisecond) // Copy the object destAttrs, err := dest.CopierFrom(src).Run(ctx) assert.NilError(t, err, "failed to copy") // Read the object. r, err := dest.NewReader(ctx) assert.NilError(t, err, "failed") data, err := io.ReadAll(r) assert.NilError(t, err, "failed") assert.NilError(t, r.Close(), "failed") assert.Equal(t, string(data), v1, "wrong data") // Check the metadata reread correct reDestAttrs, err := dest.Attrs(ctx) assert.NilError(t, err, "failed") assert.DeepEqual(t, destAttrs, reDestAttrs) // Check the metadata was copied and makes sense. srcAttrs, err := src.Attrs(ctx) assert.NilError(t, err, "failed") expectAttrs := *srcAttrs // Some things should be different assert.Assert(t, srcAttrs.Name != destAttrs.Name, "should not equal: %s", destAttrs.Name) expectAttrs.Name = destAttrs.Name assert.Assert(t, srcAttrs.MediaLink != destAttrs.MediaLink, "should not equal: %s", destAttrs.MediaLink) expectAttrs.MediaLink = destAttrs.MediaLink assert.Assert(t, srcAttrs.Generation != destAttrs.Generation, "should not equal: %d", destAttrs.Generation) expectAttrs.Generation = destAttrs.Generation assert.Assert(t, srcAttrs.Created != destAttrs.Created, "should not equal: %s", destAttrs.Created) expectAttrs.Created = destAttrs.Created assert.Assert(t, srcAttrs.Updated != destAttrs.Updated, "should not equal: %s", destAttrs.Updated) expectAttrs.Updated = destAttrs.Updated expectAttrs.Etag = destAttrs.Etag // Rest should be same assert.DeepEqual(t, expectAttrs, *destAttrs) // Delete the object. assert.NilError(t, src.Delete(ctx), "failed") assert.NilError(t, dest.Delete(ctx), "failed") // Copy an object that doesn't exist _, err = dest.CopierFrom(src).Run(ctx) assert.Equal(t, http.StatusNotFound, httpStatusCodeOf(err), "wrong error %T: %s", err, err) } func testCompose(t *testing.T, bh BucketHandle) { ctx := context.Background() srcFiles := []string{source1, source2} srcGens := []int64{0, 0} manualCompose := "" dstName := "gcs-test-data/dest.txt" dstNameSecondary := "gcs-test-data/dest-secondary.txt" srcs := make([]*storage.ObjectHandle, len(srcFiles)) for i, src := range srcFiles { name := fmt.Sprintf("gcs-test-sources/src-%d.txt", i) srcs[i] = bh.Object(name) // Forcibly delete the object at the start, make sure it doesn't exist. _ = srcs[i].Delete(ctx) // Should not exist. _, err := srcs[i].Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") // Create the object. w := srcs[i].NewWriter(ctx) w.ContentType = "text/csv" n, err := io.Copy(w, strings.NewReader(src)) assert.NilError(t, err, "failed") assert.Equal(t, n, int64(len(src)), "wrong length") assert.NilError(t, w.Close(), "failed") srcGens[i] = w.Attrs().Generation manualCompose += src } dest := bh.Object(dstName) destSecondary := bh.Object(dstNameSecondary) err := destSecondary.Delete(ctx) // this one needs to be deleted to start assert.Assert(t, err == nil || err == storage.ErrObjectNotExist, "failed to delete secondary") composer := dest.ComposerFrom(srcs...) composer.ContentType = "text/plain" attrs, err := composer.Run(ctx) assert.NilError(t, err, "failed to run compose") assert.Equal(t, dest.BucketName(), attrs.Bucket, "bucket doesn't match") assert.Equal(t, dest.ObjectName(), attrs.Name, "object name doesn't match") assert.Equal(t, "text/plain", attrs.ContentType, "content type doesn't match") r, err := dest.NewReader(ctx) assert.NilError(t, err, "failed to create reader for composed file") data, err := io.ReadAll(r) assert.NilError(t, err, "failed to read from composed file") assert.NilError(t, r.Close(), "failed to close composed file reader") assert.Equal(t, manualCompose, string(data), "content doesn't match") // Issue the same request with incorrect generation on a source. composer = dest.If(storage.Conditions{GenerationMatch: attrs.Generation}).ComposerFrom( srcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}), // correct srcs[1].If(storage.Conditions{GenerationMatch: srcGens[1] + 1})) // incorrect composer.ContentType = "text/plain" _, err = composer.Run(ctx) assert.ErrorContains(t, err, "googleapi: Error 412") assert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), "expected precondition failed") // Issue the same request with incorrect generation on the destination. composer = dest.If(storage.Conditions{DoesNotExist: true}).ComposerFrom( srcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}), srcs[1].If(storage.Conditions{GenerationMatch: srcGens[1]})) composer.ContentType = "text/plain" _, err = composer.Run(ctx) assert.ErrorContains(t, err, "googleapi: Error 412") assert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), "expected precondition failed") // Issue the a request does not exist destination. composer = destSecondary.If(storage.Conditions{DoesNotExist: true}).ComposerFrom( srcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}), srcs[1].If(storage.Conditions{GenerationMatch: srcGens[1]})) composer.ContentType = "text/plain" _, err = composer.Run(ctx) assert.NilError(t, err, "failed to run compose") // The resulting data should be correct (like in the original test). r, err = destSecondary.NewReader(ctx) assert.NilError(t, err, "failed to create reader for composed file") data, err = io.ReadAll(r) assert.NilError(t, err, "failed to read from composed file") assert.NilError(t, r.Close(), "failed to close composed file reader") assert.Equal(t, manualCompose, string(data), "content doesn't match") // Use the new destination as the source for another compose. This is how we append // Additionally, use generation conditions for all of the source objects. composer = dest.ComposerFrom( dest.If(storage.Conditions{GenerationMatch: attrs.Generation}), srcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]})) newAttrs, err := composer.Run(ctx) assert.NilError(t, err, "failed to run compose") assert.Equal(t, "", newAttrs.ContentType, "content type doesn't match") r, err = dest.NewReader(ctx) assert.NilError(t, err, "failed to create reader for composed file") data, err = io.ReadAll(r) assert.NilError(t, err, "failed to read from composed file") assert.NilError(t, r.Close(), "failed to close composed file reader") assert.Equal(t, manualCompose+source1, string(data), "content doesn't match") // Make sure we get a 404 if the source doesn't exist dneObj := bh.Object("dneObject") _ = dneObj.Delete(ctx) // Should not exist. _, err = dneObj.Attrs(ctx) assert.Equal(t, storage.ErrObjectNotExist, err, "wrong error") composer = dest.ComposerFrom(dneObj) _, err = composer.Run(ctx) assert.Equal(t, http.StatusNotFound, httpStatusCodeOf(err), "wrong error returned") } func testCopyMetadata(t *testing.T, bh BucketHandle) { // TODO(dk): Metadata-rewriting on copy is not currently implemented. t.Skip() } func testCopyConditionals(t *testing.T, bh BucketHandle) { // TODO(dk): Conditional support for copy is not currently implemented. t.Skip() } func write(w *storage.Writer, content string) error { n, err := io.Copy(w, strings.NewReader(content)) if err != nil { return err } if n != int64(len(content)) { panic("not all content sent") } return w.Close() } ================================================ FILE: pkg/emulators/storage/gcsemu/http_wrappers.go ================================================ package gcsemu import ( "compress/gzip" "io" "net/http" ) // DrainRequestHandler wraps the given handler to drain the incoming request body on exit. func DrainRequestHandler(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { // Always drain and close the request body to properly free up the connection. // See https://groups.google.com/forum/#!topic/golang-nuts/pP3zyUlbT00 _, _ = io.Copy(io.Discard, r.Body) _ = r.Body.Close() }() h(w, r) } } // GzipRequestHandler wraps the given handler to automatically decompress gzipped content. func GzipRequestHandler(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Encoding") == "gzip" { gzr, err := gzip.NewReader(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } r.Body = gzr } h(w, r) } } ================================================ FILE: pkg/emulators/storage/gcsemu/memstore.go ================================================ package gcsemu import ( "context" "os" "sync" "time" "fmt" "github.com/google/btree" "google.golang.org/api/storage/v1" ) type memstore struct { mu sync.RWMutex buckets map[string]*memBucket } var _ Store = (*memstore)(nil) // NewMemStore returns a Store that operates purely in memory. func NewMemStore() *memstore { return &memstore{buckets: map[string]*memBucket{}} } type memBucket struct { created time.Time // mutex required (despite lock map in gcsemu), because btree mutations are not structurally safe mu sync.RWMutex files *btree.BTree } func (ms *memstore) getBucket(bucket string) *memBucket { ms.mu.RLock() defer ms.mu.RUnlock() return ms.buckets[bucket] } type memFile struct { meta storage.Object data []byte } func (mf *memFile) Less(than btree.Item) bool { // TODO(dragonsinth): is a simple lexical sort ok for Walk? return mf.meta.Name < than.(*memFile).meta.Name } var _ btree.Item = (*memFile)(nil) func (ms *memstore) CreateBucket(bucket string) error { ms.mu.Lock() defer ms.mu.Unlock() if ms.buckets[bucket] == nil { ms.buckets[bucket] = &memBucket{ created: time.Now(), files: btree.New(16), } } return nil } func (ms *memstore) GetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error) { if b := ms.getBucket(bucket); b != nil { obj := BucketMeta(baseUrl, bucket) obj.Updated = b.created.UTC().Format(time.RFC3339Nano) return obj, nil } return nil, nil } func (ms *memstore) Get(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error) { f := ms.find(bucket, filename) if f != nil { return &f.meta, f.data, nil } return nil, nil, nil } func (ms *memstore) GetMeta(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, error) { f := ms.find(bucket, filename) if f != nil { meta := f.meta InitMetaWithUrls(baseUrl, &meta, bucket, filename, uint64(len(f.data))) return &meta, nil } return nil, nil } func (ms *memstore) Add(bucket string, filename string, contents []byte, meta *storage.Object) error { _ = ms.CreateBucket(bucket) InitScrubbedMeta(meta, filename) meta.Metageneration = 1 // Cannot be overridden by caller now := time.Now().UTC() meta.Updated = now.UTC().Format(time.RFC3339Nano) meta.Generation = now.UnixNano() if meta.TimeCreated == "" { meta.TimeCreated = meta.Updated } meta.Id = fmt.Sprintf("%s/%s/%d", bucket, filename, meta.Generation) meta.Etag = fmt.Sprintf("%d", meta.Generation) b := ms.getBucket(bucket) b.mu.Lock() defer b.mu.Unlock() b.files.ReplaceOrInsert(&memFile{ meta: *meta, data: contents, }) return nil } func (ms *memstore) UpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error { f := ms.find(bucket, filename) if f == nil { return os.ErrNotExist } InitScrubbedMeta(meta, filename) meta.Metageneration = metagen b := ms.getBucket(bucket) b.mu.Lock() defer b.mu.Unlock() b.files.ReplaceOrInsert(&memFile{ meta: *meta, data: f.data, }) return nil } func (ms *memstore) Copy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error) { src := ms.find(srcBucket, srcFile) if src == nil { return false, nil } // Copy with metadata meta := src.meta meta.TimeCreated = "" // reset creation time on the dest file err := ms.Add(dstBucket, dstFile, src.data, &meta) if err != nil { return false, err } return true, nil } func (ms *memstore) Delete(bucket string, filename string) error { if filename == "" { // Remove the bucket ms.mu.Lock() defer ms.mu.Unlock() if _, ok := ms.buckets[bucket]; !ok { return os.ErrNotExist } delete(ms.buckets, bucket) } else if b := ms.getBucket(bucket); b != nil { // Remove just the file b.mu.Lock() defer b.mu.Unlock() if b.files.Delete(ms.key(filename)) == nil { // case file does not exist return os.ErrNotExist } } else { return os.ErrNotExist } return nil } func (ms *memstore) ReadMeta(baseUrl HttpBaseUrl, bucket string, filename string, _ os.FileInfo) (*storage.Object, error) { return ms.GetMeta(baseUrl, bucket, filename) } func (ms *memstore) Walk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error { if b := ms.getBucket(bucket); b != nil { var err error b.mu.RLock() defer b.mu.RUnlock() b.files.Ascend(func(i btree.Item) bool { mf := i.(*memFile) err = cb(ctx, mf.meta.Name, nil) return err == nil }) return nil } return os.ErrNotExist } func (ms *memstore) key(filename string) btree.Item { return &memFile{ meta: storage.Object{ Name: filename, }, } } func (ms *memstore) find(bucket string, filename string) *memFile { if b := ms.getBucket(bucket); b != nil { b.mu.Lock() defer b.mu.Unlock() f := b.files.Get(ms.key(filename)) if f != nil { return f.(*memFile) } } return nil } ================================================ FILE: pkg/emulators/storage/gcsemu/memstore_test.go ================================================ package gcsemu import ( "context" "net/http" "net/http/httptest" "testing" "gotest.tools/v3/assert" ) func TestMemStore(t *testing.T) { // Setup an in-memory emulator. gcsEmu := NewGcsEmu(Options{ Verbose: true, Log: func(err error, fmt string, args ...interface{}) { t.Helper() if err != nil { fmt = "ERROR: " + fmt + ": %s" args = append(args, err) } t.Logf(fmt, args...) }, }) mux := http.NewServeMux() gcsEmu.Register(mux) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("about to method=%s host=%s u=%s", r.Method, r.Host, r.URL) mux.ServeHTTP(w, r) })) t.Cleanup(svr.Close) gcsClient, err := NewTestClientWithHost(context.Background(), svr.URL) assert.NilError(t, err) t.Cleanup(func() { _ = gcsClient.Close() }) bh := BucketHandle{ Name: "mem-bucket", BucketHandle: gcsClient.Bucket("mem-bucket"), } initBucket(t, bh) attrs, err := bh.Attrs(context.Background()) assert.NilError(t, err) assert.Equal(t, bh.Name, attrs.Name) t.Parallel() for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() tc.f(t, bh) }) } t.Run("RawHttp", func(t *testing.T) { t.Parallel() testRawHttp(t, bh, http.DefaultClient, svr.URL) }) } ================================================ FILE: pkg/emulators/storage/gcsemu/meta.go ================================================ package gcsemu import ( "fmt" "mime" "strings" "google.golang.org/api/storage/v1" ) // BucketMeta returns a default bucket metadata for the given name and base url. func BucketMeta(baseUrl HttpBaseUrl, bucket string) *storage.Bucket { return &storage.Bucket{ Kind: "storage#bucket", Name: bucket, SelfLink: BucketUrl(baseUrl, bucket), StorageClass: "STANDARD", } } // InitScrubbedMeta "bakes" metadata with intrinsic values and removes fields that are intrinsic / computed. func InitScrubbedMeta(meta *storage.Object, filename string) { parts := strings.Split(filename, ".") ext := parts[len(parts)-1] if meta.ContentType == "" { meta.ContentType = mime.TypeByExtension(ext) } meta.Name = filename ScrubMeta(meta) } // InitMetaWithUrls "bakes" metadata with intrinsic values, including computed links. func InitMetaWithUrls(baseUrl HttpBaseUrl, meta *storage.Object, bucket string, filename string, size uint64) { parts := strings.Split(filename, ".") ext := parts[len(parts)-1] meta.Bucket = bucket if meta.ContentType == "" { meta.ContentType = mime.TypeByExtension(ext) } meta.Kind = "storage#object" meta.MediaLink = ObjectUrl(baseUrl, bucket, filename) + "?alt=media" meta.Name = filename meta.SelfLink = ObjectUrl(baseUrl, bucket, filename) meta.Size = size meta.StorageClass = "STANDARD" } // ScrubMeta removes fields that are intrinsic / computed for minimal storage. func ScrubMeta(meta *storage.Object) { meta.Bucket = "" meta.Kind = "" meta.MediaLink = "" meta.SelfLink = "" meta.Size = 0 meta.StorageClass = "" } // BucketUrl returns the URL for a bucket. func BucketUrl(baseUrl HttpBaseUrl, bucket string) string { return fmt.Sprintf("%sstorage/v1/b/%s", normalizeBaseUrl(baseUrl), bucket) } // ObjectUrl returns the URL for a file. func ObjectUrl(baseUrl HttpBaseUrl, bucket string, filepath string) string { return fmt.Sprintf("%sstorage/v1/b/%s/o/%s", normalizeBaseUrl(baseUrl), bucket, filepath) } // HttpBaseUrl represents the emulator base URL, including trailing slash; e.g. https://www.googleapis.com/ type HttpBaseUrl string // when the caller doesn't really care about the object meta URLs const dontNeedUrls = HttpBaseUrl("") func normalizeBaseUrl(baseUrl HttpBaseUrl) HttpBaseUrl { if baseUrl == dontNeedUrls || baseUrl == "https://storage.googleapis.com/" { return "https://www.googleapis.com/" } else if baseUrl == "http://storage.googleapis.com/" { return "http://www.googleapis.com/" } else { return baseUrl } } ================================================ FILE: pkg/emulators/storage/gcsemu/multipart.go ================================================ package gcsemu import ( "encoding/json" "fmt" "io" "mime" "mime/multipart" "net/http" "google.golang.org/api/storage/v1" ) func readMultipartInsert(r *http.Request) (*storage.Object, []byte, error) { v := r.Header.Get("Content-Type") if v == "" { return nil, nil, fmt.Errorf("failed to parse Content-Type header: %q", v) } d, params, err := mime.ParseMediaType(v) if err != nil || d != "multipart/related" { return nil, nil, fmt.Errorf("failed to parse Content-Type header: %q", v) } boundary, ok := params["boundary"] if !ok { return nil, nil, fmt.Errorf("Content-Type header is missing boundary: %q", v) } reader := multipart.NewReader(r.Body, boundary) readPart := func() ([]byte, error) { part, err := reader.NextPart() if err != nil { return nil, fmt.Errorf("failed to get multipart: %w", err) } b, err := io.ReadAll(part) if err != nil { return nil, fmt.Errorf("failed to get read multipart: %w", err) } return b, nil } // read the first part to get the storage.Object (in json) b, err := readPart() if err != nil { return nil, nil, fmt.Errorf("failed to read first part of body: %w", err) } var obj storage.Object err = json.Unmarshal(b, &obj) if err != nil { return nil, nil, fmt.Errorf("failed to parse body as json: %w", err) } // read the next part to get the file contents contents, err := readPart() if err != nil { return nil, nil, fmt.Errorf("failed to read second part of body: %w", err) } obj.Size = uint64(len(contents)) return &obj, contents, nil } ================================================ FILE: pkg/emulators/storage/gcsemu/parse.go ================================================ package gcsemu import ( "net/url" "regexp" ) const ( // example: "/storage/v1/b/my-bucket/o/2013-tax-returns.pdf" (for a file) or "/storage/v1/b/my-bucket/o" (for a bucket) gcsObjectPathPattern = "/storage/v1/b/([^\\/]+)/o(?:/(.+))?" // example: "//b/my-bucket/o/2013-tax-returns.pdf" (for a file) or "/b/my-bucket/o" (for a bucket) gcsObjectPathPattern2 = "/b/([^\\/]+)/o(?:/(.+))?" // example: "/storage/v1/b/my-bucket gcsBucketPathPattern = "/storage/v1/b(?:/([^\\/]+))?" // example: "/my-bucket/2013-tax-returns.pdf" (for a file) gcsStoragePathPattern = "/([^\\/]+)/(.+)" ) var ( gcsObjectPathRegex = regexp.MustCompile(gcsObjectPathPattern) gcsObjectPathRegex2 = regexp.MustCompile(gcsObjectPathPattern2) gcsBucketPathRegex = regexp.MustCompile(gcsBucketPathPattern) gcsStoragePathRegex = regexp.MustCompile(gcsStoragePathPattern) ) // GcsParams represent a parsed GCS url. type GcsParams struct { Bucket string Object string IsPublic bool } // ParseGcsUrl parses a GCS url. func ParseGcsUrl(u *url.URL) (*GcsParams, bool) { if g, ok := parseGcsUrl(gcsObjectPathRegex, u); ok { return g, true } if g, ok := parseGcsUrl(gcsBucketPathRegex, u); ok { return g, true } if g, ok := parseGcsUrl(gcsObjectPathRegex2, u); ok { return g, true } if g, ok := parseGcsUrl(gcsStoragePathRegex, u); ok { g.IsPublic = true return g, true } return nil, false } func parseGcsUrl(re *regexp.Regexp, u *url.URL) (*GcsParams, bool) { submatches := re.FindStringSubmatch(u.Path) if submatches == nil { return nil, false } g := &GcsParams{} if len(submatches) > 1 { g.Bucket = submatches[1] } if len(submatches) > 2 { g.Object = submatches[2] } return g, true } ================================================ FILE: pkg/emulators/storage/gcsemu/range.go ================================================ package gcsemu import ( "strconv" "strings" ) type byteRange struct { lo, hi, sz int64 } func parseByteRange(in string) *byteRange { var err error if !strings.HasPrefix(in, "bytes ") { return nil } in = strings.TrimPrefix(in, "bytes ") parts := strings.Split(in, "/") if len(parts) != 2 { return nil } ret := byteRange{ lo: -1, hi: -1, sz: -1, } if parts[0] != "*" { parts := strings.Split(parts[0], "-") if len(parts) != 2 { return nil } ret.lo, err = strconv.ParseInt(parts[0], 10, 64) if err != nil { return nil } ret.hi, err = strconv.ParseInt(parts[1], 10, 64) if err != nil { return nil } } if parts[1] != "*" { ret.sz, err = strconv.ParseInt(parts[1], 10, 64) if err != nil { return nil } } return &ret } ================================================ FILE: pkg/emulators/storage/gcsemu/range_test.go ================================================ package gcsemu import ( "testing" "gotest.tools/v3/assert" ) func TestParseByteRange(t *testing.T) { tcs := []struct { in string expect byteRange }{ {in: "bytes 0-8388607/*", expect: byteRange{lo: 0, hi: 8388607, sz: -1}}, {in: "bytes 8388608-10485759/10485760", expect: byteRange{lo: 8388608, hi: 10485759, sz: 10485760}}, {in: "bytes */10485760", expect: byteRange{lo: -1, hi: -1, sz: 10485760}}, } for _, tc := range tcs { t.Logf("test case: %s", tc.in) assert.Equal(t, tc.expect, *parseByteRange(tc.in)) } } ================================================ FILE: pkg/emulators/storage/gcsemu/raw_http_test.go ================================================ package gcsemu import ( "bufio" "bytes" "context" "encoding/json" "fmt" api "google.golang.org/api/storage/v1" "gotest.tools/v3/assert" "io" "mime" "mime/multipart" "net/http" "net/http/httputil" "net/textproto" "strings" "testing" ) func testRawHttp(t *testing.T, bh BucketHandle, httpClient *http.Client, url string) { const name = "gscemu-test3.txt" const name2 = "gscemu-test4.txt" const delName = "gscemu-test-deletion.txt" // used for successful deletion const delName2 = "gscemu-test-deletion-2.txt" // used for not found deletion expectMetaGen := int64(1) tcs := []struct { name string makeRequest func(*testing.T) *http.Request checkResponse func(*testing.T, *http.Response) }{ { name: "rawGetObject", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/download/storage/v1/b/%s/o/%s?alt=media", url, bh.Name, name) t.Log(u) req, err := http.NewRequest("GET", u, nil) assert.NilError(t, err) return req }, checkResponse: func(t *testing.T, rsp *http.Response) { body, err := io.ReadAll(rsp.Body) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) assert.Equal(t, v1, string(body)) }, }, { name: "rawGetMeta", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", url, bh.Name, name) t.Log(u) req, err := http.NewRequest("GET", u, nil) assert.NilError(t, err) return req }, checkResponse: func(t *testing.T, rsp *http.Response) { body, err := io.ReadAll(rsp.Body) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) var attrs api.Object err = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs) assert.NilError(t, err) assert.Equal(t, name, attrs.Name) assert.Equal(t, bh.Name, attrs.Bucket) assert.Equal(t, uint64(len(v1)), attrs.Size) assert.Equal(t, expectMetaGen, attrs.Metageneration) }, }, { name: "rawPatchMeta", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", url, bh.Name, name) t.Log(u) req, err := http.NewRequest("PATCH", u, strings.NewReader(`{"metadata": {"type": "tabby"}}`)) assert.NilError(t, err) req.Header.Set("Content-Type", "application/json") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { body, err := io.ReadAll(rsp.Body) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) expectMetaGen++ var attrs api.Object err = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs) assert.NilError(t, err) assert.Equal(t, name, attrs.Name) assert.Equal(t, bh.Name, attrs.Bucket) assert.Equal(t, uint64(len(v1)), attrs.Size) assert.Equal(t, expectMetaGen, attrs.Metageneration) assert.Equal(t, "tabby", attrs.Metadata["type"]) }, }, { name: "rawDeleteObject-Success", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", url, bh.Name, delName) t.Log(u) req, err := http.NewRequest("DELETE", u, nil) assert.NilError(t, err) req.Header.Set("Content-Type", "text/plain") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { assert.Equal(t, http.StatusNoContent, rsp.StatusCode) }, }, { name: "rawDeleteObject-ObjectNotFound", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", url, bh.Name, delName2) t.Log(u) req, err := http.NewRequest("DELETE", u, nil) assert.NilError(t, err) req.Header.Set("Content-Type", "text/plain") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { assert.Equal(t, http.StatusNotFound, rsp.StatusCode) }, }, { name: "rawDeleteObject-BucketNotFound", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", url, invalidBucketName, delName) t.Log(u) req, err := http.NewRequest("DELETE", u, nil) assert.NilError(t, err) req.Header.Set("Content-Type", "text/plain") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { assert.Equal(t, http.StatusNotFound, rsp.StatusCode) }, }, { name: "rawDeleteBucket-BucketNotFound", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/storage/v1/b/%s", url, invalidBucketName) t.Log(u) req, err := http.NewRequest("DELETE", u, nil) assert.NilError(t, err) req.Header.Set("Content-Type", "text/plain") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { assert.Equal(t, http.StatusNotFound, rsp.StatusCode) }, }, { name: "rawUpload", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/upload/storage/v1/b/%s/o?uploadType=media&name=%s", url, bh.Name, name2) t.Log(u) req, err := http.NewRequest("POST", u, strings.NewReader(v2)) assert.NilError(t, err) req.Header.Set("Content-Type", "text/plain") return req }, checkResponse: func(t *testing.T, rsp *http.Response) { body, err := io.ReadAll(rsp.Body) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) var attrs api.Object err = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs) assert.NilError(t, err) assert.Equal(t, name2, attrs.Name) assert.Equal(t, bh.Name, attrs.Bucket) assert.Equal(t, uint64(len(v2)), attrs.Size) assert.Equal(t, int64(1), attrs.Metageneration) }, }, { name: "publicUrl", makeRequest: func(t *testing.T) *http.Request { u := fmt.Sprintf("%s/%s/%s?alt=media", url, bh.Name, name) t.Log(u) req, err := http.NewRequest("GET", u, nil) assert.NilError(t, err) return req }, checkResponse: func(t *testing.T, rsp *http.Response) { body, err := io.ReadAll(rsp.Body) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) assert.Equal(t, v1, string(body)) }, }, } ctx := context.Background() oh := bh.Object(name) // Create the object 1. w := oh.NewWriter(ctx) assert.NilError(t, write(w, v1)) // Make sure object 2 is not there. _ = bh.Object(name2).Delete(ctx) // batch setup // Create the object for successful deletion. w = bh.Object(delName).NewWriter(ctx) assert.NilError(t, write(w, v1)) // Make sure object for not found deletion is not there. _ = bh.Object(delName2).Delete(ctx) // Run each test individually. for _, tc := range tcs { tc := tc t.Run(tc.name, func(t *testing.T) { req := tc.makeRequest(t) rsp, err := httpClient.Do(req) assert.NilError(t, err) body, err := httputil.DumpResponse(rsp, true) assert.NilError(t, err) t.Log(string(body)) tc.checkResponse(t, rsp) }) } // batch setup again for batch deletion step // Create the object for successful deletion. w = bh.Object(delName).NewWriter(ctx) assert.NilError(t, write(w, v1)) // Make sure object for not found deletion is not there. _ = bh.Object(delName2).Delete(ctx) // Batch requests don't support upload and download, only metadata stuff. t.Run("batch", func(t *testing.T) { var buf bytes.Buffer w := multipart.NewWriter(&buf) // Only use the [second, fifth] requests. batchTcs := tcs[1:6] for i, tc := range batchTcs { req := tc.makeRequest(t) req.Host = "" req.URL.Host = "" p, _ := w.CreatePart(textproto.MIMEHeader{ "Content-Type": []string{"application/http"}, "Content-Transfer-Encoding": []string{"binary"}, "Content-ID": []string{fmt.Sprintf("", i)}, }) buf, err := httputil.DumpRequest(req, true) assert.NilError(t, err) _, _ = p.Write(buf) } _ = w.Close() // Compile the request req, err := http.NewRequest("POST", fmt.Sprintf("%s/batch/storage/v1", url), &buf) assert.NilError(t, err) req.Header.Set("Content-Type", "multipart/mixed; boundary="+w.Boundary()) body, err := httputil.DumpRequest(req, true) assert.NilError(t, err) t.Log(string(body)) rsp, err := httpClient.Do(req) assert.NilError(t, err) assert.Equal(t, http.StatusOK, rsp.StatusCode) body, err = httputil.DumpResponse(rsp, true) assert.NilError(t, err) t.Log(string(body)) // decode the multipart response v := rsp.Header.Get("Content-type") assert.Check(t, v != "") d, params, err := mime.ParseMediaType(v) assert.NilError(t, err) assert.Equal(t, "multipart/mixed", d) boundary, ok := params["boundary"] assert.Check(t, ok) r := multipart.NewReader(rsp.Body, boundary) for i, tc := range batchTcs { part, err := r.NextPart() assert.NilError(t, err) assert.Equal(t, "application/http", part.Header.Get("Content-Type")) assert.Equal(t, fmt.Sprintf("", i), part.Header.Get("Content-ID")) b, err := io.ReadAll(part) assert.NilError(t, err) // Decode the buffer into an http.Response rsp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(b)), nil) assert.NilError(t, err) tc.checkResponse(t, rsp) } }) } ================================================ FILE: pkg/emulators/storage/gcsemu/remote_test.go ================================================ package gcsemu import ( "context" "os" "testing" "cloud.google.com/go/storage" "golang.org/x/oauth2/google" "gotest.tools/v3/assert" ) func TestRealStore(t *testing.T) { bucket := os.Getenv("BUCKET_ID") if bucket == "" { t.Skip("BUCKET_ID must be set to run this") } ctx := context.Background() gcsClient, err := storage.NewClient(ctx) assert.NilError(t, err) t.Cleanup(func() { _ = gcsClient.Close() }) bh := BucketHandle{ Name: bucket, BucketHandle: gcsClient.Bucket(bucket), } // Instead of `initBucket`, just check that it exists. _, err = bh.Attrs(ctx) assert.NilError(t, err) t.Parallel() for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() tc.f(t, bh) }) } httpClient, err := google.DefaultClient(context.Background()) assert.NilError(t, err) t.Run("RawHttp", func(t *testing.T) { t.Parallel() testRawHttp(t, bh, httpClient, "https://storage.googleapis.com") }) } ================================================ FILE: pkg/emulators/storage/gcsemu/server.go ================================================ package gcsemu import ( "fmt" "net" "net/http" "net/http/httptest" "strings" ) // Server is an in-memory Cloud Storage emulator; it is unauthenticated, and only a rough approximation. type Server struct { Addr string *httptest.Server *GcsEmu } // NewServer creates a new Server with the given options. // The Server will be listening for HTTP connections, without TLS, // on the provided address. The resolved address is named by the Addr field. // An address with a port of 0 will bind to an open port on the system. // // For running a full in-process setup (e.g. unit tests), initialize // os.Setenv("GCS_EMULATOR_HOST", srv.Addr) so that subsequent calls to NewClient() // will return an in-process targeted storage client. func NewServer(laddr string, opts Options) (*Server, error) { gcsEmu := NewGcsEmu(opts) mux := http.NewServeMux() gcsEmu.Register(mux) srv := httptest.NewUnstartedServer(mux) l, err := net.Listen("tcp", laddr) if err != nil { return nil, fmt.Errorf("failed to listen on addr %s: %w", laddr, err) } srv.Listener = l srv.Start() return &Server{ Addr: strings.TrimPrefix(srv.URL, "http://"), Server: srv, GcsEmu: gcsEmu, }, nil } ================================================ FILE: pkg/emulators/storage/gcsemu/store.go ================================================ package gcsemu import ( "context" "os" "google.golang.org/api/storage/v1" ) // Store is an interface to either on-disk or in-mem storage type Store interface { // CreateBucket creates a bucket; no error if the bucket already exists. CreateBucket(bucket string) error // Get returns a bucket's metadata. GetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error) // Get returns a file's contents and metadata. Get(url HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error) // GetMeta returns a file's metadata. GetMeta(url HttpBaseUrl, bucket string, filename string) (*storage.Object, error) // Add creates the specified file. Add(bucket string, filename string, contents []byte, meta *storage.Object) error // UpdateMeta updates the given file's metadata. UpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error // Copy copies the file Copy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error) // Delete deletes the file. Delete(bucket string, filename string) error // ReadMeta reads the GCS metadata for a file, when you already have file info. ReadMeta(url HttpBaseUrl, bucket string, filename string, fInfo os.FileInfo) (*storage.Object, error) // Walks the given bucket. Walk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error } ================================================ FILE: pkg/emulators/storage/gcsemu/util.go ================================================ package gcsemu import ( "encoding/json" "errors" "net/http" "regexp" "strings" "google.golang.org/api/googleapi" ) // jsonRespond json-encodes rsp and writes it to w. If an error occurs, then it is logged and a 500 error is written to w. func (g *GcsEmu) jsonRespond(w http.ResponseWriter, rsp interface{}) { // do NOT write a http status since OK will be the default and this allows the caller to use their own if they want w.Header().Set("Content-Type", "application/json; charset=utf-8") encoder := json.NewEncoder(w) if err := encoder.Encode(rsp); err != nil { g.log(err, "failed to send response") http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } type gapiErrorPartial struct { // Code is the HTTP response status code and will always be populated. Code int `json:"code"` // Message is the server response message and is only populated when // explicitly referenced by the JSON server response. Message string `json:"message"` Errors []googleapi.ErrorItem `json:"errors,omitempty"` } // gapiError responds to the client with a GAPI error func (g *GcsEmu) gapiError(w http.ResponseWriter, code int, message string) { if code == 0 { code = http.StatusInternalServerError } if code != http.StatusNotFound { g.log(errors.New(message), "responding with HTTP %d", code) } if message == "" { message = http.StatusText(code) } // format copied from errorReply struct in google.golang.org/api/googleapi rsp := struct { Error gapiErrorPartial `json:"error"` }{ Error: gapiErrorPartial{ Code: code, Message: message, }, } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(&rsp) } // mustJson serializes the given value to json, panicking on failure func mustJson(val interface{}) []byte { if val == nil { return []byte("null") } b, err := json.MarshalIndent(val, "", " ") if err != nil { panic(err) } return b } // requestHost returns the host from an http.Request, respecting proxy headers. Works locally with devproxy // and gulp proxies as well as in AppEngine (both real GAE and the dev_appserver). func requestHost(req *http.Request) string { // proxies like gulp are supposed to accumulate original host, next-step-host, etc in order from // client-most to server-most in X-ForwardedHost; return the first entry from that if any are listed if proxyHost := req.Header.Get("X-Forwarded-Host"); proxyHost != "" { // Use the first (closest to client) host splits := strings.SplitN(proxyHost, ",", 2) return splits[0] } // Forwarded is the standardized version of X-Forwarded-Host. f := parseForwardedHeader(req.Header.Get("Forwarded")) if len(f.Host) > 0 && len(f.Host[0]) > 0 { return f.Host[0] } // Clients that generate HTTP/2 requests should use the :authority header instead // of Host. See http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3 host := req.Header.Get("Authority") if len(host) > 0 { return host } // Fall back to the host line. return req.Host } // forwarded represents the values of a Forwarded HTTP header. // // For more details, see the RFC: https://tools.ietf.org/html/rfc7239 and // MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded. type forwarded struct { By []string For []string Host []string Proto []string } var ( forwardedHostRx = regexp.MustCompile(`(?i)host=(.*?)(?:[,;\s]|$)`) ) func removeDoubleQuotes(s string) string { return strings.TrimSuffix(strings.TrimPrefix(s, `"`), `"`) } // Note: this currently only supports the forwarded.Host field. func parseForwardedHeader(s string) forwarded { var f forwarded if s == "" { return f } matches := forwardedHostRx.FindAllStringSubmatch(s, -1) for _, m := range matches { if len(m) > 0 { f.Host = append(f.Host, removeDoubleQuotes(m[1])) } } return f } ================================================ FILE: pkg/emulators/storage/gcsemu/walk.go ================================================ package gcsemu import ( "context" "errors" "fmt" "net/http" "os" "path/filepath" "strings" "encr.dev/pkg/emulators/storage/gcsutil" "google.golang.org/api/storage/v1" ) // Iterate over the file system to serve a GCS list-bucket request. func (g *GcsEmu) makeBucketListResults(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, delimiter string, cursor string, prefix string, bucket string, maxResults int) { var errAbort = errors.New("sentinel error to abort walk") type item struct { filename string fInfo os.FileInfo } var found []item var prefixes []string seenPrefixes := make(map[string]bool) dbgWalk := func(fmt string, args ...interface{}) { if g.verbose { g.log(nil, fmt, args...) } } moreResults := false count := 0 err := g.store.Walk(ctx, bucket, func(ctx context.Context, filename string, fInfo os.FileInfo) error { dbgWalk("walk: %s", filename) // If we're beyond the prefix, we're completely done. if greaterThanPrefix(filename, prefix) { dbgWalk("%q > prefix=%q aborting", filename, prefix) return errAbort } // In the filesystem implementation, skip any directories strictly less than the cursor or prefix. if fInfo != nil && fInfo.IsDir() { if lessThanPrefix(filename, cursor) { dbgWalk("%q < cursor=%q skip dir", filename, cursor) return filepath.SkipDir } if lessThanPrefix(filename, prefix) { dbgWalk("%q < prefix=%q skip dir", filename, prefix) return filepath.SkipDir } return nil // keep going } // If the file is <= cursor, or < prefix, skip. if filename <= cursor { dbgWalk("%q <= cursor=%q skipping", filename, cursor) return nil } if !strings.HasPrefix(filename, prefix) { dbgWalk("%q < prefix=%q skipping", filename, prefix) return nil } if count >= maxResults { moreResults = true return errAbort } count++ if delimiter != "" { // See if the filename (beyond the prefix) contains delimiter, if it does, don't record the item, // instead record the prefix (including the delimiter). withoutPrefix := strings.TrimPrefix(filename, prefix) delimiterPos := strings.Index(withoutPrefix, delimiter) if delimiterPos >= 0 { // Got a hit, reconstruct the item's prefix, including the trailing delimiter itemPrefix := filename[:len(prefix)+delimiterPos+len(delimiter)] if !seenPrefixes[itemPrefix] { seenPrefixes[itemPrefix] = true prefixes = append(prefixes, itemPrefix) } return nil } } found = append(found, item{ filename: filename, fInfo: fInfo, }) return nil }) // Sentinel error is not an error if err == errAbort { err = nil } if err != nil { if len(found) == 0 { if os.IsNotExist(err) { g.gapiError(w, http.StatusNotFound, fmt.Sprintf("%s not found", bucket)) } else { g.gapiError(w, http.StatusInternalServerError, "failed to iterate: "+err.Error()) } return } // return our partial results + the cursor so that the client can retry from this point g.log(nil, "failed to iterate") } // Resolve the found items. var items []*storage.Object for _, item := range found { if obj, err := g.store.ReadMeta(baseUrl, bucket, item.filename, item.fInfo); err != nil { // return our partial results + the cursor so that the client can retry from this point g.log(nil, "failed to resolve: %s", item.filename) break } else { items = append(items, obj) } } var nextPageToken = "" if moreResults && len(items) > 0 { lastItemName := items[len(items)-1].Name nextPageToken = gcsutil.EncodePageToken(lastItemName) } rsp := storage.Objects{ Kind: "storage#objects", NextPageToken: nextPageToken, Items: items, Prefixes: prefixes, } g.jsonRespond(w, &rsp) } ================================================ FILE: pkg/emulators/storage/gcsutil/counted_lock.go ================================================ package gcsutil import ( "context" ) type countedLock struct { // a 1 element channel; empty == unlocked, full == locked // push an element into the channel to lock, remove the element to unlock ch chan struct{} // should only be accessed while the outer _map_ lock is held (not this key lock) refcount int64 } func newCountedLock() *countedLock { return &countedLock{ ch: make(chan struct{}, 1), refcount: 0, } } func (m *countedLock) Lock(ctx context.Context) bool { // If the context is already cancelled don't even try to lock. if ctx.Err() != nil { return false } select { case m.ch <- struct{}{}: return true case <-ctx.Done(): return false } } func (m *countedLock) Unlock() { select { case <-m.ch: return default: panic("BUG: lock not held") } } func (m *countedLock) Run(ctx context.Context, f func(ctx context.Context) error) error { if !m.Lock(ctx) { return ctx.Err() } defer m.Unlock() return f(ctx) } ================================================ FILE: pkg/emulators/storage/gcsutil/doc.go ================================================ // Package gcsutil contains some generic utilities to support gcsemu. // TODO(dragonsinth): consider open sourcing these separately, or finding open source replacements. package gcsutil ================================================ FILE: pkg/emulators/storage/gcsutil/gcspagetoken.go ================================================ package gcsutil //go:generate protoc --go_out=. --go_opt=paths=source_relative gcspagetoken.proto import ( "encoding/base64" "fmt" "google.golang.org/protobuf/proto" ) // EncodePageToken returns a synthetic page token to find files greater than the given string. // If this is part of a prefix query, the token should fall within the prefixed range. // BRITTLE: relies on a reverse-engineered internal GCS token format, which may be subject to change. func EncodePageToken(greaterThan string) string { bytes, err := proto.Marshal(&GcsPageToken{ LastFile: greaterThan, }) if err != nil { panic("could not encode gcsPageToken:" + err.Error()) } return base64.StdEncoding.EncodeToString(bytes) } // DecodePageToken decodes a GCS pageToken to the name of the last file returned. func DecodePageToken(pageToken string) (string, error) { bytes, err := base64.StdEncoding.DecodeString(pageToken) if err != nil { return "", fmt.Errorf("could not base64 decode pageToken %s: %w", pageToken, err) } var message GcsPageToken if err := proto.Unmarshal(bytes, &message); err != nil { return "", fmt.Errorf("could not unmarshal proto: %w", err) } return message.LastFile, nil } ================================================ FILE: pkg/emulators/storage/gcsutil/gcspagetoken.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 // protoc v4.23.4 // source: gcspagetoken.proto package gcsutil import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type GcsPageToken struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // The full name of the last result file, when returned from the server. // When sent as a cursor, interpreted as "return files greater than this value". LastFile string `protobuf:"bytes,1,opt,name=LastFile,proto3" json:"LastFile,omitempty"` } func (x *GcsPageToken) Reset() { *x = GcsPageToken{} if protoimpl.UnsafeEnabled { mi := &file_gcspagetoken_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GcsPageToken) String() string { return protoimpl.X.MessageStringOf(x) } func (*GcsPageToken) ProtoMessage() {} func (x *GcsPageToken) ProtoReflect() protoreflect.Message { mi := &file_gcspagetoken_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GcsPageToken.ProtoReflect.Descriptor instead. func (*GcsPageToken) Descriptor() ([]byte, []int) { return file_gcspagetoken_proto_rawDescGZIP(), []int{0} } func (x *GcsPageToken) GetLastFile() string { if x != nil { return x.LastFile } return "" } var File_gcspagetoken_proto protoreflect.FileDescriptor var file_gcspagetoken_proto_rawDesc = []byte{ 0x0a, 0x12, 0x67, 0x63, 0x73, 0x70, 0x61, 0x67, 0x65, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x67, 0x63, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x22, 0x2a, 0x0a, 0x0c, 0x47, 0x63, 0x73, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x28, 0x5a, 0x26, 0x65, 0x6e, 0x63, 0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x65, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x67, 0x63, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_gcspagetoken_proto_rawDescOnce sync.Once file_gcspagetoken_proto_rawDescData = file_gcspagetoken_proto_rawDesc ) func file_gcspagetoken_proto_rawDescGZIP() []byte { file_gcspagetoken_proto_rawDescOnce.Do(func() { file_gcspagetoken_proto_rawDescData = protoimpl.X.CompressGZIP(file_gcspagetoken_proto_rawDescData) }) return file_gcspagetoken_proto_rawDescData } var file_gcspagetoken_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_gcspagetoken_proto_goTypes = []interface{}{ (*GcsPageToken)(nil), // 0: gcsutil.GcsPageToken } var file_gcspagetoken_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_gcspagetoken_proto_init() } func file_gcspagetoken_proto_init() { if File_gcspagetoken_proto != nil { return } if !protoimpl.UnsafeEnabled { file_gcspagetoken_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GcsPageToken); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gcspagetoken_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_gcspagetoken_proto_goTypes, DependencyIndexes: file_gcspagetoken_proto_depIdxs, MessageInfos: file_gcspagetoken_proto_msgTypes, }.Build() File_gcspagetoken_proto = out.File file_gcspagetoken_proto_rawDesc = nil file_gcspagetoken_proto_goTypes = nil file_gcspagetoken_proto_depIdxs = nil } ================================================ FILE: pkg/emulators/storage/gcsutil/gcspagetoken.proto ================================================ syntax = "proto3"; package gcsutil; option go_package = "encr.dev/pkg/emulators/storage/gcsutil"; message GcsPageToken{ // The full name of the last result file, when returned from the server. // When sent as a cursor, interpreted as "return files greater than this value". string LastFile = 1; } ================================================ FILE: pkg/emulators/storage/gcsutil/gcspagetoken_test.go ================================================ package gcsutil import ( "testing" "gotest.tools/v3/assert" ) // TODO(dragonsinth): it would be nice to have an integration test that hits real GCS with known data stored. // TestGcsTokenGen tests that we produce expected GCS tokens that match real data we collected. func TestGcsTokenGen(t *testing.T) { tcs := []struct { lastFile string cursor string }{ { lastFile: "containers/images/4dcc5142000d12f1a0f67c1e95df4035ca0ebba70117cc04101e53422d391d61/json", cursor: "Cldjb250YWluZXJzL2ltYWdlcy80ZGNjNTE0MjAwMGQxMmYxYTBmNjdjMWU5NWRmNDAzNWNhMGViYmE3MDExN2NjMDQxMDFlNTM0MjJkMzkxZDYxL2pzb24=", }, { lastFile: "containers/images/sha256:0e89fc4aeb48f92acff2dddaf610b2ceea5d76a93a44d4c20b31e69d1ed68c10", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6MGU4OWZjNGFlYjQ4ZjkyYWNmZjJkZGRhZjYxMGIyY2VlYTVkNzZhOTNhNDRkNGMyMGIzMWU2OWQxZWQ2OGMxMA==", }, { lastFile: "containers/images/sha256:2072bc4567e1f13081af323d046f39453f010471701fa11fc50b786b60512e99", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6MjA3MmJjNDU2N2UxZjEzMDgxYWYzMjNkMDQ2ZjM5NDUzZjAxMDQ3MTcwMWZhMTFmYzUwYjc4NmI2MDUxMmU5OQ==", }, { lastFile: "containers/images/sha256:43ecade58b2ddff87a696fada3970491de28c8ea1dca09c988b447b5c5a56412", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NDNlY2FkZTU4YjJkZGZmODdhNjk2ZmFkYTM5NzA0OTFkZTI4YzhlYTFkY2EwOWM5ODhiNDQ3YjVjNWE1NjQxMg==", }, { lastFile: "containers/images/sha256:57076bf87737c2f448e75324ceba121b3b90372ab913f604f928a40da4ebddc7", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NTcwNzZiZjg3NzM3YzJmNDQ4ZTc1MzI0Y2ViYTEyMWIzYjkwMzcyYWI5MTNmNjA0ZjkyOGE0MGRhNGViZGRjNw==", }, { lastFile: "containers/images/sha256:6f73a9b0052f169d8296382660a31050004b691f3e6252008545a3dcb7371a49", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NmY3M2E5YjAwNTJmMTY5ZDgyOTYzODI2NjBhMzEwNTAwMDRiNjkxZjNlNjI1MjAwODU0NWEzZGNiNzM3MWE0OQ==", }, { lastFile: "containers/images/sha256:765b6a129bd04f06c876f3d5b5a346e71a72fae522b230215f67375ccb659a11", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NzY1YjZhMTI5YmQwNGYwNmM4NzZmM2Q1YjVhMzQ2ZTcxYTcyZmFlNTIyYjIzMDIxNWY2NzM3NWNjYjY1OWExMQ==", }, { lastFile: "containers/images/sha256:973d921f391393e65d20a6e990e8ad5aa1129681ab8c54bf59c9192b809594c4", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6OTczZDkyMWYzOTEzOTNlNjVkMjBhNmU5OTBlOGFkNWFhMTEyOTY4MWFiOGM1NGJmNTljOTE5MmI4MDk1OTRjNA==", }, { lastFile: "containers/images/sha256:a9d5802ef798d88c0f4f9dc0094249db5e26d8a8a18ba4c2194aab4a44983d2f", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YTlkNTgwMmVmNzk4ZDg4YzBmNGY5ZGMwMDk0MjQ5ZGI1ZTI2ZDhhOGExOGJhNGMyMTk0YWFiNGE0NDk4M2QyZg==", }, { lastFile: "containers/images/sha256:b5714cf3ed3a6b5f1fbc736ef0d5673c5637ccb14d53a23b8728dd828f21a22d", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YjU3MTRjZjNlZDNhNmI1ZjFmYmM3MzZlZjBkNTY3M2M1NjM3Y2NiMTRkNTNhMjNiODcyOGRkODI4ZjIxYTIyZA==", }, { lastFile: "containers/images/sha256:bfdef622d405cb35c466aa29ea7411fd594e7985127bec4e8080572d7ef45cfd", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YmZkZWY2MjJkNDA1Y2IzNWM0NjZhYTI5ZWE3NDExZmQ1OTRlNzk4NTEyN2JlYzRlODA4MDU3MmQ3ZWY0NWNmZA==", }, { lastFile: "containers/images/sha256:da543d0747020a528b8eee057912f6bd07e28f9c006606da28c82c70dac962e2", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6ZGE1NDNkMDc0NzAyMGE1MjhiOGVlZTA1NzkxMmY2YmQwN2UyOGY5YzAwNjYwNmRhMjhjODJjNzBkYWM5NjJlMg==", }, { lastFile: "containers/images/sha256:ee73b32b0f0a9dafabd9445a3380094f984858e79c85eafde56c2e037c039c6a", cursor: "Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6ZWU3M2IzMmIwZjBhOWRhZmFiZDk0NDVhMzM4MDA5NGY5ODQ4NThlNzljODVlYWZkZTU2YzJlMDM3YzAzOWM2YQ==", }, { lastFile: "containers/repositories/library/cpu-test/tag_v1", cursor: "Ci9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2NwdS10ZXN0L3RhZ192MQ==", }, { lastFile: "containers/repositories/library/dns-test/manifest_sha256:9759da4d052f154ba5d0ea32bf0404f442082e3dbad3f3cf6dc6529c6575aec2", cursor: "Cnljb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L21hbmlmZXN0X3NoYTI1Njo5NzU5ZGE0ZDA1MmYxNTRiYTVkMGVhMzJiZjA0MDRmNDQyMDgyZTNkYmFkM2YzY2Y2ZGM2NTI5YzY1NzVhZWMy", }, { lastFile: "containers/repositories/library/dns-test/manifest_sha256:ee49d4935c33260d84499e38dbd5ec3f426c9a2a7e100a901267c712874e4c1d", cursor: "Cnljb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L21hbmlmZXN0X3NoYTI1NjplZTQ5ZDQ5MzVjMzMyNjBkODQ0OTllMzhkYmQ1ZWMzZjQyNmM5YTJhN2UxMDBhOTAxMjY3YzcxMjg3NGU0YzFk", }, { lastFile: "containers/repositories/library/dns-test/tag_v2", cursor: "Ci9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L3RhZ192Mg==", }, { lastFile: "containers/repositories/library/kapi-test/manifest_sha256:ca6d9c13fba12363760e6d2495811081b1d2e6fcbf974e551605b65cb5b0a94e", cursor: "Cnpjb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2thcGktdGVzdC9tYW5pZmVzdF9zaGEyNTY6Y2E2ZDljMTNmYmExMjM2Mzc2MGU2ZDI0OTU4MTEwODFiMWQyZTZmY2JmOTc0ZTU1MTYwNWI2NWNiNWIwYTk0ZQ==", }, { lastFile: "containers/repositories/library/memclient-test/manifest_sha256:7b7979b351c9a019062446c1f033b2f8491868cf943758f006ae219eca231e01", cursor: "Cn9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L21lbWNsaWVudC10ZXN0L21hbmlmZXN0X3NoYTI1Njo3Yjc5NzliMzUxYzlhMDE5MDYyNDQ2YzFmMDMzYjJmODQ5MTg2OGNmOTQzNzU4ZjAwNmFlMjE5ZWNhMjMxZTAx", }, } for i, tc := range tcs { actualCursor := EncodePageToken(tc.lastFile) assert.Equal(t, tc.cursor, actualCursor, "case %d", i) lastFile, err := DecodePageToken(actualCursor) assert.NilError(t, err, "case %i: failed to decode", i) assert.Equal(t, tc.lastFile, lastFile, "case %d", i) } } ================================================ FILE: pkg/emulators/storage/gcsutil/transient_lock_map.go ================================================ package gcsutil import ( "context" "fmt" "sync" ) // TransientLockMap is a map of mutexes that is safe for concurrent access. It does not bother to save mutexes after // they have been unlocked, and thus this data structure is best for situations where the space of keys is very large. // If the space of keys is small then it may be inefficient to constantly recreate mutexes whenever they are needed. type TransientLockMap struct { mu sync.Mutex // the mutex that locks the map locks map[string]*countedLock // all locks that are currently held } // NewTransientLockMap returns a new TransientLockMap. func NewTransientLockMap() *TransientLockMap { return &TransientLockMap{ locks: make(map[string]*countedLock), } } // Lock acquires the lock for the specified key and returns true, unless the context finishes before the lock could be // acquired, in which case false is returned. func (l *TransientLockMap) Lock(ctx context.Context, key string) bool { lock := func() *countedLock { // If there is high lock contention, we could use a readonly lock to check if the lock is already in the map (and // thus no map writes are necessary), but this is complicated enough as it is so we skip that optimization for now. l.mu.Lock() defer l.mu.Unlock() // Check if there is already a lock for this key. lock, ok := l.locks[key] if !ok { // no lock yet, so make one and add it to the map lock = newCountedLock() l.locks[key] = lock } // Order is very important here. First we have to increment the refcount while we still have the map locked; this // will prevent anyone else from evicting this lock after we unlock the map but before we lock the key. Second we // have to unlock the map _before_ we start trying to lock the key (because locking the key could take a long time // and we don't want to keep the map locked that whole time). lock.refcount++ // incremented while holding _map_ lock return lock }() if !lock.Lock(ctx) { l.returnLockObj(key, lock) return false } return true } // Unlock unlocks the lock for the specified key. Panics if the lock is not currently held. func (l *TransientLockMap) Unlock(key string) { lock := func() *countedLock { l.mu.Lock() defer l.mu.Unlock() lock, ok := l.locks[key] if !ok { panic(fmt.Sprintf("lock not held for key %s", key)) } return lock }() lock.Unlock() l.returnLockObj(key, lock) } // Run runs the given callback while holding the lock, unless the context finishes before the lock could be // acquired, in which case the context error is returned. func (l *TransientLockMap) Run(ctx context.Context, key string, f func(ctx context.Context) error) error { if !l.Lock(ctx, key) { return ctx.Err() } defer l.Unlock(key) return f(ctx) } func (l *TransientLockMap) returnLockObj(key string, lock *countedLock) { l.mu.Lock() defer l.mu.Unlock() lock.refcount-- if lock.refcount < 0 { panic(fmt.Sprintf("BUG: somehow the lock.refcount for %q dropped to %d", key, lock.refcount)) } if lock.refcount == 0 { delete(l.locks, key) } } ================================================ FILE: pkg/emulators/storage/gcsutil/transient_lock_map_test.go ================================================ package gcsutil import ( "context" "sync" "sync/atomic" "testing" "time" "gotest.tools/v3/assert" ) func (l *TransientLockMap) len() int { l.mu.Lock() defer l.mu.Unlock() return len(l.locks) } func TestTransientLockMapBasics(t *testing.T) { m := NewTransientLockMap() upstream := make(chan int, 1) downstream := make(chan int, 1) waitFor := func(c <-chan int, expected int) { val := <-c assert.Equal(t, expected, val, "got an unexpected value from a channel") } readSignalNow := func(c <-chan int, expected int, msg string) { select { case val := <-c: assert.Equal(t, expected, val, "got an unexpected value from a channel") default: t.Fatal(msg) } } // wrt upstream and downstream, this goroutine is "above" the main thread, so it writes to downstream and reads from // upstream go func() { lockWithin(t, m, "foo", 10*time.Millisecond) downstream <- 1 waitFor(upstream, 2) downstream <- 3 m.Unlock("foo") }() // wait until the other goroutine has locked "foo" waitFor(downstream, 1) // prove that we can lock other keys without a problem lockWithin(t, m, "bar", 10*time.Millisecond) lockWithin(t, m, "baz", 10*time.Millisecond) assert.Equal(t, 3, m.len(), "wrong number of internal locks") // start trying to lock "foo" which will block until we signal the other goroutine to unlock it time.AfterFunc(20*time.Millisecond, func() { upstream <- 2 }) lockWithin(t, m, "foo", 200*time.Millisecond) // there better be a 3 already queued up in the downstream, otherwise we locked too fast readSignalNow(downstream, 3, "locked \"foo\" before the other goroutine unlocked it...") assert.Equal(t, 3, m.len(), "wrong number of internal locks") // we can unlock out of order, and locks go away as we unlock them m.Unlock("bar") assert.Equal(t, 2, m.len(), "wrong number of internal locks") m.Unlock("baz") assert.Equal(t, 1, m.len(), "wrong number of internal locks") m.Unlock("foo") assert.Equal(t, 0, m.len(), "wrong number of internal locks") } func TestTransientLockMapBadUnlock(t *testing.T) { // call a function, and fail the test if the function doesn't panic index := 0 shouldPanic := func(f func()) { index++ defer func() { if recovered := recover(); recovered != nil { // ok, all is well } else { // we were supposed to panic but didn't - fail the test! t.Fatalf("test #%d did not panic as expected...", index) } }() f() } // unlocking a key that has never been referenced m := NewTransientLockMap() shouldPanic(func() { m.Unlock("foo") }) assert.Equal(t, 0, m.len(), "wrong number of internal locks") // double-unlocking a key in the same goroutine m = NewTransientLockMap() shouldPanic(func() { assertLock(t, m, "foo") m.Unlock("foo") m.Unlock("foo") }) assert.Equal(t, 0, m.len(), "wrong number of internal locks") // double-unlocking a key across 2 goroutines m = NewTransientLockMap() shouldPanic(func() { signal := make(chan struct{}) go func() { assertLock(t, m, "foo") m.Unlock("foo") close(signal) }() <-signal m.Unlock("foo") }) assert.Equal(t, 0, m.len(), "wrong number of internal locks") } func TestTransientLockMapSequence(t *testing.T) { m := NewTransientLockMap() // signals partnerAboutToLock := make(chan struct{}) partnerGotLock := make(chan struct{}) assertLock(t, m, "foo") go func() { close(partnerAboutToLock) assertLock(t, m, "foo") close(partnerGotLock) time.Sleep(125 * time.Millisecond) m.Unlock("foo") }() <-partnerAboutToLock time.Sleep(100 * time.Millisecond) // give our partner time to actually call Lock() m.Unlock("foo") <-partnerGotLock start := time.Now() assertLock(t, m, "foo") // ensure that the prior Lock() call actually blocked and waited for a while, as intended if d := time.Since(start); d < 100*time.Millisecond { t.Fatalf("Lock acquired too fast (%s)", d) } m.Unlock("foo") assert.Equal(t, 0, m.len(), "wrong number of internal locks") } func TestTransientLockMapContention(t *testing.T) { m := NewTransientLockMap() var wg sync.WaitGroup assertLock(t, m, "foo") assertLock(t, m, "bar") for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() assertLock(t, m, "foo") m.Unlock("foo") assertLock(t, m, "bar") m.Unlock("bar") }() } time.Sleep(30 * time.Millisecond) m.Unlock("bar") m.Unlock("foo") wg.Wait() assert.Equal(t, 0, m.len(), "wrong number of internal locks") } func TestTransientLockMapTimeout(t *testing.T) { m := NewTransientLockMap() assertLock(t, m, "foo") ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() res := m.Lock(ctx, "foo") assert.Equal(t, false, res, "lock foo (2)") m.Unlock("foo") lockWithin(t, m, "foo", 10*time.Millisecond) // should be able to lock near instantly ctx2, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { res := m.Lock(ctx2, "foo") assert.Equal(t, false, res, "lock foo (4)") close(done) }() time.Sleep(25 * time.Millisecond) // goroutine should not be done yet (i.e. Lock() should still be blocked) select { case <-done: t.Fatal("done should not be closed yet!") default: } cancel() select { case <-time.After(25 * time.Millisecond): t.Fatal("timeout waiting for done channel to close") case <-done: // ok } // finally, verify that we can unlock and lock still m.Unlock("foo") lockWithin(t, m, "foo", 10*time.Millisecond) // should be able to lock near instantly m.Unlock("foo") assert.Equal(t, 0, m.len(), "wrong number of internal locks") // We should not be able to lock with an already-cancelled context. for i := 0; i < 10; i++ { assert.Assert(t, !m.Lock(ctx2, "foo"), "lock foo (final) expected canceled") } } func TestTransientLockMapRun(t *testing.T) { m := NewTransientLockMap() start := make(chan struct{}) var wgSucc sync.WaitGroup var wgFail sync.WaitGroup var nFail int32 var nSucc int32 ctx, cancel := context.WithCancel(context.Background()) defer cancel() for i := 0; i < 10; i++ { if i == 0 { wgSucc.Add(1) } else { wgFail.Add(1) } go func() { <-start err := m.Run(ctx, "foo", func(ctx context.Context) error { // Whoever got the lock should cancel everyone else. cancel() wgFail.Wait() return nil }) if err == nil { defer wgSucc.Done() atomic.AddInt32(&nSucc, 1) } else { defer wgFail.Done() if err == context.Canceled { atomic.AddInt32(&nFail, 1) } else { t.Errorf("expected canceled, got %T: %s", err, err) } } }() } close(start) wgSucc.Wait() assert.Equal(t, int32(1), nSucc, "wrong # success") assert.Equal(t, int32(9), nFail, "wrong # failures") } func assertLock(t *testing.T, m *TransientLockMap, key string) { assert.Assert(t, m.Lock(context.Background(), key), "should have locked") } // grabs a lock, panicking if this takes longer than expected func lockWithin(t *testing.T, m *TransientLockMap, key string, timeout time.Duration) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() assert.Assert(t, m.Lock(ctx, key), "should have locked") } ================================================ FILE: pkg/encorebuild/buildconf/config.go ================================================ package buildconf import ( "runtime" "github.com/rs/zerolog" "encr.dev/pkg/encorebuild/buildutil" "encr.dev/pkg/option" ) type Config struct { // Logger to use. Log zerolog.Logger // Target OS and architecture (in GOOS/GOARCH format) OS string Arch string // Release is true if this is a release build. Release bool // The version being built. Version string // RepoDir is the path to the encore repo on the filesystem. RepoDir string // CacheDir is the cache dir to use for the build. CacheDir string // The path to the MacOS SDK. Must be set for cross-compiles to macOS. MacSDKPath option.Option[string] // Whether or not to publish packages to NPM. Only used if Release is also true. PublishNPMPackages bool // Whether to copy the built native module back to the repo dir. CopyToRepo bool // RustBuilder will override the automatic builder selection if set. RustBuilder option.Option[string] } // IsCross reports whether the build is a cross-compile. func (cfg *Config) IsCross() bool { return cfg.OS != runtime.GOOS || cfg.Arch != runtime.GOARCH } func (cfg *Config) CrossMacSDKPath() string { if cfg.OS != "darwin" { return "" } val, ok := cfg.MacSDKPath.Get() if !ok { buildutil.Bailf("macOS SDK path must be set for cross-compiles to macOS") } return val } // Exe returns the executable file suffix for the target OS. func (cfg *Config) Exe() string { if cfg.OS == "windows" { return ".exe" } return "" } ================================================ FILE: pkg/encorebuild/buildutil/buildutil.go ================================================ package buildutil import ( "fmt" "os/exec" "sync" "github.com/cockroachdb/errors" ) type Bailout struct { Err error } func (b Bailout) Error() string { return b.Err.Error() } func Bail(err error) { panic(Bailout{err}) } func Bailf(format string, args ...any) { Bail(fmt.Errorf(format, args...)) } func Must[T any](val T, err error) T { if err != nil { Bail(err) } return val } func Check(err error) { if err != nil { Bail(err) } } func TarGzip(srcDirectory string, tarFile string) error { // Create the tar.gz file from the src directory cmd := exec.Command("tar", "-czf", tarFile, "-C", srcDirectory, ".") // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { return errors.Wrapf(err, "failed to create tar.gz: %s", out) } return nil } // RunParallel runs the given functions in parallel, bailing with the first error func RunParallel(functions ...func()) { var wg sync.WaitGroup wg.Add(len(functions)) var firstErr error var mu sync.Mutex for _, f := range functions { f := f go func() { defer wg.Done() defer func() { if err := recover(); err != nil { if b, ok := err.(Bailout); ok { mu.Lock() defer mu.Unlock() if firstErr == nil { firstErr = b.Err } } else { panic(err) } } }() f() }() } wg.Wait() Check(firstErr) } ================================================ FILE: pkg/encorebuild/cmd/build-local-binary/build-local-binary.go ================================================ package main import ( "flag" "os" "path/filepath" "runtime" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/internal/version" "encr.dev/pkg/encorebuild" "encr.dev/pkg/encorebuild/buildconf" "encr.dev/pkg/option" ) func join(strs ...string) string { return filepath.Join(strs...) } var archFlag = flag.String("arch", runtime.GOARCH, "the architecture to target") var osFlag = flag.String("os", runtime.GOOS, "the operating system to target") var builderFlag = flag.String("builder", "", "the builder to use") func main() { binary := os.Args[1] if binary == "" || binary[0] == '-' { log.Fatal().Msg("expected binary name as first argument") } os.Args = os.Args[1:] flag.Parse() log.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger() root, err := os.Getwd() if err != nil { log.Fatal().Err(err).Msg("failed to get working directory") } else if _, err := os.Stat(join(root, ".git")); err != nil { log.Fatal().Err(err).Msg("expected to run build-local-binary from encr.dev repository root") } userCacheDir, err := os.UserCacheDir() if err != nil { log.Fatal().Err(err).Msg("failed to get user cache dir") } cacheDir := filepath.Join(userCacheDir, "encore-build-cache") builder := option.None[string]() if builderFlag != nil && *builderFlag != "" { builder = option.Some(*builderFlag) } cfg := &buildconf.Config{ Log: log.Logger, OS: *osFlag, Arch: *archFlag, Release: false, Version: version.Version, RepoDir: root, CacheDir: cacheDir, MacSDKPath: option.None[string](), CopyToRepo: true, RustBuilder: builder, } switch binary { case "all": encorebuild.NewJSRuntimeBuilder(cfg).Build() encorebuild.NewSupervisorBuilder(cfg).Build() case "supervisor-encore": encorebuild.NewSupervisorBuilder(cfg).Build() case "encore-runtime.node": encorebuild.NewJSRuntimeBuilder(cfg).Build() default: log.Fatal().Msgf("unknown binary %s", binary) } } ================================================ FILE: pkg/encorebuild/cmd/make-release/make-release.go ================================================ package main import ( "flag" "fmt" "os" "path/filepath" "runtime/debug" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/internal/version" "encr.dev/pkg/encorebuild" "encr.dev/pkg/encorebuild/buildconf" "encr.dev/pkg/encorebuild/buildutil" "encr.dev/pkg/option" ) func join(strs ...string) string { return filepath.Join(strs...) } func main() { log.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger() dst := flag.String("dst", "", "build destination") versionStr := flag.String("v", "", "version number") onlyBuild := flag.String("only", "", "build only the valid target ('darwin-arm64' or 'darwin' or 'arm64' or '' for all)") publishNPM := flag.Bool("publish-npm", false, "publish packages to npm") flag.Parse() if *dst == "" || *versionStr == "" { log.Fatal().Msgf("missing -dst %q or -v %q", *dst, *versionStr) } if (*versionStr)[0] != 'v' { log.Fatal().Msg("version must start with 'v'") } switch version.ChannelFor(*versionStr) { case version.GA, version.Beta, version.Nightly, version.DevBuild: // no-op default: log.Fatal().Msgf("unknown version channel for %s", *versionStr) } root, err := os.Getwd() if err != nil { log.Fatal().Err(err).Msg("failed to get working directory") } else if _, err := os.Stat(join(root, "go.mod")); err != nil { log.Fatal().Err(err).Msg("expected to run make-release.go from encr.dev repository root") } userCacheDir, err := os.UserCacheDir() if err != nil { log.Fatal().Err(err).Msg("failed to get user cache dir") } cacheDir := filepath.Join(userCacheDir, "encore-build-cache") *dst, err = filepath.Abs(*dst) if err != nil { log.Fatal().Err(err).Msg("failed to get absolute path to destination") } // Prepare the target directory. if err := os.RemoveAll(*dst); err != nil { log.Fatal().Err(err).Msg("failed to remove existing target dir") } else if err := os.MkdirAll(filepath.Join(*dst, "artifacts"), 0755); err != nil { log.Fatal().Err(err).Msg("failed to create target dir") } type buildTarget struct { OS string Arch string } targets := []buildTarget{ {"darwin", "amd64"}, {"darwin", "arm64"}, {"linux", "amd64"}, {"linux", "arm64"}, {"windows", "amd64"}, } var parallelFuncs []func() // Give them the common settings for _, t := range targets { if *onlyBuild != "" && !(*onlyBuild == fmt.Sprintf("%s-%s", t.OS, t.Arch) || *onlyBuild == t.OS || *onlyBuild == t.Arch) { continue } b := &encorebuild.DistBuilder{ Cfg: &buildconf.Config{ Log: log.With().Str("os", t.OS).Str("arch", t.Arch).Logger(), OS: t.OS, Arch: t.Arch, Release: true, Version: *versionStr, RepoDir: root, CacheDir: cacheDir, MacSDKPath: option.Some("/sdk"), PublishNPMPackages: *publishNPM, }, DistBuildDir: join(*dst, t.OS+"_"+t.Arch), ArtifactsTarFile: join(*dst, "artifacts", "encore-"+*versionStr+"-"+t.OS+"_"+t.Arch+".tar.gz"), } parallelFuncs = append(parallelFuncs, b.Build) } defer func() { if err := recover(); err != nil { if b, ok := err.(buildutil.Bailout); ok { log.Fatal().Err(b.Err).Msg("failed to build") } else { stack := debug.Stack() log.Fatal().Msgf("failed to build: unrecovered panic: %v: \n%s", err, stack) } } }() buildutil.RunParallel(parallelFuncs...) log.Info().Msg("all distributions built successfully") if *publishNPM { encorebuild.PublishNPMPackages(root, *versionStr) } log.Info().Msg("successfully published NPM package") } ================================================ FILE: pkg/encorebuild/compile/compile.go ================================================ package compile import ( osPkg "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "encr.dev/pkg/encorebuild/buildconf" . "encr.dev/pkg/encorebuild/buildutil" ) // GoBinary compiles a Go binary for the given OS and architecture with GCP enabled // // This file was inspired by the blog post: https://lucor.dev/post/cross-compile-golang-fyne-project-using-zig/ func GoBinary(cfg *buildconf.Config, outputPath string, entrypointPkg string, ldFlags []string) { cc, cxx, compilerEnvs, compilerLDFlags := compilerSettings(cfg) if cfg.OS == "windows" && !strings.HasSuffix(outputPath, ".exe") { outputPath += ".exe" } combinedLDFlags := append(append([]string{}, compilerLDFlags...), ldFlags...) baseEnvs := goBaseEnvs(cfg) envs := append(baseEnvs, "CGO_ENABLED=1", "CC="+cc, "CXX="+cxx, ) envs = append(envs, compilerEnvs...) // Build the go build args args := []string{"build", "-trimpath", "-tags", "netgo", // Always force netgo otherwise we end up with segfaults on MacOS } if len(combinedLDFlags) > 0 { args = append(args, "-ldflags="+strings.Join(combinedLDFlags, " ")) } if cfg.OS == "darwin" { args = append(args, "-buildmode=pie") } args = append(args, "-o", outputPath, entrypointPkg, ) // Build the command cmd := exec.Command("go", args...) cmd.Env = envs // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to compile go binary: %v: %s", err, string(out)) } } func goBaseEnvs(cfg *buildconf.Config) []string { gocache := filepath.Join(cfg.CacheDir, "go", cfg.OS, cfg.Arch) Check(osPkg.MkdirAll(gocache, 0755)) return append(osPkg.Environ(), "GOOS="+cfg.OS, "GOARCH="+cfg.Arch, "GOCACHE="+gocache, ) } // CompileRustBinary compiles a Rust binary for the given OS and architecture // // We're using zigbuild to perform easy cross compiling func RustBinary(cfg *buildconf.Config, artifactPath, outputPath string, cratePath string, libc string, extraFeatures []string, extraEnvVars ...string) { if cfg.OS == "windows" { if !strings.HasSuffix(artifactPath, ".dll") { outputPath += ".exe" artifactPath += ".exe" } } useZig := cfg.IsCross() || cfg.Release useCross := false if cfg.IsCross() && runtime.GOOS == "darwin" { // check is cross is installed _, err := exec.LookPath("cross") if err == nil { useCross = true useZig = false } } if builder, ok := cfg.RustBuilder.Get(); ok { switch builder { case "cargo": useCross = false useZig = false case "zigbuild": useCross = false useZig = true case "cross-rs": useCross = true useZig = false default: Bailf("unknown builder: %q", cfg.RustBuilder) } } envs := append(extraEnvVars, osPkg.Environ()...) var target, zigTargetSuffix string switch cfg.OS { case "darwin": switch cfg.Arch { case "amd64": target = "x86_64-apple-darwin" case "arm64": target = "aarch64-apple-darwin" default: Bailf("unsupported architecture for darwin: %q", cfg.Arch) } // We need to set the SDKROOT for cross compiling to MacOS if cfg.IsCross() { envs = append(envs, "SDKROOT="+cfg.CrossMacSDKPath()) } case "linux": switch cfg.Arch { case "amd64": target = "x86_64-unknown-linux-" + libc case "arm64": target = "aarch64-unknown-linux-" + libc default: Bailf("unsupported architecture for linux: %q", cfg.Arch) } // If we're using zig, specify the glibc version we want. if useZig { zigTargetSuffix = ".2.31" } case "windows": switch cfg.Arch { case "amd64": target = "x86_64-pc-windows-gnu" default: Bailf("unsupported architecture for windows: %q", cfg.Arch) } default: Bailf("unsupported os: %q", cfg.OS) } // Create a cache dir for the go build cache for this specific OS and architecture pair path := filepath.Join(cfg.CacheDir, "rust", cfg.OS, cfg.Arch) Check(osPkg.MkdirAll(path, 0755)) // Build the command cargoArgs := []string{ "build", "--target", target + zigTargetSuffix, "--target-dir", path, } if useZig { cargoArgs[0] = "zigbuild" } buildMode := "debug" if cfg.Release { cargoArgs = append(cargoArgs, "--release") buildMode = "release" } if len(extraFeatures) > 0 { cargoArgs = append(cargoArgs, "--features", strings.Join(extraFeatures, ",")) } builder := "cargo" if useCross { builder = "cross" } cmd := exec.Command(builder, cargoArgs...) // forwards the output to the parent process cmd.Stdout = osPkg.Stdout cmd.Stderr = osPkg.Stderr cmd.Dir = cratePath cmd.Env = envs // Cargo can't run multiple compiles at the same time for the same crate // so let's lock here, then unlock once the compile has finished cargoLock.Lock() defer cargoLock.Unlock() // nosemgrep if err := cmd.Run(); err != nil { Bailf("failed to compile rust binary: %v", err) } // Copy the binary to the output path binaryFile := filepath.Join(path, target, buildMode, artifactPath) cmd = exec.Command("cp", binaryFile, outputPath) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to copy rust binary: %v: %s", err, string(out)) } } // compilerSettings returns the CC and CXX settings for the given OS and architecture func compilerSettings(cfg *buildconf.Config) (cc, cxx string, envs, ldFlags []string) { var zigTarget string var zigArgs string zigBinary := "zig" switch cfg.OS { case "darwin": zigBinary = "/usr/local/zig-0.9.1/zig" // We need an explicit version of Zig for darwin (0.11.0 compiles, build causes runtime errors) ldFlags = []string{"-s", "-w"} switch cfg.Arch { case "amd64": zigTarget = "x86_64-macos.10.12" case "arm64": zigTarget = "aarch64-macos.11.1" default: Bailf("unsupported architecture for darwin: %q", cfg.Arch) } // We need to set some extra stuff if we're cross compiling to MacOS if cfg.IsCross() { sdkPath := cfg.CrossMacSDKPath() zigArgs = " -isysroot " + sdkPath + " -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks" envs = []string{ "CGO_LDFLAGS=--sysroot " + sdkPath + " -F/System/Library/Frameworks -L/usr/lib", } } case "linux": switch cfg.Arch { case "amd64": // Note: we're not targeting a specific glibc version here as we tried before // with 2.35 - but for some reason we still get runtime errors not finding 2.34 or 2.33 on WSL (which had 2.35) zigTarget = "x86_64-linux-gnu.2.31" zigArgs = " -static -isystem /usr/include" case "arm64": zigTarget = "aarch64-linux-gnu.2.31" zigArgs = " -static -isystem /usr/include" envs = []string{ "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig", } default: Bailf("unsupported architecture for linux: %q", cfg.Arch) } case "windows": switch cfg.Arch { case "amd64": zigTarget = "x86_64-windows-gnu" default: Bailf("unsupported architecture for windows: %q", cfg.Arch) } ldFlags = []string{"-H=windowsgui"} default: Bailf("unsupported os: %q", cfg.OS) } cc = zigBinary + " cc -target " + zigTarget + zigArgs cxx = zigBinary + " c++ -target " + zigTarget + zigArgs return cc, cxx, envs, ldFlags } // cargoLock is a lock to prevent concurrent cargo builds. var cargoLock sync.Mutex ================================================ FILE: pkg/encorebuild/dist_builder.go ================================================ package encorebuild import ( "fmt" "os" "os/exec" "path/filepath" "encr.dev/internal/version" "encr.dev/pkg/encorebuild/buildconf" . "encr.dev/pkg/encorebuild/buildutil" "encr.dev/pkg/encorebuild/compile" "encr.dev/pkg/encorebuild/githubrelease" ) // A DistBuilder is a builder for a specific distribution of Encore. // // Anything which does not need to be built for a specific distribution // should be built in the main builder before these are invoked. // // Make release will run multiple of these in parallel to build all the // distributions. type DistBuilder struct { Cfg *buildconf.Config DistBuildDir string // The directory to build into ArtifactsTarFile string // The directory to put the final tar.gz artifact into } func (d *DistBuilder) buildEncoreCLI() { // Build the CLI binaries. d.Cfg.Log.Info().Msg("building encore binary...") linkerOpts := []string{ "-X", fmt.Sprintf("'encr.dev/internal/version.Version=%s'", d.Cfg.Version), } // If we're building a nightly, devel or beta version, we need to set the default config directory var versionSuffix string switch version.ChannelFor(d.Cfg.Version) { case version.GA: versionSuffix = "" case version.Beta: versionSuffix = "-beta" case version.Nightly: versionSuffix = "-nightly" case version.DevBuild: versionSuffix = "-develop" default: Bailf("unknown version channel for %s", d.Cfg.Version) } if versionSuffix != "" { linkerOpts = append(linkerOpts, "-X", "'encr.dev/internal/conf.defaultConfigDirectory=encore"+versionSuffix+"'", ) } compile.GoBinary( d.Cfg, join(d.DistBuildDir, "bin", "encore"+versionSuffix), "./cli/cmd/encore", linkerOpts, ) d.Cfg.Log.Info().Msg("encore built successfully") } func (d *DistBuilder) buildGitHook() { // Build the git-remote-encore binary. d.Cfg.Log.Info().Msg("building git-remote-encore binary...") compile.GoBinary( d.Cfg, join(d.DistBuildDir, "bin", "git-remote-encore"), "./cli/cmd/git-remote-encore", nil, ) d.Cfg.Log.Info().Msg("git-remote-encore built successfully") } func (d *DistBuilder) buildTSBundler() { // Build the TS bundler. d.Cfg.Log.Info().Msg("building tsbundler binary...") linkerOpts := []string{ "-X", fmt.Sprintf("'encr.dev/internal/version.Version=%s'", d.Cfg.Version), } compile.GoBinary( d.Cfg, join(d.DistBuildDir, "bin", "tsbundler-encore"), "./cli/cmd/tsbundler-encore", linkerOpts, ) d.Cfg.Log.Info().Msg("tsbundler built successfully") } func (d *DistBuilder) buildTSParser() { // Build the TS parser. d.Cfg.Log.Info().Msg("building tsparser binary...") compile.RustBinary( d.Cfg, "tsparser-encore", join(d.DistBuildDir, "bin", "tsparser-encore"), "./tsparser", "gnu", []string{}, // features fmt.Sprintf("ENCORE_VERSION=%s", d.Cfg.Version), ) d.Cfg.Log.Info().Msg("tsparser built successfully") } func (d *DistBuilder) buildNodePlugin() { builder := NewJSRuntimeBuilder(d.Cfg) builder.Build() d.Cfg.Log.Info().Msg("copying encore runtime for JS...") { cmd := exec.Command("cp", "-r", "runtimes/js/.", join(d.DistBuildDir, "runtimes", "js")+"/") cmd.Dir = d.Cfg.RepoDir // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to copy encore go runtime: %v: %s", err, out) } } { src := builder.NativeModuleOutput() dst := join(d.DistBuildDir, "runtimes", "js", "encore-runtime.node") cmd := exec.Command("cp", src, dst) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to copy encore go runtime: %v: %s", err, out) } } d.Cfg.Log.Info().Msg("encore runtime for js copied successfully") } func (d *DistBuilder) downloadEncoreGo() { // Step 1: Find out the latest release version for Encore's Go distribution d.Cfg.Log.Info().Msg("downloading latest encore-go...") encoreGoArchive := githubrelease.DownloadLatest(d.Cfg, "encoredev", "go") d.Cfg.Log.Info().Msg("extracting encore-go...") githubrelease.Extract(encoreGoArchive, d.DistBuildDir) d.Cfg.Log.Info().Msg("encore-go extracted successfully") } func (d *DistBuilder) copyEncoreRuntimeForGo() { d.Cfg.Log.Info().Msg("copying encore runtime for Go...") cmd := exec.Command("cp", "-r", "runtimes/go/.", join(d.DistBuildDir, "runtimes", "go")+"/") // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to copy encore go runtime: %v: %s", err, out) } d.Cfg.Log.Info().Msg("encore runtime for go copied successfully") } // Build builds the distribution running each step in order func (d *DistBuilder) Build() { d.Cfg.Log.Info().Msg("building distribution...") if d.DistBuildDir == "" { Bailf("DistBuildDir not set") } // Prepare the target directory. Check(os.RemoveAll(d.DistBuildDir)) Check(os.MkdirAll(d.DistBuildDir, 0755)) Check(os.MkdirAll(join(d.DistBuildDir, "bin"), 0755)) Check(os.MkdirAll(join(d.DistBuildDir, "runtimes"), 0755)) Check(os.MkdirAll(join(d.DistBuildDir, "runtimes", "go"), 0755)) Check(os.MkdirAll(join(d.DistBuildDir, "runtimes", "js"), 0755)) // Now we're prepped, start building. RunParallel( d.buildEncoreCLI, d.buildTSBundler, d.buildGitHook, d.buildTSParser, d.buildNodePlugin, d.copyEncoreRuntimeForGo, d.downloadEncoreGo, ) // Now tar gzip the directory d.Cfg.Log.Info().Str("tar_file", d.ArtifactsTarFile).Msg("creating distribution tar file...") TarGzip(d.DistBuildDir, d.ArtifactsTarFile) d.Cfg.Log.Info().Str("tar_file", d.ArtifactsTarFile).Msg("distribution built successfully") } func join(segs ...string) string { return filepath.Join(segs...) } ================================================ FILE: pkg/encorebuild/gentypedefs/gentypedefs.go ================================================ package gentypedefs import ( "bytes" _ "embed" "encoding/json" "fmt" "os" "slices" "sort" "strings" "text/template" "golang.org/x/exp/maps" ) type Config struct { // The version string to embed ReleaseVersion string // The path to the intermediate type definition file. TypeDefFile string // The path to the .d.ts output file. DtsOutputFile string // The path to the .cjs output file. CjsOutputFile string } func Generate(cfg Config) error { typeDefStr, exports, err := processTypeDef(cfg.TypeDefFile, false, DefaultTypeDefHeader) if err != nil { return fmt.Errorf("failed to parse type definitions: %v", err) } err = os.WriteFile(cfg.DtsOutputFile, []byte(typeDefStr), 0644) if err != nil { return fmt.Errorf("failed to write .d.ts file: %v", err) } { out, err := renderExportTemplate(cfg.ReleaseVersion, exports) if err != nil { return fmt.Errorf("failed to render .cjs file: %v", err) } err = os.WriteFile(cfg.CjsOutputFile, out, 0644) if err != nil { return fmt.Errorf("failed to write .cjs file: %v", err) } } return nil } //go:embed napi.cjs.tmpl var napiCjsTemplate string func renderExportTemplate(version string, exports []string) ([]byte, error) { tmpl, err := template.New("napi.cjs").Parse(napiCjsTemplate) if err != nil { return nil, err } var buf bytes.Buffer err = tmpl.Execute(&buf, map[string]any{ "ModuleVersion": version, "Exports": exports, }) return buf.Bytes(), err } const TopLevelNamespace = "__TOP_LEVEL_MODULE__" const DefaultTypeDefHeader = `/* tslint:disable */ /* eslint:disable */ // Automatically generated by gen_type_defs. Do not edit. ` type TypeDefKind string const ( TypeDefKindConst TypeDefKind = "const" TypeDefKindEnum TypeDefKind = "enum" TypeDefKindInterface TypeDefKind = "interface" TypeDefKindFn TypeDefKind = "fn" TypeDefKindStruct TypeDefKind = "struct" TypeDefKindImpl TypeDefKind = "impl" ) type TypeDefLine struct { Kind TypeDefKind `json:"kind"` Name string `json:"name"` OriginalName string `json:"original_name,omitempty"` Def string `json:"def"` JSDoc string `json:"js_doc,omitempty"` JSMod string `json:"js_mod,omitempty"` } func prettyPrint(line *TypeDefLine, constEnum bool, indent int) string { s := line.JSDoc switch line.Kind { case TypeDefKindInterface: s += "export interface " + line.Name + " {" if strings.TrimSpace(line.Def) != "" { s += "\n" + line.Def + "\n" } s += "}" if line.OriginalName != "" && line.OriginalName != line.Name { s += "\nexport type " + line.OriginalName + " = " + line.Name } case TypeDefKindEnum: enumName := "enum" if constEnum { enumName = "const enum" } s += "export " + enumName + " " + line.Name + " {" if strings.TrimSpace(line.Def) != "" { s += "\n" + line.Def + "\n" } s += "}" case TypeDefKindStruct: s += "export class " + line.Name + " {" if strings.TrimSpace(line.Def) != "" { s += "\n" + line.Def + "\n" } s += "}" if line.OriginalName != "" && line.OriginalName != line.Name { s += "\nexport type " + line.OriginalName + " = " + line.Name } default: s += line.Def } return correctStringIndent(s, indent) } func processTypeDef(intermediateTypeFile string, constEnum bool, header string) (string, []string, error) { var exports []string content, err := os.ReadFile(intermediateTypeFile) if err != nil { return "", nil, err } defs, err := readIntermediateTypeFile(content) if err != nil { return "", nil, err } groupedDefs := preprocessTypeDef(defs) if header != "" { header += "\n" } dts := "" namespaces := maps.Keys(groupedDefs) sort.Strings(namespaces) for _, namespace := range namespaces { defs := groupedDefs[namespace] slices.SortFunc(defs, func(a, b *TypeDefLine) int { return strings.Compare(a.Name, b.Name) }) if namespace == TopLevelNamespace { for _, def := range defs { dts += prettyPrint(def, constEnum, 0) + "\n\n" switch def.Kind { case TypeDefKindConst, TypeDefKindEnum, TypeDefKindFn, TypeDefKindStruct: exports = append(exports, def.Name) if def.OriginalName != "" && def.OriginalName != def.Name { exports = append(exports, def.OriginalName) } } } } else { exports = append(exports, namespace) dts += `export namespace ` + namespace + ` { ` for _, def := range defs { dts += prettyPrint(def, constEnum, 2) + "\n" } dts += `} ` } } if strings.Contains(dts, "ExternalObject<") { header += ` export class ExternalObject { readonly '': { readonly '': unique symbol [K: symbol]: T } } ` } sort.Strings(exports) return header + dts, exports, nil } func readIntermediateTypeFile(content []byte) ([]*TypeDefLine, error) { lines := strings.Split(string(content), "\n") var defs []*TypeDefLine for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } if !strings.HasPrefix(line, "{") { // crateName: { "def": "", ... } start := strings.IndexByte(line, ':') + 1 line = line[start:] } var def TypeDefLine err := json.Unmarshal([]byte(line), &def) if err != nil { return nil, err } defs = append(defs, &def) } slices.SortFunc(defs, func(a, b *TypeDefLine) int { if a.Kind == TypeDefKindStruct { if b.Kind == TypeDefKindStruct { return strings.Compare(a.Name, b.Name) } return -1 } else if b.Kind == TypeDefKindStruct { return 1 } else { return strings.Compare(a.Name, b.Name) } }) return defs, nil } func preprocessTypeDef(defs []*TypeDefLine) map[string][]*TypeDefLine { namespaceGrouped := make(map[string][]*TypeDefLine) classDefs := make(map[string]*TypeDefLine) for _, def := range defs { namespace := def.JSMod if namespace == "" { namespace = TopLevelNamespace } if def.Kind == TypeDefKindStruct { namespaceGrouped[namespace] = append(namespaceGrouped[namespace], def) classDefs[def.Name] = def } else if def.Kind == TypeDefKindImpl { if classDef, ok := classDefs[def.Name]; ok { if classDef.Def != "" { classDef.Def += "\n" } classDef.Def += def.Def } } else { namespaceGrouped[namespace] = append(namespaceGrouped[namespace], def) } } return namespaceGrouped } func correctStringIndent(src string, indent int) string { var result strings.Builder bracketDepth := 0 for _, line := range strings.Split(src, "\n") { line = strings.TrimSpace(line) if line == "" { result.WriteString("\n") continue } isInMultilineComment := strings.HasPrefix(line, "*") isClosingBracket := strings.HasSuffix(line, "}") isOpeningBracket := strings.HasSuffix(line, "{") rightIndent := indent if isOpeningBracket && !isInMultilineComment { bracketDepth++ rightIndent += (bracketDepth - 1) * 2 } else { if isClosingBracket && bracketDepth > 0 && !isInMultilineComment { bracketDepth-- } rightIndent += bracketDepth * 2 } if isInMultilineComment { rightIndent++ } result.WriteString(strings.Repeat(" ", rightIndent) + line + "\n") } res := result.String() if strings.HasSuffix(res, "\n") { res = res[:len(res)-1] } return res } ================================================ FILE: pkg/encorebuild/gentypedefs/napi.cjs.tmpl ================================================ // The version of the runtime this JS bundle was generated for. const version = {{.ModuleVersion | printf "%q"}}; // Load the native module. const nativeModulePath = process.env.ENCORE_RUNTIME_LIB; if (!nativeModulePath) { throw new Error( "The ENCORE_RUNTIME_LIB environment variable is not set. It must be set to the path of the Encore runtime library ('encore-runtime.node')." ); } const nativeModule = require(nativeModulePath); // Load the exported objects from the native module. const { {{- range .Exports}} {{.}}, {{- end}} } = nativeModule; // Export the objects from the native module. module.exports = { {{- range .Exports}} {{.}}, {{- end}} }; // Sanity check incase the JS bundle was built for a different version of the runtime. if (version !== Runtime.version()) { console.warn(`⚠️ WARNING: The version of the Encore runtime this JS bundle was built for (${version}) does not match the version of the Encore runtime it is running in (${Runtime.version()}). This may cause unexpected behaviour in your application. To resolve this, try update your Encore CLI using "encore version update" and then update the dependencies in your package.json file using "npm install encore.dev@latest".`); } ================================================ FILE: pkg/encorebuild/githubrelease/githubrelease.go ================================================ package githubrelease import ( "bufio" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" osPkg "os" "os/exec" "path/filepath" "strings" "encr.dev/pkg/encorebuild/buildconf" . "encr.dev/pkg/encorebuild/buildutil" "github.com/cockroachdb/errors" ) type Info struct { Version string // The version of the release Filename string // The filename of the release (inc extension) FileExt string // The file extension URL string // The URL to download the release from Checksum []byte // The checksum of the release } // getGithubRelease fetches the latest release from Github for the given org and repo. func FetchInfo(cfg *buildconf.Config, org string, repo string) *Info { rtn := &Info{} type GithubRelease struct { TagName string `json:"tag_name"` Assets []struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"` } `json:"assets"` } // Download the latest releases releasesResp := Must(http.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", org, repo))) defer func() { _ = releasesResp.Body.Close() }() if releasesResp.StatusCode != http.StatusOK { Bailf("Unexpected response status code: %s", releasesResp.Status) } releases := &GithubRelease{} Check(json.NewDecoder(releasesResp.Body).Decode(releases)) rtn.Version = releases.TagName // Build a list of possible file names osOptions := []string{cfg.OS} if cfg.OS == "darwin" { osOptions = append(osOptions, "macos") } archOptions := []string{cfg.Arch} if cfg.Arch == "amd64" { archOptions = append(archOptions, "x86_64", "x86-64") } else if cfg.Arch == "arm64" { archOptions = append(archOptions, "aarch64") } extOptions := []string{"tar.gz"} // , "zip"} var fileNameOptions []string for _, osOption := range osOptions { for _, archOption := range archOptions { for _, extOption := range extOptions { fileNameOptions = append(fileNameOptions, fmt.Sprintf("%s_%s.%s", osOption, archOption, extOption), fmt.Sprintf("%s-%s.%s", osOption, archOption, extOption), ) } } } // Find the checksum file checksumFileURL := "" for _, asset := range releases.Assets { // We want to know the checksum URL if strings.EqualFold(asset.Name, "checksums.txt") { checksumFileURL = asset.BrowserDownloadURL } for _, filenameOption := range fileNameOptions { if strings.EqualFold(asset.Name, filenameOption) { // We also want to know the asset name and download URL rtn.Filename = asset.Name rtn.URL = asset.BrowserDownloadURL if strings.HasSuffix(asset.Name, ".tar.gz") { rtn.FileExt = ".tar.gz" } else { rtn.FileExt = filepath.Ext(asset.Name) } } } } if checksumFileURL == "" { Bailf("unable to find checksum file in Github release") } if rtn.URL == "" || rtn.Filename == "" { Bailf("unable to find binary in Github release") } // Download the checksum file checksumResp := Must(http.Get(checksumFileURL)) defer func() { _ = checksumResp.Body.Close() }() if checksumResp.StatusCode != http.StatusOK { Bailf("Unexpected response status code for checksum file: %s", checksumResp.Status) } // Read the checksum file line by line scanner := bufio.NewScanner(checksumResp.Body) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasSuffix(line, rtn.Filename) { checksumStr := strings.Split(line, " ")[0] checksum := Must(hex.DecodeString(checksumStr)) rtn.Checksum = checksum return rtn } } Bailf("unable to find checksum for asset file in checksum file") panic("unreachable") } func DownloadLatest(cfg *buildconf.Config, org, repo string) (pathToFile string) { // Find the latest release release := FetchInfo(cfg, org, repo) // Create a cache dir for the download cache for this specific OS and architecture pair path := filepath.Join(cfg.CacheDir, "github-releases", org, repo, cfg.OS, cfg.Arch) Check(osPkg.MkdirAll(path, 0755)) downloadFileName := fmt.Sprintf("%s-%s%s", release.Version, hex.EncodeToString(release.Checksum), release.FileExt) downloadPath := filepath.Join(path, downloadFileName) // Check if the file already exists if _, err := osPkg.Stat(downloadPath); err == nil { return downloadPath } else if !osPkg.IsNotExist(err) { Bailf("failed to stat existing download file: %v", err) } // Now download the file downloadResp := Must(http.Get(release.URL)) defer func() { _ = downloadResp.Body.Close() }() if downloadResp.StatusCode != http.StatusOK { Bailf("Unexpected response status code for release file: %s", downloadResp.Status) } // Create the file downloadFile := Must(osPkg.Create(downloadPath)) defer func() { _ = downloadFile.Close() if r := recover(); r != nil { _ = osPkg.Remove(downloadPath) // delete any partially written file panic(r) // re-panic } }() _ = Must(io.Copy(downloadFile, downloadResp.Body)) // Now checksum the file _ = Must(downloadFile.Seek(0, 0)) checksum := Must(checksumFile(downloadFile)) // Check the checksum if !bytes.Equal(checksum, release.Checksum) { Bailf("checksum of downloaded file (%q) does not match expected checksum (%q)", hex.EncodeToString(checksum), hex.EncodeToString(release.Checksum)) } return downloadPath } func checksumFile(file *osPkg.File) ([]byte, error) { hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return nil, errors.Wrap(err, "unable to checksum file") } return hash.Sum(nil), nil } func Extract(pathToArchive string, targetDir string) { // Create the target dir Check(osPkg.MkdirAll(targetDir, 0755)) // Extract the archive cmd := exec.Command("tar", "-xzf", pathToArchive, "--strip-components", "1", "-C", targetDir) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { Bailf("failed to extract archive: %s", out) } } func copyDir(src, dst string) error { cmd := exec.Command("cp", "-r", src+"/", dst) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { return errors.Wrapf(err, "failed to copy dir: %s", out) } return nil } ================================================ FILE: pkg/encorebuild/jsruntimebuild.go ================================================ package encorebuild import ( "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/rs/zerolog" "encr.dev/pkg/encorebuild/buildconf" . "encr.dev/pkg/encorebuild/buildutil" "encr.dev/pkg/encorebuild/compile" "encr.dev/pkg/encorebuild/gentypedefs" ) func NewJSRuntimeBuilder(cfg *buildconf.Config) *JSRuntimeBuilder { if cfg.RepoDir == "" { Bailf("repo dir not set") } else if _, err := os.Stat(cfg.RepoDir); err != nil { Bailf("repo does not exist") } workdir := filepath.Join(cfg.CacheDir, "jsruntimebuild", cfg.OS, cfg.Arch) Check(os.MkdirAll(workdir, 0755)) return &JSRuntimeBuilder{ log: cfg.Log, cfg: cfg, workdir: workdir, } } type JSRuntimeBuilder struct { log zerolog.Logger cfg *buildconf.Config workdir string } func (b *JSRuntimeBuilder) Build() { b.log.Info().Msgf("Building local JS runtime targeting %s/%s", b.cfg.OS, b.cfg.Arch) b.buildRustModule() b.genTypeDefWrappers() b.makeDistFolder() if b.cfg.CopyToRepo { b.copyNativeModule() } } // buildRustModule builds the Rust module for the JS runtime. func (b *JSRuntimeBuilder) buildRustModule() { b.log.Info().Msg("building rust module") // Figure out the names of the compiled and target binaries. compiledBinaryName := func() string { switch b.cfg.OS { case "darwin": return "libencore_js_runtime.dylib" case "linux": return "libencore_js_runtime.so" case "windows": return "encore_js_runtime.dll" default: Bailf("unknown OS: %s", b.cfg.OS) panic("unreachable") } }() features := []string{} if !b.cfg.Release { // Enable runtime tracing in debug builds. features = append(features, "encore-runtime-core/rttrace") } compile.RustBinary( b.cfg, compiledBinaryName, b.NativeModuleOutput(), filepath.Join(b.cfg.RepoDir, "runtimes", "js"), "gnu", features, // features "TYPE_DEF_TMP_PATH="+b.typeDefPath(), "ENCORE_VERSION="+b.cfg.Version, "ENCORE_WORKDIR="+b.workdir, ) } // genTypeDefWrappers generates the napi.cjs and napi.d.cts files for // use by the JS SDK. func (b *JSRuntimeBuilder) genTypeDefWrappers() { b.log.Info().Msg("generating napi type definitions") napiPath := filepath.Join(b.npmPackagePath(), napiRelPath) Check(os.MkdirAll(napiPath, 0755)) cfg := gentypedefs.Config{ ReleaseVersion: b.cfg.Version, TypeDefFile: b.typeDefPath(), DtsOutputFile: filepath.Join(napiPath, "napi.d.cts"), CjsOutputFile: filepath.Join(napiPath, "napi.cjs"), } Check(gentypedefs.Generate(cfg)) } // makeDistFolder creates the dist folder for the JS runtime, // and fixes the imports to be ESM-compatible. func (b *JSRuntimeBuilder) makeDistFolder() { b.log.Info().Msg("creating dist folder") // Sanity-check the runtime dir configuration so we don't delete the wrong thing. base := filepath.Base(b.cfg.RepoDir) parentBase := filepath.Base(filepath.Dir(b.cfg.RepoDir)) if b.cfg.RepoDir == "" || (base != "encore" && base != "encr.dev" && parentBase != "encore.worktrees") { Bailf("invalid repo directory %q, aborting", b.cfg.RepoDir) } pkgPath := filepath.Join(b.jsRuntimePath(), "encore.dev") distPath := filepath.Join(pkgPath, "dist") Check(os.RemoveAll(distPath)) // Run 'npm install'. { cmd := exec.Command("npm", "install") cmd.Dir = pkgPath cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } // Run 'npm run build'. { cmd := exec.Command("npm", "run", "build") cmd.Dir = pkgPath cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } // Copy the napi directory over. { src := filepath.Join(b.npmPackagePath(), napiRelPath) dst := filepath.Join(distPath, napiRelPath) cmd := exec.Command("cp", "-r", src, dst) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } // Run 'tsc-esm-fix'. { cmd := exec.Command("./node_modules/.bin/tsc-esm-fix", "--target=dist") cmd.Dir = pkgPath cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } } func (b *JSRuntimeBuilder) copyNativeModule() { b.log.Info().Msg("copying native module") copyFile := func(src, dst string) { cmd := exec.Command("cp", src, dst) out, err := cmd.CombinedOutput() if err != nil { Bailf("unable to copy native module: %v: %s", err, out) } } src := b.NativeModuleOutput() suffix := "" if b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH { suffix = "-" + b.cfg.OS + "-" + b.cfg.Arch } dst := filepath.Join(b.jsRuntimePath(), "encore-runtime.node"+suffix) copyFile(src, dst) } func (b *JSRuntimeBuilder) NativeModuleOutput() string { return filepath.Join(b.workdir, "encore-runtime.node") } func (b *JSRuntimeBuilder) typeDefPath() string { return filepath.Join(b.workdir, "typedefs.ndjson") } func (b *JSRuntimeBuilder) npmPackagePath() string { return filepath.Join(b.jsRuntimePath(), "encore.dev") } func (b *JSRuntimeBuilder) jsRuntimePath() string { return filepath.Join(b.cfg.RepoDir, "runtimes", "js") } // napiRelPath is the relative path from the package root to the napi directory. var napiRelPath = filepath.Join("internal", "runtime", "napi") func PublishNPMPackages(repoDir, version string) { packages := []string{"encore.dev"} npmVersion := strings.TrimPrefix(version[1:], "v") npmTag := "latest" switch { case strings.Contains(version, "-beta."): npmTag = "beta" case strings.Contains(version, "-nightly."): npmTag = "nightly" } // Configure the auth token { authToken := os.Getenv("NPM_PUBLISH_TOKEN") if authToken == "" { Bailf("NPM_PUBLISH_TOKEN not set") } cmd := exec.Command("npm", "set", "//registry.npmjs.org/:_authToken="+authToken) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } for _, pkg := range packages { pkgDir := filepath.Join(repoDir, "runtimes", "js", pkg) // Run 'npm version'. { cmd := exec.Command("npm", "version", "--no-git-tag-version", "--no-commit-hooks", npmVersion) cmd.Dir = pkgDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } // Run 'npm publish'. { cmd := exec.Command("npm", "publish", "--tolerate-republish", "--access", "public", "--tag", npmTag, ) cmd.Dir = pkgDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr Check(cmd.Run()) } } } ================================================ FILE: pkg/encorebuild/supervisorbuild.go ================================================ package encorebuild import ( "os" "os/exec" "path/filepath" "runtime" "github.com/rs/zerolog" "encr.dev/pkg/encorebuild/buildconf" . "encr.dev/pkg/encorebuild/buildutil" "encr.dev/pkg/encorebuild/compile" ) func NewSupervisorBuilder(cfg *buildconf.Config) *SupervisorBuilder { if cfg.RepoDir == "" { Bailf("repo dir not set") } else if _, err := os.Stat(cfg.RepoDir); err != nil { Bailf("repo does not exist") } workdir := filepath.Join(cfg.CacheDir, "supervisorbuild", cfg.OS, cfg.Arch) Check(os.MkdirAll(workdir, 0755)) return &SupervisorBuilder{ log: cfg.Log, cfg: cfg, workdir: workdir, } } type SupervisorBuilder struct { log zerolog.Logger cfg *buildconf.Config workdir string } func (b *SupervisorBuilder) Build() { b.log.Info().Msgf("Building local Supervisor targeting %s/%s", b.cfg.OS, b.cfg.Arch) b.buildRustModule() if b.cfg.CopyToRepo { b.copyToRepo() } } // buildRustModule builds the Rust module for the Supervisor runtime. func (b *SupervisorBuilder) buildRustModule() { b.log.Info().Msg("building rust module") compile.RustBinary( b.cfg, "supervisor-encore", b.BinaryOutput(), filepath.Join(b.cfg.RepoDir, "supervisor"), "musl", []string{}, // features "ENCORE_VERSION="+b.cfg.Version, "ENCORE_WORKDIR="+b.workdir, ) } func (b *SupervisorBuilder) copyToRepo() { b.log.Info().Msg("copying binary to repo dir") copyFile := func(src, dst string) { cmd := exec.Command("cp", src, dst) out, err := cmd.CombinedOutput() if err != nil { Bailf("unable to copy binary: %v: %s", err, out) } } src := b.BinaryOutput() suffix := "" if b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH { suffix = "-" + b.cfg.OS + "-" + b.cfg.Arch } dst := filepath.Join(b.cfg.RepoDir, "runtimes", "supervisor-encore"+suffix) copyFile(src, dst) } func (b *SupervisorBuilder) BinaryOutput() string { return filepath.Join(b.workdir, "supervisor-encore") } ================================================ FILE: pkg/encorebuild/windows/.gitignore ================================================ /.deps ================================================ FILE: pkg/encorebuild/windows/build.bat ================================================ @echo off rem SPDX-License-Identifier: MIT rem Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. setlocal enableextensions enabledelayedexpansion set BUILDDIR=%~dp0 set ROOT=%BUILDDIR%\..\..\.. set DST=%ROOT%\dist\windows_amd64 set PATH=%BUILDDIR%.deps\llvm-mingw\bin;%BUILDDIR%.deps;%PATH% set PATHEXT=.EXE;.CMD if "%ENCORE_VERSION%" == "" ( echo ENCORE_VERSION not set exit /b 1 ) if "%ENCORE_GOROOT%" == "" ( echo ENCORE_GOROOT not set exit /b 1 ) :: Get absolute path cd %ENCORE_GOROOT% || exit /b 1 set ENCORE_GOROOT=%CD% cd /d %BUILDDIR% || exit /b 1 if exist .deps\prepared goto :build :installdeps rmdir /s /q .deps 2> NUL mkdir .deps || goto :error cd .deps || goto :error call :download llvm-mingw-msvcrt.zip https://download.wireguard.com/windows-toolchain/distfiles/llvm-mingw-20201020-msvcrt-x86_64.zip 2e46593245090df96d15e360e092f0b62b97e93866e0162dca7f93b16722b844 || goto :error call :download wintun.zip https://www.wintun.net/builds/wintun-0.10.2.zip fcd9f62f1bd5a550fcb9c21fbb5d6a556214753ccbbd1a3ebad4d318ec9dcbef || goto :error call :download wix-binaries.zip https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip 2c1888d5d1dba377fc7fa14444cf556963747ff9a0a289a3599cf09da03b9e2e || goto :error copy /y NUL prepared > NUL || goto :error cd .. || goto :error :build set GOOS=windows call :build_plat amd64 x86_64 amd64 || goto :error call :copy_artifacts || goto :error :success echo [+] Success! exit /b 0 :download echo [+] Downloading %1 curl -#fLo %1 %2 || exit /b 1 echo [+] Verifying %1 for /f %%a in ('CertUtil -hashfile %1 SHA256 ^| findstr /r "^[0-9a-f]*$"') do if not "%%a"=="%~3" exit /b 1 echo [+] Extracting %1 tar -xf %1 %~4 || exit /b 1 echo [+] Cleaning up %1 del %1 || exit /b 1 goto :eof :build_plat rmdir /S /Q "%DST%" mkdir %DST%\bin >NUL 2>&1 echo [+] Assembling resources x86_64-w64-mingw32-windres -I ".deps\wintun\bin\amd64" -i resources.rc -o "%ROOT%\cli\cmd\encore\resources_amd64.syso" -O coff -c 65001 || exit /b %errorlevel% set GOARCH=amd64 echo [+] Building go build -tags load_wintun_from_rsrc -ldflags "-X 'encr.dev/internal/version.Version=v%ENCORE_VERSION%'" -o "%DST%\bin\encore.exe" "%ROOT%\cli\cmd\encore" || exit /b 1 go build -trimpath -o "%DST%\bin\git-remote-encore.exe" "%ROOT%\cli\cmd\git-remote-encore" || exit /b 1 goto :eof :copy_artifacts echo [+] Copying files xcopy /S /I /E /H /Q "%ENCORE_GOROOT%" "%DST%\encore-go" || exit /b 1 xcopy /S /I /E /H /Q "%ROOT%\runtimes\go" "%DST%\runtimes\go" || exit /b 1 goto :eof :error echo [-] Failed with error #%errorlevel%. cmd /c exit %errorlevel% ================================================ FILE: pkg/encorebuild/windows/manifest.xml ================================================ PerMonitorV2, PerMonitor True ================================================ FILE: pkg/encorebuild/windows/resources.rc ================================================ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. */ #pragma code_page(65001) // UTF-8 CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml wintun.dll RCDATA wintun.dll ================================================ FILE: pkg/environ/environ.go ================================================ package environ // Environ is a slice of strings representing the environment of a process. type Environ []string // Get retrieves the value of the environment variable named by the key. // It returns the value, which will be empty if the variable is not present. // To distinguish between an empty value and an unset value, use LookupEnv. func (e Environ) Get(key string) string { v, _ := e.Lookup(key) return v } // Lookup retrieves the value of the environment variable named // by the key. If the variable is present in the environment the // value (which may be empty) is returned and the boolean is true. // Otherwise the returned value will be empty and the boolean will // be false. func (e Environ) Lookup(key string) (string, bool) { for _, env := range e { if len(env) > len(key) && env[len(key)] == '=' && env[:len(key)] == key { return env[len(key)+1:], true } } return "", false } ================================================ FILE: pkg/errinsrc/characters.go ================================================ package errinsrc // The current character set to use when rendering var set CharacterSet = unicodeSet type CharacterSet struct { HorizontalBar rune VerticalBar rune CrossBar rune VerticalBreak rune VerticalGap rune UpArrow rune RightArrow rune LeftTop rune MiddleTop rune RightTop rune LeftBottom rune RightBottom rune MiddleBottom rune LeftBracket rune RightBracket rune LeftCross rune RightCross rune UnderBar rune Underline rune } var unicodeSet = CharacterSet{ HorizontalBar: '─', VerticalBar: '│', CrossBar: '┼', VerticalBreak: '·', VerticalGap: '⋮', UpArrow: '▲', RightArrow: '▶', LeftTop: '╭', MiddleTop: '┬', RightTop: '╮', LeftBottom: '╰', MiddleBottom: '┴', RightBottom: '╯', LeftBracket: '[', RightBracket: ']', LeftCross: '├', RightCross: '┤', UnderBar: '┬', Underline: '─', } var asciiSet = CharacterSet{ HorizontalBar: '-', VerticalBar: '|', CrossBar: '+', VerticalBreak: '*', VerticalGap: ':', UpArrow: '^', RightArrow: '>', LeftTop: ',', MiddleTop: 'v', RightTop: '.', LeftBottom: '`', MiddleBottom: '^', RightBottom: '\'', LeftBracket: '[', RightBracket: ']', LeftCross: '|', RightCross: '|', UnderBar: '|', Underline: '^', } ================================================ FILE: pkg/errinsrc/errinsrc.go ================================================ package errinsrc import ( "fmt" "go/ast" "go/token" "strings" "github.com/pkg/errors" encerrors "encr.dev/pkg/errors" "encr.dev/pkg/option" "encr.dev/pkg/paths" . "encr.dev/pkg/errinsrc/internal" ) // ErrInSrc represents an error which occurred due to the source code // of the application being run through Encore. // // It supports the concept of one of more locations within the users // source code which caused the error. The locations will be rendered // in the final output. // // Construct these using helper functions in the `srcerrors` package // as we can use that as a central repository error types type ErrInSrc struct { // The parameters of the error // This is an internal data type to force the // creation of these inside `srcerrors` Params ErrParams `json:"params"` // The Stack trace of where the error was created within the Encore codebase // this will be empty if the error was created in a production build of Encore. // To populate this, build Encore with the tag `dev_build`. Stack []*StackFrame `json:"stack,omitempty"` } var _ error = (*ErrInSrc)(nil) // New returns a new ErrInSrc with a Stack trace attached func New(params ErrParams, alwaysIncludeStack bool) *ErrInSrc { var stack []*StackFrame //goland:noinspection GoBoolExpressions if IncludeStackByDefault || alwaysIncludeStack { if params.Cause != nil { stack = bottomStackTraceFrom(params.Cause) } if len(stack) == 0 { stack = GetStack() } } return &ErrInSrc{ Params: params, Stack: stack, } } // FromTemplate returns a new ErrInSrc using the [errs.Template] as a template func FromTemplate(template encerrors.Template, fileset *token.FileSet, fileReaders ...paths.FileReader) *ErrInSrc { // Setup the parameters params := ErrParams{ Code: template.Code, Title: template.Title, Summary: template.Summary, Detail: template.Detail, Cause: template.Cause, } // Read the locations for _, tmplLoc := range template.Locations { var location option.Option[*SrcLocation] switch tmplLoc.Kind { case encerrors.LocFile: params.Summary += "\n\nIn file: " + tmplLoc.Filepath continue case encerrors.LocGoNode: location = FromGoASTNode(fileset, tmplLoc.GoNode, fileReaders...) case encerrors.LocGoPos: location = FromGoTokenPos(fileset, tmplLoc.GoStartPos, tmplLoc.GoEndPos, fileReaders...) case encerrors.LocGoPositions: location = FromGoTokenPositions(tmplLoc.GoStartPosition, tmplLoc.GoEndPosition, fileReaders...) default: panic(fmt.Sprintf("unknown location kind: %v", tmplLoc.Kind)) } loc, ok := location.Get() if !ok { continue } switch tmplLoc.LocType { case encerrors.LocError: loc.Type = LocError case encerrors.LocWarning: loc.Type = LocWarning case encerrors.LocHelp: loc.Type = LocHelp default: panic(fmt.Sprintf("unknown location type: %v", tmplLoc.LocType)) } loc.Text = tmplLoc.Text params.Locations = append(params.Locations, loc) } // Create the error return New(params, template.AlwaysIncludeStack) } // TerminalWidth is the width of the terminal in columns that we're rendering to. // // We default to 100 characters, but the CLI overrides this when it renders errors. // When using the value, note it might be very small (e.g. 5) if the user has shrunk // their terminal window super small. Thus any code which uses this or creates new // widths off it should cope with <= 0 values. var TerminalWidth = 100 func (e *ErrInSrc) Unwrap() error { return e.Params.Cause } // StackTrace implements the StackTraceProvider interface for some libraries // including ZeroLog, xerrors and Sentry func (e *ErrInSrc) StackTrace() errors.StackTrace { frames := make([]errors.Frame, len(e.Stack)) for i, frame := range e.Stack { // Note: interpreted as a uintptr its value represents the program counter + 1. frames[i] = errors.Frame(frame.ProgramCounter + 1) } return frames } func (e *ErrInSrc) Is(target error) bool { if target == nil || e == nil { return target == e } if target, ok := target.(*ErrInSrc); ok && target != nil { return target.Params.Title == e.Params.Title } return false } func (e *ErrInSrc) As(target any) bool { if target, ok := target.(*ErrInSrc); ok { *target = *e return true } return false } // Bailout is a helper function which will abort the current process // and report the error func (e *ErrInSrc) Bailout() { panic(Bailout{List: List{e}}) } func (e *ErrInSrc) Title() string { return e.Params.Title } func (e *ErrInSrc) Error() string { var b strings.Builder // Write the header const headerGrayLevel = 12 const spacing = 4 + 2 + 7 // (4 = "--" on both sides, 2 = " " on the sides of the title, 7 = "[E0000]") b.WriteRune('\n') // Always start with a new line as these errors are expected to be full screen b.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf("%c%c ", set.HorizontalBar, set.HorizontalBar)).String()) b.WriteString(aurora.Red(e.Params.Title).String()) b.WriteByte(' ') headerWidth := TerminalWidth - len(e.Params.Title) - spacing if headerWidth > 0 { b.WriteString(aurora.Gray(headerGrayLevel, strings.Repeat(string(set.HorizontalBar), headerWidth)).String()) } b.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf("%cE%04d%c", set.LeftBracket, e.Params.Code, set.RightBracket)).String()) b.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf("%c%c\n\n", set.HorizontalBar, set.HorizontalBar)).String()) // Write the summary if e.Params.Summary != "" { wordWrap(e.Params.Summary, &b) b.WriteString("\n") } // List the root causes if len(e.Params.Locations) > 0 { for _, causes := range e.Params.Locations.GroupByFile() { renderSrc(&b, causes) b.WriteString("\n") } } // Write any details out if e.Params.Detail != "" { wordWrap(e.Params.Detail, &b) b.WriteString("\n") } // Write the Stack trace out (for where the error was generated within Encore's source) if len(e.Stack) > 0 { prettyPrintStack(e.Stack, &b) } return b.String() } func (e *ErrInSrc) OnSameLine(other *ErrInSrc) bool { for _, loc := range e.Params.Locations { for _, otherLoc := range other.Params.Locations { if loc.Start.Line >= otherLoc.Start.Line && loc.End.Line <= otherLoc.End.Line { return true } } } return false } // WithGoNode adds a Go AST node to the error func (e *ErrInSrc) WithGoNode(fileset *token.FileSet, node ast.Node, fileReaders ...paths.FileReader) { if val, ok := FromGoASTNode(fileset, node, fileReaders...).Get(); ok { e.Params.Locations = append(e.Params.Locations, val) } } ================================================ FILE: pkg/errinsrc/internal/cuelocation.go ================================================ package internal import ( "os" "sort" "strings" "cuelang.org/go/cue/ast" cueerrors "cuelang.org/go/cue/errors" "cuelang.org/go/cue/parser" "github.com/rs/zerolog/log" ) // LocationsFromCueError returns a list of SrcLocations based on what was given in the // cueerror.Error. func LocationsFromCueError(err cueerrors.Error, pathPrefix string) SrcLocations { // Convert cueerror.Pos to a *CueLocation rtn := make(SrcLocations, 0, len(err.InputPositions())) if pos := err.Position(); pos.IsValid() { rtn = append(rtn, FromCueTokenPos(pos, pathPrefix)) } for _, pos := range err.InputPositions() { if pos.IsValid() { rtn = append(rtn, FromCueTokenPos(pos, pathPrefix)) } } sort.Sort(rtn) return rtn } // FromCueTokenPos converts a cueerror.Pos to a SrcLocation // // We use an interface for `cue/token.Pos` so we can test it func FromCueTokenPos(cueLoc interface { Filename() string Line() int Column() int }, pathPrefix string) *SrcLocation { // Note: for CUE files we must read the bytes now, as the defer on the CUE load code will delete // the source file form the disk before this location is rendered bytes, err := os.ReadFile(cueLoc.Filename()) if err != nil { log.Err(err).Str("filename", cueLoc.Filename()).Msg("Failed to read CUE file") // Don't return, `bytes == nil` is fine here } start := Pos{Line: cueLoc.Line(), Col: cueLoc.Column()} end := convertSingleCUEPositionToRange(cueLoc.Filename(), bytes, start) return &SrcLocation{ File: &File{ RelPath: strings.TrimPrefix(cueLoc.Filename(), pathPrefix), FullPath: cueLoc.Filename(), Contents: bytes, }, Start: start, End: end, Type: LocError, } } // convertSingleCUEPositionToRange attempts to convert a CUE error from a single position to a range // // It does this by running the CUE parser over the file and looking for the AST node that starts // at the same line and column. Once found, we use the end position of that node as the end position // of the error. func convertSingleCUEPositionToRange(filename string, bytes []byte, start Pos) Pos { file, err := parser.ParseFile(filename, bytes, parser.ParseComments) if err != nil { return start } var matching ast.Node ast.Walk(file, func(node ast.Node) bool { if node.Pos().Line() == start.Line && node.Pos().Column() == start.Col { matching = node return false } return true }, nil) if matching != nil { return Pos{Line: matching.End().Line(), Col: matching.End().Column()} } return start } ================================================ FILE: pkg/errinsrc/internal/golocation.go ================================================ package internal import ( "go/ast" "go/parser" "go/token" "os" "github.com/rs/zerolog/log" "encr.dev/pkg/option" "encr.dev/pkg/paths" ) func FromGoASTNodeWithTypeAndText(fileset *token.FileSet, node ast.Node, typ LocationType, text string, fileReaders ...paths.FileReader) option.Option[*SrcLocation] { loc := FromGoASTNode(fileset, node, fileReaders...) if l, ok := loc.Get(); ok { l.Type = typ l.Text = text } return loc } // FromGoASTNode returns a SrcLocation from a Go AST node storing the start and end // locations of that node. func FromGoASTNode(fileset *token.FileSet, node ast.Node, fileReaders ...paths.FileReader) option.Option[*SrcLocation] { start := fileset.Position(node.Pos()) end := fileset.Position(node.End()) // Custom end locations for some node types switch node := node.(type) { case *ast.CallExpr: end = fileset.Position(node.Rparen + 1) } if !start.IsValid() || !end.IsValid() { return option.None[*SrcLocation]() } return FromGoTokenPositions(start, end, fileReaders...) } func FromGoTokenPos(fileset *token.FileSet, start, end token.Pos, fileReaders ...paths.FileReader) option.Option[*SrcLocation] { startPos := fileset.Position(start) endPos := fileset.Position(end) if !startPos.IsValid() || !endPos.IsValid() { return option.None[*SrcLocation]() } return FromGoTokenPositions(startPos, endPos, fileReaders...) } // FromGoTokenPositions returns a SrcLocation from two Go token positions. // They can be the same position or different positions. However, they must // be locations within the same file. // // This function will panic if the locations are in different files. func FromGoTokenPositions(start token.Position, end token.Position, fileReaders ...paths.FileReader) option.Option[*SrcLocation] { if start.Filename != end.Filename { panic("FromGoASTNode: start and end files must be the same") } fileReaders = append(fileReaders, os.ReadFile) var bytes []byte var err error for _, reader := range fileReaders { if reader == nil { continue } bytes, err = reader(start.Filename) if err == nil { break } } if err != nil { log.Err(err).Str("filename", start.Filename).Msg("Failed to read Go file") // Don't return, `bytes == nil` is fine here } // Attempt to convert a single start/end position into a range if start == end { end = convertSingleGoPositionToRange(start.Filename, bytes, start) } // If either position is invalid, return nil // as that means we're not dealing with a Go Token Position if !start.IsValid() || !end.IsValid() { log.Warn().Str("start", start.String()).Str("end", end.String()).Msg("Invalid Go token position") return option.None[*SrcLocation]() } return option.Some(&SrcLocation{ File: &File{ RelPath: start.Filename, FullPath: start.Filename, Contents: bytes, }, Start: Pos{Line: start.Line, Col: start.Column}, End: Pos{Line: end.Line, Col: end.Column}, Type: LocError, }) } // convertSingleGoPositionToRange attempts to convert a single Go token position to a range with a start and end // position. // // This is done by attempting to parse the file, and then if we are able to parse it successfully, we look for an AST // node which starts at the exact line and column of the position. If we find one, we use the end position of that // node as the end position of the range. // // We use the first found node at that position, as we assume the largest node at that position is the most relevant func convertSingleGoPositionToRange(filename string, fileBody []byte, start token.Position) (end token.Position) { fs := token.NewFileSet() file, err := parser.ParseFile(fs, filename, fileBody, parser.ParseComments) if err != nil || file == nil { end = start // If the file is not parsable for some reason (e.g. syntax error), we can't determine the end position // based on ast.Nodes. If so, we can fall back on guessing the end position by looking for common delimiters offset, ok := findPositionOffset(start, fileBody) if !ok { return end } endOffset := GuessEndColumn(fileBody, offset) end.Column += endOffset - offset return end } var match ast.Node ast.Inspect(file, func(n ast.Node) bool { if n == nil { return true } nodePos := fs.Position(n.Pos()) if nodePos.Line == start.Line && nodePos.Column == start.Column { match = n return false } return true }) if match != nil { return fs.Position(match.End()) } return start } func findPositionOffset(pos token.Position, data []byte) (int, bool) { line, col := 1, 1 for i, c := range data { if line == pos.Line && col == pos.Column { return i, true } else if line > pos.Line { return -1, false } if c == '\n' { line++ col = 1 } else { col++ } } return -1, false } func GuessEndColumn(data []byte, offset int) int { var params, brackets, braces int inBackticks := false for i := offset; i < len(data); i++ { switch data[i] { case '(': params++ case '[': brackets++ case '{': braces++ case ')': params-- if params <= 0 { return i + 1 } case ']': brackets-- if brackets <= 0 { return i + 1 } case '}': braces-- if braces <= 0 { return i + 1 } case '`': inBackticks = !inBackticks case ';', ',', ':', '"', '\'': if !inBackticks && params == 0 && brackets == 0 && braces == 0 { return i + 1 } case ' ', '\t', '\n', '\r': if params == 0 && brackets == 0 && braces == 0 { return i + 1 } } } return len(data) + 1 } ================================================ FILE: pkg/errinsrc/internal/helper.go ================================================ package internal // ErrParams are used to create *errinsrc.ErrInSrc objects. // // It exists within an `internal` package so that it can only // be used by other packages within the `errinsrc` folder. // This is enforce through the Go compiler that all // errors are created inside the `srcerrors` subpackage. type ErrParams struct { Code int `json:"code"` Title string `json:"title"` Summary string `json:"summary"` Detail string `json:"detail,omitempty"` Cause error `json:"-"` Locations SrcLocations `json:"locations,omitempty"` } ================================================ FILE: pkg/errinsrc/internal/location.go ================================================ package internal import ( "cmp" "slices" "sort" "encr.dev/pkg/option" ) type LocationType uint8 const ( LocError LocationType = iota // Renders in red LocWarning // Renders in yellow LocHelp // Renders in blue ) type SrcLocation struct { Type LocationType `json:"type"` // The type of this location Text string `json:"text,omitempty"` // Optional text to render at this location File *File `json:"file,omitempty"` // The file containing the error Start Pos `json:"start"` // The position this location starts at End Pos `json:"end"` // The position this location ends at } func (s *SrcLocation) Less(other *SrcLocation) bool { // Order by type of location first (Err, then Warn, then Help) // as we always want errors rendered above warnings and warnings above help // if s.Type != other.Type { // return s.Type < other.Type // } // Order by file first if s.File.FullPath != other.File.FullPath { return s.File.FullPath < other.File.FullPath } // And then by the line number of where they start if s.Start.Line != other.Start.Line { return s.Start.Line < other.Start.Line } // And then where they start on that line if s.Start.Col != other.Start.Col { return s.Start.Col < other.Start.Col } // And which line they end on if s.End.Line != other.End.Line { return s.End.Line < other.End.Line } // Then by ending column if s.End.Col != other.End.Col { return s.End.Col < other.End.Col } // Finally, by the text in the location if s.Text != other.Text { return s.Text < other.Text } if s.Type != other.Type { return s.Type < other.Type } return false } type Pos struct { Line int `json:"line"` Col int `json:"col"` } type File struct { RelPath string // The relative path within the project FullPath string // The full path to the file Contents []byte // The contents of the file } // SrcLocations represents a list of locations // within the source code. It can be sorted and split // up into separate lists using GroupByFile type SrcLocations []*SrcLocation var _ sort.Interface = SrcLocations{} func (s SrcLocations) Len() int { return len(s) } func (s SrcLocations) Less(i, j int) bool { return s[i].Less(s[j]) } func (s SrcLocations) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func NewSrcLocations(opts ...option.Option[*SrcLocation]) SrcLocations { var rtn SrcLocations for _, opt := range opts { if loc, ok := opt.Get(); ok { rtn = append(rtn, loc) } } return rtn } // GroupByFile groups all locations by file and returns a new // SrcLocations for each file. // // If a file has overlapping locations or two locations on the same line // then more than one SrcLocations will be returned for that file. This // is due to a limitation in the srcrender, and may be relaxed in the future. func (s SrcLocations) GroupByFile() []SrcLocations { type locationGroup struct { fileName string locations SrcLocations } var nonOverlappingLocations []*locationGroup inlineOnSameLine := func(a, b *SrcLocation) bool { return a.Start.Line == a.End.Line && b.Start.Line == b.End.Line && a.Start.Line == b.Start.Line && a.Text == "" && b.Text == "" && // Don't inline if there is text as we don't support rendering this yet ((a.Start.Col > b.End.Col) || (a.End.Col < b.Start.Col)) } // Add locations to groups on the same file without overlaps nextOriginalLoc: for _, loc := range s { // Attempt to match it into an existing group for _, grp := range nonOverlappingLocations { if grp.fileName == loc.File.FullPath { for _, other := range grp.locations { if other.Start.Line > loc.End.Line || other.End.Line < loc.Start.Line || inlineOnSameLine(other, loc) { grp.locations = append(grp.locations, loc) continue nextOriginalLoc } } } } // if here we found no matching groups nonOverlappingLocations = append(nonOverlappingLocations, &locationGroup{ fileName: loc.File.FullPath, locations: SrcLocations{loc}, }) } // Sort the locations in each group rtn := make([]SrcLocations, len(nonOverlappingLocations)) for i, grp := range nonOverlappingLocations { sort.Sort(grp.locations) rtn[i] = grp.locations } // Now sort the groups by the lowest location hint // this means that errors will be rendered first, then warnings, then help // if they are in different files slices.SortStableFunc(rtn, func(a, b SrcLocations) int { lowestA := LocHelp lowestB := LocHelp for _, loc := range a { if loc.Type < lowestA { lowestA = loc.Type } } for _, loc := range b { if loc.Type < lowestB { lowestB = loc.Type } } return cmp.Compare(lowestA, lowestB) }) return rtn } ================================================ FILE: pkg/errinsrc/list.go ================================================ package errinsrc import ( "fmt" "sort" ) // List is a list of ErrInSrc objects. type List []*ErrInSrc var _ sort.Interface = List{} var _ ErrorList = List{} var _ error = List{} func (l List) Len() int { return len(l) } func (l List) Less(i, j int) bool { // This less function follows (as much as possible) the behaviour // of scanner.ErrorList's sort, that is filename, then line, then column // We then move onto extra data only Encore has iErr, jErr := l[i], l[j] numLocationsToCompare := len(iErr.Params.Locations) if otherNum := len(jErr.Params.Locations); otherNum < numLocationsToCompare { numLocationsToCompare = otherNum } for idx := 0; idx < numLocationsToCompare; idx++ { iLoc, jLoc := iErr.Params.Locations[idx], jErr.Params.Locations[idx] if iLoc.Less(jLoc) { return true } } // Now our custom sort logic if iErr.Params.Code != jErr.Params.Code { return iErr.Params.Code < jErr.Params.Code } if iErr.Params.Title != jErr.Params.Title { return iErr.Params.Title < jErr.Params.Title } if iErr.Params.Summary != jErr.Params.Summary { return iErr.Params.Summary < jErr.Params.Summary } if iErr.Params.Detail != jErr.Params.Detail { return iErr.Params.Detail < jErr.Params.Detail } if len(iErr.Params.Locations) != len(jErr.Params.Locations) { return len(iErr.Params.Locations) < len(jErr.Params.Locations) } return false } func (l List) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l List) Error() string { switch len(l) { case 0: return "no errors" case 1: return l[0].Error() } return fmt.Sprintf("%s (and %d more errors)", l[0], len(l)-1) } func (l List) ErrorList() []*ErrInSrc { return l } ================================================ FILE: pkg/errinsrc/setup_test.go ================================================ package errinsrc import ( "os" "path" "testing" "encr.dev/pkg/golden" ) var testDataFullPath string func TestMain(m *testing.M) { ColoursInErrors(false) golden.TestMain(m) } func init() { var err error testDataFullPath, err = os.Getwd() if err != nil { panic(err) } testDataFullPath = path.Join(testDataFullPath, "testdata") } ================================================ FILE: pkg/errinsrc/srcerrors/errors.go ================================================ package srcerrors import ( "fmt" "go/ast" "go/scanner" "go/token" "strconv" "strings" "golang.org/x/tools/go/packages" "encr.dev/pkg/errinsrc" . "encr.dev/pkg/errinsrc/internal" "encr.dev/pkg/paths" ) // UnhandledPanic is an error we use to wrap a panic that was not handled // It should ideally never be seen by users, but if it is, it means we have // a bug within Encore which needs fixing. func UnhandledPanic(recovered any) error { if err := errinsrc.ExtractFromPanic(recovered); err != nil { return err } // If recovered is an error, then track it as the source var srcError error if err, ok := recovered.(error); ok { srcError = err } // If we get here, it's an unhandled panic / error return errinsrc.New(ErrParams{ Code: 1, Title: "Internal compiler error", Summary: fmt.Sprintf("An unhandled panic occurred in the Encore compiler: %v", recovered), Detail: internalErrReportToEncore, Cause: srcError, }, true) } // GenericGoParserError reports an error was that was reported from the Go parser. // It should not be returned by any errors caused by Encore's own parser as they // should have specific errors listed below func GenericGoParserError(err *scanner.Error, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc { locs := SrcLocations{} if pos, ok := FromGoTokenPositions(err.Pos, err.Pos, fileReaders...).Get(); ok { locs = SrcLocations{pos} } return errinsrc.New(ErrParams{ Code: 2, Title: "Parse Error in Go Source", Summary: err.Msg, Cause: err, Locations: locs, }, false) } // GenericGoPackageError reports an error was that was reported from the Go package loader. // It should not be returned by any errors caused by Encore's own parser as they // should have specific errors listed below func GenericGoPackageError(err packages.Error, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc { var locations SrcLocations // Extract the position from the error var pos token.Position switch p := strings.SplitN(err.Pos, ":", 3); len(p) { case 3: pos.Column, _ = strconv.Atoi(p[2]) fallthrough case 2: pos.Line, _ = strconv.Atoi(p[1]) fallthrough case 1: if p[0] != "" && p[0] != "-" { pos.Filename = p[0] } } if pos.Filename != "" && pos.Line > 0 { locations = NewSrcLocations(FromGoTokenPositions(pos, pos, fileReaders...)) } return errinsrc.New(ErrParams{ Code: 3, Title: "Go Package Error", Summary: err.Msg, Cause: err, Locations: locations, }, false) } // GenericGoCompilerError reports an error was that was reported from the Go compiler. // It should not be returned by any errors caused by Encore's own compiler as they // should have specific errors listed below. func GenericGoCompilerError(fileName string, lineNumber int, column int, error string, fileReaders ...paths.FileReader) error { errLocation := token.Position{ Filename: fileName, Offset: 0, Line: lineNumber, Column: column, } return errinsrc.New(ErrParams{ Code: 3, Title: "Go Compilation Error", Summary: strings.TrimSpace(error), Locations: NewSrcLocations(FromGoTokenPositions(errLocation, errLocation, fileReaders...)), }, false) } // StandardLibraryError is an error that is not caused by Encore, but is // returned by a standard library function. We wrap it in an ErrInSrc so that // we can still possibly provide a source location. func StandardLibraryError(err error) *errinsrc.ErrInSrc { return errinsrc.New(ErrParams{ Code: 3, Title: "Error", Summary: err.Error(), Cause: err, }, true) } // GenericError is a place holder for errors reported through perr.Add or perr.Addf func GenericError(pos token.Position, msg string, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc { return errinsrc.New(ErrParams{ Code: 3, Title: "Error", Summary: msg, Locations: NewSrcLocations(FromGoTokenPositions(pos, pos, fileReaders...)), }, false) } func UnableToLoadCUEInstances(err error, pathPrefix string) error { return handleCUEError(err, pathPrefix, ErrParams{ Code: 6, Title: "Unable to load CUE instances", }) } func UnableToAddOrphanedCUEFiles(err error, pathPrefix string) error { return handleCUEError(err, pathPrefix, ErrParams{ Code: 7, Title: "Unable to add orphaned CUE files", }) } func CUEEvaluationFailed(err error, pathPrefix string) error { return handleCUEError(err, pathPrefix, ErrParams{ Code: 8, Title: "CUE evaluation failed", Detail: "While evaluating the CUE configuration to generate a concrete configuration for your application, CUE returned an error. " + "This is usually caused by either a constraint on a field being unsatisfied or there being two different values for a given field. " + "For more information on CUE and this error, see https://cuelang.org/docs/", }) } func ResourceNameReserved(fileset *token.FileSet, node ast.Node, resourceType string, paramName string, name, reservedPrefix string, isSnakeCase bool, fileReaders ...paths.FileReader) error { suggestion := "" if strings.HasPrefix(name, reservedPrefix) { // should always be the case, but better to be safe suggestion = fmt.Sprintf("try %q?", name[len(reservedPrefix):]) } var detail string if isSnakeCase { detail = resourceNameHelpSnakeCase(resourceType, paramName) } else { detail = resourceNameHelpKebabCase(resourceType, paramName) } return errinsrc.New(ErrParams{ Code: 37, Title: "Reserved resource name", // The metrics.NewCounter metric name "e_blah" uses the reserved prefix "e_". Summary: fmt.Sprintf("The %s %s %q uses the reserved prefix %q", resourceType, paramName, name, reservedPrefix), Detail: detail, Locations: NewSrcLocations(FromGoASTNodeWithTypeAndText(fileset, node, LocError, suggestion, fileReaders...)), }, false) } ================================================ FILE: pkg/errinsrc/srcerrors/helpers.go ================================================ package srcerrors import ( "fmt" "go/ast" "go/token" "reflect" "strings" cueerrors "cuelang.org/go/cue/errors" "encr.dev/pkg/errinsrc" . "encr.dev/pkg/errinsrc/internal" schema "encr.dev/proto/encore/parser/schema/v1" ) func handleCUEError(err error, pathPrefix string, param ErrParams) error { if err == nil { return nil } toReturn := make(errinsrc.List, 0, 1) if param.Detail == "" { param.Detail = "For more information on CUE and this error, see https://cuelang.org/docs/" } for _, e := range cueerrors.Errors(err) { param.Summary = e.Error() param.Cause = e param.Locations = LocationsFromCueError(e, pathPrefix) toReturn = append(toReturn, errinsrc.New(param, false)) } switch len(toReturn) { case 0: return nil case 1: return toReturn[0] default: return toReturn } } // Converts a node to a string which looks like the original go code. // such as a ast.SelectorExpr will become "foo.Blah" // // It's not intended to be an exact representation, but rather a helperful // representation for error messages. func nodeAsGoSrc(node ast.Node) string { switch node := node.(type) { case *ast.Ident: return node.Name case *ast.SelectorExpr: return fmt.Sprintf("%s.%s", nodeAsGoSrc(node.X), node.Sel.Name) case *ast.IndexExpr: return fmt.Sprintf("%s[%s]", nodeAsGoSrc(node.X), nodeAsGoSrc(node.Index)) case *ast.IndexListExpr: indices := make([]string, 0, len(node.Indices)) for _, n := range node.Indices { indices = append(indices, nodeAsGoSrc(n)) } return fmt.Sprintf("%s[%s]", nodeAsGoSrc(node.X), strings.Join(indices, ", ")) case *ast.FuncLit: return "a function literal" case *ast.BasicLit: return node.Value case *ast.CallExpr: return fmt.Sprintf("%s(...)", nodeAsGoSrc(node.Fun)) default: return fmt.Sprintf("a %v", reflect.TypeOf(node)) } } // Converts a node to a string that can be used in an error message. // such as a ast.CallExpr will return "a function call to foo.Blah" func nodeType(node ast.Node) string { switch node := node.(type) { case *ast.Ident: return "an identifier" case *ast.SelectorExpr: return "an identifier" case *ast.IndexExpr: return "a identifier" case *ast.IndexListExpr: return "a identifier" case *ast.FuncLit: return "a function literal" case *ast.BasicLit: switch node.Kind { case token.INT: return "an integer literal" case token.FLOAT: return "a float literal" case token.IMAG: return "an imaginary literal" case token.CHAR: return "a character literal" case token.STRING: return "a string literal" default: return "a literal" } case *ast.CallExpr: return fmt.Sprintf("a function call to %s", nodeAsGoSrc(node.Fun)) default: return fmt.Sprintf("a %v", reflect.TypeOf(node)) } } // Converts a schema type to a string that can be used in an error message. // such as a ast.CallExpr will return "a function call to foo.Blah" func schemaType(typ *schema.Type) string { switch tt := typ.Typ.(type) { case *schema.Type_Named: return "a named type" case *schema.Type_Struct: return "a struct type" case *schema.Type_Map: return "a map type" case *schema.Type_Literal: return "a literal" case *schema.Type_Union: return "a union type" case *schema.Type_List: return "a list type" case *schema.Type_Builtin: return fmt.Sprintf("a builtin type (%s)", tt.Builtin) case *schema.Type_Pointer: return "a pointer to " + schemaType(tt.Pointer.Base) case *schema.Type_Option: return "an optional " + schemaType(tt.Option.Value) case *schema.Type_TypeParameter: return "a type parameter" case *schema.Type_Config: return "a config value" default: return fmt.Sprintf("a %v", reflect.TypeOf(tt)) } } ================================================ FILE: pkg/errinsrc/srcerrors/helptext.go ================================================ package srcerrors import ( "fmt" "strings" ) func combine(parts ...string) string { return strings.Join(parts, "\n\n") } const ( internalErrReportToEncore = "This is a bug in Encore and should not have occurred. Please report this issue to the " + "Encore team either on Github at https://github.com/encoredev/encore/issues/new and include this error." makeService = "To make this package a count as a service, this package or one of it's parents must have either one " + "or more API's declared within them or a PubSub subscription." configHelp = "For more information on configuration, see https://encore.dev/docs/develop/config" pubsubNewTopicHelp = "For example `pubsub.NewTopic[MyMessage](\"my-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })`" pubsubNewSubscriptionHelp = "A pubsub subscription must have a unique name per topic and be given a handler function for processing the message. " + "The handler for the subscription must be defined in the same service as the call to pubsub.NewSubscription and can be an inline function. " + "For example:\n" + "\tpubsub.NewSubscription(myTopic, \"subscription-name\", pubsub.SubscriptionConfig[MyMessage]{\n" + "\t\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\n" + "\t})" pubsubHelp = "For more information on PubSub, see https://encore.dev/docs/primitives/pubsub" metricsHelp = "For more information on metrics, see https://encore.dev/docs/observability/metrics" serviceHelp = "For more information on services and how to define them, see https://encore.dev/docs/primitives/services" authHelp = "For more information on auth handlers and how to define them, see https://encore.dev/docs/develop/auth" ) func resourceNameHelpKebabCase(resourceName string, paramName string) string { return fmt.Sprintf("%s %s's must be defined as string literals, "+ "be between 1 and 63 characters long, and defined in \"kebab-case\", meaning it must start with a letter, end with a letter "+ "or number and only contain lower case letters, numbers and dashes.", resourceName, paramName, ) } func resourceNameHelpSnakeCase(resourceName string, paramName string) string { return fmt.Sprintf("%s %s's must be defined as string literals, "+ "be between 1 and 63 characters long, and defined in \"snake_case\", meaning it must start with a letter, end with a letter "+ "or number and only contain lower case letters, numbers and underscores.", resourceName, paramName, ) } ================================================ FILE: pkg/errinsrc/srcrender.go ================================================ package errinsrc import ( "bufio" "bytes" "fmt" "math" "strings" "unicode" "github.com/alecthomas/chroma/quick" "github.com/jwalton/go-supportscolor" auroraPkg "github.com/logrusorgru/aurora/v3" "github.com/rs/zerolog/log" . "encr.dev/pkg/errinsrc/internal" ) const grayLevelOnLineNumbers = 12 const endEscape = "\x1b[0m" const tabSize = 4 var aurora auroraPkg.Aurora var enableColors bool func init() { ColoursInErrors(supportscolor.Stdout().SupportsColor) } func ColoursInErrors(enabled bool) { enableColors = enabled aurora = auroraPkg.NewAurora(enabled) } // renderSrc returns the lines of code surrounding the location with a pointer to the error on the error line func renderSrc(builder *strings.Builder, causes SrcLocations) { const linesBeforeError = 2 const linesAfterError = 2 idx := 0 currentCause := causes[idx] lastEnd := causes[len(causes)-1].End // Check if any of the causes are multiline multilineSpace := 0 for _, cause := range causes { if cause.Start.Line != cause.End.Line { multilineSpace = 4 break } } numDigitsInLineNumbers := int(math.Log10(float64(lastEnd.Line+linesAfterError+1))) + 1 lineNumberFmt := fmt.Sprintf(" %%%dd %c ", numDigitsInLineNumbers, set.VerticalBar) // Render the filename builder.WriteString(strings.Repeat(" ", numDigitsInLineNumbers+1)) builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(" %c%c%c", set.LeftTop, set.HorizontalBar, set.LeftBracket)).String()) // Note the space on both sides of this string is important // as it allows editors (such as GoLand) to pickup the filename in // terminal output and convert it into a clickable link into the code builder.WriteString(aurora.Cyan(fmt.Sprintf(" %s:%d:%d ", causes[0].File.RelPath, causes[0].Start.Line, causes[0].Start.Col, )).String()) builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, string(set.RightBracket)).String()) builder.WriteRune('\n') builder.WriteString(strings.Repeat(" ", numDigitsInLineNumbers+2)) builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c", set.VerticalBar)).String()) builder.WriteRune('\n') var currentLine int gapRenderedUntil := currentCause.Start.Line bBuffer := new(bytes.Buffer) bBuffer.Write(causes[0].File.Contents) sc := bufio.NewScanner(bBuffer) linePrintLoop: for sc.Scan() { currentLine++ if currentLine >= currentCause.Start.Line-linesBeforeError && currentLine <= currentCause.End.Line+linesAfterError { // Write the line number builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(lineNumberFmt, currentLine)).String()) // If this is a multiline error, then render the gutters if multilineSpace > 0 { if currentCause.Start.Line <= currentLine && currentLine <= currentCause.End.Line { line := fmt.Sprintf("%c ", set.VerticalBar) switch currentLine { case currentCause.Start.Line: if currentCause.Start.Col == 1 { line = fmt.Sprintf("%c%c%c ", set.LeftTop, set.HorizontalBar, set.RightArrow) } else { line = strings.Repeat(" ", multilineSpace) } case currentCause.End.Line: if currentCause.End.Col == 1 { line = fmt.Sprintf("%c%c%c ", set.LeftCross, set.HorizontalBar, set.RightArrow) } } switch currentCause.Type { case LocError: builder.WriteString(aurora.BrightRed(line).String()) case LocWarning: builder.WriteString(aurora.BrightYellow(line).String()) case LocHelp: builder.WriteString(aurora.BrightBlue(line).String()) } } else { builder.WriteString(strings.Repeat(" ", multilineSpace)) } } unifiedLine := replaceTabsWithSpaces(sc.Text()) // Then the line of code itself (attempting to highlight the syntax) // Note: we always use the "go" lexer, as it works nicely on CUE files too var subBuilder strings.Builder if err := quick.Highlight(&subBuilder, unifiedLine, "go", "terminal256", "monokai"); err != nil || !enableColors { if err != nil { log.Error().AnErr("error", err).Msg("Unable to highlight line of code") } builder.WriteString(unifiedLine) } else { syntaxHighlightedLine := subBuilder.String() // There's a bug in the quick.Highlight function that causes it sometimes add an extra line feed before the endEscape squence // so this if statement removes it if strings.HasSuffix(syntaxHighlightedLine, endEscape) { syntaxHighlightedLine = strings.TrimRight(syntaxHighlightedLine[:len(syntaxHighlightedLine)-len(endEscape)], " \n\r\t") + endEscape } builder.WriteString(syntaxHighlightedLine) } builder.WriteRune('\n') } else if currentLine >= gapRenderedUntil && idx < len(causes) { gapRenderedUntil = currentCause.Start.Line // render two spaces for a break in the file for i := 0; i < 2; i++ { lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalBreak) builder.WriteString("\n") } } errorRendered := true colOffset := 0 for errorRendered { errorRendered = false if currentCause.Start.Line < currentCause.End.Line { // If a multiline error then render the currentCause.Start() and currentCause.End() pointers color := aurora.BrightRed switch currentCause.Type { case LocWarning: color = aurora.BrightYellow case LocHelp: color = aurora.BrightBlue } switch currentLine { case currentCause.Start.Line: if currentCause.Start.Col > 1 { charCount := calcNumberCharactersForColumnNumber(sc.Text(), currentCause.Start.Col) lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap) builder.WriteString(color(fmt.Sprintf("%s%c", strings.Repeat(" ", charCount+3), set.UpArrow)).String()) builder.WriteString("\n") lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap) builder.WriteString(color(fmt.Sprintf("%c%s%c", set.LeftTop, strings.Repeat(string(set.HorizontalBar), charCount+2), set.RightBottom)).String()) builder.WriteString("\n") } case currentCause.End.Line: if currentCause.End.Col > 1 { charCount := calcNumberCharactersForColumnNumber(sc.Text(), currentCause.End.Col) lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap) builder.WriteString(color(fmt.Sprintf("%c%s%c", set.VerticalBar, strings.Repeat(" ", charCount+2), set.UpArrow)).String()) builder.WriteString("\n") lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap) builder.WriteString(color(fmt.Sprintf("%c%s%c", set.LeftCross, strings.Repeat(string(set.HorizontalBar), charCount+2), set.RightBottom)).String()) builder.WriteString("\n") } // And on the final line also render the error message renderErrorText(builder, 0, numDigitsInLineNumbers, "", 2, currentCause.Type, currentCause.Text, nil, true, true) errorRendered = true } } else if currentLine == currentCause.End.Line { var startCol, endCol int startCol = currentCause.Start.Col endCol = currentCause.End.Col // Try and guess the atom where the error is // if the currentCause.Start()/currentCause.End() point is the same position if endCol <= startCol { endCol = GuessEndColumn(sc.Bytes(), startCol) } // Work out how long the indicator is indicatorLength := endCol - startCol if indicatorLength <= 1 { indicatorLength = 1 } // Create out the error lines errorLines := []string{""} errorTextStart := indicatorLength + 1 if indicatorLength >= 2 { half := float64(indicatorLength-1) / 2 errorTextStart = int(math.Floor(half)) + 2 if currentCause.Text != "" { errorLines[0] = fmt.Sprintf( "%s%c%s", strings.Repeat(string(set.HorizontalBar), int(math.Floor(half))), set.MiddleTop, strings.Repeat(string(set.HorizontalBar), int(math.Ceil(half))), ) } else { errorLines[0] = strings.Repeat(string(set.HorizontalBar), indicatorLength) } } else { errorLines[0] = string(set.UpArrow) } renderNewLine := true if currentCause.Text == "" { if idx+1 < len(causes) { nextCause := causes[idx+1] if nextCause.Start.Line == currentLine && nextCause.End.Line == currentLine { renderNewLine = false } } } renderGutter := colOffset == 0 renderErrorText(builder, startCol-colOffset, numDigitsInLineNumbers, sc.Text(), errorTextStart, currentCause.Type, currentCause.Text, errorLines, renderNewLine, renderGutter) errorRendered = true colOffset = endCol } if errorRendered { idx = idx + 1 if idx < len(causes) { currentCause = causes[idx] } else { // stop looking for errors on this line break } } if currentLine > lastEnd.Line+linesAfterError { // stop printing al errors break linePrintLoop } } } builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, strings.Repeat(string(set.HorizontalBar), numDigitsInLineNumbers+2)+string(set.RightBottom)).String()) builder.WriteRune('\n') } func lineNumberSpacer(builder *strings.Builder, numDigitsInLineNumbers int, r rune) { builder.WriteString(strings.Repeat(" ", numDigitsInLineNumbers+1)) builder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(" %c ", r)).String()) } func replaceTabsWithSpaces(line string) string { var str strings.Builder var col int for _, r := range line { if r == '\t' { // Tabs always count as at least 1 space str.WriteRune(' ') col++ // We then align onto the next tab column for col%tabSize != 0 { str.WriteRune(' ') col++ } } else { str.WriteRune(r) col++ } } return str.String() } // calcNumberCharactersForColumnNumber calculates the number of monospaced characters we need // to render the given column number on the line - accounting for tab characters func calcNumberCharactersForColumnNumber(line string, col int) int { count := 0 for i, r := range line { if r == '\t' { count++ for count%tabSize != 0 { count++ } } else { count++ } if i+1 >= col { break } } return count } func renderErrorText(builder *strings.Builder, startCol int, numDigitsInLineNumbers int, srcLine string, errorTextStart int, typ LocationType, text string, errorLines []string, renderNewLine bool, renderGutter bool) { if text != "" { lines := splitTextOnWords(text, errorTextStart) for i, line := range lines { prefix := strings.Repeat(" ", errorTextStart+1) if i == 0 { prefix = fmt.Sprintf("%s%c%c ", strings.Repeat(" ", errorTextStart-2), set.LeftBottom, set.HorizontalBar) } errorLines = append(errorLines, prefix+line) } } var prefixWhitespace string // It's possible the start column references generated code; in that case reset // the column information as a fallback to prevent panics below. if startCol > len(srcLine) { startCol = 0 } else { // Compute the whitespace prefix we need on each line // (Note this will render tabs as tabs still if they are present) prefixWhitespace = strings.Repeat(" ", calcNumberCharactersForColumnNumber(srcLine, startCol-1)) } // Now write the error lines for _, line := range errorLines { if renderGutter { lineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap) } builder.WriteString(prefixWhitespace) switch typ { case LocError: builder.WriteString(aurora.BrightRed(line).String()) case LocWarning: builder.WriteString(aurora.BrightYellow(line).String()) case LocHelp: builder.WriteString(aurora.BrightBlue(line).String()) } if renderNewLine || len(errorLines) > 1 { builder.WriteString("\n") } } } func splitTextOnWords(text string, startingCol int) (rtn []string) { text = strings.TrimSpace(text) maxLineLength := TerminalWidth - startingCol if maxLineLength < 20 { maxLineLength = 20 } for _, line := range strings.Split(text, "\n") { if len(line) <= maxLineLength { rtn = append(rtn, line) continue } lineStart := 0 lastSpace := 0 for i := 0; i < len(line); i++ { if unicode.IsSpace(rune(line[i])) { if i-lineStart >= maxLineLength { rtn = append(rtn, line[lineStart:lastSpace]) lineStart = lastSpace + 1 } lastSpace = i } } if len(line)-lineStart >= maxLineLength { rtn = append(rtn, line[lineStart:lastSpace]) lineStart = lastSpace + 1 } if lineStart < len(line) { rtn = append(rtn, line[lineStart:]) } } return } func wordWrap(text string, b *strings.Builder) { for _, line := range splitTextOnWords(text, 0) { b.WriteString(line) b.WriteString("\n") } } ================================================ FILE: pkg/errinsrc/srcrender_test.go ================================================ package errinsrc import ( "fmt" "path" "strings" "testing" . "encr.dev/pkg/errinsrc/internal" "encr.dev/pkg/golden" ) func Test_renderSrc_Simple(t *testing.T) { testParams := []struct { testName string line, column int level LocationType message string }{ {testName: "error no text", line: 5, column: 7, level: LocError, message: ""}, {testName: "simple error", line: 5, column: 2, level: LocError, message: "What is a foo?"}, {testName: "simple warning", line: 10, column: 7, level: LocWarning, message: "You sure about this?"}, {testName: "simple help", line: 13, column: 4, level: LocHelp, message: "help: try a switch statement here"}, {testName: "single character error", line: 6, column: 11, level: LocError, message: "This looks dodgy"}, {testName: "multiline message", line: 1, column: 9, level: LocError, message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum. Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend. Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris.\n\nQuisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus. Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi.\n\n"}, } for _, tp := range testParams { tp := tp t.Run(tp.testName, func(t *testing.T) { loc := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), tp.line, tp.column}, testDataFullPath) loc.Text = tp.message loc.Type = tp.level err := New(ErrParams{ Code: 1, Title: "simple test error", Summary: "There has been a simple error in your code", Detail: "For more information please visit our great help documentation", Locations: SrcLocations{loc}, }, false) testError(t, err) }) } } func Test_renderSrc_MultipleSeperateInSameFile(t *testing.T) { t.Run("spaced apart", func(t *testing.T) { loc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 5, 7}, testDataFullPath) loc1.Text = "defined as a boolean here" loc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 13, 9}, testDataFullPath) loc2.Text = "referenced here" err := New(ErrParams{ Code: 1, Title: "simple test error", Summary: "There has been a simple error in your code", Detail: "For more information please visit our great help documentation", Locations: SrcLocations{loc1, loc2}, }, false) testError(t, err) }) t.Run("on following lines", func(t *testing.T) { loc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 5, 7}, testDataFullPath) loc1.Text = "this is weird" loc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 6, 13}, testDataFullPath) loc2.Text = "so is this!" err := New(ErrParams{ Code: 1, Title: "simple test error", Summary: "There has been a simple error in your code", Detail: "For more information please visit our great help documentation", Locations: SrcLocations{loc1, loc2}, }, false) testError(t, err) }) t.Run("on same line", func(t *testing.T) { loc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 5, 7}, testDataFullPath) loc1.Text = "hint: change this to an int" loc1.Type = LocHelp loc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 5, 2}, testDataFullPath) loc2.Text = "wrong type here" err := New(ErrParams{ Code: 1, Title: "simple test error", Summary: "There has been a simple error in your code", Detail: "For more information please visit our great help documentation", Locations: SrcLocations{loc1, loc2}, }, false) testError(t, err) }) } func Test_renderSrc_MutlilineError(t *testing.T) { loc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, "test.cue"), 4, 7}, testDataFullPath) loc1.End = Pos{7, 1} loc1.Text = "this is an error which spans multiple lines\nlike this so we can test alignment" err := New(ErrParams{ Code: 1, Title: "simple test error", Summary: "There has been a simple error in your code", Detail: "For more information please visit our great help documentation", Locations: SrcLocations{loc1}, }, false) testError(t, err) } /* helpers */ func testError(t *testing.T, err *ErrInSrc) { // Reset for stdout with colours set = unicodeSet ColoursInErrors(true) fmt.Println(err.Error()) // Now golden files without colours goldenFile := strings.Replace(t.Name(), "/", "__", -1) ColoursInErrors(false) golden.TestAgainst(t, goldenFile+"_unicode.golden", err.Error()) set = asciiSet golden.TestAgainst(t, goldenFile+"_ascii.golden", err.Error()) } type errorLoc struct { filename string line int column int } func (e *errorLoc) Filename() string { return e.filename } func (e *errorLoc) Line() int { return e.line } func (e *errorLoc) Column() int { return e.column } ================================================ FILE: pkg/errinsrc/stack.go ================================================ package errinsrc import ( "fmt" "runtime" "strings" "github.com/pkg/errors" ) // StackFrame represents a single frame in a Stack trace type StackFrame struct { ProgramCounter uintptr `json:"pc"` File string `json:"file"` Package string `json:"pkg"` Function string `json:"fun"` Line int `json:"line"` } const maxFramesOnPrettyPrint = 5 func GetStack() []*StackFrame { ret := make([]uintptr, 100) index := runtime.Callers(1, ret) if index == 0 { return nil } return convertFrames(ret[:index]) } // bottomStackTraceFrom returns the deepest stack trace from the given error func bottomStackTraceFrom(err error) (rtn []*StackFrame) { count := 0 for err != nil && count < 100 { count++ // Look for an error which provides a stack trace if e, ok := err.(*ErrInSrc); ok { rtn = e.Stack } else if e, ok := err.(interface{ StackTrace() errors.StackTrace }); ok { frames := e.StackTrace() pcs := make([]uintptr, len(frames)) for i, pc := range frames { pcs[i] = uintptr(pc) } rtn = convertFrames(pcs) } // Recurse switch typed := err.(type) { case interface{ Unwrap() error }: err = typed.Unwrap() case interface{ Unwrap() []error }: errs := typed.Unwrap() if len(errs) > 0 { err = errs[0] } else { err = nil } case interface{ Cause() error }: err = typed.Cause() } } return } func convertFrames(pcs []uintptr) []*StackFrame { cf := runtime.CallersFrames(pcs) frame, more := cf.Next() // Skip over the "errinsrc" or "errlist" package files or any subpackages // which are the top frames (as these would only be related to the creation of the error) for strings.Contains(frame.File, "errinsrc") || strings.Contains(frame.File, "errlist") || strings.Contains(frame.File, "perr") || strings.Contains(frame.File, "eerror") || strings.HasSuffix(frame.File, "errs.go") { if !more { return nil } frame, more = cf.Next() } var frames []*StackFrame for { // Skip the frame if it's Go Runtime or internal testing code related code if !strings.HasPrefix(frame.Function, "runtime.") && !strings.HasPrefix(frame.Function, "testing.") { // Separate the package name and function name pkgAndFunc := frame.Function if idx := strings.LastIndex(pkgAndFunc, "/"); idx >= 0 { pkgAndFunc = pkgAndFunc[idx+1:] } pkgName, funcName, _ := strings.Cut(pkgAndFunc, ".") // Record the frame frames = append(frames, &StackFrame{ ProgramCounter: frame.PC, Package: pkgName, Function: funcName, File: frame.File, Line: frame.Line, }) } if !more { return frames } frame, more = cf.Next() } } func prettyPrintStack(stack []*StackFrame, b *strings.Builder) string { b.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c%c%c", set.LeftTop, set.HorizontalBar, set.LeftBracket)).String()) b.WriteString(aurora.Cyan("Stack Trace").String()) b.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c", set.RightBracket)).String()) longestFunc := 0 for i, frame := range stack { name := fmt.Sprintf("%s.%s", frame.Package, frame.Function) if length := len(name); length > longestFunc { longestFunc = length } if i >= maxFramesOnPrettyPrint { break } } for i, frame := range stack { vertical := set.LeftCross if i == len(stack)-1 { vertical = set.LeftBottom } b.WriteString( fmt.Sprintf( "\n%s %s.%s%s %s:%d", aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c%c%c", vertical, set.HorizontalBar, set.RightArrow)), aurora.Gray(18, frame.Package), aurora.Magenta(frame.Function), strings.Repeat(" ", longestFunc-len(frame.Function)-len(frame.Package)-1), frame.File, frame.Line, ), ) if i >= maxFramesOnPrettyPrint && i != len(stack)-1 { b.WriteString("\n") b.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c%c%c", set.LeftBottom, set.HorizontalBar, set.LeftBracket)).String()) b.WriteString(aurora.Yellow("... remaining frames omitted ...").Italic().String()) b.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf("%c", set.RightBracket)).String()) break } } b.WriteString("\n\n") return b.String() } ================================================ FILE: pkg/errinsrc/stack_dev.go ================================================ //go:build dev_build package errinsrc // IncludeStackByDefault is whether to include the stack by default on all errors. // This is exported to allow Encore's CI platform to set this to true for CI/CD builds. var IncludeStackByDefault = true ================================================ FILE: pkg/errinsrc/stack_release.go ================================================ //go:build !dev_build package errinsrc // IncludeStackByDefault is whether to include the stack by default on all errors. // This is exported to allow Encore's CI platform to set this to true for CI/CD builds. var IncludeStackByDefault = false ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:5:7 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : -v-- : `- this is weird 6 | bar: int | *3 : v- : `- so is this! 7 | } 8 | ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:5:7 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ─┬── ⋮ ╰─ this is weird 6 │ bar: int | *3 ⋮ ┬─ ⋮ ╰─ so is this! 7 │ } 8 │ ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_same_line_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:5:2 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : ----v---- : `- wrong type here 6 | bar: int | *3 7 | } ---' ,-[ /test.cue:5:7 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : -v-- : `- hint: change this to an int 6 | bar: int | *3 7 | } ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_same_line_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:5:2 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ────┬──── ⋮ ╰─ wrong type here 6 │ bar: int | *3 7 │ } ───╯ ╭─[ /test.cue:5:7 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ─┬── ⋮ ╰─ hint: change this to an int 6 │ bar: int | *3 7 │ } ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:5:7 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : -v-- : `- defined as a boolean here * * 11 | 12 | // If foo then bar is 12 13 | if blah.foo { : -v- : `- referenced here 14 | blah: bar: 12 15 | } ----' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:5:7 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ─┬── ⋮ ╰─ defined as a boolean here · · 11 │ 12 │ // If foo then bar is 12 13 │ if blah.foo { ⋮ ─┬─ ⋮ ╰─ referenced here 14 │ blah: bar: 12 15 │ } ────╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MutlilineError_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:4:7 ] | 2 | 3 | // This is a sample file 4 | blah: { : ^ : ,---------' 5 | | foo: bool 6 | | bar: int | *3 7 | |-> } : `- this is an error which spans multiple lines : like this so we can test alignment 8 | 9 | // Let's set foo to true ----' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_MutlilineError_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:4:7 ] │ 2 │ 3 │ // This is a sample file 4 │ blah: { ⋮ ▲ ⋮ ╭─────────╯ 5 │ │ foo: bool 6 │ │ bar: int | *3 7 │ ├─▶ } ⋮ ╰─ this is an error which spans multiple lines ⋮ like this so we can test alignment 8 │ 9 │ // Let's set foo to true ────╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__error_no_text_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:5:7 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : ---- 6 | bar: int | *3 7 | } ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__error_no_text_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:5:7 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ──── 6 │ bar: int | *3 7 │ } ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__multiline_message_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:1:9 ] | 1 | package test_cue : ---v---- : `- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum. : Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et : lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est : est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo : auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque : porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend. : Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet : risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris. : : Quisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut : aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et : netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum : ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper : rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus. : Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi. 2 | 3 | // This is a sample file ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__multiline_message_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:1:9 ] │ 1 │ package test_cue ⋮ ───┬──── ⋮ ╰─ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum. ⋮ Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et ⋮ lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est ⋮ est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo ⋮ auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque ⋮ porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend. ⋮ Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet ⋮ risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris. ⋮ ⋮ Quisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut ⋮ aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et ⋮ netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum ⋮ ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper ⋮ rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus. ⋮ Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi. 2 │ 3 │ // This is a sample file ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_error_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:5:2 ] | 3 | // This is a sample file 4 | blah: { 5 | foo: bool : ----v---- : `- What is a foo? 6 | bar: int | *3 7 | } ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_error_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:5:2 ] │ 3 │ // This is a sample file 4 │ blah: { 5 │ foo: bool ⋮ ────┬──── ⋮ ╰─ What is a foo? 6 │ bar: int | *3 7 │ } ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_help_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:13:4 ] | 11 | 12 | // If foo then bar is 12 13 | if blah.foo { : ---v---- : `- help: try a switch statement here 14 | blah: bar: 12 15 | } ----' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_help_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:13:4 ] │ 11 │ 12 │ // If foo then bar is 12 13 │ if blah.foo { ⋮ ───┬──── ⋮ ╰─ help: try a switch statement here 14 │ blah: bar: 12 15 │ } ────╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_warning_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:10:7 ] | 8 | 9 | // Let's set foo to true 10 | blah: foo: true : -v- : `- You sure about this? 11 | 12 | // If foo then bar is 12 ----' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_warning_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:10:7 ] │ 8 │ 9 │ // Let's set foo to true 10 │ blah: foo: true ⋮ ─┬─ ⋮ ╰─ You sure about this? 11 │ 12 │ // If foo then bar is 12 ────╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__single_character_error_ascii.golden ================================================ -- simple test error ----------------------------------------------------------------------[E0001]-- There has been a simple error in your code ,-[ /test.cue:6:11 ] | 4 | blah: { 5 | foo: bool 6 | bar: int | *3 : ^ : `- This looks dodgy 7 | } 8 | ---' For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/Test_renderSrc_Simple__single_character_error_unicode.golden ================================================ ── simple test error ──────────────────────────────────────────────────────────────────────[E0001]── There has been a simple error in your code ╭─[ /test.cue:6:11 ] │ 4 │ blah: { 5 │ foo: bool 6 │ bar: int | *3 ⋮ ▲ ⋮ ╰─ This looks dodgy 7 │ } 8 │ ───╯ For more information please visit our great help documentation ================================================ FILE: pkg/errinsrc/testdata/test.cue ================================================ package test_cue // This is a sample file blah: { foo: bool bar: int | *3 } // Let's set foo to true blah: foo: true // If foo then bar is 12 if blah.foo { blah: bar: 12 } ================================================ FILE: pkg/errinsrc/testdata/test.go ================================================ package testdata import ( "context" "encore.dev/config" ) type Config struct { Blah struct { Foo config.Bool `json:"foo"` Bar config.Int `json:"bar"` } `json:"blah"` } var cfg = config.Load[Config]() type WhatIsBarResponse struct { Bar int } //encore:api public func WhatIsBar(ctx context.Context) (*WhatIsBarResponse, error) { return &WhatIsBarResponse{ Bar: cfg.Blah.Bar(), }, nil } ================================================ FILE: pkg/errinsrc/utils.go ================================================ package errinsrc import ( "go/ast" "go/token" . "encr.dev/pkg/errinsrc/internal" ) type ErrorList interface { Error() string ErrorList() []*ErrInSrc } // Bailout is a panic value that can be used to type Bailout struct { List ErrorList } func (b Bailout) Error() string { return b.List.Error() } func (b Bailout) Unwrap() error { return b.List } func (b Bailout) ErrorList() []*ErrInSrc { return b.List.ErrorList() } func Panic(list ErrorList) { panic(Bailout{list}) } // ExtractFromPanic returns the first ErrInSrc or ErrorList found in the recovered // value. // // If no value is recovered, then nil is returned. func ExtractFromPanic(recovered any) error { // If it's already an ErrInSrc or list of them, just return that if unwrapped, ok := recovered.(error); ok { // Check the type of the error then unwrap as needed n := 0 for unwrapped != nil { // Limit recursion to 100 unwraps to prevent infinite loops. n++ if n > 100 { return nil } switch err := unwrapped.(type) { case *ErrInSrc: return err case ErrorList: return err case Bailout: return err.List case interface{ Unwrap() error }: unwrapped = err.Unwrap() default: // If we get here, it's not an errinsrc or error list, so return nil return nil } } } return nil } func AddHintFromGo(err error, fileset *token.FileSet, node ast.Node, hint string) { switch err := err.(type) { case *ErrInSrc: if hintLoc, ok := FromGoASTNode(fileset, node).Get(); ok { hintLoc.Type = LocHelp hintLoc.Text = hint err.Params.Locations = append(err.Params.Locations, hintLoc) } case ErrorList: for _, err := range err.ErrorList() { AddHintFromGo(err, fileset, node, hint) } } } ================================================ FILE: pkg/errlist/errlist.go ================================================ package errlist import ( "encoding/json" "fmt" "go/scanner" "go/token" "io" "path/filepath" "strings" "encr.dev/pkg/errinsrc" "encr.dev/pkg/errinsrc/srcerrors" daemonpb "encr.dev/proto/encore/daemon" ) // Verbose controls whether the error list prints all errors // or just the what MaxErrorsToPrint is set to var Verbose = false // MaxErrorsToPrint is the maximum number of errors to print // if Verbose is false var MaxErrorsToPrint = 1 type List struct { List errinsrc.List `json:"list,omitempty"` fset *token.FileSet } var _ errinsrc.ErrorList = (*List)(nil) func New(fset *token.FileSet) *List { return &List{fset: fset} } // Convert attempts to convert known error types into an error list // if it can't it returns nil func Convert(err error) *List { switch err := err.(type) { case *List: return err case *errinsrc.ErrInSrc: l := New(nil) l.List = []*errinsrc.ErrInSrc{err} return l case errinsrc.ErrorList: l := New(nil) l.List = err.ErrorList() return l default: return nil } } // Report is a function that allows you to report an error into // this list, without having to check for nil. // // This function only supports error of types: // - *List // - *errinsrc.ErrInSrc // - *scanner.Error // // If too many errors have been reported it panics // with a Bailout value to abort processing. // Use HandleBailout to conveniently handle this. func (l *List) Report(err error) { if err == nil { return } var errToAdd *errinsrc.ErrInSrc switch err := (err).(type) { case *errinsrc.ErrInSrc: // the base type we expect errToAdd = err case *scanner.Error: // errors directly from the Go parser errToAdd = srcerrors.GenericGoParserError(err) case scanner.ErrorList: for _, e := range err { l.Report(e) } return case *List: // If it's a different list, then merge it in if err != l { for _, e := range err.ErrorList() { l.Report(e) } } return case errinsrc.ErrorList: // either another errlist or a list from `srcerrors` for _, e := range err.ErrorList() { l.Report(e) } return default: panic(fmt.Sprintf("unsupported type %T being reported to errlist.List", err)) } // Skip adding this error if it's on the same line as another error // we've already reported, since it's probably a spurious error caused // by the first one. for _, e := range l.List { if errToAdd.OnSameLine(e) { return } } l.List = append(l.List, errToAdd) if len(l.List) > 10 { l.Abort() } } // Add adds an error to the list. // // If too many errors have been added it panics // with a Bailout value to abort processing. // Use HandleBailout to conveniently handle this. // // Deprecated: use Report instead func (l *List) Add(pos token.Pos, msg string) { pp := l.fset.Position(pos) l.Report(&scanner.Error{ Pos: pp, Msg: msg, }) } // Addf is equivalent to Add(pos, fmt.Sprintf(format, args...)) // // Deprecated: use Report instead func (l *List) Addf(pos token.Pos, format string, args ...interface{}) { l.Add(pos, fmt.Sprintf(format, args...)) } // AddRaw adds a raw *scanner.Error to the list. // // If too many errors have been added it panics // with a Bailout value to abort processing. // Use HandleBailout to conveniently handle this. // // Deprecated: use Report instead func (l *List) AddRaw(err *scanner.Error) { l.Report(err) } // Merge merges another list into this one. // The token.FileSet in use must be the same one as this one, // or else it panics. // // Deprecated: use Report instead func (l *List) Merge(other *List) { if other.fset != l.fset { panic("errlist: cannot merge lists with different *token.FileSets") } l.List = append(l.List, other.List...) } // Err returns an error equivalent to this error list. // If the list is empty, Err returns nil. func (l *List) Err() error { if len(l.List) == 0 { return nil } return l } // Error implements the error interface. func (l *List) Error() string { var b strings.Builder if Verbose { for _, err := range l.List { b.WriteString(err.Error()) } } else { if len(l.List) == 0 { b.WriteString("no errors") } else { for i, err := range l.List { b.WriteString(err.Error()) if i >= MaxErrorsToPrint-1 { break } } if len(l.List) > MaxErrorsToPrint { b.WriteString(fmt.Sprintf("And %d more errors", len(l.List)-MaxErrorsToPrint)) } } } return b.String() } func (l *List) ErrorList() []*errinsrc.ErrInSrc { return l.List } // MakeRelative rewrites the errors by making filenames within the // app root relative to the relwd (which must be a relative path // within the root). func (l *List) MakeRelative(root, relwd string) { wdroot := filepath.Join(root, relwd) for _, e := range l.List { for _, loc := range e.Params.Locations { if loc.File != nil { fn := loc.File.RelPath if strings.HasPrefix(fn, root) { if rel, err := filepath.Rel(wdroot, fn); err == nil { loc.File.RelPath = rel } } } } } } // HandleBailout handles bailouts raised by (*List).Add and family // when too many errors have been found. func (l *List) HandleBailout(err *error) { if e := recover(); e != nil { if b, ok := e.(errinsrc.Bailout); ok { *err = b.List } else { panic(e) } } } func (l *List) Len() int { return len(l.List) } // Abort aborts early if there is an error in the list. func (l *List) Abort() { errinsrc.Panic(l) } // SendToStream sends a GRPC command with this // full errlist // // If l is nil or empty, it sends a nil command // allowing the client to know that there are no // longer an error present func (l *List) SendToStream(stream interface { Send(*daemonpb.CommandMessage) error }) error { var bytes []byte if l != nil && len(l.List) > 0 { var err error bytes, err = json.Marshal(l) if err != nil { panic("unable to marshal error list") } } return stream.Send( &daemonpb.CommandMessage{ Msg: &daemonpb.CommandMessage_Errors{ Errors: &daemonpb.CommandDisplayErrors{ Errinsrc: bytes, }, }, }, ) } // Print is a utility function that prints a list of errors to w, // one error per line, if the err parameter is an errorList. Otherwise // it prints the err string. func Print(w io.Writer, err error) { if l, ok := err.(*List); ok { for _, e := range l.List { fmt.Fprintf(w, "%s\n", e) } } else if err != nil { fmt.Fprintf(w, "%s\n", err) } } ================================================ FILE: pkg/errors/locations.go ================================================ package errors import ( goAst "go/ast" goToken "go/token" ) // LocationType represents if the locaton is the source of the error, a warning or a helpful hint type LocationType uint8 const ( LocError LocationType = iota LocWarning LocHelp ) // LocationKind tells us what language and position markers we're using to identify the source type LocationKind uint8 const ( LocFile LocationKind = iota LocGoNode LocGoPos LocGoPositions ) // SrcLocation tells us where in the code base caused the error, warning or is a hint to the end // user. type SrcLocation struct { Kind LocationKind LocType LocationType Text string Filepath string GoNode goAst.Node GoStartPos goToken.Pos GoEndPos goToken.Pos GoStartPosition goToken.Position GoEndPosition goToken.Position } type LocationOption func(*SrcLocation) // AsError allows you to set a SrcLocation's error text // // Pass this option in when you give the error [Template] a src location func AsError(errorText string) func(loc *SrcLocation) { return func(loc *SrcLocation) { loc.Text = errorText loc.LocType = LocError } } // AsWarning allows you to set a SrcLocation's text and mark it as a warning // // Pass this option in when you give the error [Template] a src location func AsWarning(warningText string) func(loc *SrcLocation) { return func(loc *SrcLocation) { loc.Text = warningText loc.LocType = LocWarning } } // AsHelp allows you to set a SrcLocation's text and mark it as a helpful hint // // Pass this option in when you give the error [Template] a src location func AsHelp(helpText string) func(loc *SrcLocation) { return func(loc *SrcLocation) { loc.LocType = LocHelp loc.Text = helpText } } ================================================ FILE: pkg/errors/range.go ================================================ package errors import ( "fmt" ) var ( nextRangeStart = 1000 ) // Range creates a new error code range for a given module. func Range( module string, defaultDetails string, options ...RangeOption, ) *TemplateRange { if nextRangeStart > 9999 { // If we hit this, we need to increase the number of digits in the error code renderer panic("too many error code ranges") } // Configure the range cfg := &rangeConfig{ rangeSize: 100, } for _, c := range options { c(cfg) } // Create the Range rangeStart := nextRangeStart nextRangeStart += cfg.rangeSize return &TemplateRange{ module: module, defaultDetails: defaultDetails, nextErrorCode: rangeStart, codeRangeEnd: nextRangeStart, } } type rangeConfig struct { rangeSize int } type RangeOption func(*rangeConfig) // WithRangeSize sets the size of the range. The default is 100. func WithRangeSize(size int) RangeOption { return func(cfg *rangeConfig) { cfg.rangeSize = size } } // TemplateRange is a helper for creating a range of error codes for a given module // and generating templates for those errors. type TemplateRange struct { module string defaultDetails string nextErrorCode int codeRangeEnd int // Exclusive } // New creates a new template for an error in this range. func (r *TemplateRange) New(title, summary string, options ...TemplateOption) Template { return r.Newf(title, summary, options...)() } // Newf creates a function to return a template for an error in this range where the summary is a format string. func (r *TemplateRange) Newf(title, summaryFmt string, options ...TemplateOption) func(summaryArgs ...any) Template { if r.nextErrorCode >= r.codeRangeEnd { panic("too many errors in range") } errorCode := r.nextErrorCode r.nextErrorCode++ return func(summaryArgs ...any) Template { tmp := Template{ Code: errorCode, Title: title, Summary: fmt.Sprintf(summaryFmt, summaryArgs...), Detail: r.defaultDetails, } for _, o := range options { o(&tmp) } return tmp } } ================================================ FILE: pkg/errors/template.go ================================================ package errors import ( goAst "go/ast" goToken "go/token" "reflect" "strings" ) // Template represents a template for a new error. // // It itself is not an error, but can be used to initialize a new [errorinsrc.ErrInSrc]. type Template struct { Code int Title string Summary string Detail string Cause error Locations []SrcLocation AlwaysIncludeStack bool } // TemplateOption can be passed into the [Range] when creating a new Template type TemplateOption func(*Template) // AlwaysIncludeStack will setup a Template so it always includes a stack trace // even in a production build of Encore. func AlwaysIncludeStack() TemplateOption { return func(template *Template) { template.AlwaysIncludeStack = true } } // WithDetails will setup a template so it uses a different details to the // range default func WithDetails(details string) TemplateOption { return func(template *Template) { template.Detail = details } } // PrependDetails will setup a template so it prepends the given details to the // range default func PrependDetails(details string) TemplateOption { return func(template *Template) { if template.Detail != "" { details = details + "\n\n" + template.Detail } template.Detail = details } } // MarkAsInternalError will setup a template so it is reported as an internal error. // // This means that the error will be reported to the user as an internal error // with a link to the Encore issue tracker and will include a stack trace. func MarkAsInternalError() TemplateOption { return func(template *Template) { template.AlwaysIncludeStack = true template.Title = "Internal Error: " + template.Title template.Detail = "This is a bug in Encore and should not have occurred. Please report this issue to the " + "Encore team either on Github at https://github.com/encoredev/encore/issues/new and include this error." } } // WithDetails will replace the details of the template with the given details func (t Template) WithDetails(details string) Template { t.Detail = details return t } // Wrapping wraps the given error with the template. // // It will append the given error to the summary of the template // as well as setting the cause of the template to the given error. func (t Template) Wrapping(err error) Template { if err == nil { return t } t.Summary += "\n\n" + err.Error() t.Summary = strings.TrimSpace(t.Summary) t.Cause = err return t } // atLocation is a helper method for the various with methods func (t Template) atLocation(location SrcLocation, options []LocationOption) Template { for _, o := range options { o(&location) } t.Locations = append([]SrcLocation{location}, t.Locations...) return t } // InFile adds the given file as a src of the error location // // Note: It is preferable to use one of the other location functions // as they will render the source around the error, not just the file name func (t Template) InFile(filepath string, options ...LocationOption) Template { if filepath == "" { return t } return t.atLocation(SrcLocation{Kind: LocFile, Filepath: filepath}, options) } // AtGoNode adds the given Go node to the template. If the node is nil, nothing happens. // // You can use the [LocationOption]s to add additional information to the location. // // Example: // // errMyErrorTemplate.AtGoNode(node, errtmp.AsHelp("this is where it was defined before")) func (t Template) AtGoNode(node goAst.Node, options ...LocationOption) Template { if node == nil { return t } // In some cases the node may be a "typed nil" which isn't caught by the check above. // Handle that separately, as otherwise we get unexpected panics later when // trying to call .Pos(). if v := reflect.ValueOf(node); v.Kind() == reflect.Pointer && v.IsNil() { return t } return t.atLocation(SrcLocation{Kind: LocGoNode, GoNode: node}, options) } // AtGoPos adds the given start and end [token.Pos] to the template. If both positions are token.NoPos, nothing happens. // If one of the two positions are token.NoPos, the other position will be used. // // It is valid to use the same value for start and end positions, in which case the error will estimate which Node // you are referencing. // // Example: // // errMyErrorTemplate.AtGoPos(start, token.NoPos, errtmp.AsHelp("this is where it was defined before")) func (t Template) AtGoPos(start, end goToken.Pos, options ...LocationOption) Template { switch { case start == goToken.NoPos && end == goToken.NoPos: return t case start == goToken.NoPos && end != goToken.NoPos: start = end case end == goToken.NoPos && start != goToken.NoPos: end = start } return t.atLocation(SrcLocation{Kind: LocGoPos, GoStartPos: start, GoEndPos: end}, options) } // AtGoPosition adds the given Go positions to the template. // // It is valid to use the same value for start and end positions, in which case the error will estimate which Node // you are referencing. // // Example: // // errMyErrorTemplate.AtGoPosition(start, end, errtmp.AsHelp("this is where it was defined before")) func (t Template) AtGoPosition(start, end goToken.Position, options ...LocationOption) Template { return t.atLocation(SrcLocation{Kind: LocGoPositions, GoStartPosition: start, GoEndPosition: end}, options) } ================================================ FILE: pkg/errors/utils.go ================================================ package errors import ( "go/ast" "encr.dev/pkg/option" ) // AtOptionalNode returns an error at the given node if it is present. // Otherwise, it returns the error unchanged. func AtOptionalNode[T ast.Node](err Template, opt option.Option[T]) Template { if node, ok := opt.Get(); ok { return err.AtGoNode(node) } return err } ================================================ FILE: pkg/fns/fns.go ================================================ package fns import ( "cmp" "context" "io" "maps" ) // Map applies fn on all elements in src, producing a new slice // with the results, in order. func Map[A, B any](src []A, fn func(A) B) []B { dst := make([]B, len(src)) for i, v := range src { dst[i] = fn(v) } return dst } func Max[A any, B cmp.Ordered](src []A, fn func(A) B) B { var m B for _, v := range src { if val := fn(v); m < val { m = val } } return m } // MapAndFilter applies fn on all elements in src, producing a new slice // with the results, in order. If fn returns false, the element is // not included in the result. func MapAndFilter[A, B any](src []A, fn func(A) (B, bool)) []B { var dst []B for _, v := range src { if r, ok := fn(v); ok { dst = append(dst, r) } } return dst } // MapErr applies fn on all elements in src, producing a new slice // with the results, in order. If fn returns an error, MapErr // returns that error and the resulting slice is nil. func MapErr[A, B any](src []A, fn func(A) (B, error)) ([]B, error) { dst := make([]B, len(src)) for i, v := range src { var err error dst[i], err = fn(v) if err != nil { return nil, err } } return dst, nil } // TransformMapKeys creates a new map with the same values as m, but with // the keys transformed by fn. func TransformMapKeys[K1, K2 comparable, V any](m map[K1]V, fn func(K1) K2) map[K2]V { dst := make(map[K2]V, len(m)) for k, v := range m { dst[fn(k)] = v } return dst } // Any returns true if any element in src satisfies the predicate. func Any[A any](src []A, pred func(A) bool) bool { for _, v := range src { if pred(v) { return true } } return false } // All returns true if all elements in src satisfy the predicate. func All[A any](src []A, pred func(A) bool) bool { for _, v := range src { if !pred(v) { return false } } return true } // FlatMap applies fn on all elements in src, producing a new slice // with the results, in order. func FlatMap[A, B any](src []A, fn func(A) []B) []B { var dst []B for _, v := range src { dst = append(dst, fn(v)...) } return dst } // Find returns the first element where pred returns true. // The second argument is true if an element was found. func Find[A any](src []A, pred func(A) bool) (A, bool) { for _, v := range src { if pred(v) { return v, true } } var zero A return zero, false } // Filter applies fn on all elements in src, producing a new slice // containing the elements for which fn returned true, preserving // the same order. func Filter[Elem any](src []Elem, fn func(Elem) bool) []Elem { dst := make([]Elem, 0, len(src)) for _, v := range src { if fn(v) { dst = append(dst, v) } } return dst } // ToMap converts a slice to a map. func ToMap[K comparable, V any](src []V, key func(V) K) map[K]V { dst := make(map[K]V, len(src)) for _, v := range src { dst[key(v)] = v } return dst } // TransformMapToSlice creates a new slice with the results of applying fn to each // key-value pair in m. func TransformMapToSlice[K comparable, V any, R any](m map[K]V, fn func(K, V) R) []R { r := make([]R, 0, len(m)) for k, v := range m { r = append(r, fn(k, v)) } return r } // MapKeys returns the keys of the map m. // The keys will be in an indeterminate order. func MapKeys[M ~map[K]V, K comparable, V any](m M) []K { r := make([]K, 0, len(m)) for k := range m { r = append(r, k) } return r } // CloseIgnore closes c, ignoring any error. // Its main use is to satisfy linters. func CloseIgnore(c io.Closer) { _ = c.Close() } func CloseIgnoreCtx(ctx context.Context, close func(ctx context.Context) error) { _ = close(ctx) } // MergeMaps merges all maps into a single map. func MergeMaps[K comparable, V any](ms ...map[K]V) map[K]V { var rtn map[K]V for i, m := range ms { if i == 0 { rtn = m continue } maps.Copy(rtn, m) } return rtn } func Delete[T comparable](slice []T, t T) ([]T, bool) { for i, v := range slice { if v == t { slice = append(slice[:i], slice[i+1:]...) return slice, true } } return slice, false } ================================================ FILE: pkg/github/github.go ================================================ // Package github provides utilities for interacting with GitHub repositories. package github import ( "archive/tar" "compress/gzip" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path" "path/filepath" "strings" "github.com/cockroachdb/errors" ) // Tree contains information about a (sub-)tree in a GitHub repository. type Tree struct { Owner string // GitHub owner (user or organization) Repo string // repository name Branch string // branch name Path string // path to subtree ("." for whole project) } // Name reports a suitable name of the top-level directory in the tree. // It defaults to the repository name, unless a Path is given // in which case it is the last component of the path. func (t *Tree) Name() string { if base := path.Base(t.Path); base != "." { return base } return t.Repo } // ParseTree parses a GitHub repository URL into a Tree. // // Valid URLs are: // - github.com/owner/repo // - github.com/owner/repo/tree/ // - github.com/owner/repo/tree// // // If the URL does not contain a branch, the default branch is queried // using GitHub's API. func ParseTree(ctx context.Context, s string) (*Tree, error) { switch { case strings.HasPrefix(s, "http"): // Already an URL; do nothing case strings.HasPrefix(s, "github.com"): // Assume a URL without the scheme s = "https://" + s } u, err := url.Parse(s) if err != nil { return nil, errors.Wrap(err, "invalid tree string") } if u.Host != "github.com" { return nil, errors.Newf("url host must be github.com, not %q", u.Host) } // Path must be one of: // "/owner/repo" // "/owner/repo/tree/" // "/owner/repo/tree//path" parts := strings.SplitN(u.Path, "/", 6) switch { case len(parts) == 3: // "/owner/repo" owner, repo := parts[1], parts[2] // Check the default branch var resp struct { DefaultBranch string `json:"default_branch"` } url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, errors.Wrap(err, "lookup default branch") } else if err := slurpJSON(req, &resp); err != nil { return nil, errors.Wrap(err, "lookup default branch") } return &Tree{ Owner: owner, Repo: repo, Branch: resp.DefaultBranch, Path: ".", }, nil case len(parts) >= 5: // "/owner/repo" owner, repo, t, branch := parts[1], parts[2], parts[3], parts[4] p := "." if len(parts) == 6 { p = parts[5] } if t != "tree" { return nil, errors.Newf("invalid url: %s", u) } return &Tree{ Owner: owner, Repo: repo, Branch: branch, Path: p, }, nil default: return nil, errors.Newf("unsupported url: %s", u) } } func slurpJSON(req *http.Request, respData any) error { resp, err := http.DefaultClient.Do(req) if err != nil { return errors.Wrap(err, "send request") } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) return errors.Newf("got non-200 response: %s: %s", resp.Status, body) } if err := json.NewDecoder(resp.Body).Decode(respData); err != nil { return errors.Wrap(err, "decode response") } return nil } var ErrEmptyTree = errors.New("empty tree") // ExtractTree downloads a (sub-)tree from a GitHub repository and writes it to dst. func ExtractTree(ctx context.Context, tree *Tree, dst string) error { url := fmt.Sprintf("https://codeload.github.com/%s/%s/tar.gz/%s", tree.Owner, tree.Repo, tree.Branch) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return errors.Wrap(err, "create request") } resp, err := http.DefaultClient.Do(req) if err != nil { return errors.Wrap(err, "send request") } defer resp.Body.Close() if resp.StatusCode != 200 { return errors.Newf("GET %s: got non-200 response: %s", url, resp.Status) } gz, err := gzip.NewReader(resp.Body) if err != nil { return errors.Wrap(err, "read gzip response") } defer gz.Close() tr := tar.NewReader(gz) prefix := path.Join(tree.Repo+"-"+tree.Branch, tree.Path) prefix += "/" files := 0 for { hdr, err := tr.Next() if err == io.EOF { if files == 0 { return ErrEmptyTree } return nil } else if err != nil { return errors.Wrap(err, "read repository data") } if hdr.FileInfo().IsDir() { continue } if p := path.Clean(hdr.Name); strings.HasPrefix(p, prefix) { files++ p = p[len(prefix):] filePath := filepath.Join(dst, filepath.FromSlash(p)) if err := createFile(tr, filePath); err != nil { return errors.Wrapf(err, "create %s", p) } } } } // createFile creates the given file, creating any non-existent parent directories // in the process. It returns an error if the file already exists. func createFile(src io.Reader, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } f, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644) if err != nil { return err } _, err = io.Copy(f, src) if err2 := f.Close(); err == nil { err = err2 } return err } ================================================ FILE: pkg/golden/golden.go ================================================ package golden import ( "flag" "os" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" ) var ( update bool // should we update golden files? testMainRan bool // did TestMain get called? ) // Test checks the test output against the golden file where the path to the golden file is // based on the test name; i.e. `testName.golden` within the `testdata` folder. // If -golden-update was passed to "go test", it writes new golden files instead. func Test(t testing.TB, output string) { fn := strings.Replace(t.Name(), "/", "__", -1) TestAgainst(t, fn+".golden", output) } // TestAgainst checks the test output against the golden file. // If -golden-update was passed to "go test", it writes new golden files instead. func TestAgainst(t testing.TB, goldenFileName string, output string) { if !testMainRan { t.Fatal("golden.TestMain was not called") } wd, err := os.Getwd() if err != nil { t.Fatal(err) } path := filepath.Join(wd, "testdata", goldenFileName) if update { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatalf("update golden: %v", err) } err := os.WriteFile(path, []byte(output), 0644) if err != nil { t.Fatalf("update golden: %v", err) } } else { expect, err := os.ReadFile(path) if err != nil { t.Fatalf("read golden: %v", err) } if diff := cmp.Diff(string(expect), output); diff != "" { t.Fatalf("bad output (-want +got):\n%s", diff) } } } // TestMain sets up the golden testing functionality for the package. // Packages that want to integrate golden testing should themselves // implement TestMain and call this function. func TestMain(m *testing.M) { Setup() os.Exit(m.Run()) } // Setup sets up the golden testing functionality for the package. // It can be called instead of TestMain if a package wants to do multiple // main function handling. func Setup() { flag.BoolVar(&update, "golden-update", os.Getenv("GOLDEN_UPDATE") != "", "update golden files") flag.Parse() testMainRan = true } ================================================ FILE: pkg/idents/identifiers.go ================================================ package idents import ( "strings" "unicode" ) type IdentFormat int const ( CamelCase IdentFormat = iota // camelCase PascalCase // PascalCase SnakeCase // snake_case ScreamingSnakeCase // SCREAMING_SNAKE_CASE KebabCase // kebab-case ) // Convert will take a given identifier and convert it to the // specified format. func Convert(goIdentifier string, format IdentFormat) string { parts := parseIdentifier(goIdentifier) // Step 1: convert case for i, part := range parts { switch format { case CamelCase: if i == 0 { parts[i] = strings.ToLower(part) } else { parts[i] = strings.Title(part) } case PascalCase: parts[i] = strings.Title(part) case SnakeCase, KebabCase: parts[i] = strings.ToLower(part) case ScreamingSnakeCase: parts[i] = strings.ToUpper(part) } } // Step 2: Join Parts switch format { case CamelCase, PascalCase: return strings.Join(parts, "") case SnakeCase, ScreamingSnakeCase: return strings.Join(parts, "_") case KebabCase: return strings.Join(parts, "-") default: panic("unknown identifier format") } } // parseIdentifier parses a Go Identifier into the separate parts. // which can then be recombined as needed. func parseIdentifier(goIdentifier string) (parts []string) { if goIdentifier == "" { return nil } type runeType int const ( other runeType = iota upper lower ) runeToType := func(r rune, lastType runeType) runeType { switch { case unicode.IsUpper(r): return upper case unicode.IsLower(r): return lower case unicode.IsDigit(r): if lastType == other { return lower } return lastType default: return other } } var str strings.Builder recordPart := func() { part := str.String() str.Reset() if part == "" { return } if !stringIsOnly(part, unicode.IsUpper) { // strings will always start with uppercase runes, so the if // the last uppercase rune is after index 0 but before the last // rune in the string, then we need to split it into two parts. // // i.e. "GetAPIDocs" => { "Get", "APIDocs" } => { "Get", "API", "Docs" } lastUpperCase := strings.LastIndexFunc(part, unicode.IsUpper) if lastUpperCase > 0 && lastUpperCase != len(part)-1 { parts = append(parts, part[:lastUpperCase]) part = part[lastUpperCase:] } part = strings.ToLower(part) } parts = append(parts, part) } lastType := runeToType(rune(goIdentifier[0]), other) for _, r := range goIdentifier { runeType := runeToType(r, lastType) // If the type of rune has changed if lastType > runeType { recordPart() } lastType = runeType if runeType != other { str.WriteRune(r) } } recordPart() return } func stringIsOnly(str string, predicate func(r rune) bool) bool { for _, r := range str { if !predicate(r) { return false } } return true } // GenerateSuggestion creates a suggestion for an identifier in the given format // from the given input string. func GenerateSuggestion(input string, format IdentFormat) string { // Clean the string up first and remove unsupported characters input = strings.TrimSpace(input) input = strings.Map(func(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) || r == '_' || r == '-' { return r } return -1 }, input) // Remove any leading or trailing characters which are not letters input = strings.TrimLeftFunc(input, func(r rune) bool { return !unicode.IsLetter(r) }) input = strings.TrimRightFunc(input, func(r rune) bool { return !unicode.IsLetter(r) }) // Now convert the cleaned input into the desired format return Convert(input, format) } ================================================ FILE: pkg/idents/identifiers_test.go ================================================ package idents import ( "testing" qt "github.com/frankban/quicktest" ) func Test_parseIdentifier(t *testing.T) { c := qt.New(t) c.Parallel() tests := []struct { input string expect []string }{ {"hello", []string{"hello"}}, {"Hello", []string{"hello"}}, {"HelloWorld", []string{"hello", "world"}}, {"hello_world", []string{"hello", "world"}}, {"Hello_World", []string{"hello", "world"}}, {"_Hello___World__", []string{"hello", "world"}}, {"RenderMarkdown", []string{"render", "markdown"}}, {"RenderHTML", []string{"render", "HTML"}}, {"getVersion2", []string{"get", "version2"}}, {"GetAPIDocs", []string{"get", "API", "docs"}}, {"EncoreResource-123abc", []string{"encore", "resource", "123abc"}}, {"EncoreResource-abs-123", []string{"encore", "resource", "abs", "123"}}, {"This is a full sentence... with \"random! bits-and_pieces123 blah", []string{"this", "is", "a", "full", "sentence", "with", "random", "bits", "and", "pieces123", "blah"}}, } for _, tt := range tests { tt := tt c.Run(tt.input, func(c *qt.C) { c.Parallel() gotParts := parseIdentifier(tt.input) c.Assert(gotParts, qt.DeepEquals, tt.expect) }) } } func Test_convertIdentifierTo(t *testing.T) { c := qt.New(t) c.Parallel() type args struct { input string camelCase string pascalCase string snakeCase string screamingSnakeCase string kebabCase string } tests := []args{ {"Hello", "hello", "Hello", "hello", "HELLO", "hello"}, {"HelloWorld", "helloWorld", "HelloWorld", "hello_world", "HELLO_WORLD", "hello-world"}, {"getVersion2", "getVersion2", "GetVersion2", "get_version2", "GET_VERSION2", "get-version2"}, {"GetAPIDocs", "getAPIDocs", "GetAPIDocs", "get_api_docs", "GET_API_DOCS", "get-api-docs"}, {"EncoreResource-123abc", "encoreResource123abc", "EncoreResource123abc", "encore_resource_123abc", "ENCORE_RESOURCE_123ABC", "encore-resource-123abc"}, } for _, tt := range tests { tt := tt c.Run(tt.input, func(c *qt.C) { c.Parallel() c.Assert(Convert(tt.input, CamelCase), qt.Equals, tt.camelCase) c.Assert(Convert(tt.input, PascalCase), qt.Equals, tt.pascalCase) c.Assert(Convert(tt.input, SnakeCase), qt.Equals, tt.snakeCase) c.Assert(Convert(tt.input, ScreamingSnakeCase), qt.Equals, tt.screamingSnakeCase) c.Assert(Convert(tt.input, KebabCase), qt.Equals, tt.kebabCase) }) } } ================================================ FILE: pkg/jsonext/listencoder.go ================================================ package jsonext import ( "reflect" "unsafe" jsoniter "github.com/json-iterator/go" "github.com/modern-go/reflect2" ) type ListEncoderExtension struct { jsoniter.DummyExtension } func NewListEncoderExtension() *ListEncoderExtension { return &ListEncoderExtension{} } func (e *ListEncoderExtension) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder { if typ.Kind() == reflect.Slice { return &sliceEncoder{typ: typ, encoder: encoder} } return encoder } type sliceEncoder struct { typ reflect2.Type encoder jsoniter.ValEncoder } func (codec *sliceEncoder) IsEmpty(ptr unsafe.Pointer) bool { return codec.encoder.IsEmpty(ptr) } func (codec *sliceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { if codec.IsEmpty(ptr) { if codec.typ.Type1().Elem().Kind() == reflect.Uint8 { stream.WriteString("") } else { stream.WriteEmptyArray() } return } codec.encoder.Encode(ptr, stream) } ================================================ FILE: pkg/jsonext/listencoder_test.go ================================================ package jsonext import ( "testing" qt "github.com/frankban/quicktest" jsoniter "github.com/json-iterator/go" ) func TestListEncoder(t *testing.T) { c := qt.New(t) jsoniterAPI := jsoniter.Config{SortMapKeys: true}.Froze() jsoniterAPI.RegisterExtension(NewListEncoderExtension()) marshal, err := jsoniterAPI.Marshal(struct { StringList []string `json:",omitempty"` IntList []string FloatList []float64 NilBytes []byte EmptyBytes []byte SomeBytes []byte StringPointer *string ZeroArray [0]int }{ FloatList: []float64{1.0, 2.0, 3.0}, NilBytes: nil, EmptyBytes: []byte{}, SomeBytes: []byte("foobar"), }) c.Assert(err, qt.IsNil) c.Assert(string(marshal), qt.Equals, `{"IntList":[],"FloatList":[1,2,3],"NilBytes":"","EmptyBytes":"","SomeBytes":"Zm9vYmFy","StringPointer":null,"ZeroArray":[]}`) } ================================================ FILE: pkg/jsonext/protojson.go ================================================ package jsonext import ( "unsafe" jsoniter "github.com/json-iterator/go" "github.com/modern-go/reflect2" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) type ProtoEncoderExtension struct { jsoniter.DummyExtension messageType reflect2.Type opts protojson.MarshalOptions } func NewProtoEncoderExtension() *ProtoEncoderExtension { msgType := reflect2.TypeOfPtr((*proto.Message)(nil)).Elem() opts := protojson.MarshalOptions{ UseProtoNames: true, EmitUnpopulated: true, } return &ProtoEncoderExtension{ messageType: msgType, opts: opts, } } func (e *ProtoEncoderExtension) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder { if typ.Implements(e.messageType) { return &messageEncoder{typ: typ, encoder: encoder, opts: e.opts} } return encoder } type messageEncoder struct { typ reflect2.Type encoder jsoniter.ValEncoder opts protojson.MarshalOptions } func (codec *messageEncoder) IsEmpty(ptr unsafe.Pointer) bool { return codec.encoder.IsEmpty(ptr) } func (codec *messageEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { if msg, ok := codec.typ.UnsafeIndirect(ptr).(proto.Message); ok { data, err := codec.opts.Marshal(msg) if err != nil { if stream.Error == nil { stream.Error = err } } else { stream.Write(data) } return } codec.encoder.Encode(ptr, stream) } var ProtoEncoder = (func() jsoniter.API { enc := jsoniter.Config{}.Froze() // Note: the order is important. We don't want the list encoder to process repeated fields in proto // messages, so it must come first so it only applies to non-protobuf slices. enc.RegisterExtension(NewListEncoderExtension()) enc.RegisterExtension(NewProtoEncoderExtension()) return enc })() ================================================ FILE: pkg/logging/zerolog_adapter.go ================================================ package logging import ( "log" "github.com/rs/zerolog" ) type zeroLogWriter struct { logger zerolog.Logger level zerolog.Level } func (z *zeroLogWriter) Write(p []byte) (n int, err error) { z.logger.WithLevel(z.level).CallerSkipFrame(3).Msg(string(p)) return len(p), nil } // NewZeroLogAdapter returns a new log.Logger that writes to the given zerolog.Logger at the given level. func NewZeroLogAdapter(logger zerolog.Logger, level zerolog.Level) *log.Logger { zlw := &zeroLogWriter{logger, level} return log.New(zlw, "", 0) } ================================================ FILE: pkg/make-release/compilers.go ================================================ package main import ( osPkg "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "github.com/cockroachdb/errors" ) // MacOSSDKPath is the path to where the MacOS SDK is located on Encore's builder systems const MacOSSDKPath = "/sdk" func GoBaseEnvs(os string, arch string) ([]string, error) { // Create a cache dir for the go build cache for this specific OS and architecture pair cacheDir, err := osPkg.UserCacheDir() if err != nil { return nil, errors.Wrap(err, "user cache dir") } path := filepath.Join(cacheDir, "encore-build-cache", "go", os, arch) err = osPkg.MkdirAll(path, 0755) if err != nil { return nil, errors.Wrap(err, "failed to make cache dir") } return append(osPkg.Environ(), "GOOS="+os, "GOARCH="+arch, "GOCACHE="+path, ), nil } // CompileGoBinary compiles a Go binary for the given OS and architecture with GCP enabled // // This file was inspired by the blog post: https://lucor.dev/post/cross-compile-golang-fyne-project-using-zig/ func CompileGoBinary(outputPath string, entrypointPkg string, ldFlags []string, os string, arch string) error { cc, cxx, compilerEnvs, compilerLDFlags, err := compilerSettings(os, arch) if err != nil { return errors.Wrap(err, "compiler settings") } if os == "windows" { outputPath += ".exe" } combinedLDFlags := append(append([]string{}, compilerLDFlags...), ldFlags...) baseEnvs, err := GoBaseEnvs(os, arch) if err != nil { return errors.Wrap(err, "go base envs") } envs := append(baseEnvs, "CGO_ENABLED=1", "CC="+cc, "CXX="+cxx, ) envs = append(envs, compilerEnvs...) // Build the go build args args := []string{"build", "-trimpath", "-tags", "netgo", // Always force netgo otherwise we end up with segfaults on MacOS } if len(combinedLDFlags) > 0 { args = append(args, "-ldflags="+strings.Join(combinedLDFlags, " ")) } if os == "darwin" { args = append(args, "-buildmode=pie") } args = append(args, "-o", outputPath, entrypointPkg, ) // Build the command cmd := exec.Command("go", args...) cmd.Env = envs // nosemgrep out, err := cmd.CombinedOutput() if err != nil { return errors.Newf("failed to compile go binary: %s", string(out)) } return nil } // CompileRustBinary compiles a Rust binary for the given OS and architecture // // We're using zigbuild to perform easy cross compiling func CompileRustBinary(artifactPath, outputPath string, cratePath string, os string, arch string, extraEnvVars ...string) error { if os == "windows" { if !strings.HasSuffix(artifactPath, ".dll") { outputPath += ".exe" artifactPath += ".exe" } } envs := append(extraEnvVars, osPkg.Environ()...) var target string switch os { case "darwin": switch arch { case "amd64": target = "x86_64-apple-darwin" case "arm64": target = "aarch64-apple-darwin" default: return errors.New("unsupported architecture for darwin: " + arch) } // We need to set the SDKROOT for cross compiling to MacOS if runtime.GOOS != "darwin" { envs = append(envs, "SDKROOT="+MacOSSDKPath, ) } case "linux": switch arch { case "amd64": target = "x86_64-unknown-linux-gnu" case "arm64": target = "aarch64-unknown-linux-gnu" default: return errors.New("unsupported architecture for linux: " + arch) } case "windows": switch arch { case "amd64": target = "x86_64-pc-windows-gnu" default: return errors.New("unsupported architecture for windows: " + arch) } default: return errors.New("unsupported os: " + os) } // Create a cache dir for the go build cache for this specific OS and architecture pair cacheDir, err := osPkg.UserCacheDir() if err != nil { return errors.Wrap(err, "user cache dir") } path := filepath.Join(cacheDir, "encore-build-cache", "rust", os, arch) err = osPkg.MkdirAll(path, 0755) if err != nil { return errors.Wrap(err, "failed to make cache dir") } // Build the command cargoArgs := []string{"zigbuild", "--target", target, "--target-dir", path, "--release", } cmd := exec.Command("cargo", cargoArgs...) cmd.Dir = cratePath cmd.Env = envs // Cargo can't run multiple compiles at the same time for the same crate // so let's lock here, then unlock once the compile has finished cargoLock.Lock() defer cargoLock.Unlock() // nosemgrep out, err := cmd.CombinedOutput() if err != nil { return errors.Newf("failed to compile rust binary: %v - %s", err, string(out)) } // Copy the binary to the output path binaryFile := filepath.Join(path, target, "release", artifactPath) cmd = exec.Command("cp", binaryFile, outputPath) // nosemgrep out, err = cmd.CombinedOutput() if err != nil { return errors.Newf("failed to copy rust binary: %v - %s", err, string(out)) } return nil } // compilerSettings returns the CC and CXX settings for the given OS and architecture func compilerSettings(os string, arch string) (cc, cxx string, envs, ldFlags []string, err error) { var zigTarget string var zigArgs string zigBinary := "zig" // pick it up off the path switch os { case "darwin": zigBinary = "/usr/local/zig-0.9.1/zig" // We need an explicit version of Zig for darwin (0.11.0 compiles, build causes runtime errors) ldFlags = []string{"-s", "-w"} switch arch { case "amd64": zigTarget = "x86_64-macos.10.12" case "arm64": zigTarget = "aarch64-macos.11.1" default: return "", "", nil, nil, errors.New("unsupported architecture for darwin: " + arch) } // We need to set some extra stuff if we're cross compiling to MacOS if runtime.GOOS != "darwin" { zigArgs = " -isysroot " + MacOSSDKPath + " -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks" envs = []string{ "CGO_LDFLAGS=--sysroot " + MacOSSDKPath + " -F/System/Library/Frameworks -L/usr/lib", } } case "linux": switch arch { case "amd64": zigTarget = "x86_64-linux-gnu" // Note: we're not targeting a specific glibc version here as we tried before with 2.35 - but for some reason we still get runtime errors not finding 2.34 or 2.33 on WSL (which had 2.35) zigArgs = " -static -isystem /usr/include" case "arm64": zigTarget = "aarch64-linux-gnu" zigArgs = " -static -isystem /usr/include" envs = []string{ "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig", } default: return "", "", nil, nil, errors.New("unsupported architecture for linux: " + arch) } case "windows": switch arch { case "amd64": zigTarget = "x86_64-windows-gnu" default: return "", "", nil, nil, errors.New("unsupported architecture for windows: " + arch) } ldFlags = []string{"-H=windowsgui"} default: return "", "", nil, nil, errors.New("unsupported os: " + os) } cc = zigBinary + " cc -target " + zigTarget + zigArgs cxx = zigBinary + " c++ -target " + zigTarget + zigArgs return cc, cxx, envs, ldFlags, nil } var ( cargoLock sync.Mutex ) ================================================ FILE: pkg/make-release/dist_builder.go ================================================ package main import ( "fmt" "os" "os/exec" "path/filepath" "sync" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/internal/version" ) // A DistBuilder is a builder for a specific distribution of Encore. // // Anything which does not need to be built for a specific distribution // should be built in the main builder before these are invoked. // // Make release will run multiple of these in parallel to build all the // distributions. type DistBuilder struct { log zerolog.Logger OS string // The OS to build for Arch string // The architecture to build for TSParserPath string // The path to the ts-parser repo DistBuildDir string // The directory to build into ArtifactsTarFile string // The directory to put the final tar.gz artifact into Version string // The version to build jsBuilder *JSPackager // The JS builder } func (d *DistBuilder) buildEncoreCLI() error { // Build the CLI binaries. d.log.Info().Msg("building encore binary...") linkerOpts := []string{ "-X", fmt.Sprintf("'encr.dev/internal/version.Version=%s'", d.Version), } // If we're building a nightly, devel or beta version, we need to set the default config directory var versionSuffix string switch version.ChannelFor(d.Version) { case version.GA: versionSuffix = "" case version.Beta: versionSuffix = "-beta" case version.Nightly: versionSuffix = "-nightly" case version.DevBuild: versionSuffix = "-develop" default: return errors.Newf("unknown version channel for %s", d.Version) } if versionSuffix != "" { linkerOpts = append(linkerOpts, "-X", "'encr.dev/internal/conf.defaultConfigDirectory=encore"+versionSuffix+"'", ) } err := CompileGoBinary( join(d.DistBuildDir, "bin", "encore"+versionSuffix), "./cli/cmd/encore", linkerOpts, d.OS, d.Arch, ) if err != nil { d.log.Err(err).Msg("encore failed to build") return errors.Wrap(err, "compile encore") } d.log.Info().Msg("encore built successfully") return nil } func (d *DistBuilder) buildGitHook() error { // Build the git-remote-encore binary. d.log.Info().Msg("building git-remote-encore binary...") err := CompileGoBinary( join(d.DistBuildDir, "bin", "git-remote-encore"), "./cli/cmd/git-remote-encore", nil, d.OS, d.Arch, ) if err != nil { d.log.Err(err).Msg("git-remote-encore failed to build") return errors.Wrap(err, "compile git-remote-encore") } d.log.Info().Msg("git-remote-encore built successfully") return nil } func (d *DistBuilder) buildTSBundler() error { // Build the TS bundler. d.log.Info().Msg("building tsbundler binary...") linkerOpts := []string{ "-X", fmt.Sprintf("'encr.dev/internal/version.Version=%s'", d.Version), } err := CompileGoBinary( join(d.DistBuildDir, "bin", "tsbundler-encore"), "./cli/cmd/tsbundler-encore", linkerOpts, d.OS, d.Arch, ) if err != nil { d.log.Err(err).Msg("tsbundler failed to build") return errors.Wrap(err, "compile tsbundler") } d.log.Info().Msg("tsbundler built successfully") return nil } func (d *DistBuilder) buildTSParser() error { // Build the TS parser. d.log.Info().Msg("building ts-parser binary...") err := CompileRustBinary( "tsparser-encore", join(d.DistBuildDir, "bin", "tsparser-encore"), d.TSParserPath, d.OS, d.Arch, fmt.Sprintf("ENCORE_VERSION=%s", d.Version), ) if err != nil { d.log.Err(err).Msg("ts-parser failed to build") return errors.Wrap(err, "compile ts-parser") } d.log.Info().Msg("ts-parser built successfully") return nil } func (d *DistBuilder) buildNodePlugin() error { d.log.Info().Msg("building node plugin...") // Figure out the names of the compiled and target binaries. compiledBinaryName, err := func() (string, error) { switch d.OS { case "darwin": return "libencore_js_runtime.dylib", nil case "linux": return "libencore_js_runtime.so", nil case "windows": return "encore_js_runtime.dll", nil default: return "", errors.Newf("unknown OS: %s", d.OS) } }() if err != nil { d.log.Err(err).Msg("node plugin failed to build") return errors.Wrap(err, "compile node plugin") } d.log.Info().Msg("Patching jscore/api/version.cjs...") err = os.WriteFile( filepath.Join(".", "runtimes", "jscore", "api", "version.cjs"), []byte(`// Code generated by /pkg/make-release. DO NOT EDIT. /** * The version of the runtime this JS bundle was built for */ module.exports.version = "`+d.Version+`"; `), 0644, ) if err != nil { d.log.Err(err).Msg("failed to patch version.cjs") return errors.Wrap(err, "write patch version.cjs") } // Build the node plugin. err = CompileRustBinary( compiledBinaryName, join(d.DistBuildDir, "bin", "encore-runtime.node"), "./runtimes/jscore", d.OS, d.Arch, fmt.Sprintf("ENCORE_VERSION=%s", d.Version), ) if err != nil { d.log.Err(err).Msg("node plugin failed to build") return errors.Wrap(err, "compile node plugin") } d.log.Info().Msg("node plugin built successfully") return nil } func (d *DistBuilder) downloadEncoreGo() error { // Step 1: Find out the latest release version for Encore's Go distribution d.log.Info().Msg("downloading latest encore-go...") encoreGoArchive, err := downloadLatestGithubRelease("encoredev", "go", d.OS, d.Arch) if err != nil { d.log.Err(err).Msg("failed to download encore-go") return errors.Wrap(err, "download encore-go") } d.log.Info().Msg("extracting encore-go...") err = extractArchive(encoreGoArchive, d.DistBuildDir) if err != nil { d.log.Err(err).Msg("failed to extract encore-go") return errors.Wrap(err, "extract encore-go") } d.log.Info().Msg("encore-go extracted successfully") return nil } func (d *DistBuilder) copyEncoreRuntimeForGo() error { d.log.Info().Msg("copying encore runtime for Go...") cmd := exec.Command("cp", "-r", "runtimes/go/.", join(d.DistBuildDir, "runtimes", "go")+"/") // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { d.log.Err(err).Str("stderr", string(out)).Msg("encore runtime for go failed to be copied") return errors.Wrapf(err, "cp go runtime: %s", out) } d.log.Info().Msg("encore runtime for go copied successfully") return nil } func (d *DistBuilder) copyEncoreRuntimeForJS() error { d.log.Info().Msg("waiting for JS packager to complete...") <-d.jsBuilder.compileCompleted if d.jsBuilder.compileFailed.Load() { d.log.Error().Msg("JS packager failed to build") return errors.New("js build failed") } d.log.Info().Msg("copying encore runtime for JS...") cmd := exec.Command("cp", "-r", d.jsBuilder.DistFolder+"/.", join(d.DistBuildDir, "runtimes", "js")+"/") // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { d.log.Err(err).Str("stderr", string(out)).Msg("encore runtime for js failed to be copied") return errors.Wrapf(err, "cp js runtime: %s", out) } d.log.Info().Msg("encore runtime for js copied successfully") return nil } // Build builds the distribution running each step in order func (d *DistBuilder) Build() error { d.log = log.With().Str("os", d.OS).Str("arch", d.Arch).Logger() d.log.Info().Msg("building distribution...") // Prepare the target directory. if err := os.RemoveAll(d.DistBuildDir); err != nil { d.log.Err(err).Msg("failed to remove existing target dir") return errors.Wrap(err, "remove target dir") } else if err := os.MkdirAll(d.DistBuildDir, 0755); err != nil { d.log.Err(err).Msg("failed to create target dir") return errors.Wrap(err, "create target dir") } else if err := os.MkdirAll(join(d.DistBuildDir, "bin"), 0755); err != nil { d.log.Err(err).Msg("failed to create bin dir") return errors.Wrap(err, "create bin dir") } else if err := os.MkdirAll(join(d.DistBuildDir, "runtimes"), 0755); err != nil { d.log.Err(err).Msg("failed to create runtimes dir") return errors.Wrap(err, "create runtimes/go dir") } else if err := os.MkdirAll(join(d.DistBuildDir, "runtimes", "go"), 0755); err != nil { d.log.Err(err).Msg("failed to create runtimes/go dir") return errors.Wrap(err, "create runtimes/go dir") } else if err := os.MkdirAll(join(d.DistBuildDir, "runtimes", "js"), 0755); err != nil { d.log.Err(err).Msg("failed to create runtimes/js dir") return errors.Wrap(err, "create runtimes/js dir") } // Now we're prepped, start building. err := runParallel( d.buildEncoreCLI, d.buildTSBundler, d.buildGitHook, d.buildTSParser, d.buildNodePlugin, d.copyEncoreRuntimeForGo, d.copyEncoreRuntimeForJS, d.downloadEncoreGo, ) if err != nil { d.log.Err(err).Msg("failed to build distribution") return errors.Wrapf(err, " os: %s, arch: %s", d.OS, d.Arch) } // Now tar gzip the directory d.log.Info().Str("tar_file", d.ArtifactsTarFile).Msg("creating distribution tar file...") err = TarGzip(d.DistBuildDir, d.ArtifactsTarFile) if err != nil { d.log.Err(err).Msg("failed to tar gzip distribution") return errors.Wrapf(err, " os: %s, arch: %s", d.OS, d.Arch) } d.log.Info().Str("tar_file", d.ArtifactsTarFile).Msg("distribution built successfully") return nil } // runParallel runs the given functions in parallel, returning the first error func runParallel(functions ...func() error) error { var wg sync.WaitGroup wg.Add(len(functions)) var firstErr error var mu sync.Mutex for _, f := range functions { f := f go func() { defer wg.Done() if err := f(); err != nil { mu.Lock() defer mu.Unlock() if firstErr == nil { firstErr = err } } }() } wg.Wait() mu.Lock() defer mu.Unlock() return firstErr } ================================================ FILE: pkg/make-release/js_packager.go ================================================ package main import ( "io/fs" osPkg "os" "os/exec" "path/filepath" "regexp" "strings" "sync/atomic" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) type JSPackager struct { log zerolog.Logger WorkspaceRoot string // Path to the yarn workspace root Version string // the new version number DistFolder string // the folder to output the compiled JS to compileCompleted chan struct{} compileFailed atomic.Bool } func (j *JSPackager) PatchVersions() error { j.log.Info().Msg("Patching versions...") replaceVersionRegex, err := regexp.Compile(`"version": "([^-"]+)(?:-[^"]+)?",`) if err != nil { j.log.Err(err).Msg("failed to compile regex") return errors.Wrap(err, "compile regex") } // For all the folders in the packages dir packages, err := j.listPackages(filepath.Join(j.WorkspaceRoot, "packages"), "") if err != nil { return errors.Wrap(err, "list packages") } for _, pkg := range packages { // Look for a line containing `"version": ".*"`, and replace it // with `"version": "0.0.0"`. // (note we drop the "v" prefix for the package.json) newFile := replaceVersionRegex.ReplaceAll(pkg.PackageJsonData, []byte(`"version": "`+j.Version[1:]+`",`)) // Write the file back to disk err = osPkg.WriteFile(pkg.PackageJsonPath, newFile, 0644) if err != nil { j.log.Err(err).Str("package", pkg.Name).Msg("failed to write package.json") return errors.Wrap(err, "write package.json") } } j.log.Info().Msg("Patching internal-runtime/conf/version.ts...") err = osPkg.WriteFile( filepath.Join(j.WorkspaceRoot, "packages", "@encore.dev", "internal-runtime", "conf", "version.ts"), []byte(`// Code generated by /pkg/make-release. DO NOT EDIT. /** * The current version of the runtime. */ export const Version = "`+j.Version+`"; `), 0644, ) if err != nil { j.log.Err(err).Msg("failed to patch version.ts") return errors.Wrap(err, "write patch version.ts") } return nil } type packageInfo struct { RootDir string Name string PackageJsonPath string PackageJsonData []byte } func (j *JSPackager) listPackages(dir string, basePkgName string) ([]packageInfo, error) { var pkgs []packageInfo // For all the folders in the packages dir entries, err := osPkg.ReadDir(dir) if err != nil { j.log.Err(err).Str("dir", dir).Msg("failed to read directory") return nil, errors.Wrap(err, "read directory") } for _, e := range entries { pkgDir := filepath.Join(dir, e.Name()) switch { case !e.IsDir(): continue case strings.HasPrefix(e.Name(), "@"): children, err := j.listPackages(pkgDir, e.Name()) if err != nil { return nil, err } pkgs = append(pkgs, children...) default: pkgJsonPath := filepath.Join(pkgDir, "package.json") pkgJson, err := osPkg.ReadFile(pkgJsonPath) if errors.Is(err, fs.ErrNotExist) { continue } else if err != nil { return nil, errors.Wrap(err, "read package.json") } pkgName := e.Name() if basePkgName != "" { pkgName = basePkgName + "/" + pkgName } pkgs = append(pkgs, packageInfo{ RootDir: pkgDir, Name: pkgName, PackageJsonPath: pkgJsonPath, PackageJsonData: pkgJson, }) } } return pkgs, nil } func (j *JSPackager) Package() (rtnErr error) { defer func() { if rtnErr != nil { j.compileFailed.Store(true) } close(j.compileCompleted) }() j.DistFolder = filepath.Join(j.WorkspaceRoot, "dist") // Remove the existing dist directory if err := osPkg.RemoveAll(j.DistFolder); err != nil { if !errors.Is(err, fs.ErrNotExist) { j.log.Err(err).Msg("failed to remove dist directory") return errors.Wrap(err, "remove dist directory") } } // Patch the versions if err := j.PatchVersions(); err != nil { j.log.Err(err).Msg("failed to patch versions") return errors.Wrap(err, "patch versions") } // Ensure our node_modules is up to date and installed log.Info().Msg("Installing JS dependencies...") err := j.yarn("install") if err != nil { j.log.Err(err).Msg("yarn install failed") return errors.Wrap(err, "yarn install") } // Now clean up previous builds inside yarn log.Info().Msg("Cleaning up from previous builds...") err = j.yarn("clean") if err != nil { j.log.Err(err).Msg("yarn clean failed") return errors.Wrap(err, "yarn clean") } log.Info().Msg("Building JS...") err = j.yarn("build", "--filter=./packages/**/*", "--force") // only build our packages if err != nil { j.log.Err(err).Msg("yarn build failed") return errors.Wrap(err, "yarn build") } log.Info().Msg("Fixing dist folder for ESM") err = j.yarn("fix-dist") if err != nil { j.log.Err(err).Msg("yarn fix-dist failed") return errors.Wrap(err, "yarn fix-dist") } // Now it's all build we can start the publish/packing process log.Info().Msg("Publishing packages to npm...") npmTag := "latest" switch { case strings.Contains(j.Version, "-beta."): npmTag = "beta" case strings.Contains(j.Version, "-nightly."): npmTag = "nightly" } err = j.yarn("workspaces", "foreach", "--no-private", "-pt", "npm", "publish", "--tolerate-republish", "--access", "public", "--tag", npmTag) if err != nil { j.log.Err(err).Msg("yarn publish failed") return errors.Wrap(err, "yarn publish") } // Create our dist folder err = osPkg.MkdirAll(j.DistFolder, 0755) if err != nil { j.log.Err(err).Msg("failed to create dist directory") return errors.Wrap(err, "create dist directory") } // Package up our JS runtime log.Info().Msg("Packaging JS runtime packages...") err = j.yarn("workspaces", "foreach", "--no-private", "pack", "--out", filepath.Join(j.DistFolder, "%s.tgz")) if err != nil { j.log.Err(err).Msg("yarn pack failed") return errors.Wrap(err, "yarn pack") } // Now extract all the tarballs in our dist folder, deleting the tarballs // as we go. log.Info().Msg("Extracting JS runtime packages...") dirEntries, err := osPkg.ReadDir(j.DistFolder) if err != nil { j.log.Err(err).Msg("failed to read dist directory") return errors.Wrap(err, "read dist directory") } for _, entry := range dirEntries { if entry.IsDir() { continue } if filepath.Ext(entry.Name()) != ".tgz" { continue } name := entry.Name() name = strings.TrimSuffix(name, filepath.Ext(name)) dirToExtractTo := filepath.Join(j.DistFolder, name) dirToExtractTo = strings.ReplaceAll(dirToExtractTo, "@encore.dev-", "@encore.dev/") // Create the target directory err = osPkg.MkdirAll(dirToExtractTo, 0755) if err != nil { j.log.Err(err).Str("tgz_file", entry.Name()).Str("target", dirToExtractTo).Msg("failed to create target directory") return errors.Wrap(err, "create target directory") } // Extract the tarball out, err := exec.Command("tar", "-xzf", filepath.Join(j.DistFolder, entry.Name()), "--strip-components", "1", "-C", dirToExtractTo).CombinedOutput() // nosemgrep if err != nil { j.log.Err(err).Str("tgz_file", entry.Name()).Str("target", dirToExtractTo).Str("output", string(out)).Msg("failed to extract tarball") return errors.Wrap(err, "extract tarball") } // Delete the tarball err = osPkg.Remove(filepath.Join(j.DistFolder, entry.Name())) if err != nil { j.log.Err(err).Str("tgz_file", entry.Name()).Str("target", dirToExtractTo).Msg("failed to delete tarball") return errors.Wrap(err, "delete tarball") } } log.Info().Msg("JS runtime packaged successfully") return nil } func (j *JSPackager) yarn(args ...string) error { cmd := exec.Command("yarn", args...) cmd.Env = osPkg.Environ() cmd.Dir = j.WorkspaceRoot cmd.Stdout = osPkg.Stdout cmd.Stderr = osPkg.Stderr j.log.Debug().Str("cmd", cmd.String()).Msg("running yarn command...") if err := cmd.Run(); err != nil { return errors.Wrap(err, "yarn command failed") } return nil } ================================================ FILE: pkg/make-release/make-release.go ================================================ package main import ( "flag" "fmt" "os" "path/filepath" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/internal/version" ) func join(strs ...string) string { return filepath.Join(strs...) } func main() { log.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger() dst := flag.String("dst", "", "build destination") versionStr := flag.String("v", "", "version number") tsParserRepo := flag.String("ts-parser", "", "path to ts-parser repo") onlyBuild := flag.String("only", "", "build only the valid target ('darwin-arm64' or 'darwin' or 'arm64' or '' for all)") flag.Parse() if *dst == "" || *versionStr == "" || *tsParserRepo == "" { log.Fatal().Msgf("missing -dst %q, -v %q or ts-parser %q", *dst, *versionStr, *tsParserRepo) } if (*versionStr)[0] != 'v' { log.Fatal().Msg("version must start with 'v'") } switch version.ChannelFor(*versionStr) { case version.GA, version.Beta, version.Nightly, version.DevBuild: // no-op default: log.Fatal().Msgf("unknown version channel for %s", *versionStr) } root, err := os.Getwd() if err != nil { log.Fatal().Err(err).Msg("failed to get working directory") } else if _, err := os.Stat(join(root, "go.mod")); err != nil { log.Fatal().Err(err).Msg("expected to run make-release.go from encr.dev repository root") } *dst, err = filepath.Abs(*dst) if err != nil { log.Fatal().Err(err).Msg("failed to get absolute path to destination") } // Prepare the target directory. if err := os.RemoveAll(*dst); err != nil { log.Fatal().Err(err).Msg("failed to remove existing target dir") } else if err := os.MkdirAll(filepath.Join(*dst, "artifacts"), 0755); err != nil { log.Fatal().Err(err).Msg("failed to create target dir") } jsBuilder := &JSPackager{ WorkspaceRoot: join(root, "runtimes", "js"), Version: *versionStr, log: log.Logger.With().Str("builder", "js").Logger(), compileCompleted: make(chan struct{}), } // Create all the builders builders := []*DistBuilder{ {OS: "darwin", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}, } parralelFuncs := make([]func() error, 1, len(builders)+1) parralelFuncs[0] = jsBuilder.Package // Give them the common settings for _, b := range builders { if *onlyBuild != "" && !(*onlyBuild == fmt.Sprintf("%s-%s", b.OS, b.Arch) || *onlyBuild == b.OS || *onlyBuild == b.Arch) { continue } b.TSParserPath = *tsParserRepo b.DistBuildDir = join(*dst, b.OS+"_"+b.Arch) b.ArtifactsTarFile = join(*dst, "artifacts", "encore-"+*versionStr+"-"+b.OS+"_"+b.Arch+".tar.gz") b.Version = *versionStr b.jsBuilder = jsBuilder parralelFuncs = append(parralelFuncs, b.Build) } if err := runParallel(parralelFuncs...); err != nil { log.Fatal().Err(err).Msg("failed to build all distributions") } log.Info().Msg("all distributions built successfully") } ================================================ FILE: pkg/make-release/utils.go ================================================ package main import ( "bufio" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" osPkg "os" "os/exec" "path/filepath" "strings" "github.com/cockroachdb/errors" ) type Release struct { Version string // The version of the release Filename string // The filename of the release (inc extension) FileExt string // The file extension URL string // The URL to download the release from Checksum []byte // The checksum of the release } // getGithubRelease fetches the latest release from Github for the given org and repo func getGithubRelease(org string, repo string, os string, arch string) (*Release, error) { rtn := &Release{} type GithubRelease struct { TagName string `json:"tag_name"` Assets []struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"` } `json:"assets"` } // Download the latest releases releasesResp, err := http.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", org, repo)) if err != nil { return nil, errors.Wrap(err, "unable to fetch latest release information") } defer func() { _ = releasesResp.Body.Close() }() if releasesResp.StatusCode != http.StatusOK { return nil, errors.Newf("Unexpected response status code: %s", releasesResp.Status) } releases := &GithubRelease{} if err := json.NewDecoder(releasesResp.Body).Decode(releases); err != nil { return nil, errors.Wrap(err, "unable to decode Github releases") } rtn.Version = releases.TagName // Build a list of possible file names osOptions := []string{os} if os == "darwin" { osOptions = append(osOptions, "macos") } archOptions := []string{arch} if arch == "amd64" { archOptions = append(archOptions, "x86_64", "x86-64") } else if arch == "arm64" { archOptions = append(archOptions, "aarch64") } extOptions := []string{"tar.gz"} // , "zip"} var fileNameOptions []string for _, osOption := range osOptions { for _, archOption := range archOptions { for _, extOption := range extOptions { fileNameOptions = append(fileNameOptions, fmt.Sprintf("%s_%s.%s", osOption, archOption, extOption), fmt.Sprintf("%s-%s.%s", osOption, archOption, extOption), ) } } } // Find the checksum file checksumFileURL := "" for _, asset := range releases.Assets { // We want to know the checksum URL if strings.EqualFold(asset.Name, "checksums.txt") { checksumFileURL = asset.BrowserDownloadURL } for _, filenameOption := range fileNameOptions { if strings.EqualFold(asset.Name, filenameOption) { // We also want to know the asset name and download URL rtn.Filename = asset.Name rtn.URL = asset.BrowserDownloadURL if strings.HasSuffix(asset.Name, ".tar.gz") { rtn.FileExt = ".tar.gz" } else { rtn.FileExt = filepath.Ext(asset.Name) } } } } if checksumFileURL == "" { return nil, errors.New("unable to find checksum file in Github release") } if rtn.URL == "" || rtn.Filename == "" { return nil, errors.New("unable to find binary in Github release") } // Download the checksum file checksumResp, err := http.Get(checksumFileURL) if err != nil { return nil, errors.Wrap(err, "unable to fetch checksum file") } defer func() { _ = checksumResp.Body.Close() }() if checksumResp.StatusCode != http.StatusOK { return nil, errors.Newf("Unexpected response status code for checksum file: %s", checksumResp.Status) } // Read the checksum file line by line scanner := bufio.NewScanner(checksumResp.Body) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasSuffix(line, rtn.Filename) { checksumStr := strings.Split(line, " ")[0] checksum, err := hex.DecodeString(checksumStr) if err != nil { return nil, errors.Wrap(err, "unable to decode checksum") } rtn.Checksum = checksum return rtn, nil } } return nil, errors.New("unable to find checksum for asset file in checksum file") } func downloadLatestGithubRelease(org, repo, os, arch string) (pathToFile string, rtnErr error) { // Find the latest release release, err := getGithubRelease(org, repo, os, arch) if err != nil { return "", err } // Create a cache dir for the download cache for this specific OS and architecture pair cacheDir, err := osPkg.UserCacheDir() if err != nil { return "", errors.Wrap(err, "user cache dir") } path := filepath.Join(cacheDir, "encore-build-cache", "github-releases", org, repo, os, arch) err = osPkg.MkdirAll(path, 0755) if err != nil { return "", errors.Wrap(err, "failed to make cache dir") } downloadFileName := fmt.Sprintf("%s-%s%s", release.Version, hex.EncodeToString(release.Checksum), release.FileExt) downloadPath := filepath.Join(path, downloadFileName) // Check if the file already exists if _, err := osPkg.Stat(downloadPath); err == nil { return downloadPath, nil } else if !osPkg.IsNotExist(err) { return "", errors.Wrap(err, "failed to stat existing download file") } // Now download the file downloadResp, err := http.Get(release.URL) if err != nil { return "", errors.Wrap(err, "unable to fetch release file") } defer func() { _ = downloadResp.Body.Close() }() if downloadResp.StatusCode != http.StatusOK { return "", errors.Newf("Unexpected response status code for release file: %s", downloadResp.Status) } // Create the file downloadFile, err := osPkg.Create(downloadPath) defer func() { _ = downloadFile.Close() if rtnErr != nil { _ = osPkg.Remove(downloadPath) // delete any partially written file } }() _, err = io.Copy(downloadFile, downloadResp.Body) if err != nil { return "", errors.Wrap(err, "unable to download release file") } // Now checksum the file if _, err := downloadFile.Seek(0, 0); err != nil { return "", errors.Wrap(err, "unable to seek to start of release file") } checksum, err := checksumFile(downloadFile) if err != nil { return "", errors.Wrap(err, "unable to checksum release file") } // Check the checksum if !bytes.Equal(checksum, release.Checksum) { return "", errors.Newf("checksum of downloaded file (%q) does not match expected checksum (%q)", hex.EncodeToString(checksum), hex.EncodeToString(release.Checksum)) } return downloadPath, nil } func checksumFile(file *osPkg.File) ([]byte, error) { hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return nil, errors.Wrap(err, "unable to checksum file") } return hash.Sum(nil), nil } func extractArchive(pathToArchive string, targetDir string) error { // Create the target dir if err := osPkg.MkdirAll(targetDir, 0755); err != nil { return errors.Wrap(err, "failed to create target dir") } // Extract the archive cmd := exec.Command("tar", "-xzf", pathToArchive, "--strip-components", "1", "-C", targetDir) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { return errors.Wrapf(err, "failed to extract archive: %s", out) } return nil } func TarGzip(srcDirectory string, tarFile string) error { // Create the tar.gz file from the src directory cmd := exec.Command("tar", "-czf", tarFile, "-C", srcDirectory, ".") // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { return errors.Wrapf(err, "failed to create tar.gz: %s", out) } return nil } func copyDir(src, dst string) error { cmd := exec.Command("cp", "-r", src+"/", dst) // nosemgrep if out, err := cmd.CombinedOutput(); err != nil { return errors.Wrapf(err, "failed to copy dir: %s", out) } return nil } ================================================ FILE: pkg/make-release/windows/.gitignore ================================================ /.deps ================================================ FILE: pkg/make-release/windows/build.bat ================================================ @echo off rem SPDX-License-Identifier: MIT rem Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. setlocal enableextensions enabledelayedexpansion set BUILDDIR=%~dp0 set ROOT=%BUILDDIR%\..\..\.. set DST=%ROOT%\dist\windows_amd64 set PATH=%BUILDDIR%.deps\llvm-mingw\bin;%BUILDDIR%.deps;%PATH% set PATHEXT=.EXE;.CMD if "%ENCORE_VERSION%" == "" ( echo ENCORE_VERSION not set exit /b 1 ) if "%ENCORE_GOROOT%" == "" ( echo ENCORE_GOROOT not set exit /b 1 ) :: Get absolute path cd %ENCORE_GOROOT% || exit /b 1 set ENCORE_GOROOT=%CD% cd /d %BUILDDIR% || exit /b 1 if exist .deps\prepared goto :build :installdeps rmdir /s /q .deps 2> NUL mkdir .deps || goto :error cd .deps || goto :error call :download llvm-mingw-msvcrt.zip https://download.wireguard.com/windows-toolchain/distfiles/llvm-mingw-20201020-msvcrt-x86_64.zip 2e46593245090df96d15e360e092f0b62b97e93866e0162dca7f93b16722b844 || goto :error call :download wintun.zip https://www.wintun.net/builds/wintun-0.10.2.zip fcd9f62f1bd5a550fcb9c21fbb5d6a556214753ccbbd1a3ebad4d318ec9dcbef || goto :error call :download wix-binaries.zip https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip 2c1888d5d1dba377fc7fa14444cf556963747ff9a0a289a3599cf09da03b9e2e || goto :error copy /y NUL prepared > NUL || goto :error cd .. || goto :error :build set GOOS=windows call :build_plat amd64 x86_64 amd64 || goto :error call :copy_artifacts || goto :error :success echo [+] Success! exit /b 0 :download echo [+] Downloading %1 curl -#fLo %1 %2 || exit /b 1 echo [+] Verifying %1 for /f %%a in ('CertUtil -hashfile %1 SHA256 ^| findstr /r "^[0-9a-f]*$"') do if not "%%a"=="%~3" exit /b 1 echo [+] Extracting %1 tar -xf %1 %~4 || exit /b 1 echo [+] Cleaning up %1 del %1 || exit /b 1 goto :eof :build_plat rmdir /S /Q "%DST%" mkdir %DST%\bin >NUL 2>&1 echo [+] Assembling resources x86_64-w64-mingw32-windres -I ".deps\wintun\bin\amd64" -i resources.rc -o "%ROOT%\cli\cmd\encore\resources_amd64.syso" -O coff -c 65001 || exit /b %errorlevel% set GOARCH=amd64 echo [+] Building go build -tags load_wintun_from_rsrc -ldflags "-X 'encr.dev/internal/version.Version=v%ENCORE_VERSION%'" -o "%DST%\bin\encore.exe" "%ROOT%\cli\cmd\encore" || exit /b 1 go build -trimpath -o "%DST%\bin\git-remote-encore.exe" "%ROOT%\cli\cmd\git-remote-encore" || exit /b 1 goto :eof :copy_artifacts echo [+] Copying files xcopy /S /I /E /H /Q "%ENCORE_GOROOT%" "%DST%\encore-go" || exit /b 1 xcopy /S /I /E /H /Q "%ROOT%\runtimes\go" "%DST%\runtimes\go" || exit /b 1 goto :eof :error echo [-] Failed with error #%errorlevel%. cmd /c exit %errorlevel% ================================================ FILE: pkg/make-release/windows/manifest.xml ================================================ PerMonitorV2, PerMonitor True ================================================ FILE: pkg/make-release/windows/resources.rc ================================================ /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. */ #pragma code_page(65001) // UTF-8 CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml wintun.dll RCDATA wintun.dll ================================================ FILE: pkg/metascrub/metascrub.go ================================================ // Package metascrub computes scrub paths for a metadata type. package metascrub import ( "slices" "strconv" "github.com/rs/zerolog" "encore.dev/appruntime/exported/scrub" meta "encr.dev/proto/encore/parser/meta/v1" schema "encr.dev/proto/encore/parser/schema/v1" ) // New constructs a new Computer for the given metadata. func New(md *meta.Data, log zerolog.Logger) *Computer { return &Computer{ md: md, log: log.With().Str("component", "metascrub").Logger(), declCache: make(map[declCacheKey]declResult), } } // Computer computes scrub paths for types, caching the computation. // It can safely be reused across multiple types for the same metadata. // It is not safe for concurrent use. type Computer struct { md *meta.Data log zerolog.Logger // declCache caches the scrub paths for encountered declarations declCache map[declCacheKey]declResult } type Desc struct { Payload []scrub.Path Headers []string } type ParseMode int const ( // AuthHandler specifies that the type is an auth handler. AuthHandler ParseMode = 1 << iota ) // Compute computes the scrub paths for the given typ. // It is not safe for concurrent use. func (c *Computer) Compute(typ *schema.Type, mode ParseMode) Desc { if typ == nil { return Desc{} } defer func() { if err := recover(); err != nil { c.log.Error().Stack().Msgf("metascrub.Compute panicked: %v", err) } }() p := &typeParser{c: c, mode: mode} res := p.typ(typ) // Did we exceed the steps? if p.steps > maxSteps { c.log.Error().Msg("metascrub.Compute aborted due to exceeding max steps") } return Desc{ Payload: res.scrub, Headers: res.headers, } } const maxSteps = 10000 type typeParser struct { c *Computer mode ParseMode // steps is a fail-safe to catch any potential infinite loops. steps int } type declCacheKey struct { id uint32 mode ParseMode } func (p *typeParser) decl(id uint32) declResult { key := declCacheKey{id, p.mode} if res, ok := p.c.declCache[key]; ok { return res } // Mark that we're beginning to process this decl, // so we avoid infinite recursion. p.c.declCache[key] = declResult{} // Do the actual parsing. decl := p.c.md.Decls[id] res := p.typ(decl.Type) // We're done, cache the final result. p.c.declCache[key] = res return res } type declResult struct { scrub []scrub.Path typeParams []typeParamPath headers []string } func (p *typeParser) typ(typ *schema.Type) declResult { if typ == nil { return declResult{} } p.steps++ if p.steps > maxSteps { return declResult{} } switch t := typ.Typ.(type) { case *schema.Type_Named: decl := p.decl(t.Named.Id) out := declResult{ // Clone the paths since we're modifying them. scrub: slices.Clone(decl.scrub), // Copy the headers directly since they never get modified, // since we only care about the top-level type's headers. headers: decl.headers, } for i, arg := range t.Named.TypeArguments { argRes := p.typ(arg) if len(argRes.scrub) == 0 && len(argRes.typeParams) == 0 { // Nothing to do continue } // For every type parameter, find the places // where it's used and copy it to the combined result. for _, declParam := range decl.typeParams { if declParam.paramIdx == uint32(i) { for _, s := range argRes.scrub { path := append(slices.Clone(declParam.p), s...) out.scrub = append(out.scrub, path) } for _, s := range argRes.typeParams { path := append(slices.Clone(declParam.p), s.p...) out.typeParams = append(out.typeParams, typeParamPath{ p: path, paramIdx: s.paramIdx, }) } } } } return out case *schema.Type_Pointer: return p.typ(t.Pointer.Base) case *schema.Type_Option: return p.typ(t.Option.Value) case *schema.Type_List: return p.typ(t.List.Elem) case *schema.Type_Union: var results []declResult var numScrub, numTypeParams, numHeaders int for _, typ := range t.Union.Types { res := p.typ(typ) results = append(results, res) numScrub += len(res.scrub) numTypeParams += len(res.typeParams) numHeaders += len(res.headers) } combined := declResult{ scrub: make([]scrub.Path, 0, numScrub), typeParams: make([]typeParamPath, 0, numTypeParams), headers: make([]string, 0, numHeaders), } for _, res := range results { combined.scrub = append(combined.scrub, res.scrub...) combined.typeParams = append(combined.typeParams, res.typeParams...) combined.headers = append(combined.headers, res.headers...) } return combined case *schema.Type_Map: key := p.typ(t.Map.Key) val := p.typ(t.Map.Value) combined := declResult{ scrub: make([]scrub.Path, 0, len(key.scrub)+len(val.scrub)), typeParams: make([]typeParamPath, 0, len(key.typeParams)+len(val.typeParams)), } for i, res := range [...]declResult{key, val} { kind := scrub.MapKey if i == 1 { kind = scrub.MapValue } for _, e := range res.scrub { path := append(scrub.Path{{Kind: kind}}, e...) combined.scrub = append(combined.scrub, path) } for _, e := range res.typeParams { path := append(scrub.Path{{Kind: kind}}, e.p...) combined.typeParams = append(combined.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx}) } } return combined case *schema.Type_Struct: var out declResult for _, f := range t.Struct.Fields { sensitive, fieldName, caseSensitive := isSensitive(f) // For Auth Handlers everything is sensitive. if (p.mode & AuthHandler) != 0 { sensitive = true } if sensitive { // If the field is sensitive add it directly. out.scrub = append(out.scrub, scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}) // If this is a header field, add it to the headers to scrub. if headerName, ok := isHeader(f); ok { out.headers = append(out.headers, headerName) } } else { // Otherwise check the type and see if there's anything to scrub within it. fieldRes := p.typ(f.Typ) for _, e := range fieldRes.scrub { path := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e...) out.scrub = append(out.scrub, path) } for _, e := range fieldRes.typeParams { path := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e.p...) out.typeParams = append(out.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx}) } } } return out case *schema.Type_Builtin, *schema.Type_Literal: // Nothing to do return declResult{} case *schema.Type_TypeParameter: return declResult{typeParams: []typeParamPath{{ paramIdx: t.TypeParameter.ParamIdx, }}} default: p.c.log.Warn().Msgf("got unexpected schema.Type %T in metascrub, skipping", t) return declResult{} } } func isSensitive(f *schema.Field) (sensitive bool, fieldName string, caseSensitive bool) { fieldName = f.Name if f.JsonName != "" { fieldName = f.JsonName caseSensitive = true } fieldName = strconv.Quote(fieldName) // the scrub package wants exact byte matches for _, t := range f.Tags { if t.Key == "encore" { sensitive = t.Name == "sensitive" || slices.Contains(t.Options, "sensitive") break } } return sensitive, fieldName, caseSensitive } func isHeader(f *schema.Field) (headerName string, ok bool) { for _, t := range f.Tags { if t.Key == "header" { name := t.Name if name == "" { name = f.Name } return name, true } } return "", false } type typeParamPath struct { p scrub.Path paramIdx uint32 } ================================================ FILE: pkg/metascrub/metascrub_test.go ================================================ package metascrub import ( "context" "os" "strconv" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/rogpeppe/go-internal/txtar" "github.com/rs/zerolog" "encore.dev/appruntime/exported/scrub" "encr.dev/cli/daemon/apps" "encr.dev/pkg/builder" meta "encr.dev/proto/encore/parser/meta/v1" "encr.dev/v2/v2builder" ) func TestScrub(t *testing.T) { c := qt.New(t) md := testParse(c, ` -- svc/svc.go -- package svc import ( "context" "encore.dev/types/option" ) type Params struct { Foo string SCRUB NestedScrub struct { Inner string SCRUB } SCRUB Nested struct { Inner string SCRUB } List []string SCRUB ListInner []struct { Inner string SCRUB } RecurseScrub *Params SCRUB // Ideally this should yield nested scrubs but we don't support that yet. Recurse *Params Gen Generic[string] GenScrub Generic[string] SCRUB GenInner Generic[Bar] Multi NestedGeneric[string, Bar] Option option.Option[Generic[string]] OptionScrub option.Option[Generic[string]] SCRUB MapOne map[Generic[Bar]]NestedGeneric[string, Bar] MapTwo GenericMap[Bar, NestedGeneric[string, Bar]] JsonKey string `+"`"+`json:"json_key" encore:"sensitive"`+"`"+` Header string `+"`"+`header:"X-Header" encore:"sensitive"`+"`"+` } type Generic[T any] struct { Foo T FooScrub T SCRUB } type NestedGeneric[A any, B any] struct { One NestedGenericTwo[B, string] Two B } type NestedGenericTwo[A any, B any] struct { Alpha A Beta B } type GenericMap[K comparable, V any] struct { Foo map[K]V } type Bar struct { Scrub string SCRUB } //encore:api public func Foo(ctx context.Context, p *Params) error { return nil } `) cmp := New(md, zerolog.New(os.Stdout)) rpc := md.Svcs[0].Rpcs[0] res := cmp.Compute(rpc.RequestSchema, 0) f := func(name string) scrub.PathEntry { return scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)} } fCase := func(name string) scrub.PathEntry { return scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name), CaseSensitive: true} } mapKey := scrub.PathEntry{Kind: scrub.MapKey} mapVal := scrub.PathEntry{Kind: scrub.MapValue} c.Assert(res.Payload, qt.DeepEquals, []scrub.Path{ {f("Foo")}, {f("NestedScrub")}, {f("Nested"), f("Inner")}, {f("List")}, {f("ListInner"), f("Inner")}, {f("RecurseScrub")}, {f("Gen"), f("FooScrub")}, {f("GenScrub")}, {f("GenInner"), f("FooScrub")}, {f("GenInner"), f("Foo"), f("Scrub")}, {f("Multi"), f("One"), f("Alpha"), f("Scrub")}, {f("Multi"), f("Two"), f("Scrub")}, {f("Option"), f("FooScrub")}, {f("OptionScrub")}, {f("MapOne"), mapKey, f("FooScrub")}, {f("MapOne"), mapKey, f("Foo"), f("Scrub")}, {f("MapOne"), mapVal, f("One"), f("Alpha"), f("Scrub")}, {f("MapOne"), mapVal, f("Two"), f("Scrub")}, {f("MapTwo"), f("Foo"), mapKey, f("Scrub")}, {f("MapTwo"), f("Foo"), mapVal, f("One"), f("Alpha"), f("Scrub")}, {f("MapTwo"), f("Foo"), mapVal, f("Two"), f("Scrub")}, {fCase("json_key")}, {f("Header")}, }) c.Assert(res.Headers, qt.DeepEquals, []string{"X-Header"}) } func TestScrubAuthHandler(t *testing.T) { c := qt.New(t) md := testParse(c, ` -- svc/svc.go -- package svc import "context" type Params struct { Header string `+"`"+`header:"Foo"`+"`"+` Query string `+"`"+`query:"query"`+"`"+` Other string } //encore:api public func Foo(ctx context.Context, p *Params) error { return nil } `) cmp := New(md, zerolog.New(os.Stdout)) rpc := md.Svcs[0].Rpcs[0] res := cmp.Compute(rpc.RequestSchema, AuthHandler) f := func(name string) scrub.PathEntry { return scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)} } c.Assert(res.Payload, qt.DeepEquals, []scrub.Path{ {f("Header")}, {f("Query")}, {f("Other")}, }) c.Assert(res.Headers, qt.DeepEquals, []string{"Foo"}) } func testParse(c *qt.C, code string) *meta.Data { code = strings.ReplaceAll(code, "SCRUB", "`encore:\"sensitive\"`") ar := txtar.Parse([]byte(code)) ar.Files = append(ar.Files, txtar.File{Name: "go.mod", Data: []byte("module test\n\nrequire (\n\tencore.dev v1.52.1\n)\n")}) root := c.TempDir() err := txtar.Write(ar, root) c.Assert(err, qt.IsNil) bld := v2builder.New() ctx := context.Background() app := apps.NewInstance(root, "test", "") prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{ Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", }) c.Assert(err, qt.IsNil) res, err := bld.Parse(ctx, builder.ParseParams{ Build: builder.DefaultBuildInfo(), App: app, Experiments: nil, WorkingDir: ".", ParseTests: false, Prepare: prepareResult, }) c.Assert(err, qt.IsNil) return res.Meta } ================================================ FILE: pkg/namealloc/namealloc.go ================================================ package namealloc import ( "go/token" "strconv" ) // Allocator helps choosing names without collisions // and without using Go keywords. The zero value is ready // to be used. type Allocator struct { // Reserved decides whether a given input is reserved. // If nil it defaults to token.IsKeyword. Reserved func(input string) bool used map[string]bool } // Get allocates a name that is a valid, unused identifier // based on the input string. It ensures the same name is not // returned multiple times even for the same input. func (a *Allocator) Get(input string) (name string) { if a.isReserved(input) { input = input + "_" } candidate := input for i := 2; a.used[candidate]; i++ { candidate = input + strconv.Itoa(i) } if a.used == nil { a.used = make(map[string]bool) } a.used[candidate] = true return candidate } func (a *Allocator) isReserved(input string) bool { reserved := a.Reserved if reserved == nil { reserved = token.IsKeyword } return reserved(input) } ================================================ FILE: pkg/namealloc/namealloc_test.go ================================================ package namealloc import ( "reflect" "testing" ) func TestAlloc(t *testing.T) { tests := []struct { name string reserved []string // reserved words in []string out []string }{ { name: "simple", in: []string{"hello", "hello", "there", "hello"}, out: []string{"hello", "hello2", "there", "hello3"}, }, { name: "reserved", reserved: []string{"hello"}, in: []string{"hello", "hello", "there", "hello"}, out: []string{"hello_", "hello_2", "there", "hello_3"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reserved := func(s string) bool { for _, r := range tt.reserved { if r == s { return true } } return false } a := &Allocator{Reserved: reserved} got := make([]string, 0, len(tt.in)) for _, in := range tt.in { v := a.Get(in) got = append(got, v) } if !reflect.DeepEqual(got, tt.out) { t.Errorf("Alloc(%+v) = %+v, want %+v", tt.in, got, tt.out) } }) } } ================================================ FILE: pkg/noopgateway/noopgateway.go ================================================ package noopgateway import ( "context" "errors" "net/http" "net/http/httputil" "net/url" "strings" "github.com/gorilla/mux" "github.com/julienschmidt/httprouter" ) // ServiceName is the name of a service. type ServiceName string // Route defines a single route in the gateway. type Route struct { Methods []string // HTTP methods Path string // "/path/:param/*wildcard" Dest ServiceName // where to route the request // RequiresAuth specifies whether the route requires authentication. RequiresAuth bool } // Service describes how to route traffic to a service. type Service struct { // URL is the URL to proxy traffic to. URL *url.URL } // AuthHandler describes the auth handler. type AuthHandler struct { // Service is the service containing the auth handler. Service ServiceName Name string // name of the auth handler // Parameters that the auth handler expects. // If any one of these are present in the request, // the auth handler is invoked. Query []Param Header []Param Cookie []Param } // Param describes a request parameter. type Param struct { // WireFormat is the name of the parameter on the wire. WireFormat string // CaseSensitive specifies whether or not // the wire format is case sensitive. CaseSensitive bool } // Description describes a gateway. type Description struct { // Routes are the routes to proxy. Routes []*Route // Services defines how to proxy traffic to each service. Services map[ServiceName]Service // Auth describes the authentication handler, if any. Auth *AuthHandler } // New constructs a new gateway. func New(desc *Description) *Gateway { // TODO validate config: // route -> service mapping // requires auth -> auth handler present gw := &Gateway{ RoundTripper: http.DefaultTransport, desc: desc, routeLookup: newRouteLookuper(desc.Routes), } // Create our proxy. gw.proxy = &httputil.ReverseProxy{ Rewrite: gw.handleRequest, Transport: &errorCheckingRoundTripper{gw: gw}, ErrorHandler: gw.errorHandler, } return gw } // Gateway implements a gateway that validates and authenticates incoming requests, // and forwards them to the appropriate service. type Gateway struct { // Rewrite allows for rewriting the request before it is proxied. // If nil it does nothing. Rewrite func(p *httputil.ProxyRequest) // RoundTripper is the http.RoundTripper to use. It defaults to http.DefaultTransport. RoundTripper http.RoundTripper // ExtraRoutes, if non-nil, specifies a router that will be consulted first. // Requests will be proxied to the backend only if the router has no matching path. ExtraRoutes *mux.Router desc *Description // routeLookup is the httprouter for resolving incoming requests // to the routes they match. routeLookup *httprouter.Router // proxy proxies requests to the right backend. proxy *httputil.ReverseProxy } // ServeHTTP implements http.Handler. func (g *Gateway) ServeHTTP(w http.ResponseWriter, req *http.Request) { if g.ExtraRoutes != nil { var match mux.RouteMatch if g.ExtraRoutes.Match(req, &match) { // ExtraRoutes has a matching route. Use it. g.ExtraRoutes.ServeHTTP(w, req) return } } g.proxy.ServeHTTP(w, req) } // handleRequest handles the request. func (g *Gateway) handleRequest(r *httputil.ProxyRequest) { // setErrResp sets an error response, by setting the // sentinel error. setErrResp := func(err error) { out := &http.Request{ // Assumed to be non-nil by httputil. Header: make(http.Header), } ctx := context.WithValue(context.Background(), errCtxKey, err) r.Out = out.WithContext(ctx) } route, ok := g.lookupRoute(r.In) if !ok { setErrResp(errRouteNotFound) return } // TODO perform authentication dest := g.desc.Services[route.Dest] r.SetURL(dest.URL) if g.Rewrite != nil { g.Rewrite(r) } r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] r.SetXForwarded() } // lookupRoute looks up the route the request is for. // If no route is found, it reports (nil, false). func (g *Gateway) lookupRoute(req *http.Request) (route *Route, ok bool) { handle, _, tsr := g.routeLookup.Lookup(req.Method, req.URL.Path) // Handle trailing slash redirects. if tsr { path := req.URL.Path if strings.HasSuffix(path, "/") { path = path[:len(path)-1] } else { path += "/" } handle, _, _ = g.routeLookup.Lookup(req.Method, path) } if handle == nil { // Route not found return nil, false } rw := &sentinelResponseWriter{} handle(rw, nil, httprouter.Params{}) return rw.route, true } func (g *Gateway) errorHandler(w http.ResponseWriter, req *http.Request, err error) { // TODO handle this properly http.Error(w, err.Error(), http.StatusInternalServerError) } var ( errRouteNotFound = errors.New("route not found") ) // sentinelResponseWriter is a sentinel value that implements // http.ResponseWriter as a way to get the Route registered for a given path. type sentinelResponseWriter struct { http.ResponseWriter // always nil; dummy value to implement http.ResponseWriter // route is the selected route (an output parameter). route *Route } // errorCheckingRoundTripper is a http.RoundTripper that // checks for specific error responses in the request context // and returns an error if they are found. type errorCheckingRoundTripper struct { gw *Gateway } func (rt errorCheckingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() if err, ok := ctx.Value(errCtxKey).(error); ok && err != nil { return nil, err } return rt.gw.RoundTripper.RoundTrip(req) } type ctxKey string const errCtxKey ctxKey = "error" func newRouteLookuper(routes []*Route) *httprouter.Router { r := httprouter.New() for _, route := range routes { route := route // for the closure below for _, m := range route.Methods { r.Handle(m, route.Path, func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { w.(*sentinelResponseWriter).route = route }) } } return r } // authInfo represents extracted auth information from a request. type authInfo struct { } // extractAuthInfo extracts the auth information from the request. // If there is no auth information (or the gateway has no auth handler) // it reports nil. func (g *Gateway) extractAuthInfo(req *http.Request) *authInfo { auth := g.desc.Auth if auth == nil { return nil } // TODO check Query, Header, Cookie etc // TODO check legacy bearer token auth return nil } ================================================ FILE: pkg/noopgateway/retry_dialer.go ================================================ package noopgateway import ( "context" "net" "strings" "time" "github.com/cenkalti/backoff/v4" ) // RetryDialer wraps another dialer and adds exponential backoff // in the case of connection refused errors. type RetryDialer struct { net.Dialer MaxBackoff time.Duration } func (d *RetryDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { var conn net.Conn b := backoff.NewExponentialBackOff() b.MaxElapsedTime = d.MaxBackoff if b.MaxElapsedTime == 0 { b.MaxElapsedTime = time.Minute // Set maximum backoff time to 1 minute } operation := func() error { var err error conn, err = d.Dialer.DialContext(ctx, network, address) if err != nil { if strings.Contains(err.Error(), "connection refused") { // Retry if connection is refused return err } // Don't retry if connection isn't refused return backoff.Permanent(err) } return nil } err := backoff.Retry(operation, b) if err != nil { return nil, err } return conn, nil } ================================================ FILE: pkg/noopgwdesc/gateway.go ================================================ package noopgwdesc import ( "fmt" "net/url" "slices" "strings" "encr.dev/pkg/noopgateway" meta "encr.dev/proto/encore/parser/meta/v1" ) // Describe computes a Description based on the given metadata and service discovery configuration. // If serviceDiscovery is nil, the routes will be added but the service discovery setup will be empty. func Describe(md *meta.Data, serviceDiscovery map[noopgateway.ServiceName]string) *noopgateway.Description { desc := &noopgateway.Description{ Services: make(map[noopgateway.ServiceName]noopgateway.Service), } for _, svc := range md.Svcs { svcName := noopgateway.ServiceName(svc.Name) if serviceDiscovery != nil { host, ok := serviceDiscovery[svcName] if !ok { continue } target := &url.URL{ Scheme: "http", Host: host, } desc.Services[svcName] = noopgateway.Service{ URL: target, } } for _, ep := range svc.Rpcs { methods := ep.HttpMethods if slices.Contains(methods, "*") { methods = []string{"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"} } desc.Routes = append(desc.Routes, &noopgateway.Route{ Methods: methods, Dest: svcName, RequiresAuth: ep.AccessType == meta.RPC_AUTH, Path: pathToString(ep.Path), }) } } return desc } func pathToString(path *meta.Path) string { parts := make([]string, 0, len(path.Segments)) paramIdx := 0 for _, seg := range path.Segments { var val string switch seg.Type { case meta.PathSegment_LITERAL: val = seg.Value case meta.PathSegment_PARAM: val = fmt.Sprintf(":p%d", paramIdx) paramIdx++ case meta.PathSegment_WILDCARD, meta.PathSegment_FALLBACK: val = fmt.Sprintf("*p%d", paramIdx) paramIdx++ } parts = append(parts, val) } return "/" + strings.Join(parts, "/") } ================================================ FILE: pkg/option/option.go ================================================ package option import ( "database/sql" "encoding/json" "fmt" "reflect" "strings" "time" "github.com/cockroachdb/errors" "github.com/google/go-cmp/cmp" ) // Option is a type that represents a value that may or may not be present // // It is different a normal value as it can distinguish between a zero value and a missing value // even on pointer types type Option[T any] struct { value T present bool } func (o *Option[T]) MarshalJSON() ([]byte, error) { if !o.present { return []byte("null"), nil } return json.Marshal(o.value) } func (o *Option[T]) UnmarshalJSON(data []byte) error { if string(data) == "null" { o.present = false return nil } o.present = true return json.Unmarshal(data, &o.value) } // CmpOpts returns the options to use to compare options // by checking the unexported fields. For testing purposes. func CmpOpts() []cmp.Option { return []cmp.Option{ cmp.Exporter(func(rt reflect.Type) bool { return rt.PkgPath() == "encr.dev/pkg/option" && strings.HasPrefix(rt.Name(), "Option[") }), } } // AsOptional returns an Option where a zero value T is considered None // and any other value is considered Some // // i.e. // // AsOptional(nil) == None() // AsOptional(0) == None() // AsOptional(false) == None() // AsOptional("") == None() // AsOptional(&MyStruct{}) == Some(&MyStruct{}) // AsOptional(1) == Some(1) // AsOptional(true) == Some(true) func AsOptional[T comparable](v T) Option[T] { var zero T if v == zero { return None[T]() } return Some[T](v) } // FromPointer returns an Option where a nil pointer is considered None // and any other value is considered Some, with the value dereferenced. func FromPointer[T any](v *T) Option[T] { if v == nil { return None[T]() } return Some[T](*v) } // FromErr returns an Option[string] where a nil error is considered None // and any other value is considered Some, with the error message as the value. func FromErr(err error) Option[string] { if err == nil { return None[string]() } return Some(err.Error()) } // Some returns an Option with the given value and present set to true // // This means Some(nil) is a valid present Option // and Some(nil) != None() func Some[T any](v T) Option[T] { return Option[T]{value: v, present: true} } // None returns an Option with no value set func None[T any]() Option[T] { return Option[T]{present: false} } // CommaOk is a helper function to convert a comma ok idiom into an Option. // If ok is true it returns Some(v), otherwise it returns None. func CommaOk[T any](v T, ok bool) Option[T] { if ok { return Some[T](v) } return None[T]() } // Present returns true if the Option has a value set func (o Option[T]) Present() bool { return o.present } // Empty returns true if the Option has no value set func (o Option[T]) Empty() bool { return !o.present } // OrElse returns an Option with the value if present, otherwise returns the alternative value func (o Option[T]) OrElse(alternative T) Option[T] { if o.present { return o } return Some(alternative) } // Get gets the option value and returns ok==true if present. func (o Option[T]) Get() (val T, ok bool) { return o.value, o.present } // GetOrElse returns the value if present, otherwise returns the alternative value func (o Option[T]) GetOrElse(alternative T) T { if o.present { return o.value } return alternative } // GetOrElseF returns the value if present, otherwise returns the alternative value func (o Option[T]) GetOrElseF(alternative func() T) T { if o.present { return o.value } return alternative() } // MustGet returns the value if present, otherwise panics func (o Option[T]) MustGet() (rtn T) { if o.present { return o.value } panic(errors.Newf("Option value is not set: %T", rtn)) } // ForAll calls the given function with the value if present func (o Option[T]) ForAll(f func(v T)) { if o.present { f(o.value) } } // ForEach returns true if the Option is empty or the given predicate returns true on the value func (o Option[T]) ForEach(predicate func(v T) bool) bool { if o.present { return predicate(o.value) } return true } // Contains returns true if the Option is present and the given predicate returns true on the value // otherwise returns false func (o Option[T]) Contains(predicate func(v T) bool) bool { if o.present { return predicate(o.value) } return false } func (o Option[T]) String() string { if o.present { return fmt.Sprintf("%v", o.value) } return "None" } // PtrOrNil returns the value as a pointer, if present, or nil otherwise. func (o Option[T]) PtrOrNil() *T { if o.present { return &o.value } return nil } func ToNullString(o Option[string]) sql.NullString { return sql.NullString{String: o.value, Valid: o.present} } func ToNullBool(o Option[bool]) sql.NullBool { return sql.NullBool{Bool: o.value, Valid: o.present} } func ToNullTime(o Option[time.Time]) sql.NullTime { return sql.NullTime{Time: o.value, Valid: o.present} } ================================================ FILE: pkg/option/pkgfn.go ================================================ package option // Contains returns true if the option is present and matches the given value func Contains[T comparable](option Option[T], matches T) bool { if option.present { return option.value == matches } return false } // Map returns an Option with the value mapped by the given function if present, otherwise returns None func Map[T, R any](option Option[T], f func(T) R) Option[R] { if option.present { return Some(f(option.value)) } return None[R]() } // FlatMap returns an Option with the value mapped by the given function if present, otherwise returns None func FlatMap[T, R any](option Option[T], f func(T) Option[R]) Option[R] { if option.present { return f(option.value) } return None[R]() } // Fold returns the result of f applied to the value if present, otherwise returns the defaultValue func Fold[T, R any](option Option[T], defaultValue R, f func(T) R) R { if option.present { return f(option.value) } return defaultValue } // FoldLeft applies the binary operator f to the value if present, otherwise returns the zero value func FoldLeft[T, R any](option Option[T], zero R, f func(accum R, value T) R) R { if option.present { return f(zero, option.value) } return zero } // Equal returns true if both Options are equal. func Equal[T comparable](a, b Option[T]) bool { if a.present != b.present { return false } if !a.present { return true } return a.value == b.value } ================================================ FILE: pkg/paths/paths.go ================================================ package paths import ( "path" "path/filepath" "strings" "encr.dev/pkg/fns" ) type FileReader func(string) ([]byte, error) // RootedFSPath returns a new FS path. // It should typically not be used except for at parser initialization. // Use FS.Join, FS.New, or FS.Resolve instead to preserve the working dir. func RootedFSPath(wd, p string) FS { if wd == "" { panic("paths: empty wd") } else if !filepath.IsAbs(wd) { panic("paths: wd is relative") } if filepath.IsAbs(p) { return FS(filepath.Clean(p)) } else { return FS(filepath.Join(wd, p)) } } // FS represents a filesystem path. // // It is an absolute path, and is always in the OS-specific format. type FS string // ToIO returns the path for use in IO operations. func (fs FS) ToIO() string { fs.checkValid() return string(fs) } // ToDisplay returns the path in a form suitable for displaying // to the user. func (fs FS) ToDisplay() string { fs.checkValid() return string(fs) } // Resolve returns a new FS path to the given path. // If p is absolute it returns p directly, // otherwise it returns the path joined with the current path. func (fs FS) Resolve(p string) FS { fs.checkValid() if filepath.IsAbs(p) { return FS(filepath.Clean(p)) } return FS(filepath.Join(string(fs), p)) } // Join joins the path with the given elems, according to filepath.Join. func (fs FS) Join(elem ...string) FS { fs.checkValid() parts := append([]string{string(fs)}, elem...) return FS(filepath.Join(parts...)) } func (fs FS) JoinSlash(rel RelSlash) FS { return fs.Join(filepath.FromSlash(rel.ToIO())) } // Base returns the filepath.Base of the path. func (fs FS) Base() string { fs.checkValid() return filepath.Base(string(fs)) } // Dir returns the filepath.Dir of the path. func (fs FS) Dir() FS { fs.checkValid() return FS(filepath.Dir(string(fs))) } // HasPrefix reports whether fs is a descendant of other // or is equal to other. (i.e. it is the given path or a subdirectory of it) func (fs FS) HasPrefix(other FS) bool { fs.checkValid() other.checkValid() // Note: we use filepath.Rel instead of strings.HasPrefix with filepath.Abs // because that wouldn't work on case-insensitive filesystems. rel, err := filepath.Rel(string(other), string(fs)) if err != nil { return false } return filepath.IsLocal(rel) } func (fs FS) checkValid() { if fs == "" { panic("empty FS path") } } // ValidPkgPath reports whether a given module path is valid. func ValidPkgPath(p string) bool { return p != "" } // PkgPath returns a new Pkg path for p. If p is not a valid // package path it reports "", false. func PkgPath(p string) (Pkg, bool) { if !ValidPkgPath(p) { return "", false } return Pkg(p), true } func MustPkgPath(p string) Pkg { if !ValidPkgPath(p) { panic("invalid Package path") } return Pkg(p) } // Pkg represents a package path within a module. // It is always slash-separated. type Pkg string // String returns the string representation of p. func (p Pkg) String() string { return string(p) } // JoinSlash joins the path with the given elems, according to path.Join. // The elems are expected to be slash-separated, not filesystem-separated. // Use filesystem.ToSlash() to convert filesystem paths to slash-separated paths. func (p Pkg) JoinSlash(elem ...RelSlash) Pkg { p.checkValid() strs := make([]string, len(elem)+1) strs[0] = string(p) copy(strs[1:], fns.Map(elem, RelSlash.String)) return Pkg(path.Join(strs...)) // Join cleans the result } func (p Pkg) checkValid() { if p == "" { panic("invalid Pkg path") } } // LexicallyContains reports whether the given package path contains the package path p // as a "sub-package". func (p Pkg) LexicallyContains(other Pkg) bool { p.checkValid() if other == "" { return false } return p == other || strings.HasPrefix(string(other), string(p)+"/") } const stdModule = "std" // Mod represents a module path. // It is always slash-separated. type Mod string // ValidModPath reports whether a given module path is valid. func ValidModPath(p string) bool { return p != "" } // MustModPath returns a new Mod path for p. func MustModPath(p string) Mod { if !ValidModPath(p) { panic("invalid Module path") } return Mod(p) } // StdlibMod returns the Mod path representing the standard library. func StdlibMod() Mod { return stdModule } // LexicallyContains reports whether the given module path contains the package path p. // It only considers the lexical path and ignores whether there exists // a nested module that contains p. func (m Mod) LexicallyContains(p Pkg) bool { m.checkValid() if p == "" { return false } // From the spec: // A module that will never be fetched as a dependency of any other module may use // any valid package path for its module path, but must take care not to collide // with paths that may be used by the module's dependencies or the Go standard // library. The Go standard library uses package paths that do not contain a dot in // the first path element, and the `go` command does not attempt to resolve such // paths from network servers. The paths `example` and `test` are reserved for // users: they will not be used in the standard library and are suitable for use in // self-contained modules, such as those defined in tutorials or example code or // created and manipulated as part of a test. ms, ps := string(m), string(p) if m == stdModule { // Treat any dotless package path as being contained, as long as // it's not one of the reserved paths. if first, _, _ := strings.Cut(ps, "/"); strings.Contains(first, ".") { return false } else if first == "example" || first == "tests" { // Reserved; guaranteed not to be part of std return false } return true } // We can treat the module path as a package path for this purpose. return ms == ps || strings.HasPrefix(ps, ms+"/") } // RelativePathToPkg returns the relative path from the module to the package. // If the package is not contained within the module it reports "", false. func (m Mod) RelativePathToPkg(p Pkg) (relative RelSlash, ok bool) { m.checkValid() p.checkValid() if !m.LexicallyContains(p) { return "", false } // The module path is a prefix of the package path. // Remove the module path and the leading slash. if m == stdModule { return RelSlash(p), true } // Is the package path the same as the module path? if string(p) == string(m) { return ".", true } suffix, ok := strings.CutPrefix(string(p), string(m)+"/") if !ok { return "", false } return RelSlash(suffix), true } func (m Mod) Pkg(rel RelSlash) Pkg { m.checkValid() if m == stdModule { return Pkg(rel) } return Pkg(path.Join(string(m), string(rel))) } func (m Mod) checkValid() { if m == "" { panic("invalid Module path") } } // IsStdlib reports whether m represents the standard library. func (m Mod) IsStdlib() bool { return m == stdModule } // RelSlash is a relative path that is always slash-separated. type RelSlash string // ToIO converts the slash-separated path to a filesystem path // using filepath.FromSlash. func (p RelSlash) ToIO() string { return filepath.FromSlash(string(p)) } // Join joins the path with the given elems, according to path.Join. func (rel RelSlash) Join(elem ...string) RelSlash { parts := append([]string{string(rel)}, elem...) return RelSlash(path.Join(parts...)) } func (p RelSlash) String() string { return string(p) } // MainModuleRelSlash is like RelSlash, but it's always relative to the application's // main module directory. type MainModuleRelSlash string // ToIO converts the slash-separated path to a filesystem path // using filepath.FromSlash. func (p MainModuleRelSlash) ToIO(mainModDir FS) string { return mainModDir.Join(filepath.FromSlash(string(p))).ToIO() } func (p MainModuleRelSlash) String() string { return string(p) } ================================================ FILE: pkg/pgproxy/README.md ================================================ # pgproxy pgproxy is a flexible proxy for the Postgres wire protocol that allows for customizing authentication and backend selection by breaking apart the startup message flow between frontend and backend. Once authenticated, it falls back to being a dumb proxy that simple shuffles bytes back and forth. ================================================ FILE: pkg/pgproxy/pgproxy.go ================================================ package pgproxy import ( "context" "crypto/md5" "crypto/tls" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "net" "sync" "time" "github.com/jackc/pgconn" "github.com/jackc/pgproto3/v2" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/pkg/fns" ) type LogicalConn interface { net.Conn Cancel(*CancelData) error } type HelloData interface { hello() } type StartupData struct { Raw *pgproto3.StartupMessage Database string Username string Password string // may be empty if RequirePassword is false } type CancelData struct { Raw *pgproto3.CancelRequest } func (*StartupData) hello() {} func (*CancelData) hello() {} type SingleBackendProxy struct { Log zerolog.Logger RequirePassword bool FrontendTLS *tls.Config DialBackend func(context.Context, *StartupData) (LogicalConn, error) gotBackend chan struct{} // closed when first connection is received mu sync.Mutex keyData map[pgproto3.BackendKeyData]LogicalConn } type DatabaseNotFoundError struct { Database string } func (e DatabaseNotFoundError) Error() string { return fmt.Sprintf("database %s not found", e.Database) } func (p *SingleBackendProxy) Serve(ctx context.Context, ln net.Listener) error { defer fns.CloseIgnore(ln) if p.gotBackend != nil { panic("SingleBackendProxy: Serve called twice") } p.gotBackend = make(chan struct{}) go func() { ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() select { case <-p.gotBackend: case <-ctx.Done(): _ = ln.Close() } }() var tempDelay time.Duration // how long to sleep on accept failure gotBackend := false for { frontend, err := ln.Accept() if err != nil { if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Printf("pgproxy: accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } return fmt.Errorf("pgproxy: could not accept: %v", err) } if !gotBackend { close(p.gotBackend) gotBackend = true } go p.ProxyConn(ctx, frontend) tempDelay = 0 } } func (p *SingleBackendProxy) ProxyConn(ctx context.Context, client net.Conn) { defer fns.CloseIgnore(client) cl, err := SetupClient(client, &ClientConfig{ TLS: p.FrontendTLS, WantPassword: p.RequirePassword, }) if err != nil { p.Log.Error().Err(err).Msg("unable to setup frontend") return } switch data := cl.Hello.(type) { case *StartupData: if err := p.doRunProxy(ctx, cl); err != nil { p.Log.Error().Err(err).Msg("unable to run backend proxy") } case *CancelData: p.cancelRequest(ctx, data) default: p.Log.Error().Msgf("unknown hello message type: %T", data) } } func (p *SingleBackendProxy) doRunProxy(ctx context.Context, cl *Client) error { startup := cl.Hello.(*StartupData) server, err := p.DialBackend(ctx, startup) if err != nil { _ = cl.Backend.Send(&pgproto3.ErrorResponse{ Severity: "FATAL", Message: err.Error(), }) return err } defer fns.CloseIgnore(server) fe := pgproto3.NewFrontend(pgproto3.NewChunkReader(server), server) log.Trace().Msg("successfully setup server connection") err = AuthenticateClient(cl.Backend) if err != nil { log.Error().Err(err).Msg("unable to authenticate client") return err } log.Trace().Msg("successfully authenticated client") key, err := FinalizeInitialHandshake(cl.Backend, fe) if err != nil { log.Error().Err(err).Msg("unable to finalize handshake") return err } if key != nil { p.mu.Lock() if p.keyData == nil { p.keyData = make(map[pgproto3.BackendKeyData]LogicalConn) } p.keyData[*key] = server p.mu.Unlock() } err = CopySteadyState(cl.Backend, fe) if err != nil { log.Error().Err(err).Msg("unable to copy steady state") return err } return nil } type ServerConfig struct { TLS *tls.Config // nil indicates no TLS Startup *StartupData } // SetupServer sets up a frontend connected to the given server. func SetupServer(server net.Conn, cfg *ServerConfig) (*pgproto3.Frontend, error) { fe, err := serverTLSNegotiate(server, cfg.TLS) if err != nil { return nil, err } raw := cfg.Startup.Raw raw.Parameters["database"] = cfg.Startup.Database raw.Parameters["user"] = cfg.Startup.Username log.Trace().Msg("sending startup message to server") if err := fe.Send(raw); err != nil { return nil, fmt.Errorf("unable to send startup message: %v", err) } // Handle authentication for { msg, err := fe.Receive() if err != nil { return nil, fmt.Errorf("unexpected message from server: %v", err) } switch msg := msg.(type) { case *pgproto3.ErrorResponse: return nil, pgconn.ErrorResponseToPgError(msg) case pgproto3.AuthenticationResponseMessage: if len(cfg.Startup.Password) == 0 { if _, ok := msg.(*pgproto3.AuthenticationOk); !ok { return nil, fmt.Errorf("backend requested authentication but no password given") } } switch msg := msg.(type) { case *pgproto3.AuthenticationOk: // We're done! return fe, nil case *pgproto3.AuthenticationCleartextPassword: err := fe.Send(&pgproto3.PasswordMessage{Password: cfg.Startup.Password}) if err != nil { return nil, err } case *pgproto3.AuthenticationMD5Password: password := computeMD5(cfg.Startup.Username, cfg.Startup.Password, msg.Salt) err := fe.Send(&pgproto3.PasswordMessage{Password: password}) if err != nil { return nil, err } case *pgproto3.AuthenticationSASL: if err := scramAuth(fe, cfg.Startup.Password, msg.AuthMechanisms); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported auth message: %T", msg) } default: return nil, fmt.Errorf("unexpected message type from backend: %T", msg) } } } func serverTLSNegotiate(server net.Conn, tlsConfig *tls.Config) (*pgproto3.Frontend, error) { cr := pgproto3.NewChunkReader(server) frontend := pgproto3.NewFrontend(cr, server) if tlsConfig == nil { return frontend, nil } log.Trace().Msg("negotiating tls with server") if err := frontend.Send(&pgproto3.SSLRequest{}); err != nil { return nil, err } // Read the TLS response. resp, err := cr.Next(1) if err != nil { return nil, err } switch resp[0] { case 'S': log.Trace().Msg("server accepted tls request") tlsConn := tls.Client(server, tlsConfig) if err := tlsConn.Handshake(); err != nil { log.Error().Err(err).Msg("server tls handshake failed") _ = tlsConn.Close() return nil, fmt.Errorf("server tls handshake failed: %v", err) } log.Trace().Msg("completed server tls handshake") // Return a new backend that wraps the tls conn. return pgproto3.NewFrontend(pgproto3.NewChunkReader(tlsConn), tlsConn), nil case 'N': log.Trace().Msg("server rejected tls request") return nil, fmt.Errorf("server rejected tls") case 'E': // ErrorMessage: we've already parsed the first byte so read it manually. hdr, err := cr.Next(4) if err != nil { return nil, err } bodyLen := int(binary.BigEndian.Uint32(hdr)) - 4 msgBody, err := cr.Next(bodyLen) if err != nil { return nil, err } var errMsg pgproto3.ErrorResponse if err := errMsg.Decode(msgBody); err != nil { return nil, err } log.Error().Msgf("server tls negotiation error: %+v", errMsg) return nil, fmt.Errorf("could not negotiate tls with server: error %s: %s", errMsg.Code, errMsg.Message) default: return nil, fmt.Errorf("got unexpected response to tls request: %v", resp[0]) } } type ClientConfig struct { // TLS, if non-nil, indicates we support TLS connections. TLS *tls.Config // WantPassword, if true, indicates we want to capture // the password sent by the frontend. WantPassword bool } type Client struct { Backend *pgproto3.Backend Hello HelloData } // SetupClient sets up a backend connected to the given client. // If tlsConfig is non-nil it negotiates TLS if requested by the client. // // On successful startup the returned message is either *pgproto3.StartupMessage or *pgproto3.CancelRequest. // // It is up to the caller to authenticate the client using AuthenticateClient. func SetupClient(client net.Conn, cfg *ClientConfig) (*Client, error) { log.Trace().Msg("setting up client backend") be, msg, err := clientTLSNegotiate(client, cfg.TLS) if err != nil { return nil, err } if cancel, ok := msg.(*pgproto3.CancelRequest); ok { return &Client{ Backend: be, Hello: &CancelData{Raw: cancel}, }, nil } startup := msg.(*pgproto3.StartupMessage) hello := &StartupData{ Raw: startup, Database: startup.Parameters["database"], Username: startup.Parameters["user"], } if cfg.WantPassword { err := be.Send(&pgproto3.AuthenticationCleartextPassword{}) if err != nil { return nil, err } msg, err := be.Receive() if err != nil { return nil, err } passwd, ok := msg.(*pgproto3.PasswordMessage) if !ok { return nil, fmt.Errorf("expected PasswordMessage, got %T", msg) } hello.Password = passwd.Password } return &Client{ Backend: be, Hello: hello, }, nil } func clientTLSNegotiate(client net.Conn, tlsConfig *tls.Config) (*pgproto3.Backend, pgproto3.FrontendMessage, error) { log.Trace().Msg("negotiating TLS with client") backend := pgproto3.NewBackend(pgproto3.NewChunkReader(client), client) hasTLS := false StartupMessageLoop: for { startup, err := backend.ReceiveStartupMessage() if err != nil { return nil, nil, err } switch startup := startup.(type) { case *pgproto3.SSLRequest: if hasTLS { return nil, nil, fmt.Errorf("received duplicate SSL request") } else if tlsConfig == nil { // We got an SSL request but we don't want to use TLS if _, err := client.Write([]byte{'N'}); err != nil { return nil, nil, err } continue StartupMessageLoop } if _, err := client.Write([]byte{'S'}); err != nil { return nil, nil, err } tlsConn := tls.Server(client, tlsConfig) if err := tlsConn.Handshake(); err != nil { _ = tlsConn.Close() return nil, nil, fmt.Errorf("client tls handshake failed: %v", err) } log.Trace().Msg("client tls handshake successful") // The TLS handshake was successful. // Create a new backend that reads from the now-encrypted TLS connection. hasTLS = true backend = pgproto3.NewBackend(pgproto3.NewChunkReader(tlsConn), tlsConn) case *pgproto3.CancelRequest, *pgproto3.StartupMessage: // Startup complete. log.Debug().Msg("startup completed") return backend, startup, nil case *pgproto3.GSSEncRequest: // We got a GSSAPI encryption request but we don't support it. if _, err := client.Write([]byte{'N'}); err != nil { return nil, nil, err } continue StartupMessageLoop } } } type AuthData struct { Username string Password string } // AuthenticateClient tells the client they've successfully authenticated. func AuthenticateClient(be *pgproto3.Backend) error { _ = be.SetAuthType(pgproto3.AuthTypeOk) return be.Send(&pgproto3.AuthenticationOk{}) } func computeMD5(username, password string, salt [4]byte) string { // concat('md5', md5(concat(md5(concat(password, username)), random-salt))) // s1 := md5(concat(password, username)) // nosemgrep s1 := md5.Sum([]byte(password + username)) // s2 := md5(concat(s1, random-salt)) // nosemgrep s2 := md5.Sum([]byte(hex.EncodeToString(s1[:]) + string(salt[:]))) return "md5" + hex.EncodeToString(s2[:]) } func SendCancelRequest(conn io.ReadWriter, req *pgproto3.CancelRequest) error { buf := make([]byte, 16) binary.BigEndian.PutUint32(buf[0:4], 16) binary.BigEndian.PutUint32(buf[4:8], 80877102) binary.BigEndian.PutUint32(buf[8:12], uint32(req.ProcessID)) binary.BigEndian.PutUint32(buf[12:16], uint32(req.SecretKey)) _, err := conn.Write(buf) if err != nil { return err } _, err = conn.Read(buf) if err != io.EOF { return err } return nil } // FinalizeInitialHandshake completes the handshake between client and server, // snooping the BackendKeyData from the server if sent. // It is nil if the server did not send any backend key data. func FinalizeInitialHandshake(client *pgproto3.Backend, server *pgproto3.Frontend) (*pgproto3.BackendKeyData, error) { var keyData *pgproto3.BackendKeyData // Read messages from backend until we get ReadyForQuery for { msg, err := server.Receive() if err != nil { return nil, fmt.Errorf("pgproxy: cannot read from backend: %v", err) } else if err := client.Send(msg); err != nil { return nil, fmt.Errorf("pgproxy: could not write to frontend: %v", err) } switch msg := msg.(type) { case *pgproto3.BackendKeyData: // Make a copy; this object is only valid until the next call to Receive() copy := *msg keyData = © case *pgproto3.ReadyForQuery: // Handshake completed return keyData, nil } } } // CopySteadyState copies messages back and forth after the initial handshake. func CopySteadyState(client *pgproto3.Backend, server *pgproto3.Frontend) error { errChan := make(chan error, 2) msgAck := make(chan struct{}) done := make(chan struct{}) defer close(done) runTask := func(task func() error) { go func() { errChan <- task() }() } clientMsgs := make(chan pgproto3.FrontendMessage) runTask(func() (err error) { for { msg, err := server.Receive() if errors.Is(err, io.ErrUnexpectedEOF) { log.Trace().Msg("pgproxy: connection terminated by server, closing connection") return nil } else if err != nil { return err } if err := client.Send(msg); err != nil { return err } select { case <-done: return nil default: } } }) runTask(func() error { for { msg, err := client.Receive() if err != nil { return err } select { case <-done: return nil case clientMsgs <- msg: } select { case <-done: return nil case <-msgAck: } } }) // Copy from client to server. for { select { case err := <-errChan: return err case msg := <-clientMsgs: err := server.Send(msg) if err != nil { return err } if _, ok := msg.(*pgproto3.Terminate); ok { log.Trace().Msg("received terminate from client, closing connection") return nil } msgAck <- struct{}{} } } } func (p *SingleBackendProxy) cancelRequest(ctx context.Context, cancel *CancelData) { p.Log.Trace().Msg("received cancel request") key := pgproto3.BackendKeyData{ ProcessID: cancel.Raw.ProcessID, SecretKey: cancel.Raw.SecretKey, } p.mu.Lock() conn, ok := p.keyData[key] p.mu.Unlock() if ok { if err := conn.Cancel(cancel); err != nil { p.Log.Error().Err(err).Msg("unable to send cancel request") } } else { p.Log.Error().Msg("could not find backend key data") } } ================================================ FILE: pkg/pgproxy/scram.go ================================================ package pgproxy import ( "bytes" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "strconv" "github.com/jackc/pgproto3/v2" "golang.org/x/crypto/pbkdf2" "golang.org/x/text/secure/precis" ) // The below code is taken from github.com/jackc/pgconn (auth_scram.go). /* Copyright (c) 2019-2021 Jack Christensen MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ const clientNonceLen = 18 func scramAuth(fe *pgproto3.Frontend, password string, serverAuthMechanisms []string) error { sc, err := newScramClient(serverAuthMechanisms, password) if err != nil { return err } // Send client-first-message in a SASLInitialResponse saslInitialResponse := &pgproto3.SASLInitialResponse{ AuthMechanism: "SCRAM-SHA-256", Data: sc.clientFirstMessage(), } if err := fe.Send(saslInitialResponse); err != nil { return err } // Receive server-first-message payload in a AuthenticationSASLContinue. msg, err := fe.Receive() if err != nil { return err } saslContinue, ok := msg.(*pgproto3.AuthenticationSASLContinue) if !ok { return fmt.Errorf("expected AuthenticationSASLContinue message, got %T", msg) } err = sc.recvServerFirstMessage(saslContinue.Data) if err != nil { return err } // Send client-final-message in a SASLResponse saslResponse := &pgproto3.SASLResponse{ Data: []byte(sc.clientFinalMessage()), } if err := fe.Send(saslResponse); err != nil { return err } // Receive server-final-message payload in a AuthenticationSASLFinal. msg, err = fe.Receive() if err != nil { return err } saslFinal, ok := msg.(*pgproto3.AuthenticationSASLFinal) if !ok { return fmt.Errorf("expected AuthenticationSASLFinal message, got %T", msg) } return sc.recvServerFinalMessage(saslFinal.Data) } type scramClient struct { serverAuthMechanisms []string password []byte clientNonce []byte clientFirstMessageBare []byte serverFirstMessage []byte clientAndServerNonce []byte salt []byte iterations int saltedPassword []byte authMessage []byte } func newScramClient(serverAuthMechanisms []string, password string) (*scramClient, error) { sc := &scramClient{ serverAuthMechanisms: serverAuthMechanisms, } // Ensure server supports SCRAM-SHA-256 hasScramSHA256 := false for _, mech := range sc.serverAuthMechanisms { if mech == "SCRAM-SHA-256" { hasScramSHA256 = true break } } if !hasScramSHA256 { return nil, errors.New("server does not support SCRAM-SHA-256") } // precis.OpaqueString is equivalent to SASLprep for password. var err error sc.password, err = precis.OpaqueString.Bytes([]byte(password)) if err != nil { // PostgreSQL allows passwords invalid according to SCRAM / SASLprep. sc.password = []byte(password) } buf := make([]byte, clientNonceLen) _, err = rand.Read(buf) if err != nil { return nil, err } sc.clientNonce = make([]byte, base64.RawStdEncoding.EncodedLen(len(buf))) base64.RawStdEncoding.Encode(sc.clientNonce, buf) return sc, nil } func (sc *scramClient) clientFirstMessage() []byte { sc.clientFirstMessageBare = []byte(fmt.Sprintf("n=,r=%s", sc.clientNonce)) return []byte(fmt.Sprintf("n,,%s", sc.clientFirstMessageBare)) } func (sc *scramClient) recvServerFirstMessage(serverFirstMessage []byte) error { sc.serverFirstMessage = serverFirstMessage buf := serverFirstMessage if !bytes.HasPrefix(buf, []byte("r=")) { return errors.New("invalid SCRAM server-first-message received from server: did not include r=") } buf = buf[2:] idx := bytes.IndexByte(buf, ',') if idx == -1 { return errors.New("invalid SCRAM server-first-message received from server: did not include s=") } sc.clientAndServerNonce = buf[:idx] buf = buf[idx+1:] if !bytes.HasPrefix(buf, []byte("s=")) { return errors.New("invalid SCRAM server-first-message received from server: did not include s=") } buf = buf[2:] idx = bytes.IndexByte(buf, ',') if idx == -1 { return errors.New("invalid SCRAM server-first-message received from server: did not include i=") } saltStr := buf[:idx] buf = buf[idx+1:] if !bytes.HasPrefix(buf, []byte("i=")) { return errors.New("invalid SCRAM server-first-message received from server: did not include i=") } buf = buf[2:] iterationsStr := buf var err error sc.salt, err = base64.StdEncoding.DecodeString(string(saltStr)) if err != nil { return fmt.Errorf("invalid SCRAM salt received from server: %w", err) } sc.iterations, err = strconv.Atoi(string(iterationsStr)) if err != nil || sc.iterations <= 0 { return fmt.Errorf("invalid SCRAM iteration count received from server: %w", err) } if !bytes.HasPrefix(sc.clientAndServerNonce, sc.clientNonce) { return errors.New("invalid SCRAM nonce: did not start with client nonce") } if len(sc.clientAndServerNonce) <= len(sc.clientNonce) { return errors.New("invalid SCRAM nonce: did not include server nonce") } return nil } func (sc *scramClient) clientFinalMessage() string { clientFinalMessageWithoutProof := []byte(fmt.Sprintf("c=biws,r=%s", sc.clientAndServerNonce)) sc.saltedPassword = pbkdf2.Key([]byte(sc.password), sc.salt, sc.iterations, 32, sha256.New) sc.authMessage = bytes.Join([][]byte{sc.clientFirstMessageBare, sc.serverFirstMessage, clientFinalMessageWithoutProof}, []byte(",")) clientProof := computeClientProof(sc.saltedPassword, sc.authMessage) return fmt.Sprintf("%s,p=%s", clientFinalMessageWithoutProof, clientProof) } func (sc *scramClient) recvServerFinalMessage(serverFinalMessage []byte) error { if !bytes.HasPrefix(serverFinalMessage, []byte("v=")) { return errors.New("invalid SCRAM server-final-message received from server") } serverSignature := serverFinalMessage[2:] if !hmac.Equal(serverSignature, computeServerSignature(sc.saltedPassword, sc.authMessage)) { return errors.New("invalid SCRAM ServerSignature received from server") } return nil } func computeHMAC(key, msg []byte) []byte { mac := hmac.New(sha256.New, key) mac.Write(msg) return mac.Sum(nil) } func computeClientProof(saltedPassword, authMessage []byte) []byte { clientKey := computeHMAC(saltedPassword, []byte("Client Key")) storedKey := sha256.Sum256(clientKey) clientSignature := computeHMAC(storedKey[:], authMessage) clientProof := make([]byte, len(clientSignature)) for i := 0; i < len(clientSignature); i++ { clientProof[i] = clientKey[i] ^ clientSignature[i] } buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof))) base64.StdEncoding.Encode(buf, clientProof) return buf } func computeServerSignature(saltedPassword []byte, authMessage []byte) []byte { serverKey := computeHMAC(saltedPassword, []byte("Server Key")) serverSignature := computeHMAC(serverKey, authMessage) buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature))) base64.StdEncoding.Encode(buf, serverSignature) return buf } ================================================ FILE: pkg/promise/prom.go ================================================ package promise import ( "context" "sync" ) type Value[T any] struct { val T err error done chan struct{} onResolve eventList[T] onReject eventList[error] } func (v *Value[T]) Get(ctx context.Context) (T, error) { select { case <-ctx.Done(): var zero T return zero, ctx.Err() case <-v.done: return v.val, v.err } } func (v *Value[T]) OnResolve(fn func(T)) { v.onResolve.Add(fn) } func (v *Value[T]) OnReject(fn func(error)) { v.onReject.Add(fn) } func New[T any](fn func() (T, error)) *Value[T] { val := &Value[T]{ done: make(chan struct{}), } go func() { func() { // If we panic, we want to mark the promise as done defer close(val.done) val.val, val.err = fn() }() if val.err == nil { val.onResolve.MarkDoneAndProcess(val.val) } else { val.onReject.MarkDoneAndProcess(val.err) } }() return val } func Resolved[T any](val T) *Value[T] { done := make(chan struct{}) close(done) v := &Value[T]{ done: done, val: val, } v.onResolve.MarkDoneAndProcess(val) return v } func Rejected[T any](err error) *Value[T] { done := make(chan struct{}) close(done) v := &Value[T]{ done: done, err: err, } v.onReject.MarkDoneAndProcess(err) return v } type eventList[V any] struct { mu sync.Mutex done bool val V funcs []func(V) } func (g *eventList[V]) Add(fn func(V)) { g.mu.Lock() if g.done { g.mu.Unlock() fn(g.val) } else { g.funcs = append(g.funcs, fn) g.mu.Unlock() } } func (g *eventList[V]) MarkDoneAndProcess(val V) { g.mu.Lock() g.done = true g.val = val g.mu.Unlock() for _, fn := range g.funcs { fn(val) } } func Wait2[A, B any](ctx context.Context, a *Value[A], b *Value[B]) (A, B, error) { aVal, err1 := a.Get(ctx) bVal, err2 := b.Get(ctx) err := err1 if err == nil { err = err2 } return aVal, bVal, err } func Wait3[A, B, C any](ctx context.Context, a *Value[A], b *Value[B], c *Value[C]) (A, B, C, error) { aVal, err1 := a.Get(ctx) bVal, err2 := b.Get(ctx) cVal, err3 := c.Get(ctx) err := err1 if err == nil { err = err2 } if err == nil { err = err3 } return aVal, bVal, cVal, err } ================================================ FILE: pkg/rtconfgen/base_builder.go ================================================ package rtconfgen import ( "cmp" "fmt" "slices" "time" "github.com/cockroachdb/errors" "github.com/golang/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "encr.dev/pkg/fns" "encr.dev/pkg/option" meta "encr.dev/proto/encore/parser/meta/v1" runtimev1 "encr.dev/proto/encore/runtime/v1" ) type ResourceID interface { comparable fmt.Stringer } type Builder struct { Infra *InfraBuilder rs *resourceSet env *runtimev1.Environment encore *runtimev1.EncorePlatform obs *runtimev1.Observability // Any errors encountered during the build process. err error // authMethods are the service auth methods to use for authenticating to this deployment. authMethods []*runtimev1.ServiceAuth // defaultGracefulShutdown is the graceful shutdown behavior to use by default, // unless a deployment overrides it. defaultGracefulShutdown *runtimev1.GracefulShutdown defaultDeployID string defaultDeployedAt time.Time deployments map[string]*Deployment services map[string]*runtimev1.HostedService } func NewBuilder() *Builder { rs := new(resourceSet) b := &Builder{ Infra: newInfraBuilder(rs), rs: rs, obs: &runtimev1.Observability{}, deployments: make(map[string]*Deployment), services: make(map[string]*runtimev1.HostedService), } return b } func (b *Builder) Env(env *runtimev1.Environment) *Builder { b.env = env return b } func (b *Builder) EncorePlatform(encore *runtimev1.EncorePlatform) *Builder { b.encore = encore return b } func (b *Builder) DefaultGracefulShutdown(s *runtimev1.GracefulShutdown) *Builder { b.defaultGracefulShutdown = s return b } func (b *Builder) AuthMethods(m []*runtimev1.ServiceAuth) *Builder { b.authMethods = m return b } func (b *Builder) DeployID(id string) *Builder { b.defaultDeployID = id return b } func (b *Builder) DeployedAt(t time.Time) *Builder { b.defaultDeployedAt = t return b } func (b *Builder) TracingProvider(p *runtimev1.TracingProvider) { b.TracingProviderFn(p.Rid, tofn(p)) } func (b *Builder) TracingProviderFn(rid string, fn func() *runtimev1.TracingProvider) { addResFunc(&b.obs.Tracing, b.rs, rid, fn) } func (b *Builder) MetricsProvider(p *runtimev1.MetricsProvider) { b.MetricsProviderFn(p.Rid, tofn(p)) } func (b *Builder) MetricsProviderFn(rid string, fn func() *runtimev1.MetricsProvider) { addResFunc(&b.obs.Metrics, b.rs, rid, fn) } func (b *Builder) LogsProvider(p *runtimev1.LogsProvider) { b.LogsProviderFn(p.Rid, tofn(p)) } func (b *Builder) LogsProviderFn(rid string, fn func() *runtimev1.LogsProvider) { addResFunc(&b.obs.Logs, b.rs, rid, fn) } func (b *Builder) ServiceConfig(svc *runtimev1.HostedService) { b.services[svc.Name] = svc } func (b *Builder) Deployment(rid string) *Deployment { if d, ok := b.deployments[rid]; ok { return d } d := &Deployment{b: b, rid: rid} b.deployments[rid] = d return d } type Deployment struct { b *Builder rid string deployID option.Option[string] deployedAt option.Option[time.Time] reduceWith option.Option[*meta.Data] dynamicExperiments []string // The graceful shutdown behavior to use for this deployment. // If None, uses the default graceful shutdown behavior. gracefulShutdown option.Option[*runtimev1.GracefulShutdown] // The service-discovery configuration for this deployment. sd *runtimev1.ServiceDiscovery // The base URL for reaching this deployment from another service. svc2svcBaseURL string hostedGateways []string hostedServiceNames []string } // DeployID sets the deploy id. func (d *Deployment) DeployID(id string) *Deployment { d.deployID = option.Some(id) return d } // OverrideDeployedAt sets the time of deploy. func (d *Deployment) OverrideDeployedAt(t time.Time) *Deployment { d.deployedAt = option.Some(t) return d } func (d *Deployment) DynamicExperiments(experiments []string) *Deployment { d.dynamicExperiments = experiments return d } // HostsServices adds the given service names as being hosted by this deployment. // It appends and doesn't overwrite any existing hosted services. func (d *Deployment) HostsServices(names ...string) *Deployment { d.hostedServiceNames = append(d.hostedServiceNames, names...) return d } // HostsGateways adds the given gateway names as being hosted by this deployment. // It appends and doesn't overwrite any existing hosted gateways. func (d *Deployment) HostsGateways(names ...string) *Deployment { d.hostedGateways = append(d.hostedGateways, names...) return d } // OverrideGracefulShutdown sets the graceful shutdown behavior for this specific deployment. // To set a default graceful shutdown shared for all deployments, use [Builder.DefaultGracefulShutdown] instead. func (d *Deployment) OverrideGracefulShutdown(s *runtimev1.GracefulShutdown) *Deployment { d.gracefulShutdown = option.Some(s) return d } func (d *Deployment) ServiceDiscovery(sd *runtimev1.ServiceDiscovery) *Deployment { d.sd = sd return d } func (d *Deployment) ReduceWithMeta(md *meta.Data) *Deployment { d.reduceWith = option.Some(md) return d } func (d *Deployment) BuildRuntimeConfig() (*runtimev1.RuntimeConfig, error) { b := d.b infra, err := b.Infra.get() if err != nil { return nil, err } if reduced, ok := d.reduceWith.Get(); ok { infra = reduceForServices(infra, reduced, d.hostedServiceNames) } graceful := d.gracefulShutdown.GetOrElse(d.b.defaultGracefulShutdown) var hostedServices []*runtimev1.HostedService { for _, svcName := range d.hostedServiceNames { // If we have a service config defined for this service, use it. cfg := b.services[svcName] if cfg == nil { cfg = &runtimev1.HostedService{ Name: svcName, } } hostedServices = append(hostedServices, cfg) } } slices.SortFunc(hostedServices, func(a, b *runtimev1.HostedService) int { return cmp.Compare(a.Name, b.Name) }) // Build metrics for this deployment var metrics []*runtimev1.Metric if reduced, ok := d.reduceWith.Get(); ok { // Build a map: metric name -> list of services that use it metricToServices := make(map[string][]string) for _, svc := range reduced.Svcs { // Only include metrics from services hosted by this deployment if !slices.Contains(d.hostedServiceNames, svc.Name) { continue } for _, metricName := range svc.Metrics { metricToServices[metricName] = append(metricToServices[metricName], svc.Name) } } // Convert to Metric protobuf messages for metricName, services := range metricToServices { // Sort services for consistency slices.Sort(services) metrics = append(metrics, &runtimev1.Metric{ EncoreName: metricName, Services: services, }) } } // Sort metrics by name for consistency slices.SortFunc(metrics, func(a, b *runtimev1.Metric) int { return cmp.Compare(a.EncoreName, b.EncoreName) }) gatewaysByName := make(map[string]*runtimev1.Gateway) for _, gw := range infra.Resources.Gateways { gatewaysByName[gw.EncoreName] = gw } gatewayRids := fns.Map(d.hostedGateways, func(name string) string { gw, ok := gatewaysByName[name] if !ok { b.setErrf("gateway %q not found", name) return "" } return gw.Rid }) deploy := &runtimev1.Deployment{ HostedGateways: gatewayRids, HostedServices: hostedServices, ServiceDiscovery: d.sd, GracefulShutdown: graceful, DynamicExperiments: d.dynamicExperiments, Observability: b.obs, AuthMethods: d.b.authMethods, DeployId: d.deployID.GetOrElse(b.defaultDeployID), DeployedAt: timestamppb.New(d.deployedAt.GetOrElse(b.defaultDeployedAt)), Metrics: metrics, } cfg := &runtimev1.RuntimeConfig{ Environment: b.env, EncorePlatform: b.encore, Infra: infra, Deployment: deploy, } // Deep-clone the protobuf to avoid subsequent mutations from modifying this one. cfg = cloneProto(cfg) return cfg, b.err } func (b *Builder) setErr(err error) { if b.err == nil { b.err = err } } func (b *Builder) setErrf(format string, args ...any) { b.setErr(errors.Newf(format, args...)) } func cloneProto[M proto.Message](m M) M { return proto.Clone(m).(M) } ================================================ FILE: pkg/rtconfgen/convert.go ================================================ package rtconfgen import ( "encoding/base64" "encoding/json" "reflect" "slices" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "go.encore.dev/platform-sdk/pkg/auth" runtimev1 "encr.dev/proto/encore/runtime/v1" encore "encore.dev" "encore.dev/appruntime/exported/config" "encr.dev/pkg/fns" ) func ToLegacy(conf *runtimev1.RuntimeConfig, secretEnvs map[string][]byte) (*config.Runtime, error) { conv := &legacyConverter{in: conf, secretEnvs: secretEnvs} return conv.Convert() } type legacyConverter struct { in *runtimev1.RuntimeConfig secretEnvs map[string][]byte err error } func findRID[T interface{ GetRid() string }](rid string, list []T) (T, bool) { return fns.Find(list, func(item T) bool { return item.GetRid() == rid }) } func (c *legacyConverter) Convert() (*config.Runtime, error) { cfg := &config.Runtime{ AppID: c.in.Environment.AppId, AppSlug: c.in.Environment.AppSlug, EnvID: c.in.Environment.EnvId, EnvName: c.in.Environment.EnvName, EnvType: string(c.envType()), EnvCloud: c.envCloud(), DeployID: c.in.Deployment.DeployId, DeployedAt: c.in.Deployment.DeployedAt.AsTime(), ServiceDiscovery: nil, ServiceAuth: nil, ShutdownTimeout: 0, GracefulShutdown: nil, DynamicExperiments: nil, Gateways: []config.Gateway{}, PubsubTopics: make(map[string]*config.PubsubTopic), Buckets: make(map[string]*config.Bucket), CORS: &config.CORS{}, } // Deployment handling. { deployment := c.in.Deployment cfg.HostedServices = fns.Map(deployment.HostedServices, func(s *runtimev1.HostedService) string { return s.Name }) cfg.ServiceAuth = fns.Map(deployment.AuthMethods, func(sa *runtimev1.ServiceAuth) config.ServiceAuth { switch sa.AuthMethod.(type) { case *runtimev1.ServiceAuth_EncoreAuth_: return config.ServiceAuth{Method: "encore-auth"} } return config.ServiceAuth{Method: "noop"} }) cfg.ServiceDiscovery = make(map[string]config.Service) for key, value := range deployment.ServiceDiscovery.Services { method := config.ServiceAuth{Method: "noop"} if len(value.AuthMethods) > 0 { if _, ok := value.AuthMethods[0].AuthMethod.(*runtimev1.ServiceAuth_EncoreAuth_); ok { method.Method = "encore-auth" } } cfg.ServiceDiscovery[key] = config.Service{ Name: key, URL: value.BaseUrl, Protocol: config.Http, ServiceAuth: method, } } if deployment.GracefulShutdown != nil { cfg.GracefulShutdown = &config.GracefulShutdownTimings{ Total: ptr(deployment.GracefulShutdown.Total.AsDuration()), ShutdownHooks: ptr(deployment.GracefulShutdown.ShutdownHooks.AsDuration()), Handlers: ptr(deployment.GracefulShutdown.Handlers.AsDuration()), } cfg.ShutdownTimeout = deployment.GracefulShutdown.Total.AsDuration() } cfg.DynamicExperiments = deployment.DynamicExperiments // Set the API Base URL if we have a gateway. if len(c.in.Infra.Resources.Gateways) > 0 { cfg.APIBaseURL = c.in.Infra.Resources.Gateways[0].BaseUrl } for _, gwRID := range deployment.HostedGateways { idx := slices.IndexFunc(c.in.Infra.Resources.Gateways, func(gw *runtimev1.Gateway) bool { return gw.Rid == gwRID }) if idx >= 0 { gw := c.in.Infra.Resources.Gateways[idx] if gw.Cors != nil { var allowOriginsWithCredentials []string switch data := gw.Cors.AllowedOriginsWithCredentials.(type) { case *runtimev1.Gateway_CORS_AllowedOrigins: allowOriginsWithCredentials = data.AllowedOrigins.AllowedOrigins case *runtimev1.Gateway_CORS_UnsafeAllowAllOriginsWithCredentials: if data.UnsafeAllowAllOriginsWithCredentials { allowOriginsWithCredentials = []string{config.UnsafeAllOriginWithCredentials} } } cfg.CORS = &config.CORS{ Debug: gw.Cors.Debug, DisableCredentials: gw.Cors.DisableCredentials, AllowOriginsWithCredentials: allowOriginsWithCredentials, AllowOriginsWithoutCredentials: gw.Cors.AllowedOriginsWithoutCredentials.AllowedOrigins, ExtraAllowedHeaders: gw.Cors.ExtraAllowedHeaders, ExtraExposedHeaders: gw.Cors.ExtraExposedHeaders, AllowPrivateNetworkAccess: gw.Cors.AllowPrivateNetworkAccess, } } cfg.Gateways = append(cfg.Gateways, config.Gateway{ Name: gw.EncoreName, Host: gw.Hostnames[0], }) } } // Use the most verbose logging requested. currLevel := zerolog.PanicLevel foundLevel := false for _, svc := range deployment.HostedServices { if svc.LogConfig != nil { if level, err := zerolog.ParseLevel(*svc.LogConfig); err == nil && level < currLevel { currLevel = level foundLevel = true } } } if !foundLevel { currLevel = zerolog.TraceLevel } cfg.LogConfig = currLevel.String() } // Infrastructure handling. { res := c.in.Infra.Resources getClientCert := func(certKeyRID *string) (clientCertPEM, clientKeyPem string) { if certKeyRID == nil { return "", "" } cert, ok := findRID(*certKeyRID, c.in.Infra.Credentials.ClientCerts) if !ok { return "", "" } return cert.GetCert(), c.secretString(cert.GetKey()) } // SQL Servers & Databases { for _, cluster := range res.SqlClusters { primary, ok := fns.Find(cluster.Servers, func(s *runtimev1.SQLServer) bool { return s.Kind == runtimev1.ServerKind_SERVER_KIND_PRIMARY }) if !ok { c.setErrf("unable to find primary server for SQL cluster %q", cluster.Rid) continue } for _, db := range cluster.Databases { // Find the read-write connection pool. pool, ok := fns.Find(db.ConnPools, func(pool *runtimev1.SQLConnectionPool) bool { return !pool.IsReadonly }) if !ok { // Use the first pool if none were read-write pool = db.ConnPools[0] } role, ok := findRID(pool.RoleRid, c.in.Infra.Credentials.SqlRoles) if !ok { c.setErrf("unable to find sql role %q", pool.RoleRid) continue } clientCert, clientKey := getClientCert(role.ClientCertRid) candidateServer := &config.SQLServer{ Host: primary.Host, ClientCert: clientCert, ClientKey: clientKey, } if primary.TlsConfig != nil { candidateServer.ServerCACert = primary.TlsConfig.GetServerCaCert() } serverIdx := slices.IndexFunc(cfg.SQLServers, func(s *config.SQLServer) bool { return s.Host == candidateServer.Host && s.ServerCACert == candidateServer.ServerCACert && s.ClientCert == candidateServer.ClientCert && s.ClientKey == candidateServer.ClientKey }) if serverIdx == -1 { serverIdx = len(cfg.SQLServers) cfg.SQLServers = append(cfg.SQLServers, candidateServer) } cfg.SQLDatabases = append(cfg.SQLDatabases, &config.SQLDatabase{ ServerID: serverIdx, EncoreName: db.EncoreName, DatabaseName: db.CloudName, User: role.Username, Password: c.secretString(role.Password), MinConnections: int(pool.MinConnections), MaxConnections: int(pool.MaxConnections), }) } } } // Redis Servers & Databases { for _, cluster := range res.RedisClusters { if cluster.InMemory { idx := len(cfg.RedisServers) cfg.RedisServers = append(cfg.RedisServers, &config.RedisServer{ InMemory: true, }) for i, db := range cluster.Databases { cfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{ ServerID: idx, EncoreName: db.EncoreName, Database: i, MinConnections: 0, MaxConnections: 0, KeyPrefix: "", }) } continue } primary, ok := fns.Find(cluster.Servers, func(s *runtimev1.RedisServer) bool { return s.Kind == runtimev1.ServerKind_SERVER_KIND_PRIMARY }) if !ok { c.setErrf("unable to find primary server for Redis cluster %q", cluster.Rid) continue } for _, db := range cluster.Databases { // Find the read-write connection pool. pool, ok := fns.Find(db.ConnPools, func(pool *runtimev1.RedisConnectionPool) bool { return !pool.IsReadonly }) if !ok { // Use the first pool if none were read-write pool = db.ConnPools[0] } role, ok := findRID(pool.RoleRid, c.in.Infra.Credentials.RedisRoles) if !ok { c.setErrf("unable to find Redis role %q", pool.RoleRid) continue } user, password := func() (string, string) { switch s := role.Auth.(type) { case nil: return "", "" // no authentication case *runtimev1.RedisRole_Acl: return s.Acl.Username, c.secretString(s.Acl.Password) case *runtimev1.RedisRole_AuthString: return "", c.secretString(s.AuthString) default: c.setErrf("unknown redis auth type %T", s) return "", "" } }() clientCert, clientKey := getClientCert(role.ClientCertRid) candidateServer := &config.RedisServer{ Host: primary.Host, ClientCert: clientCert, ClientKey: clientKey, User: user, Password: password, } if primary.TlsConfig != nil { candidateServer.EnableTLS = true candidateServer.ServerCACert = primary.TlsConfig.GetServerCaCert() } serverIdx := slices.IndexFunc(cfg.RedisServers, func(s *config.RedisServer) bool { return reflect.DeepEqual(s, candidateServer) }) if serverIdx == -1 { serverIdx = len(cfg.RedisServers) cfg.RedisServers = append(cfg.RedisServers, candidateServer) } cfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{ ServerID: serverIdx, EncoreName: db.EncoreName, Database: int(db.DatabaseIdx), MinConnections: int(pool.MinConnections), MaxConnections: int(pool.MaxConnections), KeyPrefix: nilPtrToZero(db.KeyPrefix), }) } } } // Pubsub Providers & Topics { for _, cluster := range c.in.Infra.Resources.PubsubClusters { p := &config.PubsubProvider{} switch prov := cluster.Provider.(type) { case *runtimev1.PubSubCluster_Encore: p.EncoreCloud = &config.EncoreCloudPubsubProvider{} case *runtimev1.PubSubCluster_Aws: p.AWS = &config.AWSPubsubProvider{} case *runtimev1.PubSubCluster_Gcp: p.GCP = &config.GCPPubsubProvider{} case *runtimev1.PubSubCluster_Nsq: p.NSQ = &config.NSQProvider{Host: prov.Nsq.Hosts[0]} case *runtimev1.PubSubCluster_Azure: p.Azure = &config.AzureServiceBusProvider{Namespace: prov.Azure.Namespace} default: c.setErrf("unknown pubsub provider type %T", prov) continue } providerID := len(cfg.PubsubProviders) cfg.PubsubProviders = append(cfg.PubsubProviders, p) for _, top := range cluster.Topics { cfg.PubsubTopics[top.EncoreName] = &config.PubsubTopic{ EncoreName: top.EncoreName, ProviderID: providerID, ProviderName: top.CloudName, Limiter: nil, // TODO? Subscriptions: make(map[string]*config.PubsubSubscription), GCP: func() *config.PubsubTopicGCPData { switch pc := top.ProviderConfig.(type) { case *runtimev1.PubSubTopic_GcpConfig: return &config.PubsubTopicGCPData{ ProjectID: pc.GcpConfig.ProjectId, } } return nil }(), } } for _, sub := range cluster.Subscriptions { topic := cfg.PubsubTopics[sub.TopicEncoreName] if topic == nil { // In the new config we could end up with a subscription where the // corresponding topic wasn't included, as we only include what's needed. // That doesn't work with the legacy metadata, so add it here in that case. topic = &config.PubsubTopic{ EncoreName: sub.TopicEncoreName, ProviderID: providerID, ProviderName: sub.TopicCloudName, Limiter: nil, Subscriptions: make(map[string]*config.PubsubSubscription), GCP: func() *config.PubsubTopicGCPData { // HACK: this synthesizes a topic config based on the subscription's config. // That's not correct in the general case, but we only get here // if the service doesn't have access to the topic in the first place, // so this should be fine and prevents the runtime from exploding since // it doesn't expect to get a nil topic config. switch sub.ProviderConfig.(type) { case *runtimev1.PubSubSubscription_GcpConfig: return &config.PubsubTopicGCPData{ ProjectID: "", } } return nil }(), } cfg.PubsubTopics[sub.TopicEncoreName] = topic } topic.Subscriptions[sub.SubscriptionEncoreName] = &config.PubsubSubscription{ ID: sub.Rid, EncoreName: sub.SubscriptionEncoreName, ProviderName: sub.SubscriptionCloudName, PushOnly: sub.PushOnly, GCP: func() *config.PubsubSubscriptionGCPData { switch pc := sub.ProviderConfig.(type) { case *runtimev1.PubSubSubscription_GcpConfig: return &config.PubsubSubscriptionGCPData{ ProjectID: pc.GcpConfig.ProjectId, PushServiceAccount: pc.GcpConfig.GetPushServiceAccount(), } } return nil }(), } } } } // Cloud Storage { for _, cluster := range c.in.Infra.Resources.BucketClusters { p := &config.BucketProvider{} switch prov := cluster.Provider.(type) { case *runtimev1.BucketCluster_S3_: p.S3 = &config.S3BucketProvider{ Region: prov.S3.GetRegion(), Endpoint: prov.S3.Endpoint, AccessKeyID: prov.S3.AccessKeyId, SecretAccessKey: ptrOrNil(c.secretString(prov.S3.SecretAccessKey)), } case *runtimev1.BucketCluster_Gcs: p.GCS = &config.GCSBucketProvider{ Endpoint: prov.Gcs.GetEndpoint(), Anonymous: prov.Gcs.Anonymous, } if opt := prov.Gcs.LocalSign; opt != nil { p.GCS.LocalSign = &config.GCSLocalSignOptions{ BaseURL: opt.BaseUrl, AccessID: opt.AccessId, PrivateKey: opt.PrivateKey, } } default: c.setErrf("unknown object storage provider type %T", prov) continue } providerID := len(cfg.BucketProviders) cfg.BucketProviders = append(cfg.BucketProviders, p) for _, bkt := range cluster.Buckets { cfg.Buckets[bkt.EncoreName] = &config.Bucket{ ProviderID: providerID, EncoreName: bkt.EncoreName, CloudName: bkt.CloudName, KeyPrefix: bkt.GetKeyPrefix(), PublicBaseURL: bkt.GetPublicBaseUrl(), } } } } } // Observability. { obs := c.in.Deployment.Observability if len(obs.Metrics) > 0 { prov := obs.Metrics[0] m := &config.Metrics{ CollectionInterval: prov.CollectionInterval.AsDuration(), } switch p := prov.Provider.(type) { case *runtimev1.MetricsProvider_Gcp: pp := p.Gcp m.CloudMonitoring = &config.GCPCloudMonitoringProvider{ ProjectID: pp.ProjectId, MonitoredResourceType: pp.MonitoredResourceType, MonitoredResourceLabels: pp.MonitoredResourceLabels, MetricNames: pp.MetricNames, } case *runtimev1.MetricsProvider_EncoreCloud: pp := p.EncoreCloud m.EncoreCloud = &config.GCPCloudMonitoringProvider{ ProjectID: pp.ProjectId, MonitoredResourceType: pp.MonitoredResourceType, MonitoredResourceLabels: pp.MonitoredResourceLabels, MetricNames: pp.MetricNames, } case *runtimev1.MetricsProvider_Aws: pp := p.Aws m.CloudWatch = &config.AWSCloudWatchMetricsProvider{Namespace: pp.Namespace} case *runtimev1.MetricsProvider_Datadog_: pp := p.Datadog m.Datadog = &config.DatadogProvider{ APIKey: c.secretString(pp.ApiKey), Site: pp.Site, } case *runtimev1.MetricsProvider_PromRemoteWrite: pp := p.PromRemoteWrite m.Prometheus = &config.PrometheusRemoteWriteProvider{ RemoteWriteURL: c.secretString(pp.RemoteWriteUrl), } default: c.setErrf("unknown metrics provider type %T", p) } cfg.Metrics = m } // Add the Encore Tracing endpoint, if any. for _, prov := range obs.Tracing { if enc := prov.GetEncore(); enc != nil { cfg.TraceEndpoint = enc.TraceEndpoint cfg.TraceSamplingConfig = convertSamplingConfig(enc.SamplingConfig) cfg.TraceSamplingRate = enc.SamplingRate //nolint:staticcheck // backward compat break } } } if c.in.EncorePlatform != nil { cfg.AuthKeys = c.authKeys(c.in.EncorePlatform.PlatformSigningKeys) } if ec := c.in.EncorePlatform.GetEncoreCloud(); ec != nil { cfg.EncoreCloudAPI = &config.EncoreCloudAPI{ Server: ec.ServerUrl, AuthKeys: fns.Map(c.authKeys(ec.AuthKeys), func(k config.EncoreAuthKey) auth.Key { return auth.Key{KeyID: k.KeyID, Data: k.Data} }), } } if c.err != nil { return nil, c.err } return cfg, nil } // convertSamplingConfig converts the typed proto SamplingConfig entries // to a map[string]float64 with prefixed keys: // - "_" for the global default // - "service:" for service-level // - "endpoint:." for endpoint-level // - "topic:" for topic-level // - "subscription:." for subscription-level func convertSamplingConfig(configs []*runtimev1.TracingProvider_SamplingConfig) map[string]float64 { if len(configs) == 0 { return nil } m := make(map[string]float64, len(configs)) for _, sc := range configs { switch s := sc.Scope.(type) { case *runtimev1.TracingProvider_SamplingConfig_Default: m["_"] = sc.Rate case *runtimev1.TracingProvider_SamplingConfig_Service: m["service:"+s.Service] = sc.Rate case *runtimev1.TracingProvider_SamplingConfig_Endpoint_: m["endpoint:"+s.Endpoint.Service+"."+s.Endpoint.Endpoint] = sc.Rate case *runtimev1.TracingProvider_SamplingConfig_Topic: m["topic:"+s.Topic] = sc.Rate case *runtimev1.TracingProvider_SamplingConfig_PubsubSubscription: m["subscription:"+s.PubsubSubscription.Topic+"."+s.PubsubSubscription.Subscription] = sc.Rate } } return m } func (c *legacyConverter) envType() encore.EnvironmentType { switch c.in.Environment.EnvType { case runtimev1.Environment_TYPE_DEVELOPMENT: return encore.EnvDevelopment case runtimev1.Environment_TYPE_PRODUCTION: return encore.EnvProduction case runtimev1.Environment_TYPE_EPHEMERAL: return encore.EnvEphemeral case runtimev1.Environment_TYPE_TEST: return encore.EnvTest default: c.setErrf("unknown environment type %+v", c.in.Environment.EnvType) return "" } } func (c *legacyConverter) envCloud() encore.CloudProvider { switch c.in.Environment.Cloud { case runtimev1.Environment_CLOUD_LOCAL: return encore.CloudLocal case runtimev1.Environment_CLOUD_AWS: return encore.CloudAWS case runtimev1.Environment_CLOUD_GCP: return encore.CloudGCP case runtimev1.Environment_CLOUD_AZURE: return encore.CloudAzure case runtimev1.Environment_CLOUD_ENCORE: return encore.EncoreCloud default: c.setErrf("unknown cloud %+v", c.in.Environment.Cloud) return "" } } func (c *legacyConverter) authKeys(keys []*runtimev1.EncoreAuthKey) []config.EncoreAuthKey { return fns.Map(keys, func(k *runtimev1.EncoreAuthKey) config.EncoreAuthKey { return config.EncoreAuthKey{ KeyID: k.Id, Data: c.secretBytes(k.Data), } }) } func (c *legacyConverter) limiter(lim *runtimev1.RateLimiter) *config.Limiter { if lim == nil { return nil } switch lim := lim.Kind.(type) { case *runtimev1.RateLimiter_TokenBucket_: return &config.Limiter{ TokenBucket: &config.TokenBucketLimiter{ PerSecondRate: lim.TokenBucket.Rate, BucketSize: int(lim.TokenBucket.Burst), }, } default: c.setErrf("unknown rate limiter type %T", lim) return nil } } func (c *legacyConverter) secretString(s *runtimev1.SecretData) string { return string(c.secretBytes(s)) } func (c *legacyConverter) secretBytes(s *runtimev1.SecretData) []byte { if s == nil { return nil } // First resolve the secret data var secretData []byte switch data := s.Source.(type) { case *runtimev1.SecretData_Embedded: secretData = data.Embedded case *runtimev1.SecretData_Env: val, ok := c.secretEnvs[data.Env] if !ok { c.setErrf("missing secret env var %q", data.Env) } secretData = val default: c.setErrf("unknown secret data type %T", data) return nil } // Resolve a sub-path, if any. switch sub := s.SubPath.(type) { case nil: // No sub-path defined. return secretData case *runtimev1.SecretData_JsonKey: jsonObj := map[string]any{} if err := json.Unmarshal(secretData, &jsonObj); err != nil { c.setErrf("secret data is not valid json: %v", err) return nil } val, ok := jsonObj[sub.JsonKey] if !ok { c.setErrf("missing json key %q", sub.JsonKey) } switch val := val.(type) { case string: return []byte(val) case map[string]any: baseVal, ok := val["bytes"] if !ok { panic("missing bytes key") } b64Str, ok := baseVal.(string) if !ok { panic("bytes key is not a string") } bytes, err := base64.StdEncoding.DecodeString(b64Str) if err != nil { panic(err) } return bytes default: panic("unexpected value type") } default: c.setErrf("unknown secret sub-path type %T", s) return nil } } func (c *legacyConverter) setErr(err error) { if c.err == nil { c.err = err } } func (c *legacyConverter) setErrf(format string, args ...any) { c.setErr(errors.Newf(format, args...)) } func nilPtrToZero[T comparable](val *T) T { if val == nil { var zero T return zero } return *val } func ptr[T comparable](val T) *T { return &val } func ptrOrNil[T comparable](val T) *T { var zero T if val == zero { return nil } return &val } func randomMapValue[K comparable, V any](m map[K]V) (V, bool) { for _, v := range m { return v, true } var zero V return zero, false } ================================================ FILE: pkg/rtconfgen/infra_builder.go ================================================ package rtconfgen import ( "slices" meta "encr.dev/proto/encore/parser/meta/v1" runtimev1 "encr.dev/proto/encore/runtime/v1" ) type InfraBuilder struct { infra *runtimev1.Infrastructure rs *resourceSet } func newInfraBuilder(rs *resourceSet) *InfraBuilder { infra := &runtimev1.Infrastructure{ Credentials: &runtimev1.Infrastructure_Credentials{}, Resources: &runtimev1.Infrastructure_Resources{}, } return &InfraBuilder{ infra: infra, rs: rs, } } func (b *InfraBuilder) ClientCert(rid string, fn func() *runtimev1.ClientCert) *ClientCert { val := addResFunc(&b.infra.Credentials.ClientCerts, b.rs, rid, fn) return &ClientCert{Val: val, b: b} } type ClientCert struct { Val *runtimev1.ClientCert b *InfraBuilder } func (b *InfraBuilder) SQLRole(p *runtimev1.SQLRole) *SQLRole { return b.SQLRoleFn(p.Rid, tofn(p)) } func (b *InfraBuilder) SQLRoleFn(rid string, fn func() *runtimev1.SQLRole) *SQLRole { val := addResFunc(&b.infra.Credentials.SqlRoles, b.rs, rid, fn) return &SQLRole{Val: val, b: b} } type SQLRole struct { Val *runtimev1.SQLRole b *InfraBuilder } func (b *InfraBuilder) SQLCluster(p *runtimev1.SQLCluster) *SQLCluster { return b.SQLClusterFn(p.Rid, tofn(p)) } func (b *InfraBuilder) SQLClusterFn(rid string, fn func() *runtimev1.SQLCluster) *SQLCluster { val := addResFunc(&b.infra.Resources.SqlClusters, b.rs, rid, fn) return &SQLCluster{Val: val, b: b} } type SQLCluster struct { Val *runtimev1.SQLCluster b *InfraBuilder } func (c *SQLCluster) SQLDatabase(p *runtimev1.SQLDatabase) *SQLDatabase { return c.SQLDatabaseFn(p.Rid, tofn(p)) } func (c *SQLCluster) SQLDatabaseFn(rid string, fn func() *runtimev1.SQLDatabase) *SQLDatabase { val := addResFunc(&c.Val.Databases, c.b.rs, rid, fn) return &SQLDatabase{Val: val, b: c.b} } type SQLDatabase struct { Val *runtimev1.SQLDatabase b *InfraBuilder } func (c *SQLDatabase) AddConnectionPool(p *runtimev1.SQLConnectionPool) { c.Val.ConnPools = append(c.Val.ConnPools, p) } func (c *SQLCluster) SQLServer(p *runtimev1.SQLServer) *SQLServer { return c.SQLServerFn(p.Rid, tofn(p)) } func (c *SQLCluster) SQLServerFn(rid string, fn func() *runtimev1.SQLServer) *SQLServer { val := addResFunc(&c.Val.Servers, c.b.rs, rid, fn) return &SQLServer{Val: val, b: c.b} } type SQLServer struct { Val *runtimev1.SQLServer b *InfraBuilder } func (b *InfraBuilder) PubSubCluster(p *runtimev1.PubSubCluster) *PubSubCluster { return b.PubSubClusterFn(p.Rid, tofn(p)) } func (b *InfraBuilder) PubSubClusterFn(rid string, fn func() *runtimev1.PubSubCluster) *PubSubCluster { val := addResFunc(&b.infra.Resources.PubsubClusters, b.rs, rid, fn) return &PubSubCluster{Val: val, b: b} } type PubSubCluster struct { Val *runtimev1.PubSubCluster b *InfraBuilder } func (c *PubSubCluster) PubSubTopic(p *runtimev1.PubSubTopic) *PubSubTopic { return c.PubSubTopicFn(p.Rid, tofn(p)) } func (c *PubSubCluster) PubSubTopicFn(rid string, fn func() *runtimev1.PubSubTopic) *PubSubTopic { val := addResFunc(&c.Val.Topics, c.b.rs, rid, fn) return &PubSubTopic{Val: val, b: c.b} } type PubSubTopic struct { Val *runtimev1.PubSubTopic b *InfraBuilder } func (c *PubSubCluster) PubSubSubscription(p *runtimev1.PubSubSubscription) *PubSubSubscription { return c.PubSubSubscriptionFn(p.Rid, tofn(p)) } func (c *PubSubCluster) PubSubSubscriptionFn(rid string, fn func() *runtimev1.PubSubSubscription) *PubSubSubscription { val := addResFunc(&c.Val.Subscriptions, c.b.rs, rid, fn) return &PubSubSubscription{Val: val, b: c.b} } type PubSubSubscription struct { Val *runtimev1.PubSubSubscription b *InfraBuilder } func (b *InfraBuilder) RedisRole(p *runtimev1.RedisRole) *RedisRole { return b.RedisRoleFn(p.Rid, tofn(p)) } func (b *InfraBuilder) RedisRoleFn(rid string, fn func() *runtimev1.RedisRole) *RedisRole { val := addResFunc(&b.infra.Credentials.RedisRoles, b.rs, rid, fn) return &RedisRole{Val: val, b: b} } type RedisRole struct { Val *runtimev1.RedisRole b *InfraBuilder } func (b *InfraBuilder) RedisCluster(p *runtimev1.RedisCluster) *RedisCluster { return b.RedisClusterFn(p.Rid, tofn(p)) } func (b *InfraBuilder) RedisClusterFn(rid string, fn func() *runtimev1.RedisCluster) *RedisCluster { val := addResFunc(&b.infra.Resources.RedisClusters, b.rs, rid, fn) return &RedisCluster{Val: val, b: b} } type RedisCluster struct { Val *runtimev1.RedisCluster b *InfraBuilder } func (c *RedisCluster) RedisDatabase(p *runtimev1.RedisDatabase) *RedisDatabase { return c.RedisDatabaseFn(p.Rid, tofn(p)) } func (c *RedisCluster) RedisDatabaseFn(rid string, fn func() *runtimev1.RedisDatabase) *RedisDatabase { val := addResFunc(&c.Val.Databases, c.b.rs, rid, fn) return &RedisDatabase{Val: val, b: c.b} } type RedisDatabase struct { Val *runtimev1.RedisDatabase b *InfraBuilder } func (c *RedisDatabase) AddConnectionPool(p *runtimev1.RedisConnectionPool) { c.Val.ConnPools = append(c.Val.ConnPools, p) } func (c *RedisCluster) RedisServer(p *runtimev1.RedisServer) *RedisServer { return c.RedisServerFn(p.Rid, tofn(p)) } func (c *RedisCluster) RedisServerFn(rid string, fn func() *runtimev1.RedisServer) *RedisServer { val := addResFunc(&c.Val.Servers, c.b.rs, rid, fn) return &RedisServer{Val: val, b: c.b} } type RedisServer struct { Val *runtimev1.RedisServer b *InfraBuilder } func (b *InfraBuilder) BucketCluster(p *runtimev1.BucketCluster) *BucketCluster { return b.BucketClusterFn(p.Rid, tofn(p)) } func (b *InfraBuilder) BucketClusterFn(rid string, fn func() *runtimev1.BucketCluster) *BucketCluster { val := addResFunc(&b.infra.Resources.BucketClusters, b.rs, rid, fn) return &BucketCluster{Val: val, b: b} } type BucketCluster struct { Val *runtimev1.BucketCluster b *InfraBuilder } func (c *BucketCluster) Bucket(p *runtimev1.Bucket) *Bucket { return c.BucketFn(p.Rid, tofn(p)) } func (c *BucketCluster) BucketFn(rid string, fn func() *runtimev1.Bucket) *Bucket { val := addResFunc(&c.Val.Buckets, c.b.rs, rid, fn) return &Bucket{Val: val, b: c.b} } type Bucket struct { Val *runtimev1.Bucket b *InfraBuilder } func (b *InfraBuilder) Gateway(gw *runtimev1.Gateway) *Gateway { return b.GatewayFn(gw.Rid, tofn(gw)) } func (b *InfraBuilder) GatewayFn(rid string, fn func() *runtimev1.Gateway) *Gateway { val := addResFunc(&b.infra.Resources.Gateways, b.rs, rid, fn) return &Gateway{Val: val, b: b} } type Gateway struct { Val *runtimev1.Gateway b *InfraBuilder } func (b *InfraBuilder) AppSecret(p *runtimev1.AppSecret) *AppSecret { return b.AppSecretFn(p.Rid, tofn(p)) } func (b *InfraBuilder) AppSecretFn(rid string, fn func() *runtimev1.AppSecret) *AppSecret { val := addResFunc(&b.infra.Resources.AppSecrets, b.rs, rid, fn) return &AppSecret{Val: val, b: b} } type AppSecret struct { Val *runtimev1.AppSecret b *InfraBuilder } func (b *InfraBuilder) get() (*runtimev1.Infrastructure, error) { return b.infra, nil } func tofn[V any](v V) func() V { return func() V { return v } } // reduceForServices reduces the given infrastructure to only include resource accessible by // the given services, using the metadata for access control. func reduceForServices(infra *runtimev1.Infrastructure, md *meta.Data, svcs []string) *runtimev1.Infrastructure { // Clone the protobuf so the changes don't affect the original. infra = cloneProto(infra) svcNames := make(map[string]bool) for _, svc := range svcs { svcNames[svc] = true } dbsToKeep := make(map[string]bool) for _, svc := range md.Svcs { if !svcNames[svc.Name] { continue } for _, dbName := range svc.Databases { dbsToKeep[dbName] = true } } bucketsToKeep := make(map[string]bool) for _, svc := range md.Svcs { if !svcNames[svc.Name] { continue } for _, bktName := range svc.Buckets { bucketsToKeep[bktName.Bucket] = true } } type subKey struct { topicName string subName string } topicsToKeep := make(map[string]bool) subsToKeep := make(map[subKey]bool) for _, topic := range md.PubsubTopics { for _, publisher := range topic.Publishers { if svcNames[publisher.ServiceName] { topicsToKeep[topic.Name] = true } } for _, subscriber := range topic.Subscriptions { if svcNames[subscriber.ServiceName] { subsToKeep[subKey{topicName: topic.Name, subName: subscriber.Name}] = true } } } cachesToKeep := make(map[string]bool) for _, cacheCluster := range md.CacheClusters { for _, keySpace := range cacheCluster.Keyspaces { if svcNames[keySpace.Service] { cachesToKeep[cacheCluster.Name] = true } } } for _, cluster := range infra.Resources.PubsubClusters { cluster.Topics = slices.DeleteFunc(cluster.Topics, func(t *runtimev1.PubSubTopic) bool { _, found := topicsToKeep[t.EncoreName] return !found }) cluster.Subscriptions = slices.DeleteFunc(cluster.Subscriptions, func(t *runtimev1.PubSubSubscription) bool { _, found := subsToKeep[subKey{topicName: t.TopicEncoreName, subName: t.SubscriptionEncoreName}] return !found }) } for _, cluster := range infra.Resources.RedisClusters { cluster.Databases = slices.DeleteFunc(cluster.Databases, func(t *runtimev1.RedisDatabase) bool { _, found := cachesToKeep[t.EncoreName] return !found }) } for _, cluster := range infra.Resources.BucketClusters { cluster.Buckets = slices.DeleteFunc(cluster.Buckets, func(t *runtimev1.Bucket) bool { _, found := bucketsToKeep[t.EncoreName] return !found }) } secretsToKeep := secretsUsedByServices(md, svcNames) infra.Resources.AppSecrets = slices.DeleteFunc(infra.Resources.AppSecrets, func(t *runtimev1.AppSecret) bool { _, found := secretsToKeep[t.EncoreName] return !found }) return infra } // secretsUsedByServices returns the set of secrets that are accessible by the given services, using the metadata for access control. func secretsUsedByServices(md *meta.Data, svcNames map[string]bool) (secretNames map[string]bool) { secretNames = make(map[string]bool) for _, pkg := range md.Pkgs { if len(pkg.Secrets) > 0 && (pkg.ServiceName == "" || svcNames[pkg.ServiceName]) { for _, secret := range pkg.Secrets { secretNames[secret] = true } } } return secretNames } ================================================ FILE: pkg/rtconfgen/resource_map.go ================================================ package rtconfgen import "fmt" // A resource is an object with a unique resource id (rid). type resource interface { GetRid() string } type resourceKey struct { typ string rid string } // A resourceSet tracks whether a resource has been seen before based on its id, // and allows efficient lookup of a resource by id. type resourceSet struct { m map[resourceKey]any } // rsAdd adds a resource to the set. It reports whether the resource was added. func rsAdd[R any](rs *resourceSet, rid string, fn func() R) (val R, added bool) { key := internalRSKeyByID[R](rid) if existing := rs.m[key]; existing != nil { return existing.(R), false } if rs.m == nil { rs.m = make(map[resourceKey]any) } val = fn() rs.m[key] = val return val, true } func internalRSKeyByID[R any](rid string) resourceKey { var zero R typ := fmt.Sprintf("%T", zero) return resourceKey{typ, rid} } func addResFunc[R any](dst *[]R, rs *resourceSet, rid string, fn func() R) (stored R) { *dst, stored = appendResFunc(*dst, rs, rid, fn) return stored } func appendResFunc[R any](dst []R, rs *resourceSet, rid string, fn func() R) (result []R, stored R) { stored, updated := rsAdd(rs, rid, fn) if updated { dst = append(dst, stored) } return dst, stored } ================================================ FILE: pkg/supervisor/cmd/supervisor-encore/main.go ================================================ package main import ( "context" "encoding/base64" "encoding/json" "flag" "os" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/protobuf/proto" "encr.dev/pkg/supervisor" runtimev1 "encr.dev/proto/encore/runtime/v1" ) func main() { log.Info().Msg("supervisor starting") if err := run(); err != nil { log.Fatal().Err(err).Msg("unable to run supervisor") } } func run() error { cfgPath := flag.String("c", "", "path to the config file") flag.Parse() if *cfgPath == "" { return errors.New("missing config file") } cfg, err := loadSupervisorConfig(*cfgPath) if err != nil { return errors.Wrap(err, "load supervisor config") } // Configure the logger. rtcfg, err := loadRuntimeConfig() if err != nil { return errors.Wrap(err, "load runtime config") } configureLogger(rtcfg) super, err := supervisor.New(cfg, rtcfg) if err != nil { return errors.Wrap(err, "create supervisor") } err = super.Run(context.Background()) return errors.Wrap(err, "run supervisor") } func loadSupervisorConfig(path string) (*supervisor.Config, error) { var cfg supervisor.Config data, err := os.ReadFile(path) if err != nil { return nil, errors.Wrap(err, "read config file") } if err := json.Unmarshal(data, &cfg); err != nil { return nil, errors.Wrap(err, "unmarshal config file") } return &cfg, nil } // loadRuntimeConfig loads the runtime config from the ENCORE_RUNTIME_CONFIG env var. func loadRuntimeConfig() (*runtimev1.RuntimeConfig, error) { val, ok := os.LookupEnv("ENCORE_RUNTIME_CONFIG") if !ok { return nil, errors.New("ENCORE_RUNTIME_CONFIG not set") } decoded, err := base64.StdEncoding.DecodeString(val) if err != nil { return nil, errors.Wrap(err, "unable to decode ENCORE_RUNTIME_CONFIG") } var cfg runtimev1.RuntimeConfig if err := proto.Unmarshal(decoded, &cfg); err != nil { return nil, errors.Wrap(err, "unable to unmarshal runtime config") } return &cfg, nil } func configureLogger(cfg *runtimev1.RuntimeConfig) { // Log in GCP's log format for Encore Cloud and GCP. if cloud := cfg.Environment.Cloud; cloud == runtimev1.Environment_CLOUD_GCP || cloud == runtimev1.Environment_CLOUD_ENCORE { zerolog.LevelFieldName = "severity" zerolog.TimestampFieldName = "timestamp" zerolog.TimeFieldFormat = time.RFC3339Nano } // Create our root logger logger := zerolog.New(os.Stderr). Level(zerolog.DebugLevel). With().Caller().Timestamp().Stack().Str("process", "supervisor"). Logger() log.Logger = logger } ================================================ FILE: pkg/supervisor/supervisor.go ================================================ package supervisor import ( "context" "encoding/json" "fmt" "net" "net/http" "net/http/httputil" "net/netip" "net/url" "os" "os/exec" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "go4.org/syncutil" "encr.dev/pkg/noopgateway" runtimev1 "encr.dev/proto/encore/runtime/v1" ) // Config is the configuration used by the supervisor. type Config struct { Procs []Proc `json:"procs"` // NoopGateways are the noop-gateways to start up, // keyed by gateway name. NoopGateways map[string]*noopgateway.Description } // Proc represents a supervised proc. type Proc struct { // ID is a unique id representing this process. ID string `json:"id"` // Command and arguments for running the proc. Command []string `json:"command"` // Extra environment variables to pass. Env []string `json:"env"` // supervisedProc and gateway names included in this proc. Services []string `json:"services"` Gateways []string `json:"gateways"` } // New creates a new supervisor. func New(cfg *Config, rtCfg *runtimev1.RuntimeConfig) (*Supervisor, error) { log := zerolog.New(os.Stderr).With().Timestamp().Logger() super := &Supervisor{ cfg: cfg, rt: rtCfg, procs: make(map[string]*supervisedProc), log: log, } return super, nil } type Supervisor struct { cfg *Config rt *runtimev1.RuntimeConfig procs map[string]*supervisedProc buildInfoOnce syncutil.Once buildInfo BuildInfo log zerolog.Logger } // Run runs the supervisor. It returns immediately on error, // and otherwise blocks until ctx is canceled. func (s *Supervisor) Run(ctx context.Context) error { // Start up the procs to supervise. for i, proc := range s.cfg.Procs { services := strings.Join(proc.Services, ",") gateways := strings.Join(proc.Gateways, ",") logger := s.log.With(). Str("services", services). Str("gateways", gateways). Str("proc_id", proc.ID). Logger() port := 12000 + i sp := &supervisedProc{ super: s, proc: proc, log: logger, port: port, } s.procs[proc.ID] = sp go sp.Supervise() } // Start up the noop-gateways. go s.runNoopGateway(ctx, "noop") <-ctx.Done() return nil } // runNoopGateway runs a noop-gateway until ctx is canceled. func (s *Supervisor) runNoopGateway(ctx context.Context, name string) { logger := s.log.With().Str("gateway", name).Logger() logger.Info().Msg("starting noop-gateway") // Find the default gateway var targetGW *supervisedProc for _, proc := range s.procs { if len(proc.proc.Gateways) > 0 { targetGW = proc break } } if targetGW == nil { logger.Error().Msg("no gateway found") return } target := &url.URL{ Scheme: "http", Host: fmt.Sprintf("localhost:%d", targetGW.port), } rp := httputil.NewSingleHostReverseProxy(target) ln, err := listenGateway() if err != nil { logger.Error().Err(err).Msg("listen") return } logger.Info().Msgf("listening on %s", ln.Addr().String()) srv := &http.Server{ BaseContext: func(_ net.Listener) context.Context { return ctx }, Handler: rp, } if err := srv.Serve(ln); err != nil { logger.Error().Err(err).Msg("serve") return } } func listenGateway() (net.Listener, error) { listenAddr := os.Getenv("ENCORE_LISTEN_ADDR") if listenAddr != "" { addrPort, err := netip.ParseAddrPort(listenAddr) if err != nil { return nil, err } return net.Listen("tcp", addrPort.String()) } port, _ := strconv.Atoi(os.Getenv("PORT")) if port == 0 { port = 8080 } return net.Listen("tcp", ":"+strconv.Itoa(port)) } // supervisedProc is a supervised process. type supervisedProc struct { super *Supervisor proc Proc log zerolog.Logger healthy atomic.Bool port int } // Healthy reports whether the service is currently healthy. func (s *supervisedProc) Healthy() bool { return s.healthy.Load() } // Supervise supervises the service, starting it and restarting it if it exits. func (p *supervisedProc) Supervise() { const ( minSleep = 100 * time.Millisecond maxSleep = 10 * time.Second ) var ( mu sync.Mutex generation uint64 = 1 retrySleep = minSleep ) errSleep := func(msg string, err error) { // Mark the service as unhealthy. p.healthy.Store(false) // Increment retry sleep and generation. mu.Lock() generation++ toSleep := retrySleep retrySleep *= 2 if retrySleep > maxSleep { retrySleep = maxSleep } mu.Unlock() p.log.Error().Err(err).Msgf("%s: %v, retrying in %v", msg, err, toSleep) time.Sleep(toSleep) } // If this proc is using the sidecar, add its environment variables. env := os.Environ() env = append(env, p.proc.Env...) // Add the port to listen on. env = append(env, fmt.Sprintf("PORT=%d", p.port)) for { mu.Lock() currGen := generation mu.Unlock() cmd := exec.Command(p.proc.Command[0], p.proc.Command[1:]...) cmd.Env = env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr p.log.Info().Msg("starting proc") if err := cmd.Start(); err != nil { errSleep("service startup failed", err) continue } // Check in on the process after a few seconds and reset the retry sleep if it's still alive. go func() { time.Sleep(1 * time.Second) mu.Lock() defer mu.Unlock() if generation == currGen { // Still alive. Reset the retry sleep. retrySleep = minSleep p.healthy.Store(true) } }() if err := cmd.Wait(); err != nil { errSleep("proc exited", err) continue } } } func readBuildInfo() (BuildInfo, error) { var info BuildInfo data, err := os.ReadFile("/encore/build-info.json") if err == nil { err = json.Unmarshal(data, &info) } return info, errors.Wrap(err, "read build info") } type BuildInfo struct { // The version of Encore with which the app was compiled. // This is string is for informational use only, and its format should not be relied on. EncoreCompiler string // AppCommit describes the commit of the app. AppCommit CommitInfo } type CommitInfo struct { Revision string Uncommitted bool } func (ci CommitInfo) AsRevisionString() string { if ci.Uncommitted { return fmt.Sprintf("%s-modified", ci.Revision) } return ci.Revision } ================================================ FILE: pkg/svcproxy/dialer.go ================================================ package svcproxy import ( "context" "net" "strings" "time" "github.com/cenkalti/backoff/v4" ) type retryDialer struct { net.Dialer } func (d *retryDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { var conn net.Conn b := backoff.NewExponentialBackOff() b.MaxElapsedTime = time.Minute // Set maximum backoff time to 1 minute operation := func() error { var err error conn, err = d.Dialer.DialContext(ctx, network, address) if err != nil { if strings.Contains(err.Error(), "connection refused") { // Retry if connection is refused return err } // Don't retry if connection isn't refused return backoff.Permanent(err) } return nil } err := backoff.Retry(operation, b) if err != nil { return nil, err } return conn, nil } ================================================ FILE: pkg/svcproxy/doc.go ================================================ // Package svcproxy provides an HTTP proxy which allows the daemon to // serve HTTP requests on behalf of services within the app and forward // then to the appropriate process. package svcproxy ================================================ FILE: pkg/svcproxy/svcproxy.go ================================================ package svcproxy import ( "context" "fmt" "net" "net/http" "net/http/httputil" "net/netip" "strings" "sync" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog" "encr.dev/pkg/logging" ) type SvcProxy struct { listener net.Listener logger zerolog.Logger httpServer *http.Server mu sync.RWMutex gateways map[string]*httputil.ReverseProxy // Map of the gateway name to address and port it's listening on services map[string]*httputil.ReverseProxy // Map of service name to address and port it's listening on } var ( _ http.Handler = (*SvcProxy)(nil) ) // New creates a new service proxy and it starts listening on a random port // // You must call Close() on the returned service proxy when you are done with it. func New(ctx context.Context, logger zerolog.Logger) (*SvcProxy, error) { var lc net.ListenConfig ln, err := lc.Listen(ctx, "tcp", "127.0.0.1:0") if err != nil { return nil, errors.Wrap(err, "unable to listen") } proxy := &SvcProxy{ listener: ln, logger: logger, gateways: make(map[string]*httputil.ReverseProxy), services: make(map[string]*httputil.ReverseProxy), } proxy.httpServer = &http.Server{ Addr: ln.Addr().String(), BaseContext: func(_ net.Listener) context.Context { return ctx }, Handler: proxy, } go func() { if err := proxy.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Err(err).Msg("error serving") } }() return proxy, nil } // Close stops running the service proxy func (p *SvcProxy) Close() { _ = p.httpServer.Close() _ = p.listener.Close() } func (p *SvcProxy) RegisterGateway(name string, addr netip.AddrPort) string { p.mu.Lock() defer p.mu.Unlock() p.gateways[name] = p.createReverseProxy("gateway", name, addr) return fmt.Sprintf("http://%s/gateway/%s", p.listener.Addr().String(), name) } // RegisterService registers a service with the proxy and returns the BaseURL to be used // to access the service. func (p *SvcProxy) RegisterService(name string, addr netip.AddrPort) string { p.mu.Lock() defer p.mu.Unlock() p.services[name] = p.createReverseProxy("service", name, addr) return fmt.Sprintf("http://%s/service/%s", p.listener.Addr().String(), name) } func (p *SvcProxy) createReverseProxy(what, name string, listener netip.AddrPort) *httputil.ReverseProxy { return &httputil.ReverseProxy{ // This transport is copied from the default transport in the http package just with the dial context // wrapped in our retry dialer. Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&retryDialer{net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }}).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, Rewrite: func(request *httputil.ProxyRequest) { request.Out.URL.Scheme = "http" request.Out.URL.Host = listener.String() request.Out.URL.Path = strings.TrimPrefix(request.In.URL.Path, fmt.Sprintf("/%s/%s", what, name)) }, ErrorLog: logging.NewZeroLogAdapter(p.logger.With().Str(what, name).Logger(), zerolog.ErrorLevel), } } // ServeHTTP implements the http.Handler interface func (p *SvcProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Resolve the handler with the lock held. proxy, err := func() (http.Handler, error) { p.mu.RLock() defer p.mu.RUnlock() parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3) if len(parts) >= 2 { switch parts[0] { case "gateway": proxy, ok := p.gateways[parts[1]] if !ok { return nil, errors.Newf("unknown gateway: %s", parts[1]) } else { return proxy, nil } case "service": proxy, ok := p.services[parts[1]] if !ok { return nil, errors.Newf("unknown service: %s", parts[1]) } else { return proxy, nil } default: return nil, errors.Newf("unknown path prefix: %s", parts[0]) } } else { return nil, errors.Newf("unknown path prefix format: %s", req.URL.Path) } }() if err != nil { p.logger.Err(err).Str("url", req.URL.String()).Msg("error proxying service request") http.Error(w, err.Error(), http.StatusInternalServerError) return } proxy.ServeHTTP(w, req) } ================================================ FILE: pkg/tarstream/LICENSE ================================================ MIT License Copyright (c) 2016 Ben McClelland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: pkg/tarstream/datavec.go ================================================ package tarstream import ( "bytes" "io" "os" ) // MemVec is a buffer vec type type MemVec struct { Data []byte } // PathVec is a filename vec type type PathVec struct { Path string Info os.FileInfo } // PadVec is a padding (0s) vec type type PadVec struct { Size int64 } type DataReader interface { io.ReaderAt io.Closer } func nopCloser(r io.ReaderAt) DataReader { return noopCloser{r} } type noopCloser struct { io.ReaderAt } func (n noopCloser) Close() error { return nil } // Datavec is an interface for all vector types type Datavec interface { Clone() Datavec GetSize() int64 Open() (DataReader, error) } // GetSize gets the size of the memory vec func (m MemVec) GetSize() int64 { return int64(len(m.Data)) } func (m MemVec) Clone() Datavec { return m } // Open opens a memory vec func (m MemVec) Open() (DataReader, error) { return nopCloser(bytes.NewReader(m.Data)), nil } // GetSize gets the file size of the path vec func (p PathVec) GetSize() int64 { return p.Info.Size() } // Open opens a file represented by a path vec func (p *PathVec) Open() (DataReader, error) { return os.Open(p.Path) } func (p *PathVec) Clone() Datavec { return p } // GetSize gets the size of the padding vec func (p PadVec) GetSize() int64 { return p.Size } // Open opens the padding vec func (p PadVec) Open() (DataReader, error) { return padReader{p.Size}, nil } func (p PadVec) Clone() Datavec { return p } type padReader struct { size int64 } func (r padReader) ReadAt(b []byte, off int64) (int, error) { rem := int(r.size - off) if rem == 0 { return 0, io.EOF } n := min(rem, len(b)) for i := range n { b[i] = 0 } return n, nil } func (r padReader) Close() error { return nil } ================================================ FILE: pkg/tarstream/datavec_test.go ================================================ package tarstream ================================================ FILE: pkg/tarstream/tarstream.go ================================================ package tarstream import ( "archive/tar" "fmt" "io" "os" "sort" "github.com/pkg/errors" ) func NewTarVec(vecs []Datavec) *TarVec { var totalSize int64 starts := make([]int64, len(vecs)) ends := make([]int64, len(vecs)) for i, dv := range vecs { size := dv.GetSize() starts[i] = totalSize ends[i] = totalSize + size totalSize += size } return &TarVec{ vecs: vecs, starts: starts, ends: ends, size: totalSize, } } // TarVec is an array of datavecs representing a tarball type TarVec struct { vecs []Datavec starts []int64 // starting offset for each vec, inclusive ends []int64 // ending offset for each vec, exclusive size int64 // Current reading pos pos int64 curr *currReader } type currReader struct { idx int // datavec index size int64 data DataReader } // Size gets the size of the tarball represented by the tarvec func (tv *TarVec) Size() int64 { return tv.size } func (tv *TarVec) Clone() *TarVec { return &TarVec{ vecs: tv.vecs, starts: tv.starts, ends: tv.ends, pos: tv.pos, } } func (tv *TarVec) getReader() (*currReader, error) { // Do we have a current reader? if tv.curr != nil { // Is the current position within the current datavec? if tv.pos >= tv.starts[tv.curr.idx] && tv.pos < tv.ends[tv.curr.idx] { return tv.curr, nil } _ = tv.curr.data.Close() tv.curr = nil } // Find the datavec that contains the current position candidate := sort.Search(len(tv.ends), func(i int) bool { return tv.ends[i] > tv.pos }) if candidate == len(tv.ends) { // Position exceeds the end of the last data vec. return nil, io.EOF } vec := tv.vecs[candidate] data, err := vec.Open() if err != nil { return nil, fmt.Errorf("error opening data vec: %v", err) } tv.curr = &currReader{ idx: candidate, size: vec.GetSize(), data: data, } return tv.curr, nil } // Read the data represented by the tarvec func (tv *TarVec) Read(b []byte) (int, error) { cr, err := tv.getReader() if err != nil { return 0, err } off := tv.pos - tv.starts[cr.idx] // Sanity checks remaining := cr.size - off hasMoreVecs := cr.idx+1 < len(tv.vecs) if remaining < 0 { panic("TarVec: negative remaining size") } else if remaining == 0 && hasMoreVecs && cr.size > 0 { panic("TarVec: zero remaining size but more vecs exist") } n, err := cr.data.ReadAt(b, off) if err == io.EOF { // Ignore EOF from individual readers. // getReader reports EOF when running out of readers. err = nil } if n == 0 && len(b) > 0 && (err == nil || err == io.EOF) { panic("TarVec: empty read from vec, more data remaining") } tv.pos += int64(n) return n, err } // Seek the virtual offset of the tarvec func (tv *TarVec) Seek(offset int64, whence int) (int64, error) { switch whence { case io.SeekStart: if offset < 0 { return 0, os.ErrInvalid } tv.pos = offset return tv.pos, nil case io.SeekCurrent: if tv.pos+offset < 0 { return 0, os.ErrInvalid } tv.pos += offset return tv.pos, nil case io.SeekEnd: if tv.size+offset < 0 { return 0, os.ErrInvalid } tv.pos = tv.size + offset return tv.pos, nil } return 0, os.ErrInvalid } func (tv *TarVec) Close() error { if tv.curr != nil { err := tv.curr.data.Close() tv.curr = nil return err } return nil } // Validate gets and validates the next header within the tarfile func Validate(r io.Reader) (*tar.Header, error) { tr := tar.NewReader(r) // Next will find the next header and read it in // this skips all meta-headers like long names, etc // hopefully thats what we want here hdr, err := tr.Next() if err != nil { return &tar.Header{}, errors.Wrap(err, fmt.Sprintf("read header")) } return hdr, nil } ================================================ FILE: pkg/tarstream/tarstream_test.go ================================================ package tarstream import ( "math/rand" "slices" "testing" "testing/iotest" "testing/quick" ) func TestReader(t *testing.T) { err := quick.Check(func(data []byte) bool { tv := genRandomVec(data) if err := iotest.TestReader(tv, data); err != nil { t.Logf("got read err %v", err) return false } return true }, nil) if err != nil { t.Fatal(err) } } func genRandomVec(data []byte) *TarVec { var vecs []Datavec for len(data) > 0 { n := rand.Intn(len(data) + 1) vecData := data[:n] data = data[n:] allZeroes := !slices.ContainsFunc(vecData, func(b byte) bool { return b != 0 }) if allZeroes { vecs = append(vecs, PadVec{Size: int64(len(vecData))}) } else { vecs = append(vecs, MemVec{Data: vecData}) } } return NewTarVec(vecs) } ================================================ FILE: pkg/traceparser/binreader.go ================================================ package traceparser import ( "bufio" "encoding/binary" "io" "math" "strings" "time" "unicode/utf8" "google.golang.org/protobuf/types/known/timestamppb" "encore.dev/appruntime/exported/trace2" ) var bin = binary.LittleEndian type traceReader struct { buf *bufio.Reader version trace2.Version bytesRead int timeAnchor int64 err error // any error encountered during reading } func (tr *traceReader) setErr(err error) { if tr.err == nil { tr.err = err } } // Err reports any error encountered during reading. func (tr *traceReader) Err() error { return tr.err } func (tr *traceReader) Bytes(b []byte) { n, err := io.ReadFull(tr.buf, b) tr.bytesRead += n tr.setErr(err) } func (tr *traceReader) Skip(n int) { discarded, err := tr.buf.Discard(n) tr.bytesRead += discarded tr.setErr(err) } func (tr *traceReader) Byte() byte { b, err := tr.buf.ReadByte() tr.setErr(err) if err == nil { tr.bytesRead++ } return b } func (tr *traceReader) Bool() bool { return tr.Byte() != 0 } func (tr *traceReader) String() string { s := string(tr.ByteString()) // Ensure the string is valid UTF-8. // Needed because proto.Marshal requires strings to be valid utf8. s = strings.ToValidUTF8(s, string(utf8.RuneError)) return s } func (tr *traceReader) OptString() *string { return ptrOrNil(tr.String()) } func (tr *traceReader) OptUVarint() *uint64 { return ptrOrNil(tr.UVarint()) } func (tr *traceReader) ByteString() []byte { size := tr.UVarint() if (size) == 0 { return nil } b := make([]byte, int(size)) tr.Bytes(b) return b } func (tr *traceReader) Time() *timestamppb.Timestamp { sec := tr.Int64() nsec := tr.Int32() t := time.Unix(sec, int64(nsec)).UTC() return timestamppb.New(t) } func (tr *traceReader) Nanotime() int64 { return tr.Int64() } func (tr *traceReader) Int32() int32 { u := tr.Uint32() var v int32 if u&1 == 0 { v = int32(u >> 1) } else { v = ^int32(u >> 1) } return v } func (tr *traceReader) Uint32() uint32 { var buf [4]byte tr.Bytes(buf[:]) return bin.Uint32(buf[:]) } func (tr *traceReader) Int64() int64 { return unsignedToSigned(tr.Uint64()) } func (tr *traceReader) Uint64() uint64 { var buf [8]byte tr.Bytes(buf[:]) return bin.Uint64(buf[:]) } func (tr *traceReader) Varint() int64 { u := tr.UVarint() var v int64 if u&1 == 0 { v = int64(u >> 1) } else { v = ^int64(u >> 1) } return v } func (tr *traceReader) UVarint() uint64 { i := 0 var u uint64 for { b, err := tr.buf.ReadByte() tr.setErr(err) if err != nil { return 0 } tr.bytesRead++ u |= uint64(b&^0x80) << i if b&0x80 == 0 { return u } i += 7 } } func (tr *traceReader) Float32() float32 { b := tr.Uint32() return math.Float32frombits(b) } func (tr *traceReader) Float64() float64 { b := tr.Uint64() return math.Float64frombits(b) } func (tr *traceReader) EventID() trace2.EventID { return trace2.EventID(tr.UVarint()) } func (tr *traceReader) Duration() time.Duration { return time.Duration(tr.Varint()) } func ptrOrNil[T comparable](val T) *T { var zero T if val == zero { return nil } return &val } func unsignedToSigned(u uint64) int64 { var v int64 if u&1 == 0 { v = int64(u >> 1) } else { v = ^int64(u >> 1) } return v } type versionFilterReader struct { traceReader *traceReader filtered bool } func (tr *traceReader) FromVer(version trace2.Version) versionFilterReader { return versionFilterReader{traceReader: tr, filtered: tr.version < version} } func (tr versionFilterReader) Bool(defaultForOlderVersions bool) bool { if tr.filtered { return defaultForOlderVersions } return tr.traceReader.Bool() } ================================================ FILE: pkg/traceparser/parser.go ================================================ package traceparser import ( "bufio" "errors" "fmt" "io" "runtime/debug" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/protobuf/types/known/timestamppb" "encore.dev/appruntime/exported/model" "encore.dev/appruntime/exported/trace2" "encr.dev/pkg/option" tracepb2 "encr.dev/proto/encore/engine/trace2" ) // ParseEvent parses a single event from the buffer. func ParseEvent(buf *bufio.Reader, ta trace2.TimeAnchor, version trace2.Version) (*tracepb2.TraceEvent, error) { tp := &traceParser{traceReader: traceReader{buf: buf, version: version}, ta: ta, log: &log.Logger} // If we already have an error, return it. if err := tp.Err(); err != nil { return nil, err } typ := trace2.EventType(tp.Byte()) if err := tp.Err(); err != nil { return nil, err } h := header{ Type: typ, EventID: trace2.EventID(tp.Uint64()), Nanotime: tp.Nanotime(), TraceID: tp.traceID(), SpanID: tp.Uint64(), Len: tp.Uint32(), } if err := tp.Err(); err != nil { log.Error().Err(err).Any("header", h).Msgf("failed to parse event header") return nil, err } bytesReadAfterHeader := tp.bytesRead ev, err := tp.parseEvent(h) if err != nil { return nil, fmt.Errorf("parse event %v: %v", h.Type, err) } err = tp.Err() if err == io.EOF { // If we have an io.EOF and we've read exactly the right amount of bytes, // treat it as a non-error. if n := tp.bytesRead - bytesReadAfterHeader; n == int(h.Len) { err = io.EOF } else if n < int(h.Len) { err = io.ErrUnexpectedEOF } else { err = fmt.Errorf("parser of event %s overflowed event buffer", h.Type) } } if n := tp.bytesRead - bytesReadAfterHeader; n != int(h.Len) { log.Info().Msgf("event %s: read %d bytes, expected %d", h.Type, n, h.Len) } return ev, err } type spanStartEvent struct { Goid uint32 ParentTraceID option.Option[*tracepb2.TraceID] ParentSpanID option.Option[uint64] DefLoc option.Option[uint32] CallerEventID option.Option[trace2.EventID] ExtCorrelationID option.Option[string] } type spanEndEvent struct { DurationNanos uint64 StatusCode tracepb2.StatusCode Err *tracepb2.Error PanicStack option.Option[*tracepb2.StackTrace] ParentTraceID option.Option[*tracepb2.TraceID] ParentSpanID option.Option[uint64] } type traceParser struct { traceReader ta trace2.TimeAnchor log *zerolog.Logger } type header struct { Type trace2.EventType EventID trace2.EventID // TS is a monotonic timestamp in nanoseconds. // It can be converted to an actual timestamp using the trace stream's epoch. Nanotime int64 TraceID *tracepb2.TraceID SpanID uint64 Len uint32 } var errUnknownEvent = errors.New("unknown event") func (tp *traceParser) parseEvent(h header) (ev *tracepb2.TraceEvent, err error) { defer func() { if r := recover(); r != nil { if b, ok := r.(bailout); ok { err = b.err } else { err = fmt.Errorf("panic parsing event: %v\n%s", r, debug.Stack()) } } }() ev = &tracepb2.TraceEvent{ TraceId: h.TraceID, SpanId: h.SpanID, EventId: uint64(h.EventID), EventTime: timestamppb.New(tp.ta.ToReal(h.Nanotime)), } switch h.Type { case trace2.RequestSpanStart: ev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.requestSpanStart()} case trace2.RequestSpanEnd: ev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.requestSpanEnd()} case trace2.AuthSpanStart: ev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.authSpanStart()} case trace2.AuthSpanEnd: ev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.authSpanEnd()} case trace2.PubsubMessageSpanStart: ev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.pubsubMessageSpanStart()} case trace2.PubsubMessageSpanEnd: ev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.pubsubMessageSpanEnd()} case trace2.TestStart: ev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.testSpanStart()} case trace2.TestEnd: ev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.testSpanEnd()} default: ev.Event = &tracepb2.TraceEvent_SpanEvent{SpanEvent: tp.spanEvent(h.Type)} } return ev, nil } func (tp *traceParser) spanStartEvent() spanStartEvent { goid := uint32(tp.UVarint()) parentTraceID := tp.traceID() parentSpanID := tp.Uint64() defLoc := uint32(tp.UVarint()) callerEventID := trace2.EventID(tp.UVarint()) extCorrelationID := tp.String() ev := spanStartEvent{ Goid: goid, ParentSpanID: option.AsOptional(parentSpanID), DefLoc: option.AsOptional(defLoc), CallerEventID: option.AsOptional(callerEventID), ExtCorrelationID: option.AsOptional(extCorrelationID), } if !parentTraceID.IsZero() { ev.ParentTraceID = option.Some(parentTraceID) } return ev } func (tp *traceParser) spanEndEvent() spanEndEvent { dur := tp.Duration() if dur < 0 { dur = 0 } var ( status tracepb2.StatusCode err *tracepb2.Error ) if tp.version >= 17 { status = tp.statusCode() err = tp.errWithStack() } else { err = tp.errWithStack() if err != nil { status = tracepb2.StatusCode_STATUS_CODE_UNKNOWN } else { status = tracepb2.StatusCode_STATUS_CODE_OK } } panicStack := tp.formattedStack() parentTraceID := tp.traceID() parentSpanID := tp.Uint64() ev := spanEndEvent{ DurationNanos: uint64(dur), StatusCode: status, Err: err, PanicStack: option.AsOptional(panicStack), ParentSpanID: option.AsOptional(parentSpanID), } if !parentTraceID.IsZero() { ev.ParentTraceID = option.Some(parentTraceID) } return ev } func (tp *traceParser) spanEvent(eventType trace2.EventType) *tracepb2.SpanEvent { defLoc := uint32(tp.UVarint()) goid := uint32(tp.UVarint()) correlationEventID := tp.EventID() ev := &tracepb2.SpanEvent{Goid: goid} if defLoc > 0 { ev.DefLoc = &defLoc } if correlationEventID > 0 { ev.CorrelationEventId = (*uint64)(&correlationEventID) } switch eventType { case trace2.RPCCallStart: ev.Data = &tracepb2.SpanEvent_RpcCallStart{RpcCallStart: tp.rpcCallStart()} case trace2.RPCCallEnd: ev.Data = &tracepb2.SpanEvent_RpcCallEnd{RpcCallEnd: tp.rpcCallEnd()} case trace2.DBQueryStart: ev.Data = &tracepb2.SpanEvent_DbQueryStart{DbQueryStart: tp.dbQueryStart()} case trace2.DBQueryEnd: ev.Data = &tracepb2.SpanEvent_DbQueryEnd{DbQueryEnd: tp.dbQueryEnd()} case trace2.DBTransactionStart: ev.Data = &tracepb2.SpanEvent_DbTransactionStart{DbTransactionStart: tp.dbTransactionStart()} case trace2.DBTransactionEnd: ev.Data = &tracepb2.SpanEvent_DbTransactionEnd{DbTransactionEnd: tp.dbTransactionEnd()} case trace2.PubsubPublishStart: ev.Data = &tracepb2.SpanEvent_PubsubPublishStart{PubsubPublishStart: tp.pubsubPublishStart()} case trace2.PubsubPublishEnd: ev.Data = &tracepb2.SpanEvent_PubsubPublishEnd{PubsubPublishEnd: tp.pubsubPublishEnd()} case trace2.HTTPCallStart: ev.Data = &tracepb2.SpanEvent_HttpCallStart{HttpCallStart: tp.httpCallStart()} case trace2.HTTPCallEnd: ev.Data = &tracepb2.SpanEvent_HttpCallEnd{HttpCallEnd: tp.httpCallEnd()} case trace2.LogMessage: ev.Data = &tracepb2.SpanEvent_LogMessage{LogMessage: tp.logMessage()} case trace2.ServiceInitStart: ev.Data = &tracepb2.SpanEvent_ServiceInitStart{ServiceInitStart: tp.serviceInitStart()} case trace2.ServiceInitEnd: ev.Data = &tracepb2.SpanEvent_ServiceInitEnd{ServiceInitEnd: tp.serviceInitEnd()} case trace2.CacheCallStart: ev.Data = &tracepb2.SpanEvent_CacheCallStart{CacheCallStart: tp.cacheCallStart()} case trace2.CacheCallEnd: ev.Data = &tracepb2.SpanEvent_CacheCallEnd{CacheCallEnd: tp.cacheCallEnd()} case trace2.BodyStream: ev.Data = &tracepb2.SpanEvent_BodyStream{BodyStream: tp.bodyStream()} case trace2.BucketObjectUploadStart: ev.Data = &tracepb2.SpanEvent_BucketObjectUploadStart{BucketObjectUploadStart: tp.bucketObjectUploadStart()} case trace2.BucketObjectUploadEnd: ev.Data = &tracepb2.SpanEvent_BucketObjectUploadEnd{BucketObjectUploadEnd: tp.bucketObjectUploadEnd()} case trace2.BucketObjectDownloadStart: ev.Data = &tracepb2.SpanEvent_BucketObjectDownloadStart{BucketObjectDownloadStart: tp.bucketObjectDownloadStart()} case trace2.BucketObjectDownloadEnd: ev.Data = &tracepb2.SpanEvent_BucketObjectDownloadEnd{BucketObjectDownloadEnd: tp.bucketObjectDownloadEnd()} case trace2.BucketObjectGetAttrsStart: ev.Data = &tracepb2.SpanEvent_BucketObjectGetAttrsStart{BucketObjectGetAttrsStart: tp.bucketObjectGetAttrsStart()} case trace2.BucketObjectGetAttrsEnd: ev.Data = &tracepb2.SpanEvent_BucketObjectGetAttrsEnd{BucketObjectGetAttrsEnd: tp.bucketObjectGetAttrsEnd()} case trace2.BucketListObjectsStart: ev.Data = &tracepb2.SpanEvent_BucketListObjectsStart{BucketListObjectsStart: tp.bucketListObjectsStart()} case trace2.BucketListObjectsEnd: ev.Data = &tracepb2.SpanEvent_BucketListObjectsEnd{BucketListObjectsEnd: tp.bucketListObjectsEnd()} case trace2.BucketDeleteObjectsStart: ev.Data = &tracepb2.SpanEvent_BucketDeleteObjectsStart{BucketDeleteObjectsStart: tp.bucketDeleteObjectsStart()} case trace2.BucketDeleteObjectsEnd: ev.Data = &tracepb2.SpanEvent_BucketDeleteObjectsEnd{BucketDeleteObjectsEnd: tp.bucketDeleteObjectsEnd()} default: tp.bailout(fmt.Errorf("unknown event %v", eventType)) } return ev } func (tp *traceParser) requestSpanStart() *tracepb2.SpanStart { spanStart := tp.spanStartEvent() start := &tracepb2.SpanStart{ Goid: spanStart.Goid, ParentTraceId: spanStart.ParentTraceID.GetOrElse(nil), ParentSpanId: spanStart.ParentSpanID.PtrOrNil(), DefLoc: spanStart.DefLoc.PtrOrNil(), CallerEventId: (*uint64)(spanStart.CallerEventID.PtrOrNil()), ExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(), Data: &tracepb2.SpanStart_Request{ Request: &tracepb2.RequestSpanStart{ ServiceName: tp.String(), EndpointName: tp.String(), HttpMethod: tp.String(), Path: tp.String(), PathParams: (func() []string { n := tp.UVarint() if n == 0 { return nil } params := make([]string, n) for i := 0; i < int(n); i++ { params[i] = tp.String() } return params })(), RequestHeaders: tp.headers(), RequestPayload: tp.ByteString(), ExtCorrelationId: ptrOrNil(tp.String()), Uid: ptrOrNil(tp.String()), Mocked: tp.FromVer(15).Bool(false), }, }, } return start } func (tp *traceParser) requestSpanEnd() *tracepb2.SpanEnd { spanEnd := tp.spanEndEvent() return &tracepb2.SpanEnd{ DurationNanos: spanEnd.DurationNanos, StatusCode: spanEnd.StatusCode, Error: spanEnd.Err, PanicStack: spanEnd.PanicStack.GetOrElse(nil), ParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil), ParentSpanId: spanEnd.ParentSpanID.PtrOrNil(), Data: &tracepb2.SpanEnd_Request{ Request: &tracepb2.RequestSpanEnd{ ServiceName: tp.String(), EndpointName: tp.String(), HttpStatusCode: uint32(tp.UVarint()), ResponseHeaders: tp.headers(), ResponsePayload: tp.ByteString(), CallerEventId: (func() *uint64 { if tp.version >= 16 { id := uint64(tp.EventID()) return &id } return nil })(), Uid: (func() *string { if tp.version >= 17 { return tp.OptString() } return nil })(), }, }, } } func (tp *traceParser) authSpanStart() *tracepb2.SpanStart { spanStart := tp.spanStartEvent() return &tracepb2.SpanStart{ Goid: spanStart.Goid, ParentTraceId: spanStart.ParentTraceID.GetOrElse(nil), ParentSpanId: spanStart.ParentSpanID.PtrOrNil(), DefLoc: spanStart.DefLoc.PtrOrNil(), CallerEventId: (*uint64)(spanStart.CallerEventID.PtrOrNil()), ExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(), Data: &tracepb2.SpanStart_Auth{ Auth: &tracepb2.AuthSpanStart{ ServiceName: tp.String(), EndpointName: tp.String(), AuthPayload: tp.ByteString(), }, }, } } func (tp *traceParser) authSpanEnd() *tracepb2.SpanEnd { spanEnd := tp.spanEndEvent() return &tracepb2.SpanEnd{ DurationNanos: spanEnd.DurationNanos, StatusCode: spanEnd.StatusCode, Error: spanEnd.Err, PanicStack: spanEnd.PanicStack.GetOrElse(nil), ParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil), ParentSpanId: spanEnd.ParentSpanID.PtrOrNil(), Data: &tracepb2.SpanEnd_Auth{ Auth: &tracepb2.AuthSpanEnd{ ServiceName: tp.String(), EndpointName: tp.String(), Uid: tp.String(), UserData: tp.ByteString(), }, }, } } func (tp *traceParser) pubsubMessageSpanStart() *tracepb2.SpanStart { spanStart := tp.spanStartEvent() return &tracepb2.SpanStart{ Goid: spanStart.Goid, ParentTraceId: spanStart.ParentTraceID.GetOrElse(nil), ParentSpanId: spanStart.ParentSpanID.PtrOrNil(), DefLoc: spanStart.DefLoc.PtrOrNil(), CallerEventId: (*uint64)(spanStart.CallerEventID.PtrOrNil()), ExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(), Data: &tracepb2.SpanStart_PubsubMessage{ PubsubMessage: &tracepb2.PubsubMessageSpanStart{ ServiceName: tp.String(), TopicName: tp.String(), SubscriptionName: tp.String(), MessageId: tp.String(), Attempt: uint32(tp.UVarint()), PublishTime: tp.Time(), // TODO use nanotime MessagePayload: tp.ByteString(), }, }, } } func (tp *traceParser) pubsubMessageSpanEnd() *tracepb2.SpanEnd { spanEnd := tp.spanEndEvent() return &tracepb2.SpanEnd{ DurationNanos: spanEnd.DurationNanos, StatusCode: spanEnd.StatusCode, Error: spanEnd.Err, PanicStack: spanEnd.PanicStack.GetOrElse(nil), ParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil), ParentSpanId: spanEnd.ParentSpanID.PtrOrNil(), Data: &tracepb2.SpanEnd_PubsubMessage{ PubsubMessage: &tracepb2.PubsubMessageSpanEnd{ ServiceName: tp.String(), TopicName: tp.String(), SubscriptionName: tp.String(), MessageId: (func() string { if tp.version >= 17 { return tp.String() } return "" })(), }, }, } } func (tp *traceParser) testSpanStart() *tracepb2.SpanStart { spanStart := tp.spanStartEvent() return &tracepb2.SpanStart{ Goid: spanStart.Goid, ParentTraceId: spanStart.ParentTraceID.GetOrElse(nil), ParentSpanId: spanStart.ParentSpanID.PtrOrNil(), DefLoc: spanStart.DefLoc.PtrOrNil(), CallerEventId: (*uint64)(spanStart.CallerEventID.PtrOrNil()), ExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(), Data: &tracepb2.SpanStart_Test{ Test: &tracepb2.TestSpanStart{ ServiceName: tp.String(), TestName: tp.String(), Uid: tp.String(), TestFile: tp.String(), TestLine: tp.Uint32(), }, }, } } func (tp *traceParser) testSpanEnd() *tracepb2.SpanEnd { spanEnd := tp.spanEndEvent() return &tracepb2.SpanEnd{ DurationNanos: spanEnd.DurationNanos, StatusCode: spanEnd.StatusCode, Error: spanEnd.Err, PanicStack: spanEnd.PanicStack.GetOrElse(nil), ParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil), ParentSpanId: spanEnd.ParentSpanID.PtrOrNil(), Data: &tracepb2.SpanEnd_Test{ Test: &tracepb2.TestSpanEnd{ ServiceName: tp.String(), TestName: tp.String(), Failed: tp.Bool(), Skipped: tp.Bool(), Uid: (func() *string { if tp.version >= 17 { return ptrOrNil(tp.String()) } return nil })(), }, }, } } func (tp *traceParser) rpcCallStart() *tracepb2.RPCCallStart { return &tracepb2.RPCCallStart{ TargetServiceName: tp.String(), TargetEndpointName: tp.String(), Stack: tp.stack(), } } func (tp *traceParser) rpcCallEnd() *tracepb2.RPCCallEnd { return &tracepb2.RPCCallEnd{ Err: tp.errWithStack(), } } func (tp *traceParser) dbQueryStart() *tracepb2.DBQueryStart { return &tracepb2.DBQueryStart{ Query: tp.String(), Stack: tp.stack(), } } func (tp *traceParser) dbQueryEnd() *tracepb2.DBQueryEnd { return &tracepb2.DBQueryEnd{ Err: tp.errWithStack(), } } func (tp *traceParser) dbTransactionStart() *tracepb2.DBTransactionStart { return &tracepb2.DBTransactionStart{ Stack: tp.stack(), } } func (tp *traceParser) dbTransactionEnd() *tracepb2.DBTransactionEnd { return &tracepb2.DBTransactionEnd{ Completion: (func() tracepb2.DBTransactionEnd_CompletionType { if commit := tp.Bool(); commit { return tracepb2.DBTransactionEnd_COMMIT } else { return tracepb2.DBTransactionEnd_ROLLBACK } })(), Stack: tp.stack(), Err: tp.errWithStack(), } } func (tp *traceParser) pubsubPublishStart() *tracepb2.PubsubPublishStart { return &tracepb2.PubsubPublishStart{ Topic: tp.String(), Message: tp.ByteString(), Stack: tp.stack(), } } func (tp *traceParser) pubsubPublishEnd() *tracepb2.PubsubPublishEnd { return &tracepb2.PubsubPublishEnd{ MessageId: ptrOrNil(tp.String()), Err: tp.errWithStack(), } } func (tp *traceParser) serviceInitStart() *tracepb2.ServiceInitStart { return &tracepb2.ServiceInitStart{ Service: tp.String(), } } func (tp *traceParser) serviceInitEnd() *tracepb2.ServiceInitEnd { return &tracepb2.ServiceInitEnd{ Err: tp.errWithStack(), } } func (tp *traceParser) httpCallStart() *tracepb2.HTTPCallStart { return &tracepb2.HTTPCallStart{ CorrelationParentSpanId: tp.Uint64(), Method: tp.String(), Url: tp.String(), Stack: tp.stack(), StartNanotime: tp.Int64(), } } func (tp *traceParser) httpCallEnd() *tracepb2.HTTPCallEnd { return &tracepb2.HTTPCallEnd{ StatusCode: ptrOrNil(uint32(tp.UVarint())), Err: tp.errWithStack(), TraceEvents: (func() []*tracepb2.HTTPTraceEvent { n := tp.UVarint() events := make([]*tracepb2.HTTPTraceEvent, 0, n) for i := 0; i < int(n); i++ { if ev := tp.httpEvent(); ev != nil { events = append(events, ev) } } return events })(), } } func (tp *traceParser) cacheCallStart() *tracepb2.CacheCallStart { return &tracepb2.CacheCallStart{ Operation: tp.String(), Write: tp.Bool(), Stack: tp.stack(), Keys: (func() []string { n := tp.UVarint() keys := make([]string, n) for i := 0; i < int(n); i++ { keys[i] = tp.String() } return keys })(), } } func (tp *traceParser) cacheCallEnd() *tracepb2.CacheCallEnd { return &tracepb2.CacheCallEnd{ Result: (func() tracepb2.CacheCallEnd_Result { res := tp.Byte() switch trace2.CacheCallResult(res) { case trace2.CacheOK: return tracepb2.CacheCallEnd_OK case trace2.CacheNoSuchKey: return tracepb2.CacheCallEnd_NO_SUCH_KEY case trace2.CacheConflict: return tracepb2.CacheCallEnd_CONFLICT case trace2.CacheErr: return tracepb2.CacheCallEnd_ERR default: return tracepb2.CacheCallEnd_UNKNOWN } })(), Err: tp.errWithStack(), } } func (tp *traceParser) bucketObjectUploadStart() *tracepb2.BucketObjectUploadStart { return &tracepb2.BucketObjectUploadStart{ Bucket: tp.String(), Object: tp.String(), Attrs: tp.bucketObjectAttrs(), Stack: tp.stack(), } } func (tp *traceParser) bucketObjectAttrs() *tracepb2.BucketObjectAttributes { return &tracepb2.BucketObjectAttributes{ Size: tp.OptUVarint(), Version: tp.OptString(), Etag: tp.OptString(), ContentType: tp.OptString(), } } func (tp *traceParser) bucketObjectUploadEnd() *tracepb2.BucketObjectUploadEnd { return &tracepb2.BucketObjectUploadEnd{ Size: tp.OptUVarint(), Version: tp.OptString(), Err: tp.errWithStack(), } } func (tp *traceParser) bucketObjectDownloadStart() *tracepb2.BucketObjectDownloadStart { return &tracepb2.BucketObjectDownloadStart{ Bucket: tp.String(), Object: tp.String(), Version: tp.OptString(), Stack: tp.stack(), } } func (tp *traceParser) bucketObjectDownloadEnd() *tracepb2.BucketObjectDownloadEnd { return &tracepb2.BucketObjectDownloadEnd{ Size: tp.OptUVarint(), Err: tp.errWithStack(), } } func (tp *traceParser) bucketDeleteObjectsStart() *tracepb2.BucketDeleteObjectsStart { ev := &tracepb2.BucketDeleteObjectsStart{ Bucket: tp.String(), Stack: tp.stack(), } num := tp.UVarint() for i := 0; i < int(num); i++ { ev.Entries = append(ev.Entries, &tracepb2.BucketDeleteObjectEntry{ Object: tp.String(), Version: tp.OptString(), }) } return ev } func (tp *traceParser) bucketDeleteObjectsEnd() *tracepb2.BucketDeleteObjectsEnd { return &tracepb2.BucketDeleteObjectsEnd{ Err: tp.errWithStack(), } } func (tp *traceParser) bucketListObjectsStart() *tracepb2.BucketListObjectsStart { return &tracepb2.BucketListObjectsStart{ Bucket: tp.String(), Prefix: tp.OptString(), Stack: tp.stack(), } } func (tp *traceParser) bucketListObjectsEnd() *tracepb2.BucketListObjectsEnd { return &tracepb2.BucketListObjectsEnd{ Err: tp.errWithStack(), Observed: tp.UVarint(), HasMore: tp.Bool(), } } func (tp *traceParser) bucketObjectGetAttrsStart() *tracepb2.BucketObjectGetAttrsStart { return &tracepb2.BucketObjectGetAttrsStart{ Bucket: tp.String(), Object: tp.String(), Version: tp.OptString(), Stack: tp.stack(), } } func (tp *traceParser) bucketObjectGetAttrsEnd() *tracepb2.BucketObjectGetAttrsEnd { ev := &tracepb2.BucketObjectGetAttrsEnd{ Err: tp.errWithStack(), } if ev.Err == nil { ev.Attrs = tp.bucketObjectAttrs() } return ev } func (tp *traceParser) bodyStream() *tracepb2.BodyStream { flags := tp.Byte() data := tp.ByteString() return &tracepb2.BodyStream{ IsResponse: flags&0b01 == 0b01, Overflowed: flags&0b10 == 0b10, Data: data, } } func (tp *traceParser) headers() map[string]string { n := tp.UVarint() if n == 0 { return nil } headers := make(map[string]string, n) for i := 0; i < int(n); i++ { headers[tp.String()] = tp.String() } return headers } func (tp *traceParser) httpEvent() *tracepb2.HTTPTraceEvent { code := trace2.HTTPEventCode(tp.Byte()) ev := &tracepb2.HTTPTraceEvent{ Nanotime: tp.Int64(), } switch code { case trace2.GetConn: ev.Data = &tracepb2.HTTPTraceEvent_GetConn{ GetConn: &tracepb2.HTTPGetConn{ HostPort: tp.String(), }, } case trace2.GotConn: ev.Data = &tracepb2.HTTPTraceEvent_GotConn{ GotConn: &tracepb2.HTTPGotConn{ Reused: tp.Bool(), WasIdle: tp.Bool(), IdleDurationNs: tp.Int64(), }, } case trace2.GotFirstResponseByte: ev.Data = &tracepb2.HTTPTraceEvent_GotFirstResponseByte{ GotFirstResponseByte: &tracepb2.HTTPGotFirstResponseByte{ // No data }, } case trace2.Got1xxResponse: ev.Data = &tracepb2.HTTPTraceEvent_Got_1XxResponse{ Got_1XxResponse: &tracepb2.HTTPGot1XxResponse{ Code: int32(tp.Varint()), }, } case trace2.DNSStart: ev.Data = &tracepb2.HTTPTraceEvent_DnsStart{ DnsStart: &tracepb2.HTTPDNSStart{ Host: tp.String(), }, } case trace2.DNSDone: data := &tracepb2.HTTPDNSDone{ Err: tp.ByteString(), } addrs := int(tp.UVarint()) for j := 0; j < addrs; j++ { data.Addrs = append(data.Addrs, &tracepb2.DNSAddr{ Ip: tp.ByteString(), }) } ev.Data = &tracepb2.HTTPTraceEvent_DnsDone{DnsDone: data} case trace2.ConnectStart: ev.Data = &tracepb2.HTTPTraceEvent_ConnectStart{ ConnectStart: &tracepb2.HTTPConnectStart{ Network: tp.String(), Addr: tp.String(), }, } case trace2.ConnectDone: ev.Data = &tracepb2.HTTPTraceEvent_ConnectDone{ ConnectDone: &tracepb2.HTTPConnectDone{ Network: tp.String(), Addr: tp.String(), Err: tp.ByteString(), }, } case trace2.TLSHandshakeStart: ev.Data = &tracepb2.HTTPTraceEvent_TlsHandshakeStart{ TlsHandshakeStart: &tracepb2.HTTPTLSHandshakeStart{ // No data }, } case trace2.TLSHandshakeDone: ev.Data = &tracepb2.HTTPTraceEvent_TlsHandshakeDone{ TlsHandshakeDone: &tracepb2.HTTPTLSHandshakeDone{ Err: tp.ByteString(), TlsVersion: tp.Uint32(), CipherSuite: tp.Uint32(), ServerName: tp.String(), NegotiatedProtocol: tp.String(), }, } case trace2.WroteHeaders: ev.Data = &tracepb2.HTTPTraceEvent_WroteHeaders{ WroteHeaders: &tracepb2.HTTPWroteHeaders{ // No data }, } case trace2.WroteRequest: ev.Data = &tracepb2.HTTPTraceEvent_WroteRequest{ WroteRequest: &tracepb2.HTTPWroteRequest{ Err: tp.ByteString(), }, } case trace2.Wait100Continue: // no data ev.Data = &tracepb2.HTTPTraceEvent_Wait_100Continue{ Wait_100Continue: &tracepb2.HTTPWait100Continue{ // No data }, } case trace2.ClosedBody: ev.Data = &tracepb2.HTTPTraceEvent_ClosedBody{ ClosedBody: &tracepb2.HTTPClosedBodyData{ Err: tp.ByteString(), }, } default: // TODO bailout tp.log.Error().Int32("code", int32(code)).Msg("unknown http event code") return nil } return ev } func (tp *traceParser) logMessage() *tracepb2.LogMessage { return &tracepb2.LogMessage{ Level: (func() tracepb2.LogMessage_Level { switch model.LogLevel(tp.Byte()) { case model.LevelTrace: return tracepb2.LogMessage_TRACE case model.LevelDebug: return tracepb2.LogMessage_DEBUG case model.LevelInfo: return tracepb2.LogMessage_INFO case model.LevelWarn: return tracepb2.LogMessage_WARN case model.LevelError: return tracepb2.LogMessage_ERROR default: return tracepb2.LogMessage_TRACE } })(), Msg: tp.String(), Fields: (func() []*tracepb2.LogField { n := int(tp.UVarint()) if n > 64 { // TODO bailout } fields := make([]*tracepb2.LogField, 0, n) for i := 0; i < n; i++ { fields = append(fields, tp.logField()) } return fields })(), Stack: tp.stack(), } } func (tp *traceParser) logField() *tracepb2.LogField { typ := model.LogFieldType(tp.Byte()) f := &tracepb2.LogField{ Key: tp.String(), } switch typ { case model.ErrField: f.Value = &tracepb2.LogField_Error{Error: tp.errWithStack()} case model.StringField: f.Value = &tracepb2.LogField_Str{Str: tp.String()} case model.BoolField: f.Value = &tracepb2.LogField_Bool{Bool: tp.Bool()} case model.TimeField: f.Value = &tracepb2.LogField_Time{Time: tp.Time()} case model.DurationField: f.Value = &tracepb2.LogField_Dur{Dur: tp.Int64()} case model.UUIDField: b := make([]byte, 16) tp.Bytes(b) f.Value = &tracepb2.LogField_Uuid{Uuid: b} case model.JSONField: val := tp.ByteString() err := tp.errWithStack() if err != nil { f.Value = &tracepb2.LogField_Error{Error: err} } else { f.Value = &tracepb2.LogField_Json{Json: val} } case model.IntField: f.Value = &tracepb2.LogField_Int{Int: tp.Varint()} case model.UintField: f.Value = &tracepb2.LogField_Uint{Uint: tp.UVarint()} case model.Float32Field: f.Value = &tracepb2.LogField_Float32{Float32: tp.Float32()} case model.Float64Field: f.Value = &tracepb2.LogField_Float64{Float64: tp.Float64()} default: // TODO bailout tp.log.Error().Msgf("unknown log field type %v", typ) return nil } return f } func (tp *traceParser) stack() *tracepb2.StackTrace { n := int(tp.Byte()) if n == 0 { return nil } tr := &tracepb2.StackTrace{} diffs := make([]int64, n) for i := 0; i < n; i++ { diff := tp.Varint() diffs[i] = diff } tr.Pcs = diffs prev := int64(0) pcs := make([]uint64, n) for i := 0; i < n; i++ { x := prev + diffs[i] prev = x pcs[i] = uint64(x) } return tr } func (tp *traceParser) formattedStack() *tracepb2.StackTrace { n := int(tp.Byte()) if n == 0 { return nil } tr := &tracepb2.StackTrace{ Frames: make([]*tracepb2.StackFrame, n), } for i := 0; i < n; i++ { tr.Frames[i] = &tracepb2.StackFrame{ Filename: tp.String(), Line: int32(tp.UVarint()), Func: tp.String(), } } return tr } // statusCode parses a status code. func (tp *traceParser) statusCode() tracepb2.StatusCode { return tracepb2.StatusCode(tp.Byte()) } // errWithStack parses an error with stack information. func (tp *traceParser) errWithStack() *tracepb2.Error { msg := tp.String() if len(msg) == 0 { return nil } stack := tp.stack() return &tracepb2.Error{ Msg: msg, Stack: stack, } } func (tp *traceParser) traceID() *tracepb2.TraceID { var traceID [16]byte tp.Bytes(traceID[:]) return &tracepb2.TraceID{ Low: bin.Uint64(traceID[:8]), High: bin.Uint64(traceID[8:]), } } func (tp *traceParser) spanID() uint64 { var spanID [8]byte tp.Bytes(spanID[:]) return bin.Uint64(spanID[:]) } type bailout struct { err error } func (tp *traceParser) bailout(err error) { panic(bailout{err: err}) } // httpStatusToStatusCode converts an HTTP status code to a tracepb2.StatusCode. func httpStatusToStatusCode(status uint32) tracepb2.StatusCode { switch status { case 200: return tracepb2.StatusCode_STATUS_CODE_OK case 499: return tracepb2.StatusCode_STATUS_CODE_CANCELED case 500: return tracepb2.StatusCode_STATUS_CODE_INTERNAL case 400: return tracepb2.StatusCode_STATUS_CODE_INVALID_ARGUMENT case 401: return tracepb2.StatusCode_STATUS_CODE_UNAUTHENTICATED case 403: return tracepb2.StatusCode_STATUS_CODE_PERMISSION_DENIED case 404: return tracepb2.StatusCode_STATUS_CODE_NOT_FOUND case 409: return tracepb2.StatusCode_STATUS_CODE_ALREADY_EXISTS case 429: return tracepb2.StatusCode_STATUS_CODE_RESOURCE_EXHAUSTED case 501: return tracepb2.StatusCode_STATUS_CODE_UNIMPLEMENTED case 503: return tracepb2.StatusCode_STATUS_CODE_UNAVAILABLE case 504: return tracepb2.StatusCode_STATUS_CODE_DEADLINE_EXCEEDED default: if status >= 200 && status < 300 { return tracepb2.StatusCode_STATUS_CODE_OK } return tracepb2.StatusCode_STATUS_CODE_UNKNOWN } } ================================================ FILE: pkg/traceparser/parser_test.go ================================================ package traceparser import ( "bufio" "bytes" "errors" "net/http" "testing" "time" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" "encore.dev/appruntime/exported/model" "encore.dev/appruntime/exported/stack" "encore.dev/appruntime/exported/trace2" "encore.dev/types/uuid" tracepb2 "encr.dev/proto/encore/engine/trace2" ) func TestParse(t *testing.T) { traceID := model.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} spanID := model.SpanID{8, 7, 6, 5, 4, 3, 2, 1} now := time.Now() err := errors.New("some-error") goid := uint32(123) defLoc := uint32(456) udefLoc := uint32(defLoc) // for compat uuidVal := uuid.UUID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} pbNow := timestamppb.New(now) pbTraceID := &tracepb2.TraceID{High: 1157159078456920585, Low: 578437695752307201} pbSpanID := uint64(72623859790382856) pbErr := &tracepb2.Error{Msg: "some-error"} pbUUID := uuidVal.Bytes() ep := trace2.EventParams{TraceID: traceID, SpanID: spanID, Goid: goid, DefLoc: defLoc} tests := []struct { Name string Emit func(l *trace2.Log) Want *tracepb2.TraceEvent }{ { Name: "RequestSpanStart", Emit: func(l *trace2.Log) { l.RequestSpanStart(&model.Request{ Type: model.RPCCall, TraceID: traceID, SpanID: spanID, ParentSpanID: model.SpanID{}, Start: now, Traced: true, DefLoc: defLoc, RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", Raw: false, }, HTTPMethod: "POST", Path: "/path/hello", PathParams: model.PathParams{{Name: "one", Value: "hello"}}, UserID: "userid", AuthData: nil, NonRawPayload: []byte(`{"Body":"foo"}`), RequestHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, }, goid) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{ ParentTraceId: nil, ParentSpanId: nil, ExternalCorrelationId: nil, DefLoc: &udefLoc, Goid: goid, Data: &tracepb2.SpanStart_Request{ Request: &tracepb2.RequestSpanStart{ ServiceName: "service", EndpointName: "endpoint", HttpMethod: "POST", Path: "/path/hello", PathParams: []string{"hello"}, RequestHeaders: map[string]string{"Content-Type": "application/json"}, RequestPayload: []byte(`{"Body":"foo"}`), ExtCorrelationId: nil, Uid: ptr("userid"), }, }, }}, }, }, { Name: "RequestSpanEnd", Emit: func(l *trace2.Log) { l.RequestSpanEnd(trace2.RequestSpanEndParams{ EventParams: ep, Req: &model.Request{ RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", }, }, }, Resp: &model.Response{ HTTPStatus: 123, Err: err, Payload: []byte("payload"), RawResponseHeaders: map[string][]string{"Content-Type": {"application/json"}}, }, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{ StatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN, Error: pbErr, Data: &tracepb2.SpanEnd_Request{ Request: &tracepb2.RequestSpanEnd{ ServiceName: "service", EndpointName: "endpoint", HttpStatusCode: 123, ResponseHeaders: map[string]string{"Content-Type": "application/json"}, ResponsePayload: []byte("payload"), CallerEventId: ptr(uint64(0)), Uid: nil, // ptrOrNil returns nil for empty strings }, }, }}, }, }, { Name: "AuthSpanStart", Emit: func(l *trace2.Log) { l.AuthSpanStart(&model.Request{ Type: model.AuthHandler, TraceID: traceID, SpanID: spanID, ParentSpanID: model.SpanID{}, Start: now, Traced: true, DefLoc: defLoc, RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", Raw: false, }, NonRawPayload: []byte(`{"Body":"foo"}`), }, }, goid) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{ ParentTraceId: nil, ParentSpanId: nil, ExternalCorrelationId: nil, DefLoc: &udefLoc, Goid: goid, Data: &tracepb2.SpanStart_Auth{ Auth: &tracepb2.AuthSpanStart{ ServiceName: "service", EndpointName: "endpoint", AuthPayload: []byte(`{"Body":"foo"}`), }, }, }}, }, }, { Name: "AuthSpanEnd", Emit: func(l *trace2.Log) { l.AuthSpanEnd(trace2.AuthSpanEndParams{ EventParams: ep, Req: &model.Request{ RPCData: &model.RPCData{ Desc: &model.RPCDesc{ Service: "service", Endpoint: "endpoint", }, }, }, Resp: &model.Response{ HTTPStatus: 123, AuthUID: "userid", Err: err, Payload: []byte("payload"), }, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{ StatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN, Error: pbErr, Data: &tracepb2.SpanEnd_Auth{ Auth: &tracepb2.AuthSpanEnd{ ServiceName: "service", EndpointName: "endpoint", Uid: "userid", UserData: []byte("payload"), }, }, }}, }, }, { Name: "PubsubMessageSpanStart", Emit: func(l *trace2.Log) { l.PubsubMessageSpanStart(&model.Request{ Type: model.PubSubMessage, TraceID: traceID, SpanID: spanID, ParentSpanID: model.SpanID{}, Start: now, Traced: true, DefLoc: defLoc, MsgData: &model.PubSubMsgData{ Desc: &model.PubSubSubscriptionDesc{ Service: "service", Topic: "topic", Subscription: "subscription", }, MessageID: "message-id", Attempt: 3, Published: now, Payload: []byte("payload"), }, }, goid) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{ ParentTraceId: nil, ParentSpanId: nil, ExternalCorrelationId: nil, DefLoc: &udefLoc, Goid: goid, Data: &tracepb2.SpanStart_PubsubMessage{ PubsubMessage: &tracepb2.PubsubMessageSpanStart{ ServiceName: "service", TopicName: "topic", SubscriptionName: "subscription", MessageId: "message-id", Attempt: 3, PublishTime: pbNow, MessagePayload: []byte("payload"), }, }, }}, }, }, { Name: "PubsubMessageSpanEnd", Emit: func(l *trace2.Log) { l.PubsubMessageSpanEnd(trace2.PubsubMessageSpanEndParams{ EventParams: ep, Req: &model.Request{ MsgData: &model.PubSubMsgData{ Desc: &model.PubSubSubscriptionDesc{ Service: "service", Topic: "topic", Subscription: "subscription", }, }, }, Resp: &model.Response{Err: err}, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{ StatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN, Error: pbErr, Data: &tracepb2.SpanEnd_PubsubMessage{ PubsubMessage: &tracepb2.PubsubMessageSpanEnd{ ServiceName: "service", TopicName: "topic", SubscriptionName: "subscription", MessageId: "", }, }, }}, }, }, { Name: "RPCCallStart", Emit: func(l *trace2.Log) { l.RPCCallStart(&model.APICall{ Source: &model.Request{TraceID: traceID, SpanID: spanID}, TargetServiceName: "service", TargetEndpointName: "endpoint", DefLoc: defLoc, StartEventID: 0, }, goid) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_RpcCallStart{ RpcCallStart: &tracepb2.RPCCallStart{ TargetServiceName: "service", TargetEndpointName: "endpoint", Stack: nil, }, }, }}, }, }, { Name: "RPCCallEnd", Emit: func(l *trace2.Log) { l.RPCCallEnd(&model.APICall{ Source: &model.Request{TraceID: traceID, SpanID: spanID}, DefLoc: defLoc, StartEventID: 1, }, goid, err) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_RpcCallEnd{ RpcCallEnd: &tracepb2.RPCCallEnd{ Err: pbErr, }, }, }}, }, }, { Name: "DBQueryStart", Emit: func(l *trace2.Log) { l.DBQueryStart(trace2.DBQueryStartParams{ EventParams: ep, TxStartID: 1, Query: "query", }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_DbQueryStart{ DbQueryStart: &tracepb2.DBQueryStart{ Query: "query", }, }, }}, }, }, { Name: "DBQueryEnd", Emit: func(l *trace2.Log) { l.DBQueryEnd(ep, 1, err) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_DbQueryEnd{ DbQueryEnd: &tracepb2.DBQueryEnd{ Err: pbErr, }, }, }}, }, }, { Name: "DBTransactionStart", Emit: func(l *trace2.Log) { l.DBTransactionStart(ep, stack.Stack{}) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_DbTransactionStart{ DbTransactionStart: &tracepb2.DBTransactionStart{}, }, }}, }, }, { Name: "DBTransactionEnd", Emit: func(l *trace2.Log) { l.DBTransactionEnd(trace2.DBTransactionEndParams{ EventParams: ep, StartID: 1, Commit: true, Err: err, Stack: stack.Stack{}, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_DbTransactionEnd{ DbTransactionEnd: &tracepb2.DBTransactionEnd{ Completion: tracepb2.DBTransactionEnd_COMMIT, Err: pbErr, Stack: nil, }, }, }}, }, }, { Name: "PubsubPublishStart", Emit: func(l *trace2.Log) { l.PubsubPublishStart(trace2.PubsubPublishStartParams{ EventParams: ep, Desc: &model.PubSubTopicDesc{ Topic: "topic", }, Message: []byte("message"), Stack: stack.Stack{}, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_PubsubPublishStart{ PubsubPublishStart: &tracepb2.PubsubPublishStart{ Topic: "topic", Message: []byte("message"), Stack: nil, }, }, }}, }, }, { Name: "PubsubPublishEnd", Emit: func(l *trace2.Log) { l.PubsubPublishEnd(trace2.PubsubPublishEndParams{ EventParams: ep, StartID: 1, MessageID: "message-id", Err: err, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_PubsubPublishEnd{ PubsubPublishEnd: &tracepb2.PubsubPublishEnd{ MessageId: ptr("message-id"), Err: pbErr, }, }, }}, }, }, { Name: "ServiceInitStart", Emit: func(l *trace2.Log) { l.ServiceInitStart(trace2.ServiceInitStartParams{ EventParams: ep, Service: "service", }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_ServiceInitStart{ ServiceInitStart: &tracepb2.ServiceInitStart{ Service: "service", }, }, }}, }, }, { Name: "ServiceInitEnd", Emit: func(l *trace2.Log) { l.ServiceInitEnd(ep, 1, err) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_ServiceInitEnd{ ServiceInitEnd: &tracepb2.ServiceInitEnd{ Err: pbErr, }, }, }}, }, }, { Name: "CacheCallStart", Emit: func(l *trace2.Log) { l.CacheCallStart(trace2.CacheCallStartParams{ EventParams: ep, Operation: "operation", IsWrite: true, Keys: []string{"one", "two"}, Stack: stack.Stack{}, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_CacheCallStart{ CacheCallStart: &tracepb2.CacheCallStart{ Operation: "operation", Write: true, Keys: []string{"one", "two"}, Stack: nil, }, }, }}, }, }, { Name: "CacheCallEnd", Emit: func(l *trace2.Log) { l.CacheCallEnd(trace2.CacheCallEndParams{ EventParams: ep, StartID: 1, Res: trace2.CacheErr, Err: err, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, CorrelationEventId: ptr[uint64](1), Data: &tracepb2.SpanEvent_CacheCallEnd{ CacheCallEnd: &tracepb2.CacheCallEnd{ Result: tracepb2.CacheCallEnd_ERR, Err: pbErr, }, }, }}, }, }, { Name: "LogMessage", Emit: func(l *trace2.Log) { l.LogMessage(trace2.LogMessageParams{ EventParams: ep, Level: model.LevelWarn, Msg: "message", Stack: stack.Stack{}, Fields: []trace2.LogField{ {Key: "error", Value: err}, {Key: "string", Value: "string"}, {Key: "bool", Value: true}, {Key: "time", Value: now}, {Key: "duration", Value: time.Second}, {Key: "uuid", Value: uuidVal}, {Key: "json", Value: map[string]any{"json": true}}, {Key: "json_err", Value: func() {}}, {Key: "int8", Value: int8(-8)}, {Key: "int16", Value: int16(-16)}, {Key: "int32", Value: int32(-32)}, {Key: "int64", Value: int64(-64)}, {Key: "int", Value: int(-1)}, {Key: "uint8", Value: uint8(8)}, {Key: "uint16", Value: uint16(16)}, {Key: "uint32", Value: uint32(32)}, {Key: "uint64", Value: uint64(64)}, {Key: "uint", Value: uint(1)}, {Key: "float32", Value: float32(1.2)}, {Key: "float64", Value: float64(3.4)}, }, }) }, Want: &tracepb2.TraceEvent{ TraceId: pbTraceID, SpanId: pbSpanID, Event: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{ Goid: goid, DefLoc: &udefLoc, Data: &tracepb2.SpanEvent_LogMessage{ LogMessage: &tracepb2.LogMessage{ Level: tracepb2.LogMessage_WARN, Msg: "message", Stack: nil, Fields: []*tracepb2.LogField{ {Key: "error", Value: &tracepb2.LogField_Error{Error: pbErr}}, {Key: "string", Value: &tracepb2.LogField_Str{Str: "string"}}, {Key: "bool", Value: &tracepb2.LogField_Bool{Bool: true}}, {Key: "time", Value: &tracepb2.LogField_Time{Time: pbNow}}, {Key: "duration", Value: &tracepb2.LogField_Dur{Dur: int64(time.Second)}}, {Key: "uuid", Value: &tracepb2.LogField_Uuid{Uuid: pbUUID}}, {Key: "json", Value: &tracepb2.LogField_Json{Json: []byte(`{"json":true}`)}}, {Key: "json_err", Value: &tracepb2.LogField_Error{Error: &tracepb2.Error{Msg: "json: unsupported type: func()"}}}, {Key: "int8", Value: &tracepb2.LogField_Int{Int: -8}}, {Key: "int16", Value: &tracepb2.LogField_Int{Int: -16}}, {Key: "int32", Value: &tracepb2.LogField_Int{Int: -32}}, {Key: "int64", Value: &tracepb2.LogField_Int{Int: -64}}, {Key: "int", Value: &tracepb2.LogField_Int{Int: -1}}, {Key: "uint8", Value: &tracepb2.LogField_Uint{Uint: 8}}, {Key: "uint16", Value: &tracepb2.LogField_Uint{Uint: 16}}, {Key: "uint32", Value: &tracepb2.LogField_Uint{Uint: 32}}, {Key: "uint64", Value: &tracepb2.LogField_Uint{Uint: 64}}, {Key: "uint", Value: &tracepb2.LogField_Uint{Uint: 1}}, {Key: "float32", Value: &tracepb2.LogField_Float32{Float32: 1.2}}, {Key: "float64", Value: &tracepb2.LogField_Float64{Float64: 3.4}}, }, }, }, }}, }, }, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { log := trace2.NewLog() tt.Emit(log) data, _ := log.GetAndClear() ta := trace2.NewTimeAnchor(0, now) got, err := ParseEvent(bufio.NewReader(bytes.NewReader(data)), ta, 99) if err != nil { t.Fatal(err) } opt := []cmp.Option{ protocmp.Transform(), protocmp.IgnoreFields(&tracepb2.TraceEvent{}, "event_time"), protocmp.IgnoreFields(&tracepb2.TraceEvent{}, "event_id"), protocmp.IgnoreMessages(&tracepb2.StackTrace{}), } if diff := cmp.Diff(tt.Want, got, opt...); diff != "" { t.Errorf("ParseEvent() mismatch (-want +got):\n%s", diff) } }) } } func ptr[T any](val T) *T { return &val } ================================================ FILE: pkg/vcs/app.go ================================================ package vcs import ( "github.com/rs/zerolog/log" ) // GetRevision returns the version control system (VCS) revision information for the Encore application. // // If there is an error getting the revision information, no revision information is returned and the App is flagged as // having uncommitted files. This will happen most likely because no supported VCS system can be found. // // Supported VCS systems include; // - Hg // - Git // - Svn // - Bzr // - Fossil func GetRevision(appRoot string) Status { appRoot, cmd, err := fromDir(appRoot, "", false) if err != nil { log.Err(err).Str("app", appRoot).Msg("unable to determine VCS system") return Status{Uncommitted: true} } status, err := cmd.Status(cmd, appRoot) if err != nil { log.Err(err).Str("app", appRoot).Msg("unable to get VCS status") return Status{Uncommitted: true} } return status } ================================================ FILE: pkg/vcs/vcs.go ================================================ // Simplified version of the version from Go Get: // https://github.com/golang/go/blob/f87e28d1b9ab33491b32255f333f1f1d83eeb6fc/src/cmd/go/internal/vcs/vcs.go // Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package vcs import ( "bytes" "errors" "fmt" "io/fs" urlpkg "net/url" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" ) // A cmd describes how to use a version control system // like Mercurial, Git, or Subversion. type cmd struct { Name string Cmd string // name of binary to invoke command RootNames []string // filename indicating the root of a checkout directory CreateCmd []string // commands to download a fresh copy of a repository DownloadCmd []string // commands to download updates into an existing repository TagCmd []tagCmd // commands to list tags TagLookupCmd []tagCmd // commands to lookup tags before running tagSyncCmd TagSyncCmd []string // commands to sync to specific tag TagSyncDefault []string // commands to sync to default tag Scheme []string PingCmd string RemoteRepo func(v *cmd, rootDir string) (remoteRepo string, err error) ResolveRepo func(v *cmd, rootDir, remoteRepo string) (realRepo string, err error) Status func(v *cmd, rootDir string) (Status, error) } // Status is the current state of a local repository. type Status struct { Revision string // Optional. CommitTime time.Time // Optional. Uncommitted bool // Required. } var defaultSecureScheme = map[string]bool{ "https": true, "git+ssh": true, "bzr+ssh": true, "svn+ssh": true, "ssh": true, } func (v *cmd) IsSecure(repo string) bool { u, err := urlpkg.Parse(repo) if err != nil { // If repo is not a URL, it's not secure. return false } return v.isSecureScheme(u.Scheme) } func (v *cmd) isSecureScheme(scheme string) bool { switch v.Cmd { case "git": // GIT_ALLOW_PROTOCOL is an environment variable defined by Git. It is a // colon-separated list of schemes that are allowed to be used with git // fetch/clone. Any scheme not mentioned will be considered insecure. if allow := os.Getenv("GIT_ALLOW_PROTOCOL"); allow != "" { for _, s := range strings.Split(allow, ":") { if s == scheme { return true } } return false } } return defaultSecureScheme[scheme] } // A tagCmd describes a command to list available tags // that can be passed to tagSyncCmd. type tagCmd struct { cmd string // command to list tags pattern string // regexp to extract tags from list } // vcsList lists the known version control systems var vcsList = []*cmd{ vcsHg, vcsGit, vcsSvn, vcsBzr, vcsFossil, } // vcsHg describes how to use Mercurial. var vcsHg = &cmd{ Name: "Mercurial", Cmd: "hg", RootNames: []string{".hg"}, CreateCmd: []string{"clone -U -- {repo} {dir}"}, DownloadCmd: []string{"pull"}, // We allow both tag and branch names as 'tags' // for selecting a version. This lets people have // a go.release.r60 branch and a go1 branch // and make changes in both, without constantly // editing .hgtags. TagCmd: []tagCmd{ {"tags", `^(\S+)`}, {"branches", `^(\S+)`}, }, TagSyncCmd: []string{"update -r {tag}"}, TagSyncDefault: []string{"update default"}, Scheme: []string{"https", "http", "ssh"}, PingCmd: "identify -- {scheme}://{repo}", RemoteRepo: hgRemoteRepo, Status: hgStatus, } func hgRemoteRepo(vcsHg *cmd, rootDir string) (remoteRepo string, err error) { out, err := vcsHg.runOutput(rootDir, "paths default") if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } func hgStatus(vcsHg *cmd, rootDir string) (Status, error) { // Output changeset ID and seconds since epoch. out, err := vcsHg.runOutputVerboseOnly(rootDir, `log -l1 -T {node}:{date|hgdate}`) if err != nil { return Status{}, err } // Successful execution without output indicates an empty repo (no commits). var rev string var commitTime time.Time if len(out) > 0 { // Strip trailing timezone offset. if i := bytes.IndexByte(out, ' '); i > 0 { out = out[:i] } rev, commitTime, err = parseRevTime(out) if err != nil { return Status{}, err } } // Also look for untracked files. out, err = vcsHg.runOutputVerboseOnly(rootDir, "status") if err != nil { return Status{}, err } uncommitted := len(out) > 0 return Status{ Revision: rev, CommitTime: commitTime, Uncommitted: uncommitted, }, nil } // parseRevTime parses commit details in "revision:seconds" format. func parseRevTime(out []byte) (string, time.Time, error) { buf := string(bytes.TrimSpace(out)) i := strings.IndexByte(buf, ':') if i < 1 { return "", time.Time{}, errors.New("unrecognized VCS tool output") } rev := buf[:i] secs, err := strconv.ParseInt(string(buf[i+1:]), 10, 64) if err != nil { return "", time.Time{}, fmt.Errorf("unrecognized VCS tool output: %v", err) } return rev, time.Unix(secs, 0), nil } // vcsGit describes how to use Git. var vcsGit = &cmd{ Name: "Git", Cmd: "git", RootNames: []string{".git"}, CreateCmd: []string{"clone -- {repo} {dir}", "-go-internal-cd {dir} submodule update --init --recursive"}, DownloadCmd: []string{"pull --ff-only", "submodule update --init --recursive"}, TagCmd: []tagCmd{ // tags/xxx matches a git tag named xxx // origin/xxx matches a git branch named xxx on the default remote repository {"show-ref", `(?:tags|origin)/(\S+)$`}, }, TagLookupCmd: []tagCmd{ {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`}, }, TagSyncCmd: []string{"checkout {tag}", "submodule update --init --recursive"}, // both createCmd and downloadCmd update the working dir. // No need to do more here. We used to 'checkout master' // but that doesn't work if the default branch is not named master. // DO NOT add 'checkout master' here. // See golang.org/issue/9032. TagSyncDefault: []string{"submodule update --init --recursive"}, Scheme: []string{"git", "https", "http", "git+ssh", "ssh"}, // Leave out the '--' separator in the ls-remote command: git 2.7.4 does not // support such a separator for that command, and this use should be safe // without it because the {scheme} value comes from the predefined list above. // See golang.org/issue/33836. PingCmd: "ls-remote {scheme}://{repo}", RemoteRepo: gitRemoteRepo, Status: gitStatus, } // scpSyntaxRe matches the SCP-like addresses used by Git to access // repositories by SSH. var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) func gitRemoteRepo(vcsGit *cmd, rootDir string) (remoteRepo string, err error) { cmd := "config remote.origin.url" errParse := errors.New("unable to parse output of git " + cmd) errRemoteOriginNotFound := errors.New("remote origin not found") outb, err := vcsGit.run1(rootDir, cmd, nil, false) if err != nil { // if it doesn't output any message, it means the config argument is correct, // but the config value itself doesn't exist if outb != nil && len(outb) == 0 { return "", errRemoteOriginNotFound } return "", err } out := strings.TrimSpace(string(outb)) var repoURL *urlpkg.URL if m := scpSyntaxRe.FindStringSubmatch(out); m != nil { // Match SCP-like syntax and convert it to a URL. // Eg, "git@github.com:user/repo" becomes // "ssh://git@github.com/user/repo". repoURL = &urlpkg.URL{ Scheme: "ssh", User: urlpkg.User(m[1]), Host: m[2], Path: m[3], } } else { repoURL, err = urlpkg.Parse(out) if err != nil { return "", err } } // Iterate over insecure schemes too, because this function simply // reports the state of the repo. If we can't see insecure schemes then // we can't report the actual repo URL. for _, s := range vcsGit.Scheme { if repoURL.Scheme == s { return repoURL.String(), nil } } return "", errParse } func gitStatus(vcsGit *cmd, rootDir string) (Status, error) { out, err := vcsGit.runOutputVerboseOnly(rootDir, "status --porcelain") if err != nil { return Status{}, err } uncommitted := len(out) > 0 // "git status" works for empty repositories, but "git show" does not. // Assume there are no commits in the repo when "git show" fails with // uncommitted files and skip tagging revision / committime. var rev string var commitTime time.Time out, err = vcsGit.runOutputVerboseOnly(rootDir, "-c log.showsignature=false show -s --format=%H:%ct") if err != nil && !uncommitted { return Status{}, err } else if err == nil { rev, commitTime, err = parseRevTime(out) if err != nil { return Status{}, err } } return Status{ Revision: rev, CommitTime: commitTime, Uncommitted: uncommitted, }, nil } // vcsBzr describes how to use Bazaar. var vcsBzr = &cmd{ Name: "Bazaar", Cmd: "bzr", RootNames: []string{".bzr"}, CreateCmd: []string{"branch -- {repo} {dir}"}, // Without --overwrite bzr will not pull tags that changed. // Replace by --overwrite-tags after http://pad.lv/681792 goes in. DownloadCmd: []string{"pull --overwrite"}, TagCmd: []tagCmd{{"tags", `^(\S+)`}}, TagSyncCmd: []string{"update -r {tag}"}, TagSyncDefault: []string{"update -r revno:-1"}, Scheme: []string{"https", "http", "bzr", "bzr+ssh"}, PingCmd: "info -- {scheme}://{repo}", RemoteRepo: bzrRemoteRepo, ResolveRepo: bzrResolveRepo, Status: bzrStatus, } func bzrRemoteRepo(vcsBzr *cmd, rootDir string) (remoteRepo string, err error) { outb, err := vcsBzr.runOutput(rootDir, "config parent_location") if err != nil { return "", err } return strings.TrimSpace(string(outb)), nil } func bzrResolveRepo(vcsBzr *cmd, rootDir, remoteRepo string) (realRepo string, err error) { outb, err := vcsBzr.runOutput(rootDir, "info "+remoteRepo) if err != nil { return "", err } out := string(outb) // Expect: // ... // (branch root|repository branch): // ... found := false for _, prefix := range []string{"\n branch root: ", "\n repository branch: "} { i := strings.Index(out, prefix) if i >= 0 { out = out[i+len(prefix):] found = true break } } if !found { return "", fmt.Errorf("unable to parse output of bzr info") } i := strings.Index(out, "\n") if i < 0 { return "", fmt.Errorf("unable to parse output of bzr info") } out = out[:i] return strings.TrimSpace(out), nil } func bzrStatus(vcsBzr *cmd, rootDir string) (Status, error) { outb, err := vcsBzr.runOutputVerboseOnly(rootDir, "version-info") if err != nil { return Status{}, err } out := string(outb) // Expect (non-empty repositories only): // // revision-id: gopher@gopher.net-20211021072330-qshok76wfypw9lpm // date: 2021-09-21 12:00:00 +1000 // ... var rev string var commitTime time.Time for _, line := range strings.Split(out, "\n") { i := strings.IndexByte(line, ':') if i < 0 { continue } key := line[:i] value := strings.TrimSpace(line[i+1:]) switch key { case "revision-id": rev = value case "date": var err error commitTime, err = time.Parse("2006-01-02 15:04:05 -0700", value) if err != nil { return Status{}, errors.New("unable to parse output of bzr version-info") } } } outb, err = vcsBzr.runOutputVerboseOnly(rootDir, "status") if err != nil { return Status{}, err } // Skip warning when working directory is set to an older revision. if bytes.HasPrefix(outb, []byte("working tree is out of date")) { i := bytes.IndexByte(outb, '\n') if i < 0 { i = len(outb) } outb = outb[:i] } uncommitted := len(outb) > 0 return Status{ Revision: rev, CommitTime: commitTime, Uncommitted: uncommitted, }, nil } // vcsSvn describes how to use Subversion. var vcsSvn = &cmd{ Name: "Subversion", Cmd: "svn", RootNames: []string{".svn"}, CreateCmd: []string{"checkout -- {repo} {dir}"}, DownloadCmd: []string{"update"}, // There is no tag command in subversion. // The branch information is all in the path names. Scheme: []string{"https", "http", "svn", "svn+ssh"}, PingCmd: "info -- {scheme}://{repo}", RemoteRepo: svnRemoteRepo, } func svnRemoteRepo(vcsSvn *cmd, rootDir string) (remoteRepo string, err error) { outb, err := vcsSvn.runOutput(rootDir, "info") if err != nil { return "", err } out := string(outb) // Expect: // // ... // URL: // ... // // Note that we're not using the Repository Root line, // because svn allows checking out subtrees. // The URL will be the URL of the subtree (what we used with 'svn co') // while the Repository Root may be a much higher parent. i := strings.Index(out, "\nURL: ") if i < 0 { return "", fmt.Errorf("unable to parse output of svn info") } out = out[i+len("\nURL: "):] i = strings.Index(out, "\n") if i < 0 { return "", fmt.Errorf("unable to parse output of svn info") } out = out[:i] return strings.TrimSpace(out), nil } // fossilRepoName is the name go get associates with a fossil repository. In the // real world the file can be named anything. const fossilRepoName = ".fossil" // vcsFossil describes how to use Fossil (fossil-scm.org) var vcsFossil = &cmd{ Name: "Fossil", Cmd: "fossil", RootNames: []string{".fslckout", "_FOSSIL_"}, CreateCmd: []string{"-go-internal-mkdir {dir} clone -- {repo} " + filepath.Join("{dir}", fossilRepoName), "-go-internal-cd {dir} open .fossil"}, DownloadCmd: []string{"up"}, TagCmd: []tagCmd{{"tag ls", `(.*)`}}, TagSyncCmd: []string{"up tag:{tag}"}, TagSyncDefault: []string{"up trunk"}, Scheme: []string{"https", "http"}, RemoteRepo: fossilRemoteRepo, Status: fossilStatus, } func fossilRemoteRepo(vcsFossil *cmd, rootDir string) (remoteRepo string, err error) { out, err := vcsFossil.runOutput(rootDir, "remote-url") if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } var errFossilInfo = errors.New("unable to parse output of fossil info") func fossilStatus(vcsFossil *cmd, rootDir string) (Status, error) { outb, err := vcsFossil.runOutputVerboseOnly(rootDir, "info") if err != nil { return Status{}, err } out := string(outb) // Expect: // ... // checkout: 91ed71f22c77be0c3e250920f47bfd4e1f9024d2 2021-09-21 12:00:00 UTC // ... // Extract revision and commit time. // Ensure line ends with UTC (known timezone offset). const prefix = "\ncheckout:" const suffix = " UTC" i := strings.Index(out, prefix) if i < 0 { return Status{}, errFossilInfo } checkout := out[i+len(prefix):] i = strings.Index(checkout, suffix) if i < 0 { return Status{}, errFossilInfo } checkout = strings.TrimSpace(checkout[:i]) i = strings.IndexByte(checkout, ' ') if i < 0 { return Status{}, errFossilInfo } rev := checkout[:i] commitTime, err := time.ParseInLocation("2006-01-02 15:04:05", checkout[i+1:], time.UTC) if err != nil { return Status{}, fmt.Errorf("%v: %v", errFossilInfo, err) } // Also look for untracked changes. outb, err = vcsFossil.runOutputVerboseOnly(rootDir, "changes --differ") if err != nil { return Status{}, err } uncommitted := len(outb) > 0 return Status{ Revision: rev, CommitTime: commitTime, Uncommitted: uncommitted, }, nil } func (v *cmd) String() string { return v.Name } // run runs the command line cmd in the given directory. // keyval is a list of key, value pairs. run expands // instances of {key} in cmd into value, but only after // splitting cmd into individual arguments. // If an error occurs, run prints the command line and the // command's combined stdout+stderr to standard error. // Otherwise run discards the command's output. func (v *cmd) run(dir string, cmd string, keyval ...string) error { _, err := v.run1(dir, cmd, keyval, true) return err } // runVerboseOnly is like run but only generates error output to standard error in verbose mode. func (v *cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error { _, err := v.run1(dir, cmd, keyval, false) return err } // runOutput is like run but returns the output of the command. func (v *cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) { return v.run1(dir, cmd, keyval, true) } // runOutputVerboseOnly is like runOutput but only generates error output to // standard error in verbose mode. func (v *cmd) runOutputVerboseOnly(dir string, cmd string, keyval ...string) ([]byte, error) { return v.run1(dir, cmd, keyval, false) } // run1 is the generalized implementation of run and runOutput. func (v *cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) { m := make(map[string]string) for i := 0; i < len(keyval); i += 2 { m[keyval[i]] = keyval[i+1] } args := strings.Fields(cmdline) for i, arg := range args { args[i] = expand(m, arg) } if len(args) >= 2 && args[0] == "-go-internal-mkdir" { var err error if filepath.IsAbs(args[1]) { err = os.Mkdir(args[1], fs.ModePerm) } else { err = os.Mkdir(filepath.Join(dir, args[1]), fs.ModePerm) } if err != nil { return nil, err } args = args[2:] } if len(args) >= 2 && args[0] == "-go-internal-cd" { if filepath.IsAbs(args[1]) { dir = args[1] } else { dir = filepath.Join(dir, args[1]) } args = args[2:] } _, err := exec.LookPath(v.Cmd) if err != nil { fmt.Fprintf(os.Stderr, "go: missing %s command. See https://golang.org/s/gogetcmd\n", v.Name) return nil, err } cmd := exec.Command(v.Cmd, args...) cmd.Dir = dir out, err := cmd.Output() if err != nil { if verbose { fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " ")) if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { os.Stderr.Write(ee.Stderr) } else { fmt.Fprintln(os.Stderr, err.Error()) } } } return out, err } // Create creates a new copy of repo in dir. // The parent of dir must exist; dir must not. func (v *cmd) Create(dir, repo string) error { for _, cmd := range v.CreateCmd { if err := v.run(filepath.Dir(dir), cmd, "dir", dir, "repo", repo); err != nil { return err } } return nil } // Download downloads any new changes for the repo in dir. func (v *cmd) Download(dir string) error { for _, cmd := range v.DownloadCmd { if err := v.run(dir, cmd); err != nil { return err } } return nil } // Tags returns the list of available tags for the repo in dir. func (v *cmd) Tags(dir string) ([]string, error) { var tags []string for _, tc := range v.TagCmd { out, err := v.runOutput(dir, tc.cmd) if err != nil { return nil, err } re := regexp.MustCompile(`(?m-s)` + tc.pattern) for _, m := range re.FindAllStringSubmatch(string(out), -1) { tags = append(tags, m[1]) } } return tags, nil } // TagSync syncs the repo in dir to the named tag, // which either is a tag returned by tags or is v.tagDefault. func (v *cmd) TagSync(dir, tag string) error { if v.TagSyncCmd == nil { return nil } if tag != "" { for _, tc := range v.TagLookupCmd { out, err := v.runOutput(dir, tc.cmd, "tag", tag) if err != nil { return err } re := regexp.MustCompile(`(?m-s)` + tc.pattern) m := re.FindStringSubmatch(string(out)) if len(m) > 1 { tag = m[1] break } } } if tag == "" && v.TagSyncDefault != nil { for _, cmd := range v.TagSyncDefault { if err := v.run(dir, cmd); err != nil { return err } } return nil } for _, cmd := range v.TagSyncCmd { if err := v.run(dir, cmd, "tag", tag); err != nil { return err } } return nil } // fromDir inspects dir and its parents to determine the // version control system and code repository to use. // If no repository is found, fromDir returns an error // equivalent to os.ErrNotExist. func fromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *cmd, err error) { // Clean and double-check that dir is in (a subdirectory of) srcRoot. dir = filepath.Clean(dir) if srcRoot != "" { srcRoot = filepath.Clean(srcRoot) if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { return "", nil, fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) } } origDir := dir for len(dir) > len(srcRoot) { for _, vcs := range vcsList { if _, err := statAny(dir, vcs.RootNames); err == nil { // Record first VCS we find. // If allowNesting is false (as it is in GOPATH), keep looking for // repositories in parent directories and report an error if one is // found to mitigate VCS injection attacks. if vcsCmd == nil { vcsCmd = vcs repoDir = dir if allowNesting { return repoDir, vcsCmd, nil } continue } // Allow .git inside .git, which can arise due to submodules. if vcsCmd == vcs && vcs.Cmd == "git" { continue } // Otherwise, we have one VCS inside a different VCS. return "", nil, fmt.Errorf("directory %q uses %s, but parent %q uses %s", repoDir, vcsCmd.Cmd, dir, vcs.Cmd) } } // Move to parent. ndir := filepath.Dir(dir) if len(ndir) >= len(dir) { break } dir = ndir } if vcsCmd == nil { return "", nil, &vcsNotFoundError{dir: origDir} } return repoDir, vcsCmd, nil } // statAny provides FileInfo for the first filename found in the directory. // Otherwise, it returns the last error seen. func statAny(dir string, filenames []string) (os.FileInfo, error) { if len(filenames) == 0 { return nil, errors.New("invalid argument: no filenames provided") } var err error var fi os.FileInfo for _, name := range filenames { fi, err = os.Stat(filepath.Join(dir, name)) if err == nil { return fi, nil } } return nil, err } type vcsNotFoundError struct { dir string } func (e *vcsNotFoundError) Error() string { return fmt.Sprintf("directory %q is not using a known version control system", e.dir) } func (e *vcsNotFoundError) Is(err error) bool { return err == os.ErrNotExist } // expand rewrites s to replace {k} with match[k] for each key k in match. func expand(match map[string]string, s string) string { // We want to replace each match exactly once, and the result of expansion // must not depend on the iteration order through the map. // A strings.Replacer has exactly the properties we're looking for. oldNew := make([]string, 0, 2*len(match)) for k, v := range match { oldNew = append(oldNew, "{"+k+"}", v) } return strings.NewReplacer(oldNew...).Replace(s) } ================================================ FILE: pkg/vfs/directory.go ================================================ package vfs import ( "errors" "io" "io/fs" "sort" "time" ) type directoryContents struct { node childDirs map[string]*directoryContents files map[string]*fileContents } // Directory is a wrapper around directoryContents which stores the state // needed to implement ReadDirFile type Directory struct { *directoryContents readDirCount int readEntries []fs.DirEntry closed bool } var ( _ fs.FileInfo = (*Directory)(nil) _ fs.DirEntry = (*Directory)(nil) _ fs.ReadDirFile = (*Directory)(nil) ) func newDirectoryContents(name string) *directoryContents { return &directoryContents{ node: node{ name: name, parent: nil, modTime: time.Now(), }, childDirs: make(map[string]*directoryContents), files: make(map[string]*fileContents), } } func (d *Directory) createEntries() []fs.DirEntry { rtn := make([]fs.DirEntry, 0, len(d.childDirs)+len(d.files)) for _, child := range d.childDirs { rtn = append(rtn, &Directory{directoryContents: child}) } for _, child := range d.files { rtn = append(rtn, &File{fileContents: child}) } sort.SliceStable(rtn, func(i, j int) bool { return rtn[i].Name() < rtn[j].Name() }) return rtn } // ReadDir reads the contents of the directory and returns // a slice of up to n DirEntry values in directory order. // Subsequent calls on the same file will yield further DirEntry values. // // If n > 0, ReadDir returns at most n DirEntry structures. // In this case, if ReadDir returns an empty slice, it will return // a non-nil error explaining why. // At the end of a directory, the error is io.EOF. // // If n <= 0, ReadDir returns all the DirEntry values from the directory // in a single slice. In this case, if ReadDir succeeds (reads all the way // to the end of the directory), it returns the slice and a nil error. // If it encounters an error before the end of the directory, // ReadDir returns the DirEntry list read until that point and a non-nil error. func (d *Directory) ReadDir(n int) ([]fs.DirEntry, error) { if d.closed { return nil, fs.ErrClosed } // Create the directory listing if it doesn't exist already if d.readEntries == nil { d.readEntries = d.createEntries() } // detect an EOF if d.readDirCount == len(d.readEntries) { if n <= 0 { // special case return return nil, nil } return nil, io.EOF } // Create the return data with the number of requested entries readNum := len(d.readEntries) - d.readDirCount if readNum > n && n > 0 { readNum = n } rtn := make([]fs.DirEntry, readNum) for i := 0; i < readNum; i++ { rtn[i] = d.readEntries[d.readDirCount] d.readDirCount++ } return rtn, nil } func (d *Directory) Read(_ []byte) (int, error) { return 0, errors.New("cannot read directory") } func (d *Directory) Name() string { return d.node.name } func (d *Directory) IsDir() bool { return true } func (d *Directory) Type() fs.FileMode { return fs.ModeDir } func (d *Directory) Info() (fs.FileInfo, error) { return d, nil } func (d *Directory) Size() int64 { return 0 } func (d *Directory) Mode() fs.FileMode { return d.Type() } func (d *Directory) ModTime() time.Time { return d.node.modTime } func (d *Directory) Stat() (fs.FileInfo, error) { return d.Info() } func (d *Directory) Close() error { d.closed = true return nil } func (d *Directory) Sys() any { return nil } ================================================ FILE: pkg/vfs/doc.go ================================================ // Package vfs represents a virtual filesystem package vfs ================================================ FILE: pkg/vfs/file.go ================================================ package vfs import ( "io" "io/fs" "time" ) type fileContents struct { node contents []byte } type File struct { *fileContents bytesRead int closed bool } var ( _ fs.File = (*File)(nil) _ fs.FileInfo = (*File)(nil) _ fs.DirEntry = (*File)(nil) ) func (f *File) Name() string { return f.node.name } // Read implements io.Reader func (f *File) Read(bytes []byte) (int, error) { if f.closed { return 0, fs.ErrClosed } if f.bytesRead >= len(f.contents) { return 0, io.EOF } bytesRead := copy(bytes, f.contents[f.bytesRead:]) f.bytesRead += bytesRead if f.bytesRead >= len(f.contents) { return bytesRead, io.EOF } return bytesRead, nil } func (f *File) Close() error { f.closed = true return nil } func (f *File) Size() int64 { return int64(len(f.contents)) } func (f *File) Mode() fs.FileMode { return fs.ModePerm & 0444 // files in this file system are read only } func (f *File) ModTime() time.Time { return f.node.modTime } func (f *File) IsDir() bool { return false } func (f *File) Sys() any { return nil } func (f *File) Type() fs.FileMode { return f.Mode() & fs.ModeType } func (f *File) Info() (fs.FileInfo, error) { return f, nil } func (f *File) Stat() (fs.FileInfo, error) { return f, nil } ================================================ FILE: pkg/vfs/node.go ================================================ package vfs import ( "time" ) type node struct { name string // The name of this node parent *directoryContents modTime time.Time } ================================================ FILE: pkg/vfs/testdata/filteredglob/blahsvc/another.json ================================================ { "foo": false } ================================================ FILE: pkg/vfs/testdata/filteredglob/blahsvc/test.json ================================================ { "foo": "blah" } ================================================ FILE: pkg/vfs/testdata/filteredglob/foosystem/README.md ================================================ This is a example file ================================================ FILE: pkg/vfs/testdata/filteredglob/foosystem/anotherservice/test.txt ================================================ another ================================================ FILE: pkg/vfs/testdata/filteredglob/foosystem/barservice/blah.json ================================================ { "foo": "bar" } ================================================ FILE: pkg/vfs/testdata/filteredglob/foosystem/barservice/test.txt ================================================ Hello world ================================================ FILE: pkg/vfs/testdata/filteredglob/nope/ignored.txt ================================================ ================================================ FILE: pkg/vfs/utils.go ================================================ package vfs import ( "io/fs" "os" "encr.dev/pkg/eerror" ) // FromDir creates a Virtual File System (VFS) from the workingDir on the local computer // // Only files or folders which match the predicate will be added into the file system // A nil predicate will result in all files and folders being added into the file system func FromDir(workingDir string, predicate func(fileName string, info fs.DirEntry) bool) (*VFS, error) { dirFS := os.DirFS(workingDir) rtn := New() if predicate == nil { predicate = func(fileName string, info fs.DirEntry) bool { return true } } err := fs.WalkDir(dirFS, ".", func(path string, info fs.DirEntry, err error) error { if err != nil { return eerror.Wrap(err, "vfs", "error walking directory", map[string]any{"path": path}) } if predicate(path, info) { if info.IsDir() { _ = rtn.AddDir(path) } else { bytes, err := fs.ReadFile(dirFS, path) if err != nil { return eerror.Wrap(err, "vfs", "error reading file", map[string]any{"path": path}) } stat, err := fs.Stat(dirFS, path) if err != nil { return eerror.Wrap(err, "vfs", "error stat file", map[string]any{"path": path}) } if _, err := rtn.AddFile(path, bytes, stat.ModTime()); err != nil { return eerror.Wrap(err, "vfs", "unable to add file", map[string]any{"path": path}) } } } return nil }) if err != nil { return nil, err } return rtn, nil } ================================================ FILE: pkg/vfs/vfs.go ================================================ package vfs import ( "errors" "io/fs" "path/filepath" "strings" "time" ) type VFS struct { root *directoryContents } var ( _ fs.FS = (*VFS)(nil) _ fs.ReadDirFS = (*VFS)(nil) _ fs.ReadFileFS = (*VFS)(nil) _ fs.SubFS = (*VFS)(nil) _ fs.StatFS = (*VFS)(nil) ) func New() *VFS { return &VFS{root: newDirectoryContents("")} } // Open opens the named file. // // When Open returns an error, it should be of type *PathError // with the Op field set to "open", the Path field set to name, // and the Err field describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. func (v *VFS) Open(name string) (fs.File, error) { if name == "." { return &Directory{directoryContents: v.root}, nil } if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrInvalid, } } parts := strings.Split(name, "/") pathTravelled := make([]string, 0, len(parts)) dir := v.root pathTravelled = append(pathTravelled, dir.name) if len(parts) > 1 { for _, subDir := range parts[:len(parts)-1] { pathTravelled = append(pathTravelled, subDir) dir = dir.childDirs[subDir] if dir == nil { return nil, &fs.PathError{ Op: "open", Path: strings.Join(pathTravelled, "/"), Err: fs.ErrNotExist, } } } } // Return the child childName := parts[len(parts)-1] if childFile, found := dir.files[childName]; found { return &File{fileContents: childFile}, nil } if childDir, found := dir.childDirs[childName]; found { return &Directory{directoryContents: childDir}, nil } return nil, &fs.PathError{ Op: "open", Path: strings.Join(pathTravelled, "/"), Err: fs.ErrNotExist, } } // ReadDir reads the named directory // and returns a list of directory entries sorted by filename. func (v *VFS) ReadDir(name string) ([]fs.DirEntry, error) { file, err := v.Open(name) if err != nil { return nil, &fs.PathError{ Op: "ReadDir", Path: name, Err: err, } } switch file := file.(type) { case *Directory: return file.createEntries(), nil default: return nil, &fs.PathError{ Op: "ReadDir", Path: name, Err: errors.New("not a directory"), } } } // ReadFile reads the named file and returns its contents. // A successful call returns a nil error, not io.EOF. // (Because ReadFile reads the whole file, the expected EOF // from the final Read is not treated as an error to be reported.) // // The caller is permitted to modify the returned byte slice. // This method should return a copy of the underlying data. func (v *VFS) ReadFile(name string) ([]byte, error) { file, err := v.Open(name) if err != nil { return nil, &fs.PathError{ Op: "ReadFile", Path: name, Err: err, } } switch file := file.(type) { case *File: rtnBytes := make([]byte, len(file.contents)) copy(rtnBytes, file.contents) return rtnBytes, nil default: return nil, &fs.PathError{ Op: "ReadFile", Path: name, Err: errors.New("not a file"), } } } // Sub returns an FS corresponding to the subtree rooted at dir. func (v *VFS) Sub(dir string) (fs.FS, error) { file, err := v.Open(dir) if err != nil { return nil, &fs.PathError{ Op: "Sub", Path: dir, Err: err, } } switch file := file.(type) { case *Directory: return &VFS{root: file.directoryContents}, nil default: return nil, &fs.PathError{ Op: "Sub", Path: dir, Err: errors.New("not a directory"), } } } // Stat returns a FileInfo describing the file. // If there is an error, it should be of type *PathError. func (v *VFS) Stat(name string) (fs.FileInfo, error) { file, err := v.Open(name) if err != nil { return nil, &fs.PathError{ Op: "Stat", Path: name, Err: err, } } return file.Stat() } // AddFile records a file into the VFS func (v *VFS) AddFile(path string, bytes []byte, time time.Time) (*fileContents, error) { dirPath, filename := filepath.Split(path) dir := v.AddDir(dirPath) dir.files[filename] = &fileContents{ node: node{ name: filename, parent: dir, modTime: time, }, contents: bytes, } return dir.files[filename], nil } // AddDir records a directory into the VFS. func (v *VFS) AddDir(dirPath string) *directoryContents { dirParts := strings.Split(dirPath, "/") dir := v.root for _, dirPart := range dirParts { if dirPart == "." || dirPart == "" { continue } if dirPart == ".." { if dir.node.parent == nil { return nil } dir = dir.node.parent continue } child := dir.childDirs[dirPart] if child == nil { child = newDirectoryContents(dirPart) child.node.parent = dir dir.childDirs[dirPart] = child } dir = child } return dir } ================================================ FILE: pkg/vfs/vfs_test.go ================================================ package vfs import ( "io/fs" "path/filepath" "testing" "testing/fstest" qt "github.com/frankban/quicktest" ) func TestFromDir(t *testing.T) { c := qt.New(t) dir, err := FromDir( filepath.Join(".", "testdata", "filteredglob"), func(file string, info fs.DirEntry) bool { return filepath.Ext(file) == ".json" }, ) c.Assert(err, qt.IsNil, qt.Commentf("error creating VFS")) // Test that the VFS contains the expected number of files (no more no less) fileCount := 0 dirCount := 0 err = fs.WalkDir(dir, ".", func(path string, info fs.DirEntry, err error) error { if err != nil { return err } if !info.IsDir() { fileCount++ } else { dirCount++ } return nil }) c.Assert(err, qt.IsNil, qt.Commentf("error walking directory")) c.Assert(fileCount, qt.Equals, 3, qt.Commentf("unexpected number of files")) c.Assert(dirCount, qt.Equals, 4, qt.Commentf("unexpected number of directories")) // Perform the standardised tests on the VFS implementation checking for the existence of the files we wanted if err := fstest.TestFS(dir, "blahsvc/another.json", "blahsvc/test.json", "foosystem/barservice/blah.json"); err != nil { t.Fatal(err) } } ================================================ FILE: pkg/watcher/event.go ================================================ package watcher import "os" type EventType = string const ( CREATED EventType = "Created" MODIFIED EventType = "Modified" DELETED EventType = "Deleted" ) type Event struct { EventType EventType Path string Info os.FileInfo } type Events struct { latestEvents map[string]Event // Latest event per file } func newEventBatch() *Events { return &Events{ latestEvents: make(map[string]Event), } } func (e *Events) addEvent(path string, event EventType, info os.FileInfo) { e.latestEvents[path] = Event{ EventType: event, Path: path, Info: info, } } func (e *Events) Events() []Event { events := make([]Event, 0, len(e.latestEvents)) for _, event := range e.latestEvents { events = append(events, event) } return events } ================================================ FILE: pkg/watcher/rlimit_nix.go ================================================ //go:build linux || darwin package watcher import ( "syscall" "github.com/rs/zerolog/log" ) // BumpRLimitSoftToHardLimit bumps the soft limit of the rlimit to the hard limit. // // To go higher the user will need to update their kernel settings which can be viewed: // // sysctl -a | grep kern.maxfile func BumpRLimitSoftToHardLimit() { var rLimit syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { log.Error().Err(err).Msg("failed to get rlimit") } rLimit.Cur = rLimit.Max err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { log.Error().Err(err).Msg("failed to set rlimit") } } ================================================ FILE: pkg/watcher/rlimit_noop.go ================================================ //go:build !linux && !darwin package watcher // BumpRLimitSoftToHardLimit bumps the soft limit of the rlimit to the hard limit. // // To go higher the user will need to update their kernel settings which can be viewed: // // sysctl -a | grep kern.maxfile func BumpRLimitSoftToHardLimit() { // no-op } ================================================ FILE: pkg/watcher/util.go ================================================ package watcher import ( "os" "path/filepath" ) // IgnoreFolder returns true for folders we don't want to watch certain folders // as they'll never impact an Encore app, and they cause an extreme amount of noise. func IgnoreFolder(folder string) bool { folderName := filepath.Base(filepath.Clean(folder)) if folderName == "node_modules" || folderName == "encore.gen" { return true } if folderName == "target" { // Do we have a "Cargo.toml" file in the parent directory? If so, ignore this. cargoPath := filepath.Join(filepath.Dir(folder), "Cargo.toml") if _, err := os.Stat(cargoPath); err == nil { return true } } // Don't watch hidden folders like `.git` or `.idea` as // they also don't impact an Encore app. if len(folderName) > 1 && folderName[0] == '.' { return true } return false } ================================================ FILE: pkg/watcher/watcher.go ================================================ package watcher import ( "os" "path/filepath" "strings" "sync" "time" "github.com/bep/debounce" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "encr.dev/pkg/eerror" ) type Watcher struct { mutex sync.Mutex eventCond *sync.Cond events *Events signalDebounce func(func()) log *zerolog.Logger appRoot string watcher *fsnotify.Watcher directories map[string]struct{} stop chan struct{} } func New(appID string) (*Watcher, error) { fswatcher, err := fsnotify.NewWatcher() if err != nil { return nil, eerror.Wrap(err, "watcher", "unable to create watcher", map[string]interface{}{"app": appID}) } logger := log.With().Str("component", "watcher").Str("app", appID).Logger() logger.Debug().Msg("File system watcher created") w := &Watcher{ watcher: fswatcher, log: &logger, directories: make(map[string]struct{}), stop: make(chan struct{}), events: nil, signalDebounce: debounce.New(50 * time.Millisecond), } w.eventCond = sync.NewCond(&w.mutex) go w.listenForChangeEvents() return w, nil } func (w *Watcher) RecursivelyWatch(folder string) error { return filepath.WalkDir(folder, func(path string, info os.DirEntry, err error) error { if err != nil { return eerror.Wrap(err, "watcher", "unable to walk directory", map[string]any{"path": path}) } if info.IsDir() { folder := filepath.Clean(path) if IgnoreFolder(folder) { return filepath.SkipDir } // Track the fact we're watching this directory w.mutex.Lock() if _, found := w.directories[folder]; found { w.mutex.Unlock() return filepath.SkipDir } w.directories[folder] = struct{}{} w.mutex.Unlock() // unlock here to prevent reentrant locks during recursion // Now start watching this folder if err := w.watcher.Add(folder); err != nil { return eerror.Wrap(err, "watcher", "unable to add folder to watch", map[string]any{"folder": folder}) } } return nil }) } func (w *Watcher) listenForChangeEvents() { for { select { case <-w.stop: _ = w.watcher.Close() return case event := <-w.watcher.Events: if event.Has(fsnotify.Remove) { w.handleDeleteEvent(event.Name) } else if event.Has(fsnotify.Create) { w.handleCreateEvent(event.Name) } else if event.Has(fsnotify.Write) { w.handleWriteEvent(event.Name) } case err := <-w.watcher.Errors: w.log.Err(err).Msg("Watcher error") } } } func (w *Watcher) handleCreateEvent(path string) { if info, err := os.Stat(path); err != nil { w.log.Err(err).Str("path", path).Msg("unable to stat file") } else if info.IsDir() { if err := w.RecursivelyWatch(path); err != nil { w.log.Err(err).Str("path", path).Msg("unable to start watching new directory") } } else { w.recordEventInBatch(path, CREATED, info) } } func (w *Watcher) handleDeleteEvent(path string) { path = filepath.Clean(path) pathWithSep := path + string(filepath.Separator) // If it's a directory we're watching, stop watching it w.mutex.Lock() for watchedFolder := range w.directories { // I sthis the path itself, or a subdirectory thereof? if strings.HasPrefix(watchedFolder, pathWithSep) || watchedFolder == path { if err := w.watcher.Remove(watchedFolder); err != nil { w.log.Err(err).Str("path", watchedFolder).Msg("unable to stop watching deleted directory") } delete(w.directories, watchedFolder) } } w.mutex.Unlock() w.recordEventInBatch(path, DELETED, nil) } func (w *Watcher) handleWriteEvent(path string) { if info, err := os.Stat(path); err != nil { w.log.Err(err).Str("path", path).Msg("unable to stat file") } else if !info.IsDir() { w.recordEventInBatch(path, MODIFIED, info) } } func (w *Watcher) recordEventInBatch(path string, event EventType, info os.FileInfo) { w.mutex.Lock() defer w.mutex.Unlock() if w.events == nil { w.events = newEventBatch() } w.events.addEvent(path, event, info) // Debounce the signal to avoid waking up on every event in case of a burst of events. w.signalDebounce(func() { w.eventCond.Signal() }) } func (w *Watcher) WaitForEvents() (events []Event, ok bool) { w.eventCond.L.Lock() defer w.eventCond.L.Unlock() for { select { case <-w.stop: // We're shutting down, so return immediately. return nil, false default: if w.events == nil || len(w.events.latestEvents) == 0 { w.eventCond.Wait() } // Post-condition: we have at least one event. events := w.events.Events() w.events = newEventBatch() return events, true } } } func (w *Watcher) Close() error { close(w.stop) return nil } func (w *Watcher) Done() <-chan struct{} { return w.stop } ================================================ FILE: pkg/words/funcs.go ================================================ package words import ( "crypto/rand" "fmt" "math/big" ) // Select selects n random words from the short word list. func Select(n int) ([]string, error) { selected := make([]string, n) words := shortWords.Get() max := big.NewInt(int64(len(words))) for i := 0; i < n; i++ { j, err := rand.Int(rand.Reader, max) if err != nil { return nil, fmt.Errorf("wordlist.Select %d: %v", n, err) } selected[i] = words[j.Int64()] } return selected, nil } ================================================ FILE: pkg/words/shortwords.txt ================================================ acid acorn acre acts afar affix aged agent agile aging agony ahead aide aids aim ajar alarm alias alibi alien alike alive aloe aloft aloha alone amend amino ample amuse angel anger angle ankle apple april apron aqua area arena argue arise armed armor army aroma array arson art ashen ashes atlas atom attic audio avert avoid awake award awoke axis bacon badge bagel baggy baked baker balmy banjo barge barn bash basil bask batch bath baton bats blade blank blast blaze bleak blend bless blimp blink bloat blob blog blot blunt blurt blush boast boat body boil bok bolt boned boney bonus bony book booth boots boss botch both boxer breed bribe brick bride brim bring brink brisk broad broil broke brook broom brush buck bud buggy bulge bulk bully bunch bunny bunt bush bust busy buzz cable cache cadet cage cake calm cameo canal candy cane canon cape card cargo carol carry carve case cash cause cedar chain chair chant chaos charm chase cheek cheer chef chess chest chew chief chili chill chip chomp chop chow chuck chump chunk churn chute cider cinch city civic civil clad claim clamp clap clash clasp class claw clay clean clear cleat cleft clerk click cling clink clip cloak clock clone cloth cloud clump coach coast coat cod coil coke cola cold colt coma come comic comma cone cope copy coral cork cost cot couch cough cover cozy craft cramp crane crank crate crave crawl crazy creme crepe crept crib cried crisp crook crop cross crowd crown crumb crush crust cub cult cupid cure curl curry curse curve curvy cushy cut cycle dab dad daily dairy daisy dance dandy darn dart dash data date dawn deaf deal dean debit debt debug decaf decal decay deck decor decoy deed delay denim dense dent depth derby desk dial diary dice dig dill dime dimly diner dingy disco dish disk ditch ditzy dizzy dock dodge doing doll dome donor donut dose dot dove down dowry doze drab drama drank draw dress dried drift drill drive drone droop drove drown drum dry duck duct dude dug duke duo dusk dust duty dwarf dwell eagle early earth easel east eaten eats ebay ebony ebook echo edge eel eject elbow elder elf elk elm elope elude elves email emit empty emu enter entry envoy equal erase error erupt essay etch evade even evict evil evoke exact exit fable faced fact fade fall false fancy fang fax feast feed femur fence fend ferry fetal fetch fever fiber fifth fifty film filth final finch fit five flag flaky flame flap flask fled flick fling flint flip flirt float flock flop floss flyer foam foe fog foil folic folk food fool found fox foyer frail frame fray fresh fried frill frisk from front frost froth frown froze fruit gag gains gala game gap gas gave gear gecko geek gem genre gift gig gills given giver glad glass glide gloss glove glow glue goal going golf gong good gooey goofy gore gown grab grain grant grape graph grasp grass grave gravy gray green greet grew grid grief grill grip grit groom grope growl grub grunt guide gulf gulp gummy guru gush gut guy habit half halo halt happy harm hash hasty hatch hate haven hazel hazy heap heat heave hedge hefty help herbs hers hub hug hula hull human humid hump hung hunk hunt hurry hurt hush hut ice icing icon icy igloo image ion iron islam issue item ivory ivy jab jam jaws jazz jeep jelly jet jiffy job jog jolly jolt jot joy judge juice juicy july jumbo jump junky juror jury keep keg kept kick kilt king kite kitty kiwi knee knelt koala kung ladle lady lair lake lance land lapel large lash lasso last latch late lazy left legal lemon lend lens lent level lever lid life lift lilac lily limb limes line lint lion lip list lived liver lunar lunch lung lurch lure lurk lying lyric mace maker malt mama mango manor many map march mardi marry mash match mate math moan mocha moist mold mom moody mop morse most motor motto mount mouse mousy mouth move movie mower mud mug mulch mule mull mumbo mummy mural muse music musky mute nacho nag nail name nanny nap navy near neat neon nerd nest net next niece ninth nutty oak oasis oat ocean oil old olive omen onion only ooze opal open opera opt otter ouch ounce outer oval oven owl ozone pace pagan pager palm panda panic pants panty paper park party pasta patch path patio payer pecan penny pep perch perky perm pest petal petri petty photo plank plant plaza plead plot plow pluck plug plus poach pod poem poet pogo point poise poker polar polio polka polo pond pony poppy pork poser pouch pound pout power prank press print prior prism prize probe prong proof props prude prune pry pug pull pulp pulse puma punch punk pupil puppy purr purse push putt quack quake query quiet quill quilt quit quota quote rabid race rack radar radio raft rage raid rail rake rally ramp ranch range rank rant rash raven reach react ream rebel recap relax relay relic remix repay repel reply rerun reset rhyme rice rich ride rigid rigor rinse riot ripen rise risk ritzy rival river roast robe robin rock rogue roman romp rope rover royal ruby rug ruin rule runny rush rust rut sadly sage said saint salad salon salsa salt same sandy santa satin sauna saved savor sax say scale scam scan scare scarf scary scoff scold scoop scoot scope score scorn scout scowl scrap scrub scuba scuff sect sedan self send sepia serve set seven shack shade shady shaft shaky sham shape share sharp shed sheep sheet shelf shell shine shiny ship shirt shock shop shore shout shove shown showy shred shrug shun shush shut shy sift silk silly silo sip siren sixth size skate skew skid skier skies skip skirt skit sky slab slack slain slam slang slash slate slaw sled sleek sleep sleet slept slice slick slimy sling slip slit slob slot slug slum slurp slush small smash smell smile smirk smog snack snap snare snarl sneak sneer sniff snore snort snout snowy snub snuff speak speed spend spent spew spied spill spiny spoil spoke spoof spool spoon sport spot spout spray spree spur squad squat squid stack staff stage stain stall stamp stand stank stark start stash state stays steam steep stem step stew stick sting stir stock stole stomp stony stood stool stoop stop storm stout stove straw stray strut stuck stud stuff stump stung stunt suds sugar sulk surf sushi swab swan swarm sway swear sweat sweep swell swept swim swing swipe swirl swoop swore syrup tacky taco tag take tall talon tamer tank taper taps tarot tart task taste tasty taunt thank thaw theft theme thigh thing think thong thorn those throb thud thumb thump thus tiara tidal tidy tiger tile tilt tint tiny trace track trade train trait trap trash tray treat tree trek trend trial tribe trick trio trout truce truck trump trunk try tug tulip tummy turf tusk tutor tutu tux tweak tweet twice twine twins twirl twist uncle uncut undo unify union unit untie upon upper urban used user usher utter value vapor vegan venue verse vest veto vice video view viral virus visa visor vixen vocal voice void volt voter vowel wad wafer wager wages wagon wake walk wand wasp watch water wavy wheat whiff whole whoop wick widen widow width wife wifi wilt wimp wind wing wink wipe wired wiry wise wish wispy wok wolf womb wool woozy word work worry wound woven wrath wreck wrist xerox yahoo yam yard year yeast yelp yield yodel yoga yoyo yummy zebra zero zesty zippy zone zoom ================================================ FILE: pkg/words/words.go ================================================ package words import ( _ "embed" // for go:embed "strings" "sync" ) type wordList struct { raw string once sync.Once words []string } func (w *wordList) Get() []string { w.once.Do(func() { raw := strings.TrimSpace(w.raw) w.words = strings.Split(raw, "\n") }) return w.words } var ( //go:embed shortwords.txt shortRaw string shortWords = wordList{raw: shortRaw} ) ================================================ FILE: pkg/words/words_test.go ================================================ package words import ( "strings" "testing" ) func TestWords(t *testing.T) { list := shortWords.Get() if len(list) != 1295 { t.Errorf("expected 1295 words, got %d", len(list)) } seen := make(map[string]bool) for _, w := range list { if seen[w] { t.Errorf("duplicate word %q", w) } else if len(w) < 3 || len(w) > 5 { t.Errorf("expected word to be 3-5 5 characters, got %q", w) } else if strings.TrimSpace(w) != w { t.Errorf("expected word to be trimmed, got %q", w) } seen[w] = true } } ================================================ FILE: pkg/xos/xos_unix.go ================================================ //go:build !windows // +build !windows // Package xos provides cross-platform helper functions. package xos import ( "os" "os/exec" "os/user" "syscall" "github.com/cockroachdb/errors" "github.com/google/renameio/v2" ) func CreateNewProcessGroup() *syscall.SysProcAttr { return &syscall.SysProcAttr{Setpgid: true} } func SocketStat(path string) (interface{}, error) { return os.Stat(path) } func SameSocket(a, b interface{}) bool { ai := a.(os.FileInfo) bi := b.(os.FileInfo) return os.SameFile(ai, bi) } func ArrangeExtraFiles(cmd *exec.Cmd, files ...*os.File) error { cmd.ExtraFiles = files return nil } func IsAdminUser() (bool, error) { usr, err := user.Current() if err != nil { return false, err } return usr.Gid == "0", nil } // WriteFile writes the given file with the given data and permissions. // // Where possible (i.e. not on windows) it will use an atomic write process // which removes the possibility of a partial file being written during a crash // or error. func WriteFile(filename string, data []byte, perm os.FileMode) error { return errors.WithStack(renameio.WriteFile(filename, data, perm)) } // IsWindowsJunctionPoint reports whether a filename is a Windows junction point. func IsWindowsJunctionPoint(filename string) (ok bool, err error) { return false, nil } ================================================ FILE: pkg/xos/xos_windows.go ================================================ //go:build windows // +build windows package xos import ( "fmt" "os" "os/exec" "strconv" "strings" "syscall" "github.com/cockroachdb/errors" "golang.org/x/sys/windows" ) func CreateNewProcessGroup() *syscall.SysProcAttr { return &syscall.SysProcAttr{ CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, } } func SocketStat(path string) (interface{}, error) { fd, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_READ, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) if err != nil { return nil, fmt.Errorf("CreateFile %s: %w", path, err) } defer windows.CloseHandle(fd) var d syscall.ByHandleFileInformation err = syscall.GetFileInformationByHandle(syscall.Handle(fd), &d) if err != nil { return nil, &os.PathError{"GetFileInformationByHandle", path, err} } return &d, nil } func SameSocket(a, b interface{}) bool { ai := a.(*syscall.ByHandleFileInformation) bi := b.(*syscall.ByHandleFileInformation) return ai.VolumeSerialNumber == bi.VolumeSerialNumber && ai.FileIndexHigh == bi.FileIndexHigh && ai.FileIndexLow == bi.FileIndexLow } func ArrangeExtraFiles(cmd *exec.Cmd, files ...*os.File) error { attr := cmd.SysProcAttr if attr == nil { attr = &syscall.SysProcAttr{} cmd.SysProcAttr = attr } // Flag the files to bbe inherited by the child process var fds []string for _, f := range files { fd := f.Fd() fds = append(fds, strconv.FormatUint(uint64(fd), 10)) err := windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1) if err != nil { return fmt.Errorf("xos.ArrangeExtraFiles: SetHandleInformation: %v", err) } attr.AdditionalInheritedHandles = append(attr.AdditionalInheritedHandles, syscall.Handle(fd)) } // If the env hasn't been set, copy over this process' env so we preserve the cmd.Env semantics. if cmd.Env == nil { cmd.Env = os.Environ() } cmd.Env = append(cmd.Env, "ENCORE_EXTRA_FILES="+strings.Join(fds, ",")) return nil } func IsAdminUser() (bool, error) { // For Windows we elevate permissions on demand, so pretend we are admin return true, nil } // WriteFile writes the given file with the given data and permissions. // // Where possible (i.e. not on windows) it will use an atomic write process // which removes the possibility of a partial file being written during a crash // or error. func WriteFile(filename string, data []byte, perm os.FileMode) error { return errors.WithStack(os.WriteFile(filename, data, perm)) } // IsWindowsJunctionPoint reports whether a filename is a Windows junction point. func IsWindowsJunctionPoint(filename string) (ok bool, err error) { ptr, err := syscall.UTF16PtrFromString(filename) if err != nil { return false, err } attrs, err := windows.GetFileAttributes(ptr) if err != nil { return false, err } return attrs&windows.FILE_ATTRIBUTE_REPARSE_POINT == windows.FILE_ATTRIBUTE_REPARSE_POINT, nil } ================================================ FILE: proto/encore/daemon/daemon.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/daemon/daemon.proto package daemon import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type DBRole int32 const ( DBRole_DB_ROLE_UNSPECIFIED DBRole = 0 DBRole_DB_ROLE_SUPERUSER DBRole = 1 DBRole_DB_ROLE_ADMIN DBRole = 2 DBRole_DB_ROLE_WRITE DBRole = 3 DBRole_DB_ROLE_READ DBRole = 4 ) // Enum value maps for DBRole. var ( DBRole_name = map[int32]string{ 0: "DB_ROLE_UNSPECIFIED", 1: "DB_ROLE_SUPERUSER", 2: "DB_ROLE_ADMIN", 3: "DB_ROLE_WRITE", 4: "DB_ROLE_READ", } DBRole_value = map[string]int32{ "DB_ROLE_UNSPECIFIED": 0, "DB_ROLE_SUPERUSER": 1, "DB_ROLE_ADMIN": 2, "DB_ROLE_WRITE": 3, "DB_ROLE_READ": 4, } ) func (x DBRole) Enum() *DBRole { p := new(DBRole) *p = x return p } func (x DBRole) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DBRole) Descriptor() protoreflect.EnumDescriptor { return file_encore_daemon_daemon_proto_enumTypes[0].Descriptor() } func (DBRole) Type() protoreflect.EnumType { return &file_encore_daemon_daemon_proto_enumTypes[0] } func (x DBRole) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DBRole.Descriptor instead. func (DBRole) EnumDescriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{0} } type DBClusterType int32 const ( DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED DBClusterType = 0 DBClusterType_DB_CLUSTER_TYPE_RUN DBClusterType = 1 DBClusterType_DB_CLUSTER_TYPE_TEST DBClusterType = 2 DBClusterType_DB_CLUSTER_TYPE_SHADOW DBClusterType = 3 ) // Enum value maps for DBClusterType. var ( DBClusterType_name = map[int32]string{ 0: "DB_CLUSTER_TYPE_UNSPECIFIED", 1: "DB_CLUSTER_TYPE_RUN", 2: "DB_CLUSTER_TYPE_TEST", 3: "DB_CLUSTER_TYPE_SHADOW", } DBClusterType_value = map[string]int32{ "DB_CLUSTER_TYPE_UNSPECIFIED": 0, "DB_CLUSTER_TYPE_RUN": 1, "DB_CLUSTER_TYPE_TEST": 2, "DB_CLUSTER_TYPE_SHADOW": 3, } ) func (x DBClusterType) Enum() *DBClusterType { p := new(DBClusterType) *p = x return p } func (x DBClusterType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DBClusterType) Descriptor() protoreflect.EnumDescriptor { return file_encore_daemon_daemon_proto_enumTypes[1].Descriptor() } func (DBClusterType) Type() protoreflect.EnumType { return &file_encore_daemon_daemon_proto_enumTypes[1] } func (x DBClusterType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DBClusterType.Descriptor instead. func (DBClusterType) EnumDescriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{1} } type RunRequest_BrowserMode int32 const ( RunRequest_BROWSER_AUTO RunRequest_BrowserMode = 0 RunRequest_BROWSER_NEVER RunRequest_BrowserMode = 1 RunRequest_BROWSER_ALWAYS RunRequest_BrowserMode = 2 ) // Enum value maps for RunRequest_BrowserMode. var ( RunRequest_BrowserMode_name = map[int32]string{ 0: "BROWSER_AUTO", 1: "BROWSER_NEVER", 2: "BROWSER_ALWAYS", } RunRequest_BrowserMode_value = map[string]int32{ "BROWSER_AUTO": 0, "BROWSER_NEVER": 1, "BROWSER_ALWAYS": 2, } ) func (x RunRequest_BrowserMode) Enum() *RunRequest_BrowserMode { p := new(RunRequest_BrowserMode) *p = x return p } func (x RunRequest_BrowserMode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (RunRequest_BrowserMode) Descriptor() protoreflect.EnumDescriptor { return file_encore_daemon_daemon_proto_enumTypes[2].Descriptor() } func (RunRequest_BrowserMode) Type() protoreflect.EnumType { return &file_encore_daemon_daemon_proto_enumTypes[2] } func (x RunRequest_BrowserMode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use RunRequest_BrowserMode.Descriptor instead. func (RunRequest_BrowserMode) EnumDescriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6, 0} } type RunRequest_DebugMode int32 const ( RunRequest_DEBUG_DISABLED RunRequest_DebugMode = 0 RunRequest_DEBUG_ENABLED RunRequest_DebugMode = 1 RunRequest_DEBUG_BREAK RunRequest_DebugMode = 2 ) // Enum value maps for RunRequest_DebugMode. var ( RunRequest_DebugMode_name = map[int32]string{ 0: "DEBUG_DISABLED", 1: "DEBUG_ENABLED", 2: "DEBUG_BREAK", } RunRequest_DebugMode_value = map[string]int32{ "DEBUG_DISABLED": 0, "DEBUG_ENABLED": 1, "DEBUG_BREAK": 2, } ) func (x RunRequest_DebugMode) Enum() *RunRequest_DebugMode { p := new(RunRequest_DebugMode) *p = x return p } func (x RunRequest_DebugMode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (RunRequest_DebugMode) Descriptor() protoreflect.EnumDescriptor { return file_encore_daemon_daemon_proto_enumTypes[3].Descriptor() } func (RunRequest_DebugMode) Type() protoreflect.EnumType { return &file_encore_daemon_daemon_proto_enumTypes[3] } func (x RunRequest_DebugMode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use RunRequest_DebugMode.Descriptor instead. func (RunRequest_DebugMode) EnumDescriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6, 1} } type DumpMetaRequest_Format int32 const ( DumpMetaRequest_FORMAT_UNSPECIFIED DumpMetaRequest_Format = 0 DumpMetaRequest_FORMAT_JSON DumpMetaRequest_Format = 1 DumpMetaRequest_FORMAT_PROTO DumpMetaRequest_Format = 2 ) // Enum value maps for DumpMetaRequest_Format. var ( DumpMetaRequest_Format_name = map[int32]string{ 0: "FORMAT_UNSPECIFIED", 1: "FORMAT_JSON", 2: "FORMAT_PROTO", } DumpMetaRequest_Format_value = map[string]int32{ "FORMAT_UNSPECIFIED": 0, "FORMAT_JSON": 1, "FORMAT_PROTO": 2, } ) func (x DumpMetaRequest_Format) Enum() *DumpMetaRequest_Format { p := new(DumpMetaRequest_Format) *p = x return p } func (x DumpMetaRequest_Format) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DumpMetaRequest_Format) Descriptor() protoreflect.EnumDescriptor { return file_encore_daemon_daemon_proto_enumTypes[4].Descriptor() } func (DumpMetaRequest_Format) Type() protoreflect.EnumType { return &file_encore_daemon_daemon_proto_enumTypes[4] } func (x DumpMetaRequest_Format) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DumpMetaRequest_Format.Descriptor instead. func (DumpMetaRequest_Format) EnumDescriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{35, 0} } type CommandMessage struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Msg: // // *CommandMessage_Output // *CommandMessage_Exit // *CommandMessage_Errors Msg isCommandMessage_Msg `protobuf_oneof:"msg"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CommandMessage) Reset() { *x = CommandMessage{} mi := &file_encore_daemon_daemon_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CommandMessage) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommandMessage) ProtoMessage() {} func (x *CommandMessage) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommandMessage.ProtoReflect.Descriptor instead. func (*CommandMessage) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{0} } func (x *CommandMessage) GetMsg() isCommandMessage_Msg { if x != nil { return x.Msg } return nil } func (x *CommandMessage) GetOutput() *CommandOutput { if x != nil { if x, ok := x.Msg.(*CommandMessage_Output); ok { return x.Output } } return nil } func (x *CommandMessage) GetExit() *CommandExit { if x != nil { if x, ok := x.Msg.(*CommandMessage_Exit); ok { return x.Exit } } return nil } func (x *CommandMessage) GetErrors() *CommandDisplayErrors { if x != nil { if x, ok := x.Msg.(*CommandMessage_Errors); ok { return x.Errors } } return nil } type isCommandMessage_Msg interface { isCommandMessage_Msg() } type CommandMessage_Output struct { Output *CommandOutput `protobuf:"bytes,1,opt,name=output,proto3,oneof"` } type CommandMessage_Exit struct { Exit *CommandExit `protobuf:"bytes,2,opt,name=exit,proto3,oneof"` } type CommandMessage_Errors struct { Errors *CommandDisplayErrors `protobuf:"bytes,3,opt,name=errors,proto3,oneof"` } func (*CommandMessage_Output) isCommandMessage_Msg() {} func (*CommandMessage_Exit) isCommandMessage_Msg() {} func (*CommandMessage_Errors) isCommandMessage_Msg() {} type CommandOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CommandOutput) Reset() { *x = CommandOutput{} mi := &file_encore_daemon_daemon_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CommandOutput) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommandOutput) ProtoMessage() {} func (x *CommandOutput) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommandOutput.ProtoReflect.Descriptor instead. func (*CommandOutput) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{1} } func (x *CommandOutput) GetStdout() []byte { if x != nil { return x.Stdout } return nil } func (x *CommandOutput) GetStderr() []byte { if x != nil { return x.Stderr } return nil } type CommandExit struct { state protoimpl.MessageState `protogen:"open.v1"` Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` // exit code unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CommandExit) Reset() { *x = CommandExit{} mi := &file_encore_daemon_daemon_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CommandExit) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommandExit) ProtoMessage() {} func (x *CommandExit) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommandExit.ProtoReflect.Descriptor instead. func (*CommandExit) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{2} } func (x *CommandExit) GetCode() int32 { if x != nil { return x.Code } return 0 } type CommandDisplayErrors struct { state protoimpl.MessageState `protogen:"open.v1"` Errinsrc []byte `protobuf:"bytes,1,opt,name=errinsrc,proto3" json:"errinsrc,omitempty"` // error messages in source code unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CommandDisplayErrors) Reset() { *x = CommandDisplayErrors{} mi := &file_encore_daemon_daemon_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CommandDisplayErrors) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommandDisplayErrors) ProtoMessage() {} func (x *CommandDisplayErrors) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommandDisplayErrors.ProtoReflect.Descriptor instead. func (*CommandDisplayErrors) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{3} } func (x *CommandDisplayErrors) GetErrinsrc() []byte { if x != nil { return x.Errinsrc } return nil } type CreateAppRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // app_root is the absolute filesystem path to the Encore app root. AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` // template is the template used to create the app Template string `protobuf:"bytes,2,opt,name=template,proto3" json:"template,omitempty"` // tutorial is a flag to indicate if the app is a tutorial app Tutorial bool `protobuf:"varint,3,opt,name=tutorial,proto3" json:"tutorial,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreateAppRequest) Reset() { *x = CreateAppRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreateAppRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateAppRequest) ProtoMessage() {} func (x *CreateAppRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateAppRequest.ProtoReflect.Descriptor instead. func (*CreateAppRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{4} } func (x *CreateAppRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *CreateAppRequest) GetTemplate() string { if x != nil { return x.Template } return "" } func (x *CreateAppRequest) GetTutorial() bool { if x != nil { return x.Tutorial } return false } type CreateAppResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreateAppResponse) Reset() { *x = CreateAppResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreateAppResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateAppResponse) ProtoMessage() {} func (x *CreateAppResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateAppResponse.ProtoReflect.Descriptor instead. func (*CreateAppResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{5} } func (x *CreateAppResponse) GetAppId() string { if x != nil { return x.AppId } return "" } type RunRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // app_root is the absolute filesystem path to the Encore app root. AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` // working_dir is the working directory relative to the app_root, // for formatting relative paths in error messages. WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` // watch, if true, enables live reloading of the app whenever the source changes. Watch bool `protobuf:"varint,5,opt,name=watch,proto3" json:"watch,omitempty"` // listen_addr is the address to listen on. ListenAddr string `protobuf:"bytes,6,opt,name=listen_addr,json=listenAddr,proto3" json:"listen_addr,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,7,rep,name=environ,proto3" json:"environ,omitempty"` // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. TraceFile *string `protobuf:"bytes,8,opt,name=trace_file,json=traceFile,proto3,oneof" json:"trace_file,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,9,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` // browser specifies whether and how to open the browser on startup. Browser RunRequest_BrowserMode `protobuf:"varint,10,opt,name=browser,proto3,enum=encore.daemon.RunRequest_BrowserMode" json:"browser,omitempty"` // debug_mode specifies the debug mode to use. DebugMode RunRequest_DebugMode `protobuf:"varint,11,opt,name=debug_mode,json=debugMode,proto3,enum=encore.daemon.RunRequest_DebugMode" json:"debug_mode,omitempty"` // Log level override. LogLevel *string `protobuf:"bytes,12,opt,name=log_level,json=logLevel,proto3,oneof" json:"log_level,omitempty"` // scrub_sensitive_data, if true, scrubs sensitive data from local traces. ScrubSensitiveData bool `protobuf:"varint,13,opt,name=scrub_sensitive_data,json=scrubSensitiveData,proto3" json:"scrub_sensitive_data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunRequest) Reset() { *x = RunRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RunRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RunRequest) ProtoMessage() {} func (x *RunRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RunRequest.ProtoReflect.Descriptor instead. func (*RunRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6} } func (x *RunRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *RunRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *RunRequest) GetWatch() bool { if x != nil { return x.Watch } return false } func (x *RunRequest) GetListenAddr() string { if x != nil { return x.ListenAddr } return "" } func (x *RunRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *RunRequest) GetTraceFile() string { if x != nil && x.TraceFile != nil { return *x.TraceFile } return "" } func (x *RunRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } func (x *RunRequest) GetBrowser() RunRequest_BrowserMode { if x != nil { return x.Browser } return RunRequest_BROWSER_AUTO } func (x *RunRequest) GetDebugMode() RunRequest_DebugMode { if x != nil { return x.DebugMode } return RunRequest_DEBUG_DISABLED } func (x *RunRequest) GetLogLevel() string { if x != nil && x.LogLevel != nil { return *x.LogLevel } return "" } func (x *RunRequest) GetScrubSensitiveData() bool { if x != nil { return x.ScrubSensitiveData } return false } type TestRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,4,rep,name=environ,proto3" json:"environ,omitempty"` // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. TraceFile *string `protobuf:"bytes,6,opt,name=trace_file,json=traceFile,proto3,oneof" json:"trace_file,omitempty"` // codegen_debug, if true, dumps the generated code and prints where it is located. CodegenDebug bool `protobuf:"varint,7,opt,name=codegen_debug,json=codegenDebug,proto3" json:"codegen_debug,omitempty"` // temp_dir is a temp dir that will be cleaned up after tests have been executed // to write things like app meta and runtime config etc. TempDir string `protobuf:"bytes,8,opt,name=temp_dir,json=tempDir,proto3" json:"temp_dir,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TestRequest) Reset() { *x = TestRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TestRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*TestRequest) ProtoMessage() {} func (x *TestRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TestRequest.ProtoReflect.Descriptor instead. func (*TestRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{7} } func (x *TestRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *TestRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *TestRequest) GetArgs() []string { if x != nil { return x.Args } return nil } func (x *TestRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *TestRequest) GetTraceFile() string { if x != nil && x.TraceFile != nil { return *x.TraceFile } return "" } func (x *TestRequest) GetCodegenDebug() bool { if x != nil { return x.CodegenDebug } return false } func (x *TestRequest) GetTempDir() string { if x != nil { return x.TempDir } return "" } type TestSpecRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,4,rep,name=environ,proto3" json:"environ,omitempty"` // temp_dir is a temp dir that will be cleaned up after tests have been executed // to write things like app meta and runtime config etc. TempDir string `protobuf:"bytes,5,opt,name=temp_dir,json=tempDir,proto3" json:"temp_dir,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TestSpecRequest) Reset() { *x = TestSpecRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TestSpecRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*TestSpecRequest) ProtoMessage() {} func (x *TestSpecRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TestSpecRequest.ProtoReflect.Descriptor instead. func (*TestSpecRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{8} } func (x *TestSpecRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *TestSpecRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *TestSpecRequest) GetArgs() []string { if x != nil { return x.Args } return nil } func (x *TestSpecRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *TestSpecRequest) GetTempDir() string { if x != nil { return x.TempDir } return "" } type TestSpecResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` Environ []string `protobuf:"bytes,3,rep,name=environ,proto3" json:"environ,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TestSpecResponse) Reset() { *x = TestSpecResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TestSpecResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*TestSpecResponse) ProtoMessage() {} func (x *TestSpecResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TestSpecResponse.ProtoReflect.Descriptor instead. func (*TestSpecResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{9} } func (x *TestSpecResponse) GetCommand() string { if x != nil { return x.Command } return "" } func (x *TestSpecResponse) GetArgs() []string { if x != nil { return x.Args } return nil } func (x *TestSpecResponse) GetEnviron() []string { if x != nil { return x.Environ } return nil } type ExecScriptRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` ScriptArgs []string `protobuf:"bytes,4,rep,name=script_args,json=scriptArgs,proto3" json:"script_args,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,5,rep,name=environ,proto3" json:"environ,omitempty"` // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. TraceFile *string `protobuf:"bytes,6,opt,name=trace_file,json=traceFile,proto3,oneof" json:"trace_file,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,7,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExecScriptRequest) Reset() { *x = ExecScriptRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ExecScriptRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExecScriptRequest) ProtoMessage() {} func (x *ExecScriptRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExecScriptRequest.ProtoReflect.Descriptor instead. func (*ExecScriptRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{10} } func (x *ExecScriptRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *ExecScriptRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *ExecScriptRequest) GetScriptArgs() []string { if x != nil { return x.ScriptArgs } return nil } func (x *ExecScriptRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *ExecScriptRequest) GetTraceFile() string { if x != nil && x.TraceFile != nil { return *x.TraceFile } return "" } func (x *ExecScriptRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } type ExecSpecRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` ScriptArgs []string `protobuf:"bytes,4,rep,name=script_args,json=scriptArgs,proto3" json:"script_args,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,5,rep,name=environ,proto3" json:"environ,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,7,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` // temp_dir is a temp dir that will be cleaned up by the CLI after the command // has been executed, to write things like app meta and runtime config etc. TempDir string `protobuf:"bytes,8,opt,name=temp_dir,json=tempDir,proto3" json:"temp_dir,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExecSpecRequest) Reset() { *x = ExecSpecRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ExecSpecRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExecSpecRequest) ProtoMessage() {} func (x *ExecSpecRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExecSpecRequest.ProtoReflect.Descriptor instead. func (*ExecSpecRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{11} } func (x *ExecSpecRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *ExecSpecRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *ExecSpecRequest) GetScriptArgs() []string { if x != nil { return x.ScriptArgs } return nil } func (x *ExecSpecRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *ExecSpecRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } func (x *ExecSpecRequest) GetTempDir() string { if x != nil { return x.TempDir } return "" } type ExecSpecMessage struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Msg: // // *ExecSpecMessage_Output // *ExecSpecMessage_Spec Msg isExecSpecMessage_Msg `protobuf_oneof:"msg"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExecSpecMessage) Reset() { *x = ExecSpecMessage{} mi := &file_encore_daemon_daemon_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ExecSpecMessage) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExecSpecMessage) ProtoMessage() {} func (x *ExecSpecMessage) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExecSpecMessage.ProtoReflect.Descriptor instead. func (*ExecSpecMessage) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{12} } func (x *ExecSpecMessage) GetMsg() isExecSpecMessage_Msg { if x != nil { return x.Msg } return nil } func (x *ExecSpecMessage) GetOutput() *CommandOutput { if x != nil { if x, ok := x.Msg.(*ExecSpecMessage_Output); ok { return x.Output } } return nil } func (x *ExecSpecMessage) GetSpec() *ExecSpecResponse { if x != nil { if x, ok := x.Msg.(*ExecSpecMessage_Spec); ok { return x.Spec } } return nil } type isExecSpecMessage_Msg interface { isExecSpecMessage_Msg() } type ExecSpecMessage_Output struct { Output *CommandOutput `protobuf:"bytes,1,opt,name=output,proto3,oneof"` } type ExecSpecMessage_Spec struct { Spec *ExecSpecResponse `protobuf:"bytes,2,opt,name=spec,proto3,oneof"` } func (*ExecSpecMessage_Output) isExecSpecMessage_Msg() {} func (*ExecSpecMessage_Spec) isExecSpecMessage_Msg() {} type ExecSpecResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` Environ []string `protobuf:"bytes,3,rep,name=environ,proto3" json:"environ,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExecSpecResponse) Reset() { *x = ExecSpecResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ExecSpecResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExecSpecResponse) ProtoMessage() {} func (x *ExecSpecResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExecSpecResponse.ProtoReflect.Descriptor instead. func (*ExecSpecResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{13} } func (x *ExecSpecResponse) GetCommand() string { if x != nil { return x.Command } return "" } func (x *ExecSpecResponse) GetArgs() []string { if x != nil { return x.Args } return nil } func (x *ExecSpecResponse) GetEnviron() []string { if x != nil { return x.Environ } return nil } type CheckRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` // codegen_debug, if true, dumps the generated code and prints where it is located. CodegenDebug bool `protobuf:"varint,3,opt,name=codegen_debug,json=codegenDebug,proto3" json:"codegen_debug,omitempty"` // parse_tests, if true, exercises test parsing and codegen as well. ParseTests bool `protobuf:"varint,4,opt,name=parse_tests,json=parseTests,proto3" json:"parse_tests,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,5,rep,name=environ,proto3" json:"environ,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CheckRequest) Reset() { *x = CheckRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CheckRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CheckRequest) ProtoMessage() {} func (x *CheckRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead. func (*CheckRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{14} } func (x *CheckRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *CheckRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *CheckRequest) GetCodegenDebug() bool { if x != nil { return x.CodegenDebug } return false } func (x *CheckRequest) GetParseTests() bool { if x != nil { return x.ParseTests } return false } func (x *CheckRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } type ExportRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` // goos and goarch specify the platform configuration to compile // the application for. The values must be valid GOOS/GOARCH values. Goos string `protobuf:"bytes,2,opt,name=goos,proto3" json:"goos,omitempty"` Goarch string `protobuf:"bytes,3,opt,name=goarch,proto3" json:"goarch,omitempty"` // cgo_enabled specifies whether to build with cgo enabled. // The host must have a valid C compiler for the target platform // if true. CgoEnabled bool `protobuf:"varint,4,opt,name=cgo_enabled,json=cgoEnabled,proto3" json:"cgo_enabled,omitempty"` // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,5,rep,name=environ,proto3" json:"environ,omitempty"` // Types that are valid to be assigned to Format: // // *ExportRequest_Docker Format isExportRequest_Format `protobuf_oneof:"format"` InfraConfPath string `protobuf:"bytes,7,opt,name=infra_conf_path,json=infraConfPath,proto3" json:"infra_conf_path,omitempty"` Services []string `protobuf:"bytes,8,rep,name=services,proto3" json:"services,omitempty"` Gateways []string `protobuf:"bytes,9,rep,name=gateways,proto3" json:"gateways,omitempty"` SkipInfraConf bool `protobuf:"varint,10,opt,name=skip_infra_conf,json=skipInfraConf,proto3" json:"skip_infra_conf,omitempty"` // A parent path to app_root containing the .git, or the same as app_root WorkspaceRoot string `protobuf:"bytes,11,opt,name=workspace_root,json=workspaceRoot,proto3" json:"workspace_root,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ExportRequest) Reset() { *x = ExportRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ExportRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExportRequest) ProtoMessage() {} func (x *ExportRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExportRequest.ProtoReflect.Descriptor instead. func (*ExportRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{15} } func (x *ExportRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *ExportRequest) GetGoos() string { if x != nil { return x.Goos } return "" } func (x *ExportRequest) GetGoarch() string { if x != nil { return x.Goarch } return "" } func (x *ExportRequest) GetCgoEnabled() bool { if x != nil { return x.CgoEnabled } return false } func (x *ExportRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *ExportRequest) GetFormat() isExportRequest_Format { if x != nil { return x.Format } return nil } func (x *ExportRequest) GetDocker() *DockerExportParams { if x != nil { if x, ok := x.Format.(*ExportRequest_Docker); ok { return x.Docker } } return nil } func (x *ExportRequest) GetInfraConfPath() string { if x != nil { return x.InfraConfPath } return "" } func (x *ExportRequest) GetServices() []string { if x != nil { return x.Services } return nil } func (x *ExportRequest) GetGateways() []string { if x != nil { return x.Gateways } return nil } func (x *ExportRequest) GetSkipInfraConf() bool { if x != nil { return x.SkipInfraConf } return false } func (x *ExportRequest) GetWorkspaceRoot() string { if x != nil { return x.WorkspaceRoot } return "" } type isExportRequest_Format interface { isExportRequest_Format() } type ExportRequest_Docker struct { // docker specifies to export the app as a docker image. Docker *DockerExportParams `protobuf:"bytes,6,opt,name=docker,proto3,oneof"` } func (*ExportRequest_Docker) isExportRequest_Format() {} type DockerExportParams struct { state protoimpl.MessageState `protogen:"open.v1"` // local_daemon_tag specifies what to tag the image as // in the local Docker daemon. If empty the export does not // interact with (or require) the local docker daemon at all. LocalDaemonTag string `protobuf:"bytes,1,opt,name=local_daemon_tag,json=localDaemonTag,proto3" json:"local_daemon_tag,omitempty"` // push_destination_tag specifies the remote registry tag // to push the exported image to. If empty the built image // is not pushed anywhere. PushDestinationTag string `protobuf:"bytes,2,opt,name=push_destination_tag,json=pushDestinationTag,proto3" json:"push_destination_tag,omitempty"` // base_image_tag is the base image to build the image from. BaseImageTag string `protobuf:"bytes,3,opt,name=base_image_tag,json=baseImageTag,proto3" json:"base_image_tag,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DockerExportParams) Reset() { *x = DockerExportParams{} mi := &file_encore_daemon_daemon_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DockerExportParams) String() string { return protoimpl.X.MessageStringOf(x) } func (*DockerExportParams) ProtoMessage() {} func (x *DockerExportParams) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DockerExportParams.ProtoReflect.Descriptor instead. func (*DockerExportParams) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{16} } func (x *DockerExportParams) GetLocalDaemonTag() string { if x != nil { return x.LocalDaemonTag } return "" } func (x *DockerExportParams) GetPushDestinationTag() string { if x != nil { return x.PushDestinationTag } return "" } func (x *DockerExportParams) GetBaseImageTag() string { if x != nil { return x.BaseImageTag } return "" } type DBConnectRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` DbName string `protobuf:"bytes,2,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` EnvName string `protobuf:"bytes,3,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // optional ClusterType DBClusterType `protobuf:"varint,4,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType" json:"cluster_type,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,5,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` Role DBRole `protobuf:"varint,6,opt,name=role,proto3,enum=encore.daemon.DBRole" json:"role,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBConnectRequest) Reset() { *x = DBConnectRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBConnectRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBConnectRequest) ProtoMessage() {} func (x *DBConnectRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBConnectRequest.ProtoReflect.Descriptor instead. func (*DBConnectRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{17} } func (x *DBConnectRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *DBConnectRequest) GetDbName() string { if x != nil { return x.DbName } return "" } func (x *DBConnectRequest) GetEnvName() string { if x != nil { return x.EnvName } return "" } func (x *DBConnectRequest) GetClusterType() DBClusterType { if x != nil { return x.ClusterType } return DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED } func (x *DBConnectRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } func (x *DBConnectRequest) GetRole() DBRole { if x != nil { return x.Role } return DBRole_DB_ROLE_UNSPECIFIED } type DBConnectResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Dsn string `protobuf:"bytes,1,opt,name=dsn,proto3" json:"dsn,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBConnectResponse) Reset() { *x = DBConnectResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBConnectResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBConnectResponse) ProtoMessage() {} func (x *DBConnectResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBConnectResponse.ProtoReflect.Descriptor instead. func (*DBConnectResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{18} } func (x *DBConnectResponse) GetDsn() string { if x != nil { return x.Dsn } return "" } type DBProxyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // optional Port int32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` // optional ClusterType DBClusterType `protobuf:"varint,4,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType" json:"cluster_type,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,5,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` Role DBRole `protobuf:"varint,6,opt,name=role,proto3,enum=encore.daemon.DBRole" json:"role,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBProxyRequest) Reset() { *x = DBProxyRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBProxyRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBProxyRequest) ProtoMessage() {} func (x *DBProxyRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBProxyRequest.ProtoReflect.Descriptor instead. func (*DBProxyRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{19} } func (x *DBProxyRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *DBProxyRequest) GetEnvName() string { if x != nil { return x.EnvName } return "" } func (x *DBProxyRequest) GetPort() int32 { if x != nil { return x.Port } return 0 } func (x *DBProxyRequest) GetClusterType() DBClusterType { if x != nil { return x.ClusterType } return DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED } func (x *DBProxyRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } func (x *DBProxyRequest) GetRole() DBRole { if x != nil { return x.Role } return DBRole_DB_ROLE_UNSPECIFIED } type DBResetRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` DatabaseNames []string `protobuf:"bytes,2,rep,name=database_names,json=databaseNames,proto3" json:"database_names,omitempty"` // database names to reset ClusterType DBClusterType `protobuf:"varint,3,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType" json:"cluster_type,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. Namespace *string `protobuf:"bytes,4,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBResetRequest) Reset() { *x = DBResetRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBResetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBResetRequest) ProtoMessage() {} func (x *DBResetRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBResetRequest.ProtoReflect.Descriptor instead. func (*DBResetRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{20} } func (x *DBResetRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *DBResetRequest) GetDatabaseNames() []string { if x != nil { return x.DatabaseNames } return nil } func (x *DBResetRequest) GetClusterType() DBClusterType { if x != nil { return x.ClusterType } return DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED } func (x *DBResetRequest) GetNamespace() string { if x != nil && x.Namespace != nil { return *x.Namespace } return "" } type GenClientRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` Lang string `protobuf:"bytes,3,opt,name=lang,proto3" json:"lang,omitempty"` Filepath string `protobuf:"bytes,4,opt,name=filepath,proto3" json:"filepath,omitempty"` // Services to include in the output. // If the string "*" is present all services are included. Services []string `protobuf:"bytes,5,rep,name=services,proto3" json:"services,omitempty"` // Services to exclude from the output. // Takes precedence over 'services' above. ExcludedServices []string `protobuf:"bytes,6,rep,name=excluded_services,json=excludedServices,proto3" json:"excluded_services,omitempty"` // Tags of endpoints to include in the output. // Only includes endpoints from services included in 'services' above. EndpointTags []string `protobuf:"bytes,7,rep,name=endpoint_tags,json=endpointTags,proto3" json:"endpoint_tags,omitempty"` // Tags of endpoints to exclude from the output. // Takes precedence over 'endpoint_tags' above. ExcludedEndpointTags []string `protobuf:"bytes,8,rep,name=excluded_endpoint_tags,json=excludedEndpointTags,proto3" json:"excluded_endpoint_tags,omitempty"` // The OpenAPI spec generator by default includes private endpoints. // If this is set to `true`, private endpoints will not be included // in the generated OpenAPI spec. OpenapiExcludePrivateEndpoints *bool `protobuf:"varint,9,opt,name=openapi_exclude_private_endpoints,json=openapiExcludePrivateEndpoints,proto3,oneof" json:"openapi_exclude_private_endpoints,omitempty"` // The TS generator by default re-declares the api types in the client. // If this is set to `true`, the types will be imported and shared between // the client and the server. It assumes "~backend" is available in the // import path. TsSharedTypes *bool `protobuf:"varint,10,opt,name=ts_shared_types,json=tsSharedTypes,proto3,oneof" json:"ts_shared_types,omitempty"` // If set, the default export of the generate TypeScript client will be // an instantiated client with the given target. The target can be e.g. // a variable, e.g. "import.meta.env.VITE_CLIENT_TARGET" or a string literal. TsClientTarget *string `protobuf:"bytes,11,opt,name=ts_client_target,json=tsClientTarget,proto3,oneof" json:"ts_client_target,omitempty"` // The root directory of the app to generate a client for. // Included to be able to handle multi clone scenarios. AppRoot string `protobuf:"bytes,12,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GenClientRequest) Reset() { *x = GenClientRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GenClientRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GenClientRequest) ProtoMessage() {} func (x *GenClientRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GenClientRequest.ProtoReflect.Descriptor instead. func (*GenClientRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{21} } func (x *GenClientRequest) GetAppId() string { if x != nil { return x.AppId } return "" } func (x *GenClientRequest) GetEnvName() string { if x != nil { return x.EnvName } return "" } func (x *GenClientRequest) GetLang() string { if x != nil { return x.Lang } return "" } func (x *GenClientRequest) GetFilepath() string { if x != nil { return x.Filepath } return "" } func (x *GenClientRequest) GetServices() []string { if x != nil { return x.Services } return nil } func (x *GenClientRequest) GetExcludedServices() []string { if x != nil { return x.ExcludedServices } return nil } func (x *GenClientRequest) GetEndpointTags() []string { if x != nil { return x.EndpointTags } return nil } func (x *GenClientRequest) GetExcludedEndpointTags() []string { if x != nil { return x.ExcludedEndpointTags } return nil } func (x *GenClientRequest) GetOpenapiExcludePrivateEndpoints() bool { if x != nil && x.OpenapiExcludePrivateEndpoints != nil { return *x.OpenapiExcludePrivateEndpoints } return false } func (x *GenClientRequest) GetTsSharedTypes() bool { if x != nil && x.TsSharedTypes != nil { return *x.TsSharedTypes } return false } func (x *GenClientRequest) GetTsClientTarget() string { if x != nil && x.TsClientTarget != nil { return *x.TsClientTarget } return "" } func (x *GenClientRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } type GenClientResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Code []byte `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GenClientResponse) Reset() { *x = GenClientResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GenClientResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GenClientResponse) ProtoMessage() {} func (x *GenClientResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GenClientResponse.ProtoReflect.Descriptor instead. func (*GenClientResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{22} } func (x *GenClientResponse) GetCode() []byte { if x != nil { return x.Code } return nil } type GenWrappersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GenWrappersRequest) Reset() { *x = GenWrappersRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GenWrappersRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GenWrappersRequest) ProtoMessage() {} func (x *GenWrappersRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GenWrappersRequest.ProtoReflect.Descriptor instead. func (*GenWrappersRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{23} } func (x *GenWrappersRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } type GenWrappersResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GenWrappersResponse) Reset() { *x = GenWrappersResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GenWrappersResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GenWrappersResponse) ProtoMessage() {} func (x *GenWrappersResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GenWrappersResponse.ProtoReflect.Descriptor instead. func (*GenWrappersResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{24} } type SecretsRefreshRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SecretsRefreshRequest) Reset() { *x = SecretsRefreshRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SecretsRefreshRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SecretsRefreshRequest) ProtoMessage() {} func (x *SecretsRefreshRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SecretsRefreshRequest.ProtoReflect.Descriptor instead. func (*SecretsRefreshRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{25} } func (x *SecretsRefreshRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *SecretsRefreshRequest) GetKey() string { if x != nil { return x.Key } return "" } func (x *SecretsRefreshRequest) GetValue() string { if x != nil { return x.Value } return "" } type SecretsRefreshResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SecretsRefreshResponse) Reset() { *x = SecretsRefreshResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SecretsRefreshResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SecretsRefreshResponse) ProtoMessage() {} func (x *SecretsRefreshResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SecretsRefreshResponse.ProtoReflect.Descriptor instead. func (*SecretsRefreshResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{26} } type VersionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` ConfigHash string `protobuf:"bytes,2,opt,name=config_hash,json=configHash,proto3" json:"config_hash,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *VersionResponse) Reset() { *x = VersionResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *VersionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*VersionResponse) ProtoMessage() {} func (x *VersionResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VersionResponse.ProtoReflect.Descriptor instead. func (*VersionResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{27} } func (x *VersionResponse) GetVersion() string { if x != nil { return x.Version } return "" } func (x *VersionResponse) GetConfigHash() string { if x != nil { return x.ConfigHash } return "" } type Namespace struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"` CreatedAt string `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` LastActiveAt *string `protobuf:"bytes,5,opt,name=last_active_at,json=lastActiveAt,proto3,oneof" json:"last_active_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Namespace) Reset() { *x = Namespace{} mi := &file_encore_daemon_daemon_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Namespace) String() string { return protoimpl.X.MessageStringOf(x) } func (*Namespace) ProtoMessage() {} func (x *Namespace) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Namespace.ProtoReflect.Descriptor instead. func (*Namespace) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{28} } func (x *Namespace) GetId() string { if x != nil { return x.Id } return "" } func (x *Namespace) GetName() string { if x != nil { return x.Name } return "" } func (x *Namespace) GetActive() bool { if x != nil { return x.Active } return false } func (x *Namespace) GetCreatedAt() string { if x != nil { return x.CreatedAt } return "" } func (x *Namespace) GetLastActiveAt() string { if x != nil && x.LastActiveAt != nil { return *x.LastActiveAt } return "" } type CreateNamespaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CreateNamespaceRequest) Reset() { *x = CreateNamespaceRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CreateNamespaceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateNamespaceRequest) ProtoMessage() {} func (x *CreateNamespaceRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateNamespaceRequest.ProtoReflect.Descriptor instead. func (*CreateNamespaceRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{29} } func (x *CreateNamespaceRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *CreateNamespaceRequest) GetName() string { if x != nil { return x.Name } return "" } type SwitchNamespaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Create bool `protobuf:"varint,3,opt,name=create,proto3" json:"create,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SwitchNamespaceRequest) Reset() { *x = SwitchNamespaceRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SwitchNamespaceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SwitchNamespaceRequest) ProtoMessage() {} func (x *SwitchNamespaceRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SwitchNamespaceRequest.ProtoReflect.Descriptor instead. func (*SwitchNamespaceRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{30} } func (x *SwitchNamespaceRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *SwitchNamespaceRequest) GetName() string { if x != nil { return x.Name } return "" } func (x *SwitchNamespaceRequest) GetCreate() bool { if x != nil { return x.Create } return false } type ListNamespacesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListNamespacesRequest) Reset() { *x = ListNamespacesRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListNamespacesRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListNamespacesRequest) ProtoMessage() {} func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListNamespacesRequest.ProtoReflect.Descriptor instead. func (*ListNamespacesRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{31} } func (x *ListNamespacesRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } type DeleteNamespaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeleteNamespaceRequest) Reset() { *x = DeleteNamespaceRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeleteNamespaceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteNamespaceRequest) ProtoMessage() {} func (x *DeleteNamespaceRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteNamespaceRequest.ProtoReflect.Descriptor instead. func (*DeleteNamespaceRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{32} } func (x *DeleteNamespaceRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *DeleteNamespaceRequest) GetName() string { if x != nil { return x.Name } return "" } type ListNamespacesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Namespaces []*Namespace `protobuf:"bytes,1,rep,name=namespaces,proto3" json:"namespaces,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListNamespacesResponse) Reset() { *x = ListNamespacesResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ListNamespacesResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListNamespacesResponse) ProtoMessage() {} func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListNamespacesResponse.ProtoReflect.Descriptor instead. func (*ListNamespacesResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{33} } func (x *ListNamespacesResponse) GetNamespaces() []*Namespace { if x != nil { return x.Namespaces } return nil } type TelemetryConfig struct { state protoimpl.MessageState `protogen:"open.v1"` AnonId string `protobuf:"bytes,1,opt,name=anon_id,json=anonId,proto3" json:"anon_id,omitempty"` Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` Debug bool `protobuf:"varint,3,opt,name=debug,proto3" json:"debug,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TelemetryConfig) Reset() { *x = TelemetryConfig{} mi := &file_encore_daemon_daemon_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TelemetryConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*TelemetryConfig) ProtoMessage() {} func (x *TelemetryConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TelemetryConfig.ProtoReflect.Descriptor instead. func (*TelemetryConfig) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{34} } func (x *TelemetryConfig) GetAnonId() string { if x != nil { return x.AnonId } return "" } func (x *TelemetryConfig) GetEnabled() bool { if x != nil { return x.Enabled } return false } func (x *TelemetryConfig) GetDebug() bool { if x != nil { return x.Debug } return false } type DumpMetaRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` WorkingDir string `protobuf:"bytes,2,opt,name=working_dir,json=workingDir,proto3" json:"working_dir,omitempty"` // for error reporting // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). Environ []string `protobuf:"bytes,3,rep,name=environ,proto3" json:"environ,omitempty"` // Whether or not to parse tests. ParseTests bool `protobuf:"varint,4,opt,name=parse_tests,json=parseTests,proto3" json:"parse_tests,omitempty"` Format DumpMetaRequest_Format `protobuf:"varint,5,opt,name=format,proto3,enum=encore.daemon.DumpMetaRequest_Format" json:"format,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DumpMetaRequest) Reset() { *x = DumpMetaRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DumpMetaRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DumpMetaRequest) ProtoMessage() {} func (x *DumpMetaRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DumpMetaRequest.ProtoReflect.Descriptor instead. func (*DumpMetaRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{35} } func (x *DumpMetaRequest) GetAppRoot() string { if x != nil { return x.AppRoot } return "" } func (x *DumpMetaRequest) GetWorkingDir() string { if x != nil { return x.WorkingDir } return "" } func (x *DumpMetaRequest) GetEnviron() []string { if x != nil { return x.Environ } return nil } func (x *DumpMetaRequest) GetParseTests() bool { if x != nil { return x.ParseTests } return false } func (x *DumpMetaRequest) GetFormat() DumpMetaRequest_Format { if x != nil { return x.Format } return DumpMetaRequest_FORMAT_UNSPECIFIED } type DumpMetaResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Meta []byte `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DumpMetaResponse) Reset() { *x = DumpMetaResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DumpMetaResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DumpMetaResponse) ProtoMessage() {} func (x *DumpMetaResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DumpMetaResponse.ProtoReflect.Descriptor instead. func (*DumpMetaResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{36} } func (x *DumpMetaResponse) GetMeta() []byte { if x != nil { return x.Meta } return nil } // The following messages are used for sqlc plugin integration. type SQLCPlugin struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin) Reset() { *x = SQLCPlugin{} mi := &file_encore_daemon_daemon_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin) ProtoMessage() {} func (x *SQLCPlugin) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin.ProtoReflect.Descriptor instead. func (*SQLCPlugin) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37} } type SQLCPlugin_File struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Contents []byte `protobuf:"bytes,2,opt,name=contents,proto3" json:"contents,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_File) Reset() { *x = SQLCPlugin_File{} mi := &file_encore_daemon_daemon_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_File) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_File) ProtoMessage() {} func (x *SQLCPlugin_File) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_File.ProtoReflect.Descriptor instead. func (*SQLCPlugin_File) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 0} } func (x *SQLCPlugin_File) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_File) GetContents() []byte { if x != nil { return x.Contents } return nil } type SQLCPlugin_Settings struct { state protoimpl.MessageState `protogen:"open.v1"` Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` Engine string `protobuf:"bytes,2,opt,name=engine,proto3" json:"engine,omitempty"` Schema []string `protobuf:"bytes,3,rep,name=schema,proto3" json:"schema,omitempty"` Queries []string `protobuf:"bytes,4,rep,name=queries,proto3" json:"queries,omitempty"` Codegen *SQLCPlugin_Codegen `protobuf:"bytes,12,opt,name=codegen,proto3" json:"codegen,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Settings) Reset() { *x = SQLCPlugin_Settings{} mi := &file_encore_daemon_daemon_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Settings) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Settings) ProtoMessage() {} func (x *SQLCPlugin_Settings) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Settings.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Settings) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 1} } func (x *SQLCPlugin_Settings) GetVersion() string { if x != nil { return x.Version } return "" } func (x *SQLCPlugin_Settings) GetEngine() string { if x != nil { return x.Engine } return "" } func (x *SQLCPlugin_Settings) GetSchema() []string { if x != nil { return x.Schema } return nil } func (x *SQLCPlugin_Settings) GetQueries() []string { if x != nil { return x.Queries } return nil } func (x *SQLCPlugin_Settings) GetCodegen() *SQLCPlugin_Codegen { if x != nil { return x.Codegen } return nil } type SQLCPlugin_Codegen struct { state protoimpl.MessageState `protogen:"open.v1"` Out string `protobuf:"bytes,1,opt,name=out,proto3" json:"out,omitempty"` Plugin string `protobuf:"bytes,2,opt,name=plugin,proto3" json:"plugin,omitempty"` Options []byte `protobuf:"bytes,3,opt,name=options,proto3" json:"options,omitempty"` Env []string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` Process *SQLCPlugin_Codegen_Process `protobuf:"bytes,5,opt,name=process,proto3" json:"process,omitempty"` Wasm *SQLCPlugin_Codegen_WASM `protobuf:"bytes,6,opt,name=wasm,proto3" json:"wasm,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Codegen) Reset() { *x = SQLCPlugin_Codegen{} mi := &file_encore_daemon_daemon_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Codegen) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Codegen) ProtoMessage() {} func (x *SQLCPlugin_Codegen) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Codegen.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Codegen) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2} } func (x *SQLCPlugin_Codegen) GetOut() string { if x != nil { return x.Out } return "" } func (x *SQLCPlugin_Codegen) GetPlugin() string { if x != nil { return x.Plugin } return "" } func (x *SQLCPlugin_Codegen) GetOptions() []byte { if x != nil { return x.Options } return nil } func (x *SQLCPlugin_Codegen) GetEnv() []string { if x != nil { return x.Env } return nil } func (x *SQLCPlugin_Codegen) GetProcess() *SQLCPlugin_Codegen_Process { if x != nil { return x.Process } return nil } func (x *SQLCPlugin_Codegen) GetWasm() *SQLCPlugin_Codegen_WASM { if x != nil { return x.Wasm } return nil } type SQLCPlugin_Catalog struct { state protoimpl.MessageState `protogen:"open.v1"` Comment string `protobuf:"bytes,1,opt,name=comment,proto3" json:"comment,omitempty"` DefaultSchema string `protobuf:"bytes,2,opt,name=default_schema,json=defaultSchema,proto3" json:"default_schema,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` Schemas []*SQLCPlugin_Schema `protobuf:"bytes,4,rep,name=schemas,proto3" json:"schemas,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Catalog) Reset() { *x = SQLCPlugin_Catalog{} mi := &file_encore_daemon_daemon_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Catalog) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Catalog) ProtoMessage() {} func (x *SQLCPlugin_Catalog) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Catalog.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Catalog) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 3} } func (x *SQLCPlugin_Catalog) GetComment() string { if x != nil { return x.Comment } return "" } func (x *SQLCPlugin_Catalog) GetDefaultSchema() string { if x != nil { return x.DefaultSchema } return "" } func (x *SQLCPlugin_Catalog) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_Catalog) GetSchemas() []*SQLCPlugin_Schema { if x != nil { return x.Schemas } return nil } type SQLCPlugin_Schema struct { state protoimpl.MessageState `protogen:"open.v1"` Comment string `protobuf:"bytes,1,opt,name=comment,proto3" json:"comment,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Tables []*SQLCPlugin_Table `protobuf:"bytes,3,rep,name=tables,proto3" json:"tables,omitempty"` Enums []*SQLCPlugin_Enum `protobuf:"bytes,4,rep,name=enums,proto3" json:"enums,omitempty"` CompositeTypes []*SQLCPlugin_CompositeType `protobuf:"bytes,5,rep,name=composite_types,json=compositeTypes,proto3" json:"composite_types,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Schema) Reset() { *x = SQLCPlugin_Schema{} mi := &file_encore_daemon_daemon_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Schema) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Schema) ProtoMessage() {} func (x *SQLCPlugin_Schema) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Schema.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Schema) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 4} } func (x *SQLCPlugin_Schema) GetComment() string { if x != nil { return x.Comment } return "" } func (x *SQLCPlugin_Schema) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_Schema) GetTables() []*SQLCPlugin_Table { if x != nil { return x.Tables } return nil } func (x *SQLCPlugin_Schema) GetEnums() []*SQLCPlugin_Enum { if x != nil { return x.Enums } return nil } func (x *SQLCPlugin_Schema) GetCompositeTypes() []*SQLCPlugin_CompositeType { if x != nil { return x.CompositeTypes } return nil } type SQLCPlugin_CompositeType struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Comment string `protobuf:"bytes,2,opt,name=comment,proto3" json:"comment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_CompositeType) Reset() { *x = SQLCPlugin_CompositeType{} mi := &file_encore_daemon_daemon_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_CompositeType) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_CompositeType) ProtoMessage() {} func (x *SQLCPlugin_CompositeType) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_CompositeType.ProtoReflect.Descriptor instead. func (*SQLCPlugin_CompositeType) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 5} } func (x *SQLCPlugin_CompositeType) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_CompositeType) GetComment() string { if x != nil { return x.Comment } return "" } type SQLCPlugin_Enum struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Vals []string `protobuf:"bytes,2,rep,name=vals,proto3" json:"vals,omitempty"` Comment string `protobuf:"bytes,3,opt,name=comment,proto3" json:"comment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Enum) Reset() { *x = SQLCPlugin_Enum{} mi := &file_encore_daemon_daemon_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Enum) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Enum) ProtoMessage() {} func (x *SQLCPlugin_Enum) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Enum.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Enum) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 6} } func (x *SQLCPlugin_Enum) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_Enum) GetVals() []string { if x != nil { return x.Vals } return nil } func (x *SQLCPlugin_Enum) GetComment() string { if x != nil { return x.Comment } return "" } type SQLCPlugin_Table struct { state protoimpl.MessageState `protogen:"open.v1"` Rel *SQLCPlugin_Identifier `protobuf:"bytes,1,opt,name=rel,proto3" json:"rel,omitempty"` Columns []*SQLCPlugin_Column `protobuf:"bytes,2,rep,name=columns,proto3" json:"columns,omitempty"` Comment string `protobuf:"bytes,3,opt,name=comment,proto3" json:"comment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Table) Reset() { *x = SQLCPlugin_Table{} mi := &file_encore_daemon_daemon_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Table) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Table) ProtoMessage() {} func (x *SQLCPlugin_Table) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Table.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Table) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 7} } func (x *SQLCPlugin_Table) GetRel() *SQLCPlugin_Identifier { if x != nil { return x.Rel } return nil } func (x *SQLCPlugin_Table) GetColumns() []*SQLCPlugin_Column { if x != nil { return x.Columns } return nil } func (x *SQLCPlugin_Table) GetComment() string { if x != nil { return x.Comment } return "" } type SQLCPlugin_Identifier struct { state protoimpl.MessageState `protogen:"open.v1"` Catalog string `protobuf:"bytes,1,opt,name=catalog,proto3" json:"catalog,omitempty"` Schema string `protobuf:"bytes,2,opt,name=schema,proto3" json:"schema,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Identifier) Reset() { *x = SQLCPlugin_Identifier{} mi := &file_encore_daemon_daemon_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Identifier) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Identifier) ProtoMessage() {} func (x *SQLCPlugin_Identifier) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Identifier.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Identifier) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 8} } func (x *SQLCPlugin_Identifier) GetCatalog() string { if x != nil { return x.Catalog } return "" } func (x *SQLCPlugin_Identifier) GetSchema() string { if x != nil { return x.Schema } return "" } func (x *SQLCPlugin_Identifier) GetName() string { if x != nil { return x.Name } return "" } type SQLCPlugin_Column struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` NotNull bool `protobuf:"varint,3,opt,name=not_null,json=notNull,proto3" json:"not_null,omitempty"` IsArray bool `protobuf:"varint,4,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"` Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` Length int32 `protobuf:"varint,6,opt,name=length,proto3" json:"length,omitempty"` IsNamedParam bool `protobuf:"varint,7,opt,name=is_named_param,json=isNamedParam,proto3" json:"is_named_param,omitempty"` IsFuncCall bool `protobuf:"varint,8,opt,name=is_func_call,json=isFuncCall,proto3" json:"is_func_call,omitempty"` // XXX: Figure out what PostgreSQL calls `foo.id` Scope string `protobuf:"bytes,9,opt,name=scope,proto3" json:"scope,omitempty"` Table *SQLCPlugin_Identifier `protobuf:"bytes,10,opt,name=table,proto3" json:"table,omitempty"` TableAlias string `protobuf:"bytes,11,opt,name=table_alias,json=tableAlias,proto3" json:"table_alias,omitempty"` Type *SQLCPlugin_Identifier `protobuf:"bytes,12,opt,name=type,proto3" json:"type,omitempty"` IsSqlcSlice bool `protobuf:"varint,13,opt,name=is_sqlc_slice,json=isSqlcSlice,proto3" json:"is_sqlc_slice,omitempty"` EmbedTable *SQLCPlugin_Identifier `protobuf:"bytes,14,opt,name=embed_table,json=embedTable,proto3" json:"embed_table,omitempty"` OriginalName string `protobuf:"bytes,15,opt,name=original_name,json=originalName,proto3" json:"original_name,omitempty"` Unsigned bool `protobuf:"varint,16,opt,name=unsigned,proto3" json:"unsigned,omitempty"` ArrayDims int32 `protobuf:"varint,17,opt,name=array_dims,json=arrayDims,proto3" json:"array_dims,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Column) Reset() { *x = SQLCPlugin_Column{} mi := &file_encore_daemon_daemon_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Column) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Column) ProtoMessage() {} func (x *SQLCPlugin_Column) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Column.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Column) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 9} } func (x *SQLCPlugin_Column) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_Column) GetNotNull() bool { if x != nil { return x.NotNull } return false } func (x *SQLCPlugin_Column) GetIsArray() bool { if x != nil { return x.IsArray } return false } func (x *SQLCPlugin_Column) GetComment() string { if x != nil { return x.Comment } return "" } func (x *SQLCPlugin_Column) GetLength() int32 { if x != nil { return x.Length } return 0 } func (x *SQLCPlugin_Column) GetIsNamedParam() bool { if x != nil { return x.IsNamedParam } return false } func (x *SQLCPlugin_Column) GetIsFuncCall() bool { if x != nil { return x.IsFuncCall } return false } func (x *SQLCPlugin_Column) GetScope() string { if x != nil { return x.Scope } return "" } func (x *SQLCPlugin_Column) GetTable() *SQLCPlugin_Identifier { if x != nil { return x.Table } return nil } func (x *SQLCPlugin_Column) GetTableAlias() string { if x != nil { return x.TableAlias } return "" } func (x *SQLCPlugin_Column) GetType() *SQLCPlugin_Identifier { if x != nil { return x.Type } return nil } func (x *SQLCPlugin_Column) GetIsSqlcSlice() bool { if x != nil { return x.IsSqlcSlice } return false } func (x *SQLCPlugin_Column) GetEmbedTable() *SQLCPlugin_Identifier { if x != nil { return x.EmbedTable } return nil } func (x *SQLCPlugin_Column) GetOriginalName() string { if x != nil { return x.OriginalName } return "" } func (x *SQLCPlugin_Column) GetUnsigned() bool { if x != nil { return x.Unsigned } return false } func (x *SQLCPlugin_Column) GetArrayDims() int32 { if x != nil { return x.ArrayDims } return 0 } type SQLCPlugin_Query struct { state protoimpl.MessageState `protogen:"open.v1"` Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Cmd string `protobuf:"bytes,3,opt,name=cmd,proto3" json:"cmd,omitempty"` Columns []*SQLCPlugin_Column `protobuf:"bytes,4,rep,name=columns,proto3" json:"columns,omitempty"` Params []*SQLCPlugin_Parameter `protobuf:"bytes,5,rep,name=params,json=parameters,proto3" json:"params,omitempty"` Comments []string `protobuf:"bytes,6,rep,name=comments,proto3" json:"comments,omitempty"` Filename string `protobuf:"bytes,7,opt,name=filename,proto3" json:"filename,omitempty"` InsertIntoTable *SQLCPlugin_Identifier `protobuf:"bytes,8,opt,name=insert_into_table,proto3" json:"insert_into_table,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Query) Reset() { *x = SQLCPlugin_Query{} mi := &file_encore_daemon_daemon_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Query) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Query) ProtoMessage() {} func (x *SQLCPlugin_Query) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Query.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Query) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 10} } func (x *SQLCPlugin_Query) GetText() string { if x != nil { return x.Text } return "" } func (x *SQLCPlugin_Query) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLCPlugin_Query) GetCmd() string { if x != nil { return x.Cmd } return "" } func (x *SQLCPlugin_Query) GetColumns() []*SQLCPlugin_Column { if x != nil { return x.Columns } return nil } func (x *SQLCPlugin_Query) GetParams() []*SQLCPlugin_Parameter { if x != nil { return x.Params } return nil } func (x *SQLCPlugin_Query) GetComments() []string { if x != nil { return x.Comments } return nil } func (x *SQLCPlugin_Query) GetFilename() string { if x != nil { return x.Filename } return "" } func (x *SQLCPlugin_Query) GetInsertIntoTable() *SQLCPlugin_Identifier { if x != nil { return x.InsertIntoTable } return nil } type SQLCPlugin_Parameter struct { state protoimpl.MessageState `protogen:"open.v1"` Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` Column *SQLCPlugin_Column `protobuf:"bytes,2,opt,name=column,proto3" json:"column,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Parameter) Reset() { *x = SQLCPlugin_Parameter{} mi := &file_encore_daemon_daemon_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Parameter) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Parameter) ProtoMessage() {} func (x *SQLCPlugin_Parameter) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Parameter.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Parameter) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 11} } func (x *SQLCPlugin_Parameter) GetNumber() int32 { if x != nil { return x.Number } return 0 } func (x *SQLCPlugin_Parameter) GetColumn() *SQLCPlugin_Column { if x != nil { return x.Column } return nil } type SQLCPlugin_GenerateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Settings *SQLCPlugin_Settings `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` Catalog *SQLCPlugin_Catalog `protobuf:"bytes,2,opt,name=catalog,proto3" json:"catalog,omitempty"` Queries []*SQLCPlugin_Query `protobuf:"bytes,3,rep,name=queries,proto3" json:"queries,omitempty"` SqlcVersion string `protobuf:"bytes,4,opt,name=sqlc_version,proto3" json:"sqlc_version,omitempty"` PluginOptions []byte `protobuf:"bytes,5,opt,name=plugin_options,proto3" json:"plugin_options,omitempty"` GlobalOptions []byte `protobuf:"bytes,6,opt,name=global_options,proto3" json:"global_options,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_GenerateRequest) Reset() { *x = SQLCPlugin_GenerateRequest{} mi := &file_encore_daemon_daemon_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_GenerateRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_GenerateRequest) ProtoMessage() {} func (x *SQLCPlugin_GenerateRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_GenerateRequest.ProtoReflect.Descriptor instead. func (*SQLCPlugin_GenerateRequest) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 12} } func (x *SQLCPlugin_GenerateRequest) GetSettings() *SQLCPlugin_Settings { if x != nil { return x.Settings } return nil } func (x *SQLCPlugin_GenerateRequest) GetCatalog() *SQLCPlugin_Catalog { if x != nil { return x.Catalog } return nil } func (x *SQLCPlugin_GenerateRequest) GetQueries() []*SQLCPlugin_Query { if x != nil { return x.Queries } return nil } func (x *SQLCPlugin_GenerateRequest) GetSqlcVersion() string { if x != nil { return x.SqlcVersion } return "" } func (x *SQLCPlugin_GenerateRequest) GetPluginOptions() []byte { if x != nil { return x.PluginOptions } return nil } func (x *SQLCPlugin_GenerateRequest) GetGlobalOptions() []byte { if x != nil { return x.GlobalOptions } return nil } type SQLCPlugin_GenerateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Files []*SQLCPlugin_File `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_GenerateResponse) Reset() { *x = SQLCPlugin_GenerateResponse{} mi := &file_encore_daemon_daemon_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_GenerateResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_GenerateResponse) ProtoMessage() {} func (x *SQLCPlugin_GenerateResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_GenerateResponse.ProtoReflect.Descriptor instead. func (*SQLCPlugin_GenerateResponse) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 13} } func (x *SQLCPlugin_GenerateResponse) GetFiles() []*SQLCPlugin_File { if x != nil { return x.Files } return nil } type SQLCPlugin_Codegen_Process struct { state protoimpl.MessageState `protogen:"open.v1"` Cmd string `protobuf:"bytes,1,opt,name=cmd,proto3" json:"cmd,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Codegen_Process) Reset() { *x = SQLCPlugin_Codegen_Process{} mi := &file_encore_daemon_daemon_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Codegen_Process) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Codegen_Process) ProtoMessage() {} func (x *SQLCPlugin_Codegen_Process) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Codegen_Process.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Codegen_Process) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2, 0} } func (x *SQLCPlugin_Codegen_Process) GetCmd() string { if x != nil { return x.Cmd } return "" } type SQLCPlugin_Codegen_WASM struct { state protoimpl.MessageState `protogen:"open.v1"` Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` Sha256 string `protobuf:"bytes,2,opt,name=sha256,proto3" json:"sha256,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCPlugin_Codegen_WASM) Reset() { *x = SQLCPlugin_Codegen_WASM{} mi := &file_encore_daemon_daemon_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCPlugin_Codegen_WASM) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCPlugin_Codegen_WASM) ProtoMessage() {} func (x *SQLCPlugin_Codegen_WASM) ProtoReflect() protoreflect.Message { mi := &file_encore_daemon_daemon_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCPlugin_Codegen_WASM.ProtoReflect.Descriptor instead. func (*SQLCPlugin_Codegen_WASM) Descriptor() ([]byte, []int) { return file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2, 1} } func (x *SQLCPlugin_Codegen_WASM) GetUrl() string { if x != nil { return x.Url } return "" } func (x *SQLCPlugin_Codegen_WASM) GetSha256() string { if x != nil { return x.Sha256 } return "" } var File_encore_daemon_daemon_proto protoreflect.FileDescriptor const file_encore_daemon_daemon_proto_rawDesc = "" + "\n" + "\x1aencore/daemon/daemon.proto\x12\rencore.daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xc0\x01\n" + "\x0eCommandMessage\x126\n" + "\x06output\x18\x01 \x01(\v2\x1c.encore.daemon.CommandOutputH\x00R\x06output\x120\n" + "\x04exit\x18\x02 \x01(\v2\x1a.encore.daemon.CommandExitH\x00R\x04exit\x12=\n" + "\x06errors\x18\x03 \x01(\v2#.encore.daemon.CommandDisplayErrorsH\x00R\x06errorsB\x05\n" + "\x03msg\"?\n" + "\rCommandOutput\x12\x16\n" + "\x06stdout\x18\x01 \x01(\fR\x06stdout\x12\x16\n" + "\x06stderr\x18\x02 \x01(\fR\x06stderr\"!\n" + "\vCommandExit\x12\x12\n" + "\x04code\x18\x01 \x01(\x05R\x04code\"2\n" + "\x14CommandDisplayErrors\x12\x1a\n" + "\berrinsrc\x18\x01 \x01(\fR\berrinsrc\"e\n" + "\x10CreateAppRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1a\n" + "\btemplate\x18\x02 \x01(\tR\btemplate\x12\x1a\n" + "\btutorial\x18\x03 \x01(\bR\btutorial\"*\n" + "\x11CreateAppResponse\x12\x15\n" + "\x06app_id\x18\x01 \x01(\tR\x05appId\"\xf1\x04\n" + "\n" + "RunRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x14\n" + "\x05watch\x18\x05 \x01(\bR\x05watch\x12\x1f\n" + "\vlisten_addr\x18\x06 \x01(\tR\n" + "listenAddr\x12\x18\n" + "\aenviron\x18\a \x03(\tR\aenviron\x12\"\n" + "\n" + "trace_file\x18\b \x01(\tH\x00R\ttraceFile\x88\x01\x01\x12!\n" + "\tnamespace\x18\t \x01(\tH\x01R\tnamespace\x88\x01\x01\x12?\n" + "\abrowser\x18\n" + " \x01(\x0e2%.encore.daemon.RunRequest.BrowserModeR\abrowser\x12B\n" + "\n" + "debug_mode\x18\v \x01(\x0e2#.encore.daemon.RunRequest.DebugModeR\tdebugMode\x12 \n" + "\tlog_level\x18\f \x01(\tH\x02R\blogLevel\x88\x01\x01\x120\n" + "\x14scrub_sensitive_data\x18\r \x01(\bR\x12scrubSensitiveData\"F\n" + "\vBrowserMode\x12\x10\n" + "\fBROWSER_AUTO\x10\x00\x12\x11\n" + "\rBROWSER_NEVER\x10\x01\x12\x12\n" + "\x0eBROWSER_ALWAYS\x10\x02\"C\n" + "\tDebugMode\x12\x12\n" + "\x0eDEBUG_DISABLED\x10\x00\x12\x11\n" + "\rDEBUG_ENABLED\x10\x01\x12\x0f\n" + "\vDEBUG_BREAK\x10\x02B\r\n" + "\v_trace_fileB\f\n" + "\n" + "_namespaceB\f\n" + "\n" + "_log_level\"\xf0\x01\n" + "\vTestRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x12\n" + "\x04args\x18\x03 \x03(\tR\x04args\x12\x18\n" + "\aenviron\x18\x04 \x03(\tR\aenviron\x12\"\n" + "\n" + "trace_file\x18\x06 \x01(\tH\x00R\ttraceFile\x88\x01\x01\x12#\n" + "\rcodegen_debug\x18\a \x01(\bR\fcodegenDebug\x12\x19\n" + "\btemp_dir\x18\b \x01(\tR\atempDirB\r\n" + "\v_trace_fileJ\x04\b\x05\x10\x06\"\x96\x01\n" + "\x0fTestSpecRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x12\n" + "\x04args\x18\x03 \x03(\tR\x04args\x12\x18\n" + "\aenviron\x18\x04 \x03(\tR\aenviron\x12\x19\n" + "\btemp_dir\x18\x05 \x01(\tR\atempDir\"Z\n" + "\x10TestSpecResponse\x12\x18\n" + "\acommand\x18\x01 \x01(\tR\acommand\x12\x12\n" + "\x04args\x18\x02 \x03(\tR\x04args\x12\x18\n" + "\aenviron\x18\x03 \x03(\tR\aenviron\"\xee\x01\n" + "\x11ExecScriptRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x1f\n" + "\vscript_args\x18\x04 \x03(\tR\n" + "scriptArgs\x12\x18\n" + "\aenviron\x18\x05 \x03(\tR\aenviron\x12\"\n" + "\n" + "trace_file\x18\x06 \x01(\tH\x00R\ttraceFile\x88\x01\x01\x12!\n" + "\tnamespace\x18\a \x01(\tH\x01R\tnamespace\x88\x01\x01B\r\n" + "\v_trace_fileB\f\n" + "\n" + "_namespace\"\xd4\x01\n" + "\x0fExecSpecRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x1f\n" + "\vscript_args\x18\x04 \x03(\tR\n" + "scriptArgs\x12\x18\n" + "\aenviron\x18\x05 \x03(\tR\aenviron\x12!\n" + "\tnamespace\x18\a \x01(\tH\x00R\tnamespace\x88\x01\x01\x12\x19\n" + "\btemp_dir\x18\b \x01(\tR\atempDirB\f\n" + "\n" + "_namespace\"\x87\x01\n" + "\x0fExecSpecMessage\x126\n" + "\x06output\x18\x01 \x01(\v2\x1c.encore.daemon.CommandOutputH\x00R\x06output\x125\n" + "\x04spec\x18\x02 \x01(\v2\x1f.encore.daemon.ExecSpecResponseH\x00R\x04specB\x05\n" + "\x03msg\"Z\n" + "\x10ExecSpecResponse\x12\x18\n" + "\acommand\x18\x01 \x01(\tR\acommand\x12\x12\n" + "\x04args\x18\x02 \x03(\tR\x04args\x12\x18\n" + "\aenviron\x18\x03 \x03(\tR\aenviron\"\xaa\x01\n" + "\fCheckRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12#\n" + "\rcodegen_debug\x18\x03 \x01(\bR\fcodegenDebug\x12\x1f\n" + "\vparse_tests\x18\x04 \x01(\bR\n" + "parseTests\x12\x18\n" + "\aenviron\x18\x05 \x03(\tR\aenviron\"\x87\x03\n" + "\rExportRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x12\n" + "\x04goos\x18\x02 \x01(\tR\x04goos\x12\x16\n" + "\x06goarch\x18\x03 \x01(\tR\x06goarch\x12\x1f\n" + "\vcgo_enabled\x18\x04 \x01(\bR\n" + "cgoEnabled\x12\x18\n" + "\aenviron\x18\x05 \x03(\tR\aenviron\x12;\n" + "\x06docker\x18\x06 \x01(\v2!.encore.daemon.DockerExportParamsH\x00R\x06docker\x12&\n" + "\x0finfra_conf_path\x18\a \x01(\tR\rinfraConfPath\x12\x1a\n" + "\bservices\x18\b \x03(\tR\bservices\x12\x1a\n" + "\bgateways\x18\t \x03(\tR\bgateways\x12&\n" + "\x0fskip_infra_conf\x18\n" + " \x01(\bR\rskipInfraConf\x12%\n" + "\x0eworkspace_root\x18\v \x01(\tR\rworkspaceRootB\b\n" + "\x06format\"\x96\x01\n" + "\x12DockerExportParams\x12(\n" + "\x10local_daemon_tag\x18\x01 \x01(\tR\x0elocalDaemonTag\x120\n" + "\x14push_destination_tag\x18\x02 \x01(\tR\x12pushDestinationTag\x12$\n" + "\x0ebase_image_tag\x18\x03 \x01(\tR\fbaseImageTag\"\xfe\x01\n" + "\x10DBConnectRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x17\n" + "\adb_name\x18\x02 \x01(\tR\x06dbName\x12\x19\n" + "\benv_name\x18\x03 \x01(\tR\aenvName\x12?\n" + "\fcluster_type\x18\x04 \x01(\x0e2\x1c.encore.daemon.DBClusterTypeR\vclusterType\x12!\n" + "\tnamespace\x18\x05 \x01(\tH\x00R\tnamespace\x88\x01\x01\x12)\n" + "\x04role\x18\x06 \x01(\x0e2\x15.encore.daemon.DBRoleR\x04roleB\f\n" + "\n" + "_namespace\"%\n" + "\x11DBConnectResponse\x12\x10\n" + "\x03dsn\x18\x01 \x01(\tR\x03dsn\"\xf7\x01\n" + "\x0eDBProxyRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x19\n" + "\benv_name\x18\x02 \x01(\tR\aenvName\x12\x12\n" + "\x04port\x18\x03 \x01(\x05R\x04port\x12?\n" + "\fcluster_type\x18\x04 \x01(\x0e2\x1c.encore.daemon.DBClusterTypeR\vclusterType\x12!\n" + "\tnamespace\x18\x05 \x01(\tH\x00R\tnamespace\x88\x01\x01\x12)\n" + "\x04role\x18\x06 \x01(\x0e2\x15.encore.daemon.DBRoleR\x04roleB\f\n" + "\n" + "_namespace\"\xc4\x01\n" + "\x0eDBResetRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12%\n" + "\x0edatabase_names\x18\x02 \x03(\tR\rdatabaseNames\x12?\n" + "\fcluster_type\x18\x03 \x01(\x0e2\x1c.encore.daemon.DBClusterTypeR\vclusterType\x12!\n" + "\tnamespace\x18\x04 \x01(\tH\x00R\tnamespace\x88\x01\x01B\f\n" + "\n" + "_namespace\"\xae\x04\n" + "\x10GenClientRequest\x12\x15\n" + "\x06app_id\x18\x01 \x01(\tR\x05appId\x12\x19\n" + "\benv_name\x18\x02 \x01(\tR\aenvName\x12\x12\n" + "\x04lang\x18\x03 \x01(\tR\x04lang\x12\x1a\n" + "\bfilepath\x18\x04 \x01(\tR\bfilepath\x12\x1a\n" + "\bservices\x18\x05 \x03(\tR\bservices\x12+\n" + "\x11excluded_services\x18\x06 \x03(\tR\x10excludedServices\x12#\n" + "\rendpoint_tags\x18\a \x03(\tR\fendpointTags\x124\n" + "\x16excluded_endpoint_tags\x18\b \x03(\tR\x14excludedEndpointTags\x12N\n" + "!openapi_exclude_private_endpoints\x18\t \x01(\bH\x00R\x1eopenapiExcludePrivateEndpoints\x88\x01\x01\x12+\n" + "\x0fts_shared_types\x18\n" + " \x01(\bH\x01R\rtsSharedTypes\x88\x01\x01\x12-\n" + "\x10ts_client_target\x18\v \x01(\tH\x02R\x0etsClientTarget\x88\x01\x01\x12\x19\n" + "\bapp_root\x18\f \x01(\tR\aappRootB$\n" + "\"_openapi_exclude_private_endpointsB\x12\n" + "\x10_ts_shared_typesB\x13\n" + "\x11_ts_client_target\"'\n" + "\x11GenClientResponse\x12\x12\n" + "\x04code\x18\x01 \x01(\fR\x04code\"/\n" + "\x12GenWrappersRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\"\x15\n" + "\x13GenWrappersResponse\"Z\n" + "\x15SecretsRefreshRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x10\n" + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x03 \x01(\tR\x05value\"\x18\n" + "\x16SecretsRefreshResponse\"L\n" + "\x0fVersionResponse\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x1f\n" + "\vconfig_hash\x18\x02 \x01(\tR\n" + "configHash\"\xa4\x01\n" + "\tNamespace\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + "\x06active\x18\x03 \x01(\bR\x06active\x12\x1d\n" + "\n" + "created_at\x18\x04 \x01(\tR\tcreatedAt\x12)\n" + "\x0elast_active_at\x18\x05 \x01(\tH\x00R\flastActiveAt\x88\x01\x01B\x11\n" + "\x0f_last_active_at\"G\n" + "\x16CreateNamespaceRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\"_\n" + "\x16SwitchNamespaceRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + "\x06create\x18\x03 \x01(\bR\x06create\"2\n" + "\x15ListNamespacesRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\"G\n" + "\x16DeleteNamespaceRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\"R\n" + "\x16ListNamespacesResponse\x128\n" + "\n" + "namespaces\x18\x01 \x03(\v2\x18.encore.daemon.NamespaceR\n" + "namespaces\"Z\n" + "\x0fTelemetryConfig\x12\x17\n" + "\aanon_id\x18\x01 \x01(\tR\x06anonId\x12\x18\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\x12\x14\n" + "\x05debug\x18\x03 \x01(\bR\x05debug\"\x8c\x02\n" + "\x0fDumpMetaRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + "workingDir\x12\x18\n" + "\aenviron\x18\x03 \x03(\tR\aenviron\x12\x1f\n" + "\vparse_tests\x18\x04 \x01(\bR\n" + "parseTests\x12=\n" + "\x06format\x18\x05 \x01(\x0e2%.encore.daemon.DumpMetaRequest.FormatR\x06format\"C\n" + "\x06Format\x12\x16\n" + "\x12FORMAT_UNSPECIFIED\x10\x00\x12\x0f\n" + "\vFORMAT_JSON\x10\x01\x12\x10\n" + "\fFORMAT_PROTO\x10\x02\"&\n" + "\x10DumpMetaResponse\x12\x12\n" + "\x04meta\x18\x01 \x01(\fR\x04meta\"\xcb\x15\n" + "\n" + "SQLCPlugin\x1a6\n" + "\x04File\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + "\bcontents\x18\x02 \x01(\fR\bcontents\x1a\xc9\x01\n" + "\bSettings\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06engine\x18\x02 \x01(\tR\x06engine\x12\x16\n" + "\x06schema\x18\x03 \x03(\tR\x06schema\x12\x18\n" + "\aqueries\x18\x04 \x03(\tR\aqueries\x12;\n" + "\acodegen\x18\f \x01(\v2!.encore.daemon.SQLCPlugin.CodegenR\acodegenJ\x04\b\x05\x10\x06J\x04\b\b\x10\tJ\x04\b\t\x10\n" + "J\x04\b\n" + "\x10\vJ\x04\b\v\x10\f\x1a\xaf\x02\n" + "\aCodegen\x12\x10\n" + "\x03out\x18\x01 \x01(\tR\x03out\x12\x16\n" + "\x06plugin\x18\x02 \x01(\tR\x06plugin\x12\x18\n" + "\aoptions\x18\x03 \x01(\fR\aoptions\x12\x10\n" + "\x03env\x18\x04 \x03(\tR\x03env\x12C\n" + "\aprocess\x18\x05 \x01(\v2).encore.daemon.SQLCPlugin.Codegen.ProcessR\aprocess\x12:\n" + "\x04wasm\x18\x06 \x01(\v2&.encore.daemon.SQLCPlugin.Codegen.WASMR\x04wasm\x1a\x1b\n" + "\aProcess\x12\x10\n" + "\x03cmd\x18\x01 \x01(\tR\x03cmd\x1a0\n" + "\x04WASM\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + "\x06sha256\x18\x02 \x01(\tR\x06sha256\x1a\x9a\x01\n" + "\aCatalog\x12\x18\n" + "\acomment\x18\x01 \x01(\tR\acomment\x12%\n" + "\x0edefault_schema\x18\x02 \x01(\tR\rdefaultSchema\x12\x12\n" + "\x04name\x18\x03 \x01(\tR\x04name\x12:\n" + "\aschemas\x18\x04 \x03(\v2 .encore.daemon.SQLCPlugin.SchemaR\aschemas\x1a\xf7\x01\n" + "\x06Schema\x12\x18\n" + "\acomment\x18\x01 \x01(\tR\acomment\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x127\n" + "\x06tables\x18\x03 \x03(\v2\x1f.encore.daemon.SQLCPlugin.TableR\x06tables\x124\n" + "\x05enums\x18\x04 \x03(\v2\x1e.encore.daemon.SQLCPlugin.EnumR\x05enums\x12P\n" + "\x0fcomposite_types\x18\x05 \x03(\v2'.encore.daemon.SQLCPlugin.CompositeTypeR\x0ecompositeTypes\x1a=\n" + "\rCompositeType\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\acomment\x18\x02 \x01(\tR\acomment\x1aH\n" + "\x04Enum\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04vals\x18\x02 \x03(\tR\x04vals\x12\x18\n" + "\acomment\x18\x03 \x01(\tR\acomment\x1a\x95\x01\n" + "\x05Table\x126\n" + "\x03rel\x18\x01 \x01(\v2$.encore.daemon.SQLCPlugin.IdentifierR\x03rel\x12:\n" + "\acolumns\x18\x02 \x03(\v2 .encore.daemon.SQLCPlugin.ColumnR\acolumns\x12\x18\n" + "\acomment\x18\x03 \x01(\tR\acomment\x1aR\n" + "\n" + "Identifier\x12\x18\n" + "\acatalog\x18\x01 \x01(\tR\acatalog\x12\x16\n" + "\x06schema\x18\x02 \x01(\tR\x06schema\x12\x12\n" + "\x04name\x18\x03 \x01(\tR\x04name\x1a\xc4\x04\n" + "\x06Column\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bnot_null\x18\x03 \x01(\bR\anotNull\x12\x19\n" + "\bis_array\x18\x04 \x01(\bR\aisArray\x12\x18\n" + "\acomment\x18\x05 \x01(\tR\acomment\x12\x16\n" + "\x06length\x18\x06 \x01(\x05R\x06length\x12$\n" + "\x0eis_named_param\x18\a \x01(\bR\fisNamedParam\x12 \n" + "\fis_func_call\x18\b \x01(\bR\n" + "isFuncCall\x12\x14\n" + "\x05scope\x18\t \x01(\tR\x05scope\x12:\n" + "\x05table\x18\n" + " \x01(\v2$.encore.daemon.SQLCPlugin.IdentifierR\x05table\x12\x1f\n" + "\vtable_alias\x18\v \x01(\tR\n" + "tableAlias\x128\n" + "\x04type\x18\f \x01(\v2$.encore.daemon.SQLCPlugin.IdentifierR\x04type\x12\"\n" + "\ris_sqlc_slice\x18\r \x01(\bR\visSqlcSlice\x12E\n" + "\vembed_table\x18\x0e \x01(\v2$.encore.daemon.SQLCPlugin.IdentifierR\n" + "embedTable\x12#\n" + "\roriginal_name\x18\x0f \x01(\tR\foriginalName\x12\x1a\n" + "\bunsigned\x18\x10 \x01(\bR\bunsigned\x12\x1d\n" + "\n" + "array_dims\x18\x11 \x01(\x05R\tarrayDims\x1a\xca\x02\n" + "\x05Query\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" + "\x03cmd\x18\x03 \x01(\tR\x03cmd\x12:\n" + "\acolumns\x18\x04 \x03(\v2 .encore.daemon.SQLCPlugin.ColumnR\acolumns\x12?\n" + "\x06params\x18\x05 \x03(\v2#.encore.daemon.SQLCPlugin.ParameterR\n" + "parameters\x12\x1a\n" + "\bcomments\x18\x06 \x03(\tR\bcomments\x12\x1a\n" + "\bfilename\x18\a \x01(\tR\bfilename\x12R\n" + "\x11insert_into_table\x18\b \x01(\v2$.encore.daemon.SQLCPlugin.IdentifierR\x11insert_into_table\x1a]\n" + "\tParameter\x12\x16\n" + "\x06number\x18\x01 \x01(\x05R\x06number\x128\n" + "\x06column\x18\x02 \x01(\v2 .encore.daemon.SQLCPlugin.ColumnR\x06column\x1a\xbd\x02\n" + "\x0fGenerateRequest\x12>\n" + "\bsettings\x18\x01 \x01(\v2\".encore.daemon.SQLCPlugin.SettingsR\bsettings\x12;\n" + "\acatalog\x18\x02 \x01(\v2!.encore.daemon.SQLCPlugin.CatalogR\acatalog\x129\n" + "\aqueries\x18\x03 \x03(\v2\x1f.encore.daemon.SQLCPlugin.QueryR\aqueries\x12\"\n" + "\fsqlc_version\x18\x04 \x01(\tR\fsqlc_version\x12&\n" + "\x0eplugin_options\x18\x05 \x01(\fR\x0eplugin_options\x12&\n" + "\x0eglobal_options\x18\x06 \x01(\fR\x0eglobal_options\x1aH\n" + "\x10GenerateResponse\x124\n" + "\x05files\x18\x01 \x03(\v2\x1e.encore.daemon.SQLCPlugin.FileR\x05files*p\n" + "\x06DBRole\x12\x17\n" + "\x13DB_ROLE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x11DB_ROLE_SUPERUSER\x10\x01\x12\x11\n" + "\rDB_ROLE_ADMIN\x10\x02\x12\x11\n" + "\rDB_ROLE_WRITE\x10\x03\x12\x10\n" + "\fDB_ROLE_READ\x10\x04*\x7f\n" + "\rDBClusterType\x12\x1f\n" + "\x1bDB_CLUSTER_TYPE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13DB_CLUSTER_TYPE_RUN\x10\x01\x12\x18\n" + "\x14DB_CLUSTER_TYPE_TEST\x10\x02\x12\x1a\n" + "\x16DB_CLUSTER_TYPE_SHADOW\x10\x032\xf5\f\n" + "\x06Daemon\x12A\n" + "\x03Run\x12\x19.encore.daemon.RunRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12C\n" + "\x04Test\x12\x1a.encore.daemon.TestRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12K\n" + "\bTestSpec\x12\x1e.encore.daemon.TestSpecRequest\x1a\x1f.encore.daemon.TestSpecResponse\x12O\n" + "\n" + "ExecScript\x12 .encore.daemon.ExecScriptRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12L\n" + "\bExecSpec\x12\x1e.encore.daemon.ExecSpecRequest\x1a\x1e.encore.daemon.ExecSpecMessage0\x01\x12E\n" + "\x05Check\x12\x1b.encore.daemon.CheckRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12G\n" + "\x06Export\x12\x1c.encore.daemon.ExportRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12N\n" + "\tDBConnect\x12\x1f.encore.daemon.DBConnectRequest\x1a .encore.daemon.DBConnectResponse\x12I\n" + "\aDBProxy\x12\x1d.encore.daemon.DBProxyRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12I\n" + "\aDBReset\x12\x1d.encore.daemon.DBResetRequest\x1a\x1d.encore.daemon.CommandMessage0\x01\x12N\n" + "\tGenClient\x12\x1f.encore.daemon.GenClientRequest\x1a .encore.daemon.GenClientResponse\x12T\n" + "\vGenWrappers\x12!.encore.daemon.GenWrappersRequest\x1a\".encore.daemon.GenWrappersResponse\x12]\n" + "\x0eSecretsRefresh\x12$.encore.daemon.SecretsRefreshRequest\x1a%.encore.daemon.SecretsRefreshResponse\x12A\n" + "\aVersion\x12\x16.google.protobuf.Empty\x1a\x1e.encore.daemon.VersionResponse\x12R\n" + "\x0fCreateNamespace\x12%.encore.daemon.CreateNamespaceRequest\x1a\x18.encore.daemon.Namespace\x12R\n" + "\x0fSwitchNamespace\x12%.encore.daemon.SwitchNamespaceRequest\x1a\x18.encore.daemon.Namespace\x12]\n" + "\x0eListNamespaces\x12$.encore.daemon.ListNamespacesRequest\x1a%.encore.daemon.ListNamespacesResponse\x12P\n" + "\x0fDeleteNamespace\x12%.encore.daemon.DeleteNamespaceRequest\x1a\x16.google.protobuf.Empty\x12K\n" + "\bDumpMeta\x12\x1e.encore.daemon.DumpMetaRequest\x1a\x1f.encore.daemon.DumpMetaResponse\x12C\n" + "\tTelemetry\x12\x1e.encore.daemon.TelemetryConfig\x1a\x16.google.protobuf.Empty\x12N\n" + "\tCreateApp\x12\x1f.encore.daemon.CreateAppRequest\x1a .encore.daemon.CreateAppResponseB\x1eZ\x1cencr.dev/proto/encore/daemonb\x06proto3" var ( file_encore_daemon_daemon_proto_rawDescOnce sync.Once file_encore_daemon_daemon_proto_rawDescData []byte ) func file_encore_daemon_daemon_proto_rawDescGZIP() []byte { file_encore_daemon_daemon_proto_rawDescOnce.Do(func() { file_encore_daemon_daemon_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_daemon_daemon_proto_rawDesc), len(file_encore_daemon_daemon_proto_rawDesc))) }) return file_encore_daemon_daemon_proto_rawDescData } var file_encore_daemon_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_encore_daemon_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 54) var file_encore_daemon_daemon_proto_goTypes = []any{ (DBRole)(0), // 0: encore.daemon.DBRole (DBClusterType)(0), // 1: encore.daemon.DBClusterType (RunRequest_BrowserMode)(0), // 2: encore.daemon.RunRequest.BrowserMode (RunRequest_DebugMode)(0), // 3: encore.daemon.RunRequest.DebugMode (DumpMetaRequest_Format)(0), // 4: encore.daemon.DumpMetaRequest.Format (*CommandMessage)(nil), // 5: encore.daemon.CommandMessage (*CommandOutput)(nil), // 6: encore.daemon.CommandOutput (*CommandExit)(nil), // 7: encore.daemon.CommandExit (*CommandDisplayErrors)(nil), // 8: encore.daemon.CommandDisplayErrors (*CreateAppRequest)(nil), // 9: encore.daemon.CreateAppRequest (*CreateAppResponse)(nil), // 10: encore.daemon.CreateAppResponse (*RunRequest)(nil), // 11: encore.daemon.RunRequest (*TestRequest)(nil), // 12: encore.daemon.TestRequest (*TestSpecRequest)(nil), // 13: encore.daemon.TestSpecRequest (*TestSpecResponse)(nil), // 14: encore.daemon.TestSpecResponse (*ExecScriptRequest)(nil), // 15: encore.daemon.ExecScriptRequest (*ExecSpecRequest)(nil), // 16: encore.daemon.ExecSpecRequest (*ExecSpecMessage)(nil), // 17: encore.daemon.ExecSpecMessage (*ExecSpecResponse)(nil), // 18: encore.daemon.ExecSpecResponse (*CheckRequest)(nil), // 19: encore.daemon.CheckRequest (*ExportRequest)(nil), // 20: encore.daemon.ExportRequest (*DockerExportParams)(nil), // 21: encore.daemon.DockerExportParams (*DBConnectRequest)(nil), // 22: encore.daemon.DBConnectRequest (*DBConnectResponse)(nil), // 23: encore.daemon.DBConnectResponse (*DBProxyRequest)(nil), // 24: encore.daemon.DBProxyRequest (*DBResetRequest)(nil), // 25: encore.daemon.DBResetRequest (*GenClientRequest)(nil), // 26: encore.daemon.GenClientRequest (*GenClientResponse)(nil), // 27: encore.daemon.GenClientResponse (*GenWrappersRequest)(nil), // 28: encore.daemon.GenWrappersRequest (*GenWrappersResponse)(nil), // 29: encore.daemon.GenWrappersResponse (*SecretsRefreshRequest)(nil), // 30: encore.daemon.SecretsRefreshRequest (*SecretsRefreshResponse)(nil), // 31: encore.daemon.SecretsRefreshResponse (*VersionResponse)(nil), // 32: encore.daemon.VersionResponse (*Namespace)(nil), // 33: encore.daemon.Namespace (*CreateNamespaceRequest)(nil), // 34: encore.daemon.CreateNamespaceRequest (*SwitchNamespaceRequest)(nil), // 35: encore.daemon.SwitchNamespaceRequest (*ListNamespacesRequest)(nil), // 36: encore.daemon.ListNamespacesRequest (*DeleteNamespaceRequest)(nil), // 37: encore.daemon.DeleteNamespaceRequest (*ListNamespacesResponse)(nil), // 38: encore.daemon.ListNamespacesResponse (*TelemetryConfig)(nil), // 39: encore.daemon.TelemetryConfig (*DumpMetaRequest)(nil), // 40: encore.daemon.DumpMetaRequest (*DumpMetaResponse)(nil), // 41: encore.daemon.DumpMetaResponse (*SQLCPlugin)(nil), // 42: encore.daemon.SQLCPlugin (*SQLCPlugin_File)(nil), // 43: encore.daemon.SQLCPlugin.File (*SQLCPlugin_Settings)(nil), // 44: encore.daemon.SQLCPlugin.Settings (*SQLCPlugin_Codegen)(nil), // 45: encore.daemon.SQLCPlugin.Codegen (*SQLCPlugin_Catalog)(nil), // 46: encore.daemon.SQLCPlugin.Catalog (*SQLCPlugin_Schema)(nil), // 47: encore.daemon.SQLCPlugin.Schema (*SQLCPlugin_CompositeType)(nil), // 48: encore.daemon.SQLCPlugin.CompositeType (*SQLCPlugin_Enum)(nil), // 49: encore.daemon.SQLCPlugin.Enum (*SQLCPlugin_Table)(nil), // 50: encore.daemon.SQLCPlugin.Table (*SQLCPlugin_Identifier)(nil), // 51: encore.daemon.SQLCPlugin.Identifier (*SQLCPlugin_Column)(nil), // 52: encore.daemon.SQLCPlugin.Column (*SQLCPlugin_Query)(nil), // 53: encore.daemon.SQLCPlugin.Query (*SQLCPlugin_Parameter)(nil), // 54: encore.daemon.SQLCPlugin.Parameter (*SQLCPlugin_GenerateRequest)(nil), // 55: encore.daemon.SQLCPlugin.GenerateRequest (*SQLCPlugin_GenerateResponse)(nil), // 56: encore.daemon.SQLCPlugin.GenerateResponse (*SQLCPlugin_Codegen_Process)(nil), // 57: encore.daemon.SQLCPlugin.Codegen.Process (*SQLCPlugin_Codegen_WASM)(nil), // 58: encore.daemon.SQLCPlugin.Codegen.WASM (*emptypb.Empty)(nil), // 59: google.protobuf.Empty } var file_encore_daemon_daemon_proto_depIdxs = []int32{ 6, // 0: encore.daemon.CommandMessage.output:type_name -> encore.daemon.CommandOutput 7, // 1: encore.daemon.CommandMessage.exit:type_name -> encore.daemon.CommandExit 8, // 2: encore.daemon.CommandMessage.errors:type_name -> encore.daemon.CommandDisplayErrors 2, // 3: encore.daemon.RunRequest.browser:type_name -> encore.daemon.RunRequest.BrowserMode 3, // 4: encore.daemon.RunRequest.debug_mode:type_name -> encore.daemon.RunRequest.DebugMode 6, // 5: encore.daemon.ExecSpecMessage.output:type_name -> encore.daemon.CommandOutput 18, // 6: encore.daemon.ExecSpecMessage.spec:type_name -> encore.daemon.ExecSpecResponse 21, // 7: encore.daemon.ExportRequest.docker:type_name -> encore.daemon.DockerExportParams 1, // 8: encore.daemon.DBConnectRequest.cluster_type:type_name -> encore.daemon.DBClusterType 0, // 9: encore.daemon.DBConnectRequest.role:type_name -> encore.daemon.DBRole 1, // 10: encore.daemon.DBProxyRequest.cluster_type:type_name -> encore.daemon.DBClusterType 0, // 11: encore.daemon.DBProxyRequest.role:type_name -> encore.daemon.DBRole 1, // 12: encore.daemon.DBResetRequest.cluster_type:type_name -> encore.daemon.DBClusterType 33, // 13: encore.daemon.ListNamespacesResponse.namespaces:type_name -> encore.daemon.Namespace 4, // 14: encore.daemon.DumpMetaRequest.format:type_name -> encore.daemon.DumpMetaRequest.Format 45, // 15: encore.daemon.SQLCPlugin.Settings.codegen:type_name -> encore.daemon.SQLCPlugin.Codegen 57, // 16: encore.daemon.SQLCPlugin.Codegen.process:type_name -> encore.daemon.SQLCPlugin.Codegen.Process 58, // 17: encore.daemon.SQLCPlugin.Codegen.wasm:type_name -> encore.daemon.SQLCPlugin.Codegen.WASM 47, // 18: encore.daemon.SQLCPlugin.Catalog.schemas:type_name -> encore.daemon.SQLCPlugin.Schema 50, // 19: encore.daemon.SQLCPlugin.Schema.tables:type_name -> encore.daemon.SQLCPlugin.Table 49, // 20: encore.daemon.SQLCPlugin.Schema.enums:type_name -> encore.daemon.SQLCPlugin.Enum 48, // 21: encore.daemon.SQLCPlugin.Schema.composite_types:type_name -> encore.daemon.SQLCPlugin.CompositeType 51, // 22: encore.daemon.SQLCPlugin.Table.rel:type_name -> encore.daemon.SQLCPlugin.Identifier 52, // 23: encore.daemon.SQLCPlugin.Table.columns:type_name -> encore.daemon.SQLCPlugin.Column 51, // 24: encore.daemon.SQLCPlugin.Column.table:type_name -> encore.daemon.SQLCPlugin.Identifier 51, // 25: encore.daemon.SQLCPlugin.Column.type:type_name -> encore.daemon.SQLCPlugin.Identifier 51, // 26: encore.daemon.SQLCPlugin.Column.embed_table:type_name -> encore.daemon.SQLCPlugin.Identifier 52, // 27: encore.daemon.SQLCPlugin.Query.columns:type_name -> encore.daemon.SQLCPlugin.Column 54, // 28: encore.daemon.SQLCPlugin.Query.params:type_name -> encore.daemon.SQLCPlugin.Parameter 51, // 29: encore.daemon.SQLCPlugin.Query.insert_into_table:type_name -> encore.daemon.SQLCPlugin.Identifier 52, // 30: encore.daemon.SQLCPlugin.Parameter.column:type_name -> encore.daemon.SQLCPlugin.Column 44, // 31: encore.daemon.SQLCPlugin.GenerateRequest.settings:type_name -> encore.daemon.SQLCPlugin.Settings 46, // 32: encore.daemon.SQLCPlugin.GenerateRequest.catalog:type_name -> encore.daemon.SQLCPlugin.Catalog 53, // 33: encore.daemon.SQLCPlugin.GenerateRequest.queries:type_name -> encore.daemon.SQLCPlugin.Query 43, // 34: encore.daemon.SQLCPlugin.GenerateResponse.files:type_name -> encore.daemon.SQLCPlugin.File 11, // 35: encore.daemon.Daemon.Run:input_type -> encore.daemon.RunRequest 12, // 36: encore.daemon.Daemon.Test:input_type -> encore.daemon.TestRequest 13, // 37: encore.daemon.Daemon.TestSpec:input_type -> encore.daemon.TestSpecRequest 15, // 38: encore.daemon.Daemon.ExecScript:input_type -> encore.daemon.ExecScriptRequest 16, // 39: encore.daemon.Daemon.ExecSpec:input_type -> encore.daemon.ExecSpecRequest 19, // 40: encore.daemon.Daemon.Check:input_type -> encore.daemon.CheckRequest 20, // 41: encore.daemon.Daemon.Export:input_type -> encore.daemon.ExportRequest 22, // 42: encore.daemon.Daemon.DBConnect:input_type -> encore.daemon.DBConnectRequest 24, // 43: encore.daemon.Daemon.DBProxy:input_type -> encore.daemon.DBProxyRequest 25, // 44: encore.daemon.Daemon.DBReset:input_type -> encore.daemon.DBResetRequest 26, // 45: encore.daemon.Daemon.GenClient:input_type -> encore.daemon.GenClientRequest 28, // 46: encore.daemon.Daemon.GenWrappers:input_type -> encore.daemon.GenWrappersRequest 30, // 47: encore.daemon.Daemon.SecretsRefresh:input_type -> encore.daemon.SecretsRefreshRequest 59, // 48: encore.daemon.Daemon.Version:input_type -> google.protobuf.Empty 34, // 49: encore.daemon.Daemon.CreateNamespace:input_type -> encore.daemon.CreateNamespaceRequest 35, // 50: encore.daemon.Daemon.SwitchNamespace:input_type -> encore.daemon.SwitchNamespaceRequest 36, // 51: encore.daemon.Daemon.ListNamespaces:input_type -> encore.daemon.ListNamespacesRequest 37, // 52: encore.daemon.Daemon.DeleteNamespace:input_type -> encore.daemon.DeleteNamespaceRequest 40, // 53: encore.daemon.Daemon.DumpMeta:input_type -> encore.daemon.DumpMetaRequest 39, // 54: encore.daemon.Daemon.Telemetry:input_type -> encore.daemon.TelemetryConfig 9, // 55: encore.daemon.Daemon.CreateApp:input_type -> encore.daemon.CreateAppRequest 5, // 56: encore.daemon.Daemon.Run:output_type -> encore.daemon.CommandMessage 5, // 57: encore.daemon.Daemon.Test:output_type -> encore.daemon.CommandMessage 14, // 58: encore.daemon.Daemon.TestSpec:output_type -> encore.daemon.TestSpecResponse 5, // 59: encore.daemon.Daemon.ExecScript:output_type -> encore.daemon.CommandMessage 17, // 60: encore.daemon.Daemon.ExecSpec:output_type -> encore.daemon.ExecSpecMessage 5, // 61: encore.daemon.Daemon.Check:output_type -> encore.daemon.CommandMessage 5, // 62: encore.daemon.Daemon.Export:output_type -> encore.daemon.CommandMessage 23, // 63: encore.daemon.Daemon.DBConnect:output_type -> encore.daemon.DBConnectResponse 5, // 64: encore.daemon.Daemon.DBProxy:output_type -> encore.daemon.CommandMessage 5, // 65: encore.daemon.Daemon.DBReset:output_type -> encore.daemon.CommandMessage 27, // 66: encore.daemon.Daemon.GenClient:output_type -> encore.daemon.GenClientResponse 29, // 67: encore.daemon.Daemon.GenWrappers:output_type -> encore.daemon.GenWrappersResponse 31, // 68: encore.daemon.Daemon.SecretsRefresh:output_type -> encore.daemon.SecretsRefreshResponse 32, // 69: encore.daemon.Daemon.Version:output_type -> encore.daemon.VersionResponse 33, // 70: encore.daemon.Daemon.CreateNamespace:output_type -> encore.daemon.Namespace 33, // 71: encore.daemon.Daemon.SwitchNamespace:output_type -> encore.daemon.Namespace 38, // 72: encore.daemon.Daemon.ListNamespaces:output_type -> encore.daemon.ListNamespacesResponse 59, // 73: encore.daemon.Daemon.DeleteNamespace:output_type -> google.protobuf.Empty 41, // 74: encore.daemon.Daemon.DumpMeta:output_type -> encore.daemon.DumpMetaResponse 59, // 75: encore.daemon.Daemon.Telemetry:output_type -> google.protobuf.Empty 10, // 76: encore.daemon.Daemon.CreateApp:output_type -> encore.daemon.CreateAppResponse 56, // [56:77] is the sub-list for method output_type 35, // [35:56] is the sub-list for method input_type 35, // [35:35] is the sub-list for extension type_name 35, // [35:35] is the sub-list for extension extendee 0, // [0:35] is the sub-list for field type_name } func init() { file_encore_daemon_daemon_proto_init() } func file_encore_daemon_daemon_proto_init() { if File_encore_daemon_daemon_proto != nil { return } file_encore_daemon_daemon_proto_msgTypes[0].OneofWrappers = []any{ (*CommandMessage_Output)(nil), (*CommandMessage_Exit)(nil), (*CommandMessage_Errors)(nil), } file_encore_daemon_daemon_proto_msgTypes[6].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[7].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[10].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[11].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[12].OneofWrappers = []any{ (*ExecSpecMessage_Output)(nil), (*ExecSpecMessage_Spec)(nil), } file_encore_daemon_daemon_proto_msgTypes[15].OneofWrappers = []any{ (*ExportRequest_Docker)(nil), } file_encore_daemon_daemon_proto_msgTypes[17].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[19].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[20].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[21].OneofWrappers = []any{} file_encore_daemon_daemon_proto_msgTypes[28].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_daemon_daemon_proto_rawDesc), len(file_encore_daemon_daemon_proto_rawDesc)), NumEnums: 5, NumMessages: 54, NumExtensions: 0, NumServices: 1, }, GoTypes: file_encore_daemon_daemon_proto_goTypes, DependencyIndexes: file_encore_daemon_daemon_proto_depIdxs, EnumInfos: file_encore_daemon_daemon_proto_enumTypes, MessageInfos: file_encore_daemon_daemon_proto_msgTypes, }.Build() File_encore_daemon_daemon_proto = out.File file_encore_daemon_daemon_proto_goTypes = nil file_encore_daemon_daemon_proto_depIdxs = nil } ================================================ FILE: proto/encore/daemon/daemon.proto ================================================ syntax = "proto3"; package encore.daemon; import "google/protobuf/empty.proto"; option go_package = "encr.dev/proto/encore/daemon"; service Daemon { // Run runs the application. rpc Run(RunRequest) returns (stream CommandMessage); // Test runs tests. rpc Test(TestRequest) returns (stream CommandMessage); // TestSpec returns the specification for how to run tests. rpc TestSpec(TestSpecRequest) returns (TestSpecResponse); // ExecScript executes a one-off script. rpc ExecScript(ExecScriptRequest) returns (stream CommandMessage); // ExecSpec returns the specification for how to run an exec command. // It streams progress messages during setup, then sends the spec as the final message. rpc ExecSpec(ExecSpecRequest) returns (stream ExecSpecMessage); // Check checks the app for compilation errors. rpc Check(CheckRequest) returns (stream CommandMessage); // Export exports the app in various formats. rpc Export(ExportRequest) returns (stream CommandMessage); // DBConnect starts the database and returns the DSN for connecting to it. rpc DBConnect(DBConnectRequest) returns (DBConnectResponse); // DBProxy starts a local database proxy for connecting to remote databases // on the encore.dev platform. rpc DBProxy(DBProxyRequest) returns (stream CommandMessage); // DBReset resets the given databases, recreating them from scratch. rpc DBReset(DBResetRequest) returns (stream CommandMessage); // GenClient generates a client based on the app's API. rpc GenClient(GenClientRequest) returns (GenClientResponse); // GenWrappers generates user-facing wrapper code. rpc GenWrappers(GenWrappersRequest) returns (GenWrappersResponse); // SecretsRefresh tells the daemon to refresh the local development secrets // for the given application. rpc SecretsRefresh(SecretsRefreshRequest) returns (SecretsRefreshResponse); // Version reports the daemon version. rpc Version(google.protobuf.Empty) returns (VersionResponse); // CreateNamespace creates a new infra namespace. rpc CreateNamespace(CreateNamespaceRequest) returns (Namespace); // SwitchNamespace switches the active infra namespace. rpc SwitchNamespace(SwitchNamespaceRequest) returns (Namespace); // ListNamespaces lists all namespaces for the given app. rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse); // DeleteNamespace deletes an infra namespace. rpc DeleteNamespace(DeleteNamespaceRequest) returns (google.protobuf.Empty); rpc DumpMeta(DumpMetaRequest) returns (DumpMetaResponse); // Telemetry enables or disables telemetry. rpc Telemetry(TelemetryConfig) returns (google.protobuf.Empty); // InitTutorial sets the tutorial flag of the app rpc CreateApp(CreateAppRequest) returns (CreateAppResponse); } message CommandMessage { oneof msg { CommandOutput output = 1; CommandExit exit = 2; CommandDisplayErrors errors = 3; } } message CommandOutput { bytes stdout = 1; bytes stderr = 2; } message CommandExit { int32 code = 1; // exit code } message CommandDisplayErrors { bytes errinsrc = 1; // error messages in source code } message CreateAppRequest { // app_root is the absolute filesystem path to the Encore app root. string app_root = 1; // template is the template used to create the app string template = 2; // tutorial is a flag to indicate if the app is a tutorial app bool tutorial = 3; } message CreateAppResponse { string app_id = 1; } message RunRequest { // app_root is the absolute filesystem path to the Encore app root. string app_root = 1; // working_dir is the working directory relative to the app_root, // for formatting relative paths in error messages. string working_dir = 2; // watch, if true, enables live reloading of the app whenever the source changes. bool watch = 5; // listen_addr is the address to listen on. string listen_addr = 6; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 7; // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. optional string trace_file = 8; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 9; // browser specifies whether and how to open the browser on startup. BrowserMode browser = 10; // debug_mode specifies the debug mode to use. DebugMode debug_mode = 11; // Log level override. optional string log_level = 12; // scrub_sensitive_data, if true, scrubs sensitive data from local traces. bool scrub_sensitive_data = 13; enum BrowserMode { BROWSER_AUTO = 0; BROWSER_NEVER = 1; BROWSER_ALWAYS = 2; } enum DebugMode { DEBUG_DISABLED = 0; DEBUG_ENABLED = 1; DEBUG_BREAK = 2; } } message TestRequest { string app_root = 1; string working_dir = 2; repeated string args = 3; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 4; // No longer used; debug, if true, compiles the app with flags that improve the debugging experience. reserved 5; // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. optional string trace_file = 6; // codegen_debug, if true, dumps the generated code and prints where it is located. bool codegen_debug = 7; // temp_dir is a temp dir that will be cleaned up after tests have been executed // to write things like app meta and runtime config etc. string temp_dir = 8; } message TestSpecRequest { string app_root = 1; string working_dir = 2; repeated string args = 3; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 4; // temp_dir is a temp dir that will be cleaned up after tests have been executed // to write things like app meta and runtime config etc. string temp_dir = 5; } message TestSpecResponse { string command = 1; repeated string args = 2; repeated string environ = 3; } message ExecScriptRequest { string app_root = 1; string working_dir = 2; repeated string script_args = 4; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 5; // trace_file, if set specifies a trace file to write trace information // about the parse and compilation process to. optional string trace_file = 6; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 7; } message ExecSpecRequest { string app_root = 1; string working_dir = 2; repeated string script_args = 4; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 5; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 7; // temp_dir is a temp dir that will be cleaned up by the CLI after the command // has been executed, to write things like app meta and runtime config etc. string temp_dir = 8; } message ExecSpecMessage { oneof msg { CommandOutput output = 1; ExecSpecResponse spec = 2; } } message ExecSpecResponse { string command = 1; repeated string args = 2; repeated string environ = 3; } message CheckRequest { string app_root = 1; string working_dir = 2; // codegen_debug, if true, dumps the generated code and prints where it is located. bool codegen_debug = 3; // parse_tests, if true, exercises test parsing and codegen as well. bool parse_tests = 4; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 5; } message ExportRequest { string app_root = 1; // goos and goarch specify the platform configuration to compile // the application for. The values must be valid GOOS/GOARCH values. string goos = 2; string goarch = 3; // cgo_enabled specifies whether to build with cgo enabled. // The host must have a valid C compiler for the target platform // if true. bool cgo_enabled = 4; // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 5; oneof format { // docker specifies to export the app as a docker image. DockerExportParams docker = 6; } string infra_conf_path = 7; repeated string services = 8; repeated string gateways = 9; bool skip_infra_conf = 10; // A parent path to app_root containing the .git, or the same as app_root string workspace_root = 11; } message DockerExportParams { // local_daemon_tag specifies what to tag the image as // in the local Docker daemon. If empty the export does not // interact with (or require) the local docker daemon at all. string local_daemon_tag = 1; // push_destination_tag specifies the remote registry tag // to push the exported image to. If empty the built image // is not pushed anywhere. string push_destination_tag = 2; // base_image_tag is the base image to build the image from. string base_image_tag = 3; } message DBConnectRequest { string app_root = 1; string db_name = 2; string env_name = 3; // optional DBClusterType cluster_type = 4; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 5; DBRole role = 6; } enum DBRole { DB_ROLE_UNSPECIFIED = 0; DB_ROLE_SUPERUSER = 1; DB_ROLE_ADMIN = 2; DB_ROLE_WRITE = 3; DB_ROLE_READ = 4; } enum DBClusterType { DB_CLUSTER_TYPE_UNSPECIFIED = 0; DB_CLUSTER_TYPE_RUN = 1; DB_CLUSTER_TYPE_TEST = 2; DB_CLUSTER_TYPE_SHADOW = 3; } message DBConnectResponse { string dsn = 1; } message DBProxyRequest { string app_root = 1; string env_name = 2; // optional int32 port = 3; // optional DBClusterType cluster_type = 4; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 5; DBRole role = 6; } message DBResetRequest { string app_root = 1; repeated string database_names = 2; // database names to reset DBClusterType cluster_type = 3; // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 4; } message GenClientRequest { string app_id = 1; string env_name = 2; string lang = 3; string filepath = 4; // Services to include in the output. // If the string "*" is present all services are included. repeated string services = 5; // Services to exclude from the output. // Takes precedence over 'services' above. repeated string excluded_services = 6; // Tags of endpoints to include in the output. // Only includes endpoints from services included in 'services' above. repeated string endpoint_tags = 7; // Tags of endpoints to exclude from the output. // Takes precedence over 'endpoint_tags' above. repeated string excluded_endpoint_tags = 8; // The OpenAPI spec generator by default includes private endpoints. // If this is set to `true`, private endpoints will not be included // in the generated OpenAPI spec. optional bool openapi_exclude_private_endpoints = 9; // The TS generator by default re-declares the api types in the client. // If this is set to `true`, the types will be imported and shared between // the client and the server. It assumes "~backend" is available in the // import path. optional bool ts_shared_types = 10; // If set, the default export of the generate TypeScript client will be // an instantiated client with the given target. The target can be e.g. // a variable, e.g. "import.meta.env.VITE_CLIENT_TARGET" or a string literal. optional string ts_client_target = 11; // The root directory of the app to generate a client for. // Included to be able to handle multi clone scenarios. string app_root = 12; } message GenClientResponse { bytes code = 1; } message GenWrappersRequest { string app_root = 1; } message GenWrappersResponse {} message SecretsRefreshRequest { string app_root = 1; string key = 2; string value = 3; } message SecretsRefreshResponse {} message VersionResponse { string version = 1; string config_hash = 2; } // Namespaces message Namespace { string id = 1; string name = 2; bool active = 3; string created_at = 4; optional string last_active_at = 5; } message CreateNamespaceRequest { string app_root = 1; string name = 2; } message SwitchNamespaceRequest { string app_root = 1; string name = 2; bool create = 3; } message ListNamespacesRequest { string app_root = 1; } message DeleteNamespaceRequest { string app_root = 1; string name = 2; } message ListNamespacesResponse { repeated Namespace namespaces = 1; } message TelemetryConfig { string anon_id = 1; bool enabled = 2; bool debug = 3; } message DumpMetaRequest { string app_root = 1; string working_dir = 2; // for error reporting // environ is the environment to set for the running command. // Each entry is a string in the format "KEY=VALUE", identical to os.Environ(). repeated string environ = 3; // Whether or not to parse tests. bool parse_tests = 4; Format format = 5; enum Format { FORMAT_UNSPECIFIED = 0; FORMAT_JSON = 1; FORMAT_PROTO = 2; } } message DumpMetaResponse { bytes meta = 1; } // The following messages are used for sqlc plugin integration. message SQLCPlugin { message File { string name = 1 [json_name = "name"]; bytes contents = 2 [json_name = "contents"]; } message Settings { // Rename message was field 5 // Overides message was field 6 // PythonCode message was field 8 // KotlinCode message was field 9 // GoCode message was field 10; // JSONCode message was field 11; reserved 5, 8, 9, 10, 11; string version = 1 [json_name = "version"]; string engine = 2 [json_name = "engine"]; repeated string schema = 3 [json_name = "schema"]; repeated string queries = 4 [json_name = "queries"]; Codegen codegen = 12 [json_name = "codegen"]; } message Codegen { message Process { string cmd = 1; } message WASM { string url = 1; string sha256 = 2; } string out = 1 [json_name = "out"]; string plugin = 2 [json_name = "plugin"]; bytes options = 3 [json_name = "options"]; repeated string env = 4 [json_name = "env"]; Process process = 5 [json_name = "process"]; WASM wasm = 6 [json_name = "wasm"]; } message Catalog { string comment = 1; string default_schema = 2; string name = 3; repeated Schema schemas = 4; } message Schema { string comment = 1; string name = 2; repeated Table tables = 3; repeated Enum enums = 4; repeated CompositeType composite_types = 5; } message CompositeType { string name = 1; string comment = 2; } message Enum { string name = 1; repeated string vals = 2; string comment = 3; } message Table { Identifier rel = 1; repeated Column columns = 2; string comment = 3; } message Identifier { string catalog = 1; string schema = 2; string name = 3; } message Column { string name = 1; bool not_null = 3; bool is_array = 4; string comment = 5; int32 length = 6; bool is_named_param = 7; bool is_func_call = 8; // XXX: Figure out what PostgreSQL calls `foo.id` string scope = 9; Identifier table = 10; string table_alias = 11; Identifier type = 12; bool is_sqlc_slice = 13; Identifier embed_table = 14; string original_name = 15; bool unsigned = 16; int32 array_dims = 17; } message Query { string text = 1 [json_name = "text"]; string name = 2 [json_name = "name"]; string cmd = 3 [json_name = "cmd"]; repeated Column columns = 4 [json_name = "columns"]; repeated Parameter params = 5 [json_name = "parameters"]; repeated string comments = 6 [json_name = "comments"]; string filename = 7 [json_name = "filename"]; Identifier insert_into_table = 8 [json_name = "insert_into_table"]; } message Parameter { int32 number = 1 [json_name = "number"]; Column column = 2 [json_name = "column"]; } message GenerateRequest { Settings settings = 1 [json_name = "settings"]; Catalog catalog = 2 [json_name = "catalog"]; repeated Query queries = 3 [json_name = "queries"]; string sqlc_version = 4 [json_name = "sqlc_version"]; bytes plugin_options = 5 [json_name = "plugin_options"]; bytes global_options = 6 [json_name = "global_options"]; } message GenerateResponse { repeated File files = 1 [json_name = "files"]; } } ================================================ FILE: proto/encore/daemon/daemon_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 // - protoc v6.32.1 // source: encore/daemon/daemon.proto package daemon import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( Daemon_Run_FullMethodName = "/encore.daemon.Daemon/Run" Daemon_Test_FullMethodName = "/encore.daemon.Daemon/Test" Daemon_TestSpec_FullMethodName = "/encore.daemon.Daemon/TestSpec" Daemon_ExecScript_FullMethodName = "/encore.daemon.Daemon/ExecScript" Daemon_ExecSpec_FullMethodName = "/encore.daemon.Daemon/ExecSpec" Daemon_Check_FullMethodName = "/encore.daemon.Daemon/Check" Daemon_Export_FullMethodName = "/encore.daemon.Daemon/Export" Daemon_DBConnect_FullMethodName = "/encore.daemon.Daemon/DBConnect" Daemon_DBProxy_FullMethodName = "/encore.daemon.Daemon/DBProxy" Daemon_DBReset_FullMethodName = "/encore.daemon.Daemon/DBReset" Daemon_GenClient_FullMethodName = "/encore.daemon.Daemon/GenClient" Daemon_GenWrappers_FullMethodName = "/encore.daemon.Daemon/GenWrappers" Daemon_SecretsRefresh_FullMethodName = "/encore.daemon.Daemon/SecretsRefresh" Daemon_Version_FullMethodName = "/encore.daemon.Daemon/Version" Daemon_CreateNamespace_FullMethodName = "/encore.daemon.Daemon/CreateNamespace" Daemon_SwitchNamespace_FullMethodName = "/encore.daemon.Daemon/SwitchNamespace" Daemon_ListNamespaces_FullMethodName = "/encore.daemon.Daemon/ListNamespaces" Daemon_DeleteNamespace_FullMethodName = "/encore.daemon.Daemon/DeleteNamespace" Daemon_DumpMeta_FullMethodName = "/encore.daemon.Daemon/DumpMeta" Daemon_Telemetry_FullMethodName = "/encore.daemon.Daemon/Telemetry" Daemon_CreateApp_FullMethodName = "/encore.daemon.Daemon/CreateApp" ) // DaemonClient is the client API for Daemon service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type DaemonClient interface { // Run runs the application. Run(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // Test runs tests. Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // TestSpec returns the specification for how to run tests. TestSpec(ctx context.Context, in *TestSpecRequest, opts ...grpc.CallOption) (*TestSpecResponse, error) // ExecScript executes a one-off script. ExecScript(ctx context.Context, in *ExecScriptRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // ExecSpec returns the specification for how to run an exec command. // It streams progress messages during setup, then sends the spec as the final message. ExecSpec(ctx context.Context, in *ExecSpecRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSpecMessage], error) // Check checks the app for compilation errors. Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // Export exports the app in various formats. Export(ctx context.Context, in *ExportRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // DBConnect starts the database and returns the DSN for connecting to it. DBConnect(ctx context.Context, in *DBConnectRequest, opts ...grpc.CallOption) (*DBConnectResponse, error) // DBProxy starts a local database proxy for connecting to remote databases // on the encore.dev platform. DBProxy(ctx context.Context, in *DBProxyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // DBReset resets the given databases, recreating them from scratch. DBReset(ctx context.Context, in *DBResetRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) // GenClient generates a client based on the app's API. GenClient(ctx context.Context, in *GenClientRequest, opts ...grpc.CallOption) (*GenClientResponse, error) // GenWrappers generates user-facing wrapper code. GenWrappers(ctx context.Context, in *GenWrappersRequest, opts ...grpc.CallOption) (*GenWrappersResponse, error) // SecretsRefresh tells the daemon to refresh the local development secrets // for the given application. SecretsRefresh(ctx context.Context, in *SecretsRefreshRequest, opts ...grpc.CallOption) (*SecretsRefreshResponse, error) // Version reports the daemon version. Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) // CreateNamespace creates a new infra namespace. CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) // SwitchNamespace switches the active infra namespace. SwitchNamespace(ctx context.Context, in *SwitchNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) // ListNamespaces lists all namespaces for the given app. ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) // DeleteNamespace deletes an infra namespace. DeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DumpMeta(ctx context.Context, in *DumpMetaRequest, opts ...grpc.CallOption) (*DumpMetaResponse, error) // Telemetry enables or disables telemetry. Telemetry(ctx context.Context, in *TelemetryConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) // InitTutorial sets the tutorial flag of the app CreateApp(ctx context.Context, in *CreateAppRequest, opts ...grpc.CallOption) (*CreateAppResponse, error) } type daemonClient struct { cc grpc.ClientConnInterface } func NewDaemonClient(cc grpc.ClientConnInterface) DaemonClient { return &daemonClient{cc} } func (c *daemonClient) Run(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[0], Daemon_Run_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[RunRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_RunClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[1], Daemon_Test_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[TestRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_TestClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) TestSpec(ctx context.Context, in *TestSpecRequest, opts ...grpc.CallOption) (*TestSpecResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(TestSpecResponse) err := c.cc.Invoke(ctx, Daemon_TestSpec_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) ExecScript(ctx context.Context, in *ExecScriptRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[2], Daemon_ExecScript_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[ExecScriptRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExecScriptClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) ExecSpec(ctx context.Context, in *ExecSpecRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSpecMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[3], Daemon_ExecSpec_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[ExecSpecRequest, ExecSpecMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExecSpecClient = grpc.ServerStreamingClient[ExecSpecMessage] func (c *daemonClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[4], Daemon_Check_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[CheckRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_CheckClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) Export(ctx context.Context, in *ExportRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[5], Daemon_Export_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[ExportRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExportClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) DBConnect(ctx context.Context, in *DBConnectRequest, opts ...grpc.CallOption) (*DBConnectResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DBConnectResponse) err := c.cc.Invoke(ctx, Daemon_DBConnect_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) DBProxy(ctx context.Context, in *DBProxyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[6], Daemon_DBProxy_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[DBProxyRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_DBProxyClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) DBReset(ctx context.Context, in *DBResetRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[7], Daemon_DBReset_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[DBResetRequest, CommandMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_DBResetClient = grpc.ServerStreamingClient[CommandMessage] func (c *daemonClient) GenClient(ctx context.Context, in *GenClientRequest, opts ...grpc.CallOption) (*GenClientResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GenClientResponse) err := c.cc.Invoke(ctx, Daemon_GenClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) GenWrappers(ctx context.Context, in *GenWrappersRequest, opts ...grpc.CallOption) (*GenWrappersResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GenWrappersResponse) err := c.cc.Invoke(ctx, Daemon_GenWrappers_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) SecretsRefresh(ctx context.Context, in *SecretsRefreshRequest, opts ...grpc.CallOption) (*SecretsRefreshResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SecretsRefreshResponse) err := c.cc.Invoke(ctx, Daemon_SecretsRefresh_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(VersionResponse) err := c.cc.Invoke(ctx, Daemon_Version_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Namespace) err := c.cc.Invoke(ctx, Daemon_CreateNamespace_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) SwitchNamespace(ctx context.Context, in *SwitchNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Namespace) err := c.cc.Invoke(ctx, Daemon_SwitchNamespace_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListNamespacesResponse) err := c.cc.Invoke(ctx, Daemon_ListNamespaces_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) DeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, Daemon_DeleteNamespace_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) DumpMeta(ctx context.Context, in *DumpMetaRequest, opts ...grpc.CallOption) (*DumpMetaResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DumpMetaResponse) err := c.cc.Invoke(ctx, Daemon_DumpMeta_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) Telemetry(ctx context.Context, in *TelemetryConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, Daemon_Telemetry_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *daemonClient) CreateApp(ctx context.Context, in *CreateAppRequest, opts ...grpc.CallOption) (*CreateAppResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreateAppResponse) err := c.cc.Invoke(ctx, Daemon_CreateApp_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // DaemonServer is the server API for Daemon service. // All implementations must embed UnimplementedDaemonServer // for forward compatibility. type DaemonServer interface { // Run runs the application. Run(*RunRequest, grpc.ServerStreamingServer[CommandMessage]) error // Test runs tests. Test(*TestRequest, grpc.ServerStreamingServer[CommandMessage]) error // TestSpec returns the specification for how to run tests. TestSpec(context.Context, *TestSpecRequest) (*TestSpecResponse, error) // ExecScript executes a one-off script. ExecScript(*ExecScriptRequest, grpc.ServerStreamingServer[CommandMessage]) error // ExecSpec returns the specification for how to run an exec command. // It streams progress messages during setup, then sends the spec as the final message. ExecSpec(*ExecSpecRequest, grpc.ServerStreamingServer[ExecSpecMessage]) error // Check checks the app for compilation errors. Check(*CheckRequest, grpc.ServerStreamingServer[CommandMessage]) error // Export exports the app in various formats. Export(*ExportRequest, grpc.ServerStreamingServer[CommandMessage]) error // DBConnect starts the database and returns the DSN for connecting to it. DBConnect(context.Context, *DBConnectRequest) (*DBConnectResponse, error) // DBProxy starts a local database proxy for connecting to remote databases // on the encore.dev platform. DBProxy(*DBProxyRequest, grpc.ServerStreamingServer[CommandMessage]) error // DBReset resets the given databases, recreating them from scratch. DBReset(*DBResetRequest, grpc.ServerStreamingServer[CommandMessage]) error // GenClient generates a client based on the app's API. GenClient(context.Context, *GenClientRequest) (*GenClientResponse, error) // GenWrappers generates user-facing wrapper code. GenWrappers(context.Context, *GenWrappersRequest) (*GenWrappersResponse, error) // SecretsRefresh tells the daemon to refresh the local development secrets // for the given application. SecretsRefresh(context.Context, *SecretsRefreshRequest) (*SecretsRefreshResponse, error) // Version reports the daemon version. Version(context.Context, *emptypb.Empty) (*VersionResponse, error) // CreateNamespace creates a new infra namespace. CreateNamespace(context.Context, *CreateNamespaceRequest) (*Namespace, error) // SwitchNamespace switches the active infra namespace. SwitchNamespace(context.Context, *SwitchNamespaceRequest) (*Namespace, error) // ListNamespaces lists all namespaces for the given app. ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) // DeleteNamespace deletes an infra namespace. DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*emptypb.Empty, error) DumpMeta(context.Context, *DumpMetaRequest) (*DumpMetaResponse, error) // Telemetry enables or disables telemetry. Telemetry(context.Context, *TelemetryConfig) (*emptypb.Empty, error) // InitTutorial sets the tutorial flag of the app CreateApp(context.Context, *CreateAppRequest) (*CreateAppResponse, error) mustEmbedUnimplementedDaemonServer() } // UnimplementedDaemonServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedDaemonServer struct{} func (UnimplementedDaemonServer) Run(*RunRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method Run not implemented") } func (UnimplementedDaemonServer) Test(*TestRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method Test not implemented") } func (UnimplementedDaemonServer) TestSpec(context.Context, *TestSpecRequest) (*TestSpecResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TestSpec not implemented") } func (UnimplementedDaemonServer) ExecScript(*ExecScriptRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method ExecScript not implemented") } func (UnimplementedDaemonServer) ExecSpec(*ExecSpecRequest, grpc.ServerStreamingServer[ExecSpecMessage]) error { return status.Errorf(codes.Unimplemented, "method ExecSpec not implemented") } func (UnimplementedDaemonServer) Check(*CheckRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method Check not implemented") } func (UnimplementedDaemonServer) Export(*ExportRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method Export not implemented") } func (UnimplementedDaemonServer) DBConnect(context.Context, *DBConnectRequest) (*DBConnectResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DBConnect not implemented") } func (UnimplementedDaemonServer) DBProxy(*DBProxyRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method DBProxy not implemented") } func (UnimplementedDaemonServer) DBReset(*DBResetRequest, grpc.ServerStreamingServer[CommandMessage]) error { return status.Errorf(codes.Unimplemented, "method DBReset not implemented") } func (UnimplementedDaemonServer) GenClient(context.Context, *GenClientRequest) (*GenClientResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GenClient not implemented") } func (UnimplementedDaemonServer) GenWrappers(context.Context, *GenWrappersRequest) (*GenWrappersResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GenWrappers not implemented") } func (UnimplementedDaemonServer) SecretsRefresh(context.Context, *SecretsRefreshRequest) (*SecretsRefreshResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SecretsRefresh not implemented") } func (UnimplementedDaemonServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Version not implemented") } func (UnimplementedDaemonServer) CreateNamespace(context.Context, *CreateNamespaceRequest) (*Namespace, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateNamespace not implemented") } func (UnimplementedDaemonServer) SwitchNamespace(context.Context, *SwitchNamespaceRequest) (*Namespace, error) { return nil, status.Errorf(codes.Unimplemented, "method SwitchNamespace not implemented") } func (UnimplementedDaemonServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListNamespaces not implemented") } func (UnimplementedDaemonServer) DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteNamespace not implemented") } func (UnimplementedDaemonServer) DumpMeta(context.Context, *DumpMetaRequest) (*DumpMetaResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DumpMeta not implemented") } func (UnimplementedDaemonServer) Telemetry(context.Context, *TelemetryConfig) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Telemetry not implemented") } func (UnimplementedDaemonServer) CreateApp(context.Context, *CreateAppRequest) (*CreateAppResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateApp not implemented") } func (UnimplementedDaemonServer) mustEmbedUnimplementedDaemonServer() {} func (UnimplementedDaemonServer) testEmbeddedByValue() {} // UnsafeDaemonServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DaemonServer will // result in compilation errors. type UnsafeDaemonServer interface { mustEmbedUnimplementedDaemonServer() } func RegisterDaemonServer(s grpc.ServiceRegistrar, srv DaemonServer) { // If the following call pancis, it indicates UnimplementedDaemonServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&Daemon_ServiceDesc, srv) } func _Daemon_Run_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(RunRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).Run(m, &grpc.GenericServerStream[RunRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_RunServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_Test_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(TestRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).Test(m, &grpc.GenericServerStream[TestRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_TestServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_TestSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TestSpecRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).TestSpec(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_TestSpec_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).TestSpec(ctx, req.(*TestSpecRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_ExecScript_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ExecScriptRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).ExecScript(m, &grpc.GenericServerStream[ExecScriptRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExecScriptServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_ExecSpec_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ExecSpecRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).ExecSpec(m, &grpc.GenericServerStream[ExecSpecRequest, ExecSpecMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExecSpecServer = grpc.ServerStreamingServer[ExecSpecMessage] func _Daemon_Check_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(CheckRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).Check(m, &grpc.GenericServerStream[CheckRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_CheckServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_Export_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ExportRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).Export(m, &grpc.GenericServerStream[ExportRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_ExportServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_DBConnect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DBConnectRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).DBConnect(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_DBConnect_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).DBConnect(ctx, req.(*DBConnectRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_DBProxy_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(DBProxyRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).DBProxy(m, &grpc.GenericServerStream[DBProxyRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_DBProxyServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_DBReset_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(DBResetRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(DaemonServer).DBReset(m, &grpc.GenericServerStream[DBResetRequest, CommandMessage]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Daemon_DBResetServer = grpc.ServerStreamingServer[CommandMessage] func _Daemon_GenClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GenClientRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).GenClient(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_GenClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).GenClient(ctx, req.(*GenClientRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_GenWrappers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GenWrappersRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).GenWrappers(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_GenWrappers_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).GenWrappers(ctx, req.(*GenWrappersRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_SecretsRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SecretsRefreshRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).SecretsRefresh(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_SecretsRefresh_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).SecretsRefresh(ctx, req.(*SecretsRefreshRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).Version(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_Version_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).Version(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _Daemon_CreateNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateNamespaceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).CreateNamespace(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_CreateNamespace_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).CreateNamespace(ctx, req.(*CreateNamespaceRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_SwitchNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SwitchNamespaceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).SwitchNamespace(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_SwitchNamespace_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).SwitchNamespace(ctx, req.(*SwitchNamespaceRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListNamespacesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).ListNamespaces(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_ListNamespaces_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).ListNamespaces(ctx, req.(*ListNamespacesRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_DeleteNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteNamespaceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).DeleteNamespace(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_DeleteNamespace_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).DeleteNamespace(ctx, req.(*DeleteNamespaceRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_DumpMeta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DumpMetaRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).DumpMeta(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_DumpMeta_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).DumpMeta(ctx, req.(*DumpMetaRequest)) } return interceptor(ctx, in, info, handler) } func _Daemon_Telemetry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TelemetryConfig) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).Telemetry(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_Telemetry_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).Telemetry(ctx, req.(*TelemetryConfig)) } return interceptor(ctx, in, info, handler) } func _Daemon_CreateApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateAppRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DaemonServer).CreateApp(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: Daemon_CreateApp_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DaemonServer).CreateApp(ctx, req.(*CreateAppRequest)) } return interceptor(ctx, in, info, handler) } // Daemon_ServiceDesc is the grpc.ServiceDesc for Daemon service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var Daemon_ServiceDesc = grpc.ServiceDesc{ ServiceName: "encore.daemon.Daemon", HandlerType: (*DaemonServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "TestSpec", Handler: _Daemon_TestSpec_Handler, }, { MethodName: "DBConnect", Handler: _Daemon_DBConnect_Handler, }, { MethodName: "GenClient", Handler: _Daemon_GenClient_Handler, }, { MethodName: "GenWrappers", Handler: _Daemon_GenWrappers_Handler, }, { MethodName: "SecretsRefresh", Handler: _Daemon_SecretsRefresh_Handler, }, { MethodName: "Version", Handler: _Daemon_Version_Handler, }, { MethodName: "CreateNamespace", Handler: _Daemon_CreateNamespace_Handler, }, { MethodName: "SwitchNamespace", Handler: _Daemon_SwitchNamespace_Handler, }, { MethodName: "ListNamespaces", Handler: _Daemon_ListNamespaces_Handler, }, { MethodName: "DeleteNamespace", Handler: _Daemon_DeleteNamespace_Handler, }, { MethodName: "DumpMeta", Handler: _Daemon_DumpMeta_Handler, }, { MethodName: "Telemetry", Handler: _Daemon_Telemetry_Handler, }, { MethodName: "CreateApp", Handler: _Daemon_CreateApp_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "Run", Handler: _Daemon_Run_Handler, ServerStreams: true, }, { StreamName: "Test", Handler: _Daemon_Test_Handler, ServerStreams: true, }, { StreamName: "ExecScript", Handler: _Daemon_ExecScript_Handler, ServerStreams: true, }, { StreamName: "ExecSpec", Handler: _Daemon_ExecSpec_Handler, ServerStreams: true, }, { StreamName: "Check", Handler: _Daemon_Check_Handler, ServerStreams: true, }, { StreamName: "Export", Handler: _Daemon_Export_Handler, ServerStreams: true, }, { StreamName: "DBProxy", Handler: _Daemon_DBProxy_Handler, ServerStreams: true, }, { StreamName: "DBReset", Handler: _Daemon_DBReset_Handler, ServerStreams: true, }, }, Metadata: "encore/daemon/daemon.proto", } ================================================ FILE: proto/encore/engine/trace/trace.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/engine/trace/trace.proto package trace import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type HTTPTraceEventCode int32 const ( HTTPTraceEventCode_UNKNOWN HTTPTraceEventCode = 0 HTTPTraceEventCode_GET_CONN HTTPTraceEventCode = 1 HTTPTraceEventCode_GOT_CONN HTTPTraceEventCode = 2 HTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE HTTPTraceEventCode = 3 HTTPTraceEventCode_GOT_1XX_RESPONSE HTTPTraceEventCode = 4 HTTPTraceEventCode_DNS_START HTTPTraceEventCode = 5 HTTPTraceEventCode_DNS_DONE HTTPTraceEventCode = 6 HTTPTraceEventCode_CONNECT_START HTTPTraceEventCode = 7 HTTPTraceEventCode_CONNECT_DONE HTTPTraceEventCode = 8 HTTPTraceEventCode_TLS_HANDSHAKE_START HTTPTraceEventCode = 9 HTTPTraceEventCode_TLS_HANDSHAKE_DONE HTTPTraceEventCode = 10 HTTPTraceEventCode_WROTE_HEADERS HTTPTraceEventCode = 11 HTTPTraceEventCode_WROTE_REQUEST HTTPTraceEventCode = 12 HTTPTraceEventCode_WAIT_100_CONTINUE HTTPTraceEventCode = 13 ) // Enum value maps for HTTPTraceEventCode. var ( HTTPTraceEventCode_name = map[int32]string{ 0: "UNKNOWN", 1: "GET_CONN", 2: "GOT_CONN", 3: "GOT_FIRST_RESPONSE_BYTE", 4: "GOT_1XX_RESPONSE", 5: "DNS_START", 6: "DNS_DONE", 7: "CONNECT_START", 8: "CONNECT_DONE", 9: "TLS_HANDSHAKE_START", 10: "TLS_HANDSHAKE_DONE", 11: "WROTE_HEADERS", 12: "WROTE_REQUEST", 13: "WAIT_100_CONTINUE", } HTTPTraceEventCode_value = map[string]int32{ "UNKNOWN": 0, "GET_CONN": 1, "GOT_CONN": 2, "GOT_FIRST_RESPONSE_BYTE": 3, "GOT_1XX_RESPONSE": 4, "DNS_START": 5, "DNS_DONE": 6, "CONNECT_START": 7, "CONNECT_DONE": 8, "TLS_HANDSHAKE_START": 9, "TLS_HANDSHAKE_DONE": 10, "WROTE_HEADERS": 11, "WROTE_REQUEST": 12, "WAIT_100_CONTINUE": 13, } ) func (x HTTPTraceEventCode) Enum() *HTTPTraceEventCode { p := new(HTTPTraceEventCode) *p = x return p } func (x HTTPTraceEventCode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (HTTPTraceEventCode) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace_trace_proto_enumTypes[0].Descriptor() } func (HTTPTraceEventCode) Type() protoreflect.EnumType { return &file_encore_engine_trace_trace_proto_enumTypes[0] } func (x HTTPTraceEventCode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use HTTPTraceEventCode.Descriptor instead. func (HTTPTraceEventCode) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{0} } type Request_Type int32 const ( Request_RPC Request_Type = 0 Request_AUTH Request_Type = 1 Request_PUBSUB_MSG Request_Type = 2 ) // Enum value maps for Request_Type. var ( Request_Type_name = map[int32]string{ 0: "RPC", 1: "AUTH", 2: "PUBSUB_MSG", } Request_Type_value = map[string]int32{ "RPC": 0, "AUTH": 1, "PUBSUB_MSG": 2, } ) func (x Request_Type) Enum() *Request_Type { p := new(Request_Type) *p = x return p } func (x Request_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Request_Type) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace_trace_proto_enumTypes[1].Descriptor() } func (Request_Type) Type() protoreflect.EnumType { return &file_encore_engine_trace_trace_proto_enumTypes[1] } func (x Request_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Request_Type.Descriptor instead. func (Request_Type) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{1, 0} } type DBTransaction_CompletionType int32 const ( DBTransaction_ROLLBACK DBTransaction_CompletionType = 0 DBTransaction_COMMIT DBTransaction_CompletionType = 1 ) // Enum value maps for DBTransaction_CompletionType. var ( DBTransaction_CompletionType_name = map[int32]string{ 0: "ROLLBACK", 1: "COMMIT", } DBTransaction_CompletionType_value = map[string]int32{ "ROLLBACK": 0, "COMMIT": 1, } ) func (x DBTransaction_CompletionType) Enum() *DBTransaction_CompletionType { p := new(DBTransaction_CompletionType) *p = x return p } func (x DBTransaction_CompletionType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DBTransaction_CompletionType) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace_trace_proto_enumTypes[2].Descriptor() } func (DBTransaction_CompletionType) Type() protoreflect.EnumType { return &file_encore_engine_trace_trace_proto_enumTypes[2] } func (x DBTransaction_CompletionType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DBTransaction_CompletionType.Descriptor instead. func (DBTransaction_CompletionType) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{5, 0} } type CacheOp_Result int32 const ( CacheOp_UNKNOWN CacheOp_Result = 0 CacheOp_OK CacheOp_Result = 1 CacheOp_NO_SUCH_KEY CacheOp_Result = 2 CacheOp_CONFLICT CacheOp_Result = 3 CacheOp_ERR CacheOp_Result = 4 ) // Enum value maps for CacheOp_Result. var ( CacheOp_Result_name = map[int32]string{ 0: "UNKNOWN", 1: "OK", 2: "NO_SUCH_KEY", 3: "CONFLICT", 4: "ERR", } CacheOp_Result_value = map[string]int32{ "UNKNOWN": 0, "OK": 1, "NO_SUCH_KEY": 2, "CONFLICT": 3, "ERR": 4, } ) func (x CacheOp_Result) Enum() *CacheOp_Result { p := new(CacheOp_Result) *p = x return p } func (x CacheOp_Result) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (CacheOp_Result) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace_trace_proto_enumTypes[3].Descriptor() } func (CacheOp_Result) Type() protoreflect.EnumType { return &file_encore_engine_trace_trace_proto_enumTypes[3] } func (x CacheOp_Result) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use CacheOp_Result.Descriptor instead. func (CacheOp_Result) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{9, 0} } // Note: These values don't match the values used by the binary trace protocol, // as these values are stored in persisted traces and therefore must maintain // backwards compatibility. The binary trace protocol is versioned and doesn't // have the same limitations. type LogMessage_Level int32 const ( LogMessage_DEBUG LogMessage_Level = 0 LogMessage_INFO LogMessage_Level = 1 LogMessage_ERROR LogMessage_Level = 2 LogMessage_WARN LogMessage_Level = 3 LogMessage_TRACE LogMessage_Level = 4 ) // Enum value maps for LogMessage_Level. var ( LogMessage_Level_name = map[int32]string{ 0: "DEBUG", 1: "INFO", 2: "ERROR", 3: "WARN", 4: "TRACE", } LogMessage_Level_value = map[string]int32{ "DEBUG": 0, "INFO": 1, "ERROR": 2, "WARN": 3, "TRACE": 4, } ) func (x LogMessage_Level) Enum() *LogMessage_Level { p := new(LogMessage_Level) *p = x return p } func (x LogMessage_Level) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (LogMessage_Level) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace_trace_proto_enumTypes[4].Descriptor() } func (LogMessage_Level) Type() protoreflect.EnumType { return &file_encore_engine_trace_trace_proto_enumTypes[4] } func (x LogMessage_Level) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use LogMessage_Level.Descriptor instead. func (LogMessage_Level) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{23, 0} } type TraceID struct { state protoimpl.MessageState `protogen:"open.v1"` High uint64 `protobuf:"varint,1,opt,name=high,proto3" json:"high,omitempty"` Low uint64 `protobuf:"varint,2,opt,name=low,proto3" json:"low,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TraceID) Reset() { *x = TraceID{} mi := &file_encore_engine_trace_trace_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TraceID) String() string { return protoimpl.X.MessageStringOf(x) } func (*TraceID) ProtoMessage() {} func (x *TraceID) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TraceID.ProtoReflect.Descriptor instead. func (*TraceID) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{0} } func (x *TraceID) GetHigh() uint64 { if x != nil { return x.High } return 0 } func (x *TraceID) GetLow() uint64 { if x != nil { return x.Low } return 0 } type Request struct { state protoimpl.MessageState `protogen:"open.v1"` TraceId *TraceID `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` SpanId uint64 `protobuf:"varint,2,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` ParentSpanId uint64 `protobuf:"varint,3,opt,name=parent_span_id,json=parentSpanId,proto3" json:"parent_span_id,omitempty"` ParentTraceId *TraceID `protobuf:"bytes,32,opt,name=parent_trace_id,json=parentTraceId,proto3" json:"parent_trace_id,omitempty"` Goid uint32 `protobuf:"varint,4,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` DefLoc int32 `protobuf:"varint,8,opt,name=def_loc,json=defLoc,proto3" json:"def_loc,omitempty"` Err []byte `protobuf:"bytes,11,opt,name=err,proto3" json:"err,omitempty"` Events []*Event `protobuf:"bytes,12,rep,name=events,proto3" json:"events,omitempty"` Type Request_Type `protobuf:"varint,14,opt,name=type,proto3,enum=encore.engine.trace.Request_Type" json:"type,omitempty"` ErrStack *StackTrace `protobuf:"bytes,15,opt,name=err_stack,json=errStack,proto3" json:"err_stack,omitempty"` // null if unavailable PanicStack *StackTrace `protobuf:"bytes,34,opt,name=panic_stack,json=panicStack,proto3" json:"panic_stack,omitempty"` // null if unavailable // abs_start_time is the absolute unix timestamp // (in nanosecond resolution) of when the request started. AbsStartTime uint64 `protobuf:"varint,16,opt,name=abs_start_time,json=absStartTime,proto3" json:"abs_start_time,omitempty"` ServiceName string `protobuf:"bytes,17,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName string `protobuf:"bytes,18,opt,name=endpoint_name,json=endpointName,proto3" json:"endpoint_name,omitempty"` // Fields set if Type == PUBSUB_MSG TopicName string `protobuf:"bytes,19,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` SubscriptionName string `protobuf:"bytes,20,opt,name=subscription_name,json=subscriptionName,proto3" json:"subscription_name,omitempty"` MessageId string `protobuf:"bytes,21,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` Attempt uint32 `protobuf:"varint,22,opt,name=attempt,proto3" json:"attempt,omitempty"` PublishTime uint64 `protobuf:"varint,23,opt,name=publish_time,json=publishTime,proto3" json:"publish_time,omitempty"` // Fields set if Type == RPC or AUTH Inputs [][]byte `protobuf:"bytes,9,rep,name=inputs,proto3" json:"inputs,omitempty"` // Deprecated: use request_payload and path_params Outputs [][]byte `protobuf:"bytes,10,rep,name=outputs,proto3" json:"outputs,omitempty"` // Deprecated: use response_payload and uid Uid string `protobuf:"bytes,13,opt,name=uid,proto3" json:"uid,omitempty"` HttpMethod string `protobuf:"bytes,24,opt,name=http_method,json=httpMethod,proto3" json:"http_method,omitempty"` Path string `protobuf:"bytes,25,opt,name=path,proto3" json:"path,omitempty"` PathParams []string `protobuf:"bytes,26,rep,name=path_params,json=pathParams,proto3" json:"path_params,omitempty"` RequestPayload []byte `protobuf:"bytes,27,opt,name=request_payload,json=requestPayload,proto3" json:"request_payload,omitempty"` ResponsePayload []byte `protobuf:"bytes,28,opt,name=response_payload,json=responsePayload,proto3" json:"response_payload,omitempty"` RawRequestHeaders map[string]string `protobuf:"bytes,29,rep,name=raw_request_headers,json=rawRequestHeaders,proto3" json:"raw_request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` RawResponseHeaders map[string]string `protobuf:"bytes,30,rep,name=raw_response_headers,json=rawResponseHeaders,proto3" json:"raw_response_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // external_request_id is the value of the X-Request-ID header. ExternalRequestId string `protobuf:"bytes,31,opt,name=external_request_id,json=externalRequestId,proto3" json:"external_request_id,omitempty"` ExternalCorrelationId string `protobuf:"bytes,33,opt,name=external_correlation_id,json=externalCorrelationId,proto3" json:"external_correlation_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Request) Reset() { *x = Request{} mi := &file_encore_engine_trace_trace_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Request) String() string { return protoimpl.X.MessageStringOf(x) } func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Request.ProtoReflect.Descriptor instead. func (*Request) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{1} } func (x *Request) GetTraceId() *TraceID { if x != nil { return x.TraceId } return nil } func (x *Request) GetSpanId() uint64 { if x != nil { return x.SpanId } return 0 } func (x *Request) GetParentSpanId() uint64 { if x != nil { return x.ParentSpanId } return 0 } func (x *Request) GetParentTraceId() *TraceID { if x != nil { return x.ParentTraceId } return nil } func (x *Request) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *Request) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *Request) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *Request) GetDefLoc() int32 { if x != nil { return x.DefLoc } return 0 } func (x *Request) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *Request) GetEvents() []*Event { if x != nil { return x.Events } return nil } func (x *Request) GetType() Request_Type { if x != nil { return x.Type } return Request_RPC } func (x *Request) GetErrStack() *StackTrace { if x != nil { return x.ErrStack } return nil } func (x *Request) GetPanicStack() *StackTrace { if x != nil { return x.PanicStack } return nil } func (x *Request) GetAbsStartTime() uint64 { if x != nil { return x.AbsStartTime } return 0 } func (x *Request) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *Request) GetEndpointName() string { if x != nil { return x.EndpointName } return "" } func (x *Request) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *Request) GetSubscriptionName() string { if x != nil { return x.SubscriptionName } return "" } func (x *Request) GetMessageId() string { if x != nil { return x.MessageId } return "" } func (x *Request) GetAttempt() uint32 { if x != nil { return x.Attempt } return 0 } func (x *Request) GetPublishTime() uint64 { if x != nil { return x.PublishTime } return 0 } func (x *Request) GetInputs() [][]byte { if x != nil { return x.Inputs } return nil } func (x *Request) GetOutputs() [][]byte { if x != nil { return x.Outputs } return nil } func (x *Request) GetUid() string { if x != nil { return x.Uid } return "" } func (x *Request) GetHttpMethod() string { if x != nil { return x.HttpMethod } return "" } func (x *Request) GetPath() string { if x != nil { return x.Path } return "" } func (x *Request) GetPathParams() []string { if x != nil { return x.PathParams } return nil } func (x *Request) GetRequestPayload() []byte { if x != nil { return x.RequestPayload } return nil } func (x *Request) GetResponsePayload() []byte { if x != nil { return x.ResponsePayload } return nil } func (x *Request) GetRawRequestHeaders() map[string]string { if x != nil { return x.RawRequestHeaders } return nil } func (x *Request) GetRawResponseHeaders() map[string]string { if x != nil { return x.RawResponseHeaders } return nil } func (x *Request) GetExternalRequestId() string { if x != nil { return x.ExternalRequestId } return "" } func (x *Request) GetExternalCorrelationId() string { if x != nil { return x.ExternalCorrelationId } return "" } type Event struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Data: // // *Event_Rpc // *Event_Tx // *Event_Query // *Event_Goroutine // *Event_Http // *Event_Log // *Event_PublishedMsg // *Event_ServiceInit // *Event_Cache // *Event_BodyStream Data isEvent_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Event) Reset() { *x = Event{} mi := &file_encore_engine_trace_trace_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Event) String() string { return protoimpl.X.MessageStringOf(x) } func (*Event) ProtoMessage() {} func (x *Event) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Event.ProtoReflect.Descriptor instead. func (*Event) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{2} } func (x *Event) GetData() isEvent_Data { if x != nil { return x.Data } return nil } func (x *Event) GetRpc() *RPCCall { if x != nil { if x, ok := x.Data.(*Event_Rpc); ok { return x.Rpc } } return nil } func (x *Event) GetTx() *DBTransaction { if x != nil { if x, ok := x.Data.(*Event_Tx); ok { return x.Tx } } return nil } func (x *Event) GetQuery() *DBQuery { if x != nil { if x, ok := x.Data.(*Event_Query); ok { return x.Query } } return nil } func (x *Event) GetGoroutine() *Goroutine { if x != nil { if x, ok := x.Data.(*Event_Goroutine); ok { return x.Goroutine } } return nil } func (x *Event) GetHttp() *HTTPCall { if x != nil { if x, ok := x.Data.(*Event_Http); ok { return x.Http } } return nil } func (x *Event) GetLog() *LogMessage { if x != nil { if x, ok := x.Data.(*Event_Log); ok { return x.Log } } return nil } func (x *Event) GetPublishedMsg() *PubsubMsgPublished { if x != nil { if x, ok := x.Data.(*Event_PublishedMsg); ok { return x.PublishedMsg } } return nil } func (x *Event) GetServiceInit() *ServiceInit { if x != nil { if x, ok := x.Data.(*Event_ServiceInit); ok { return x.ServiceInit } } return nil } func (x *Event) GetCache() *CacheOp { if x != nil { if x, ok := x.Data.(*Event_Cache); ok { return x.Cache } } return nil } func (x *Event) GetBodyStream() *BodyStream { if x != nil { if x, ok := x.Data.(*Event_BodyStream); ok { return x.BodyStream } } return nil } type isEvent_Data interface { isEvent_Data() } type Event_Rpc struct { Rpc *RPCCall `protobuf:"bytes,1,opt,name=rpc,proto3,oneof"` } type Event_Tx struct { Tx *DBTransaction `protobuf:"bytes,2,opt,name=tx,proto3,oneof"` } type Event_Query struct { Query *DBQuery `protobuf:"bytes,3,opt,name=query,proto3,oneof"` } type Event_Goroutine struct { Goroutine *Goroutine `protobuf:"bytes,4,opt,name=goroutine,proto3,oneof"` } type Event_Http struct { Http *HTTPCall `protobuf:"bytes,5,opt,name=http,proto3,oneof"` } type Event_Log struct { Log *LogMessage `protobuf:"bytes,6,opt,name=log,proto3,oneof"` } type Event_PublishedMsg struct { PublishedMsg *PubsubMsgPublished `protobuf:"bytes,7,opt,name=publishedMsg,proto3,oneof"` } type Event_ServiceInit struct { ServiceInit *ServiceInit `protobuf:"bytes,8,opt,name=service_init,json=serviceInit,proto3,oneof"` } type Event_Cache struct { Cache *CacheOp `protobuf:"bytes,9,opt,name=cache,proto3,oneof"` } type Event_BodyStream struct { BodyStream *BodyStream `protobuf:"bytes,10,opt,name=body_stream,json=bodyStream,proto3,oneof"` } func (*Event_Rpc) isEvent_Data() {} func (*Event_Tx) isEvent_Data() {} func (*Event_Query) isEvent_Data() {} func (*Event_Goroutine) isEvent_Data() {} func (*Event_Http) isEvent_Data() {} func (*Event_Log) isEvent_Data() {} func (*Event_PublishedMsg) isEvent_Data() {} func (*Event_ServiceInit) isEvent_Data() {} func (*Event_Cache) isEvent_Data() {} func (*Event_BodyStream) isEvent_Data() {} type RPCCall struct { state protoimpl.MessageState `protogen:"open.v1"` SpanId uint64 `protobuf:"varint,1,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` Goid uint32 `protobuf:"varint,2,opt,name=goid,proto3" json:"goid,omitempty"` DefLoc int32 `protobuf:"varint,4,opt,name=def_loc,json=defLoc,proto3" json:"def_loc,omitempty"` StartTime uint64 `protobuf:"varint,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Err []byte `protobuf:"bytes,7,opt,name=err,proto3" json:"err,omitempty"` Stack *StackTrace `protobuf:"bytes,8,opt,name=stack,proto3" json:"stack,omitempty"` // where it was called (null if unavailable) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPCCall) Reset() { *x = RPCCall{} mi := &file_encore_engine_trace_trace_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPCCall) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPCCall) ProtoMessage() {} func (x *RPCCall) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPCCall.ProtoReflect.Descriptor instead. func (*RPCCall) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{3} } func (x *RPCCall) GetSpanId() uint64 { if x != nil { return x.SpanId } return 0 } func (x *RPCCall) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *RPCCall) GetDefLoc() int32 { if x != nil { return x.DefLoc } return 0 } func (x *RPCCall) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *RPCCall) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *RPCCall) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *RPCCall) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type Goroutine struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Goroutine) Reset() { *x = Goroutine{} mi := &file_encore_engine_trace_trace_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Goroutine) String() string { return protoimpl.X.MessageStringOf(x) } func (*Goroutine) ProtoMessage() {} func (x *Goroutine) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Goroutine.ProtoReflect.Descriptor instead. func (*Goroutine) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{4} } func (x *Goroutine) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *Goroutine) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *Goroutine) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } type DBTransaction struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,4,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,5,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Err []byte `protobuf:"bytes,6,opt,name=err,proto3" json:"err,omitempty"` Completion DBTransaction_CompletionType `protobuf:"varint,7,opt,name=completion,proto3,enum=encore.engine.trace.DBTransaction_CompletionType" json:"completion,omitempty"` Queries []*DBQuery `protobuf:"bytes,8,rep,name=queries,proto3" json:"queries,omitempty"` BeginStack *StackTrace `protobuf:"bytes,9,opt,name=begin_stack,json=beginStack,proto3" json:"begin_stack,omitempty"` // null if unavailable EndStack *StackTrace `protobuf:"bytes,10,opt,name=end_stack,json=endStack,proto3" json:"end_stack,omitempty"` // null if unavailable unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBTransaction) Reset() { *x = DBTransaction{} mi := &file_encore_engine_trace_trace_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBTransaction) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBTransaction) ProtoMessage() {} func (x *DBTransaction) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBTransaction.ProtoReflect.Descriptor instead. func (*DBTransaction) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{5} } func (x *DBTransaction) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *DBTransaction) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *DBTransaction) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *DBTransaction) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *DBTransaction) GetCompletion() DBTransaction_CompletionType { if x != nil { return x.Completion } return DBTransaction_ROLLBACK } func (x *DBTransaction) GetQueries() []*DBQuery { if x != nil { return x.Queries } return nil } func (x *DBTransaction) GetBeginStack() *StackTrace { if x != nil { return x.BeginStack } return nil } func (x *DBTransaction) GetEndStack() *StackTrace { if x != nil { return x.EndStack } return nil } type DBQuery struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Query []byte `protobuf:"bytes,5,opt,name=query,proto3" json:"query,omitempty"` Err []byte `protobuf:"bytes,6,opt,name=err,proto3" json:"err,omitempty"` Stack *StackTrace `protobuf:"bytes,7,opt,name=stack,proto3" json:"stack,omitempty"` // null if unavailable unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBQuery) Reset() { *x = DBQuery{} mi := &file_encore_engine_trace_trace_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBQuery) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBQuery) ProtoMessage() {} func (x *DBQuery) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBQuery.ProtoReflect.Descriptor instead. func (*DBQuery) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{6} } func (x *DBQuery) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *DBQuery) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *DBQuery) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *DBQuery) GetQuery() []byte { if x != nil { return x.Query } return nil } func (x *DBQuery) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *DBQuery) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type PubsubMsgPublished struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint64 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Topic string `protobuf:"bytes,5,opt,name=topic,proto3" json:"topic,omitempty"` Message []byte `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` MessageId string `protobuf:"bytes,7,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` Err []byte `protobuf:"bytes,8,opt,name=err,proto3" json:"err,omitempty"` Stack *StackTrace `protobuf:"bytes,9,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubsubMsgPublished) Reset() { *x = PubsubMsgPublished{} mi := &file_encore_engine_trace_trace_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubsubMsgPublished) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubsubMsgPublished) ProtoMessage() {} func (x *PubsubMsgPublished) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubsubMsgPublished.ProtoReflect.Descriptor instead. func (*PubsubMsgPublished) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{7} } func (x *PubsubMsgPublished) GetGoid() uint64 { if x != nil { return x.Goid } return 0 } func (x *PubsubMsgPublished) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *PubsubMsgPublished) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *PubsubMsgPublished) GetTopic() string { if x != nil { return x.Topic } return "" } func (x *PubsubMsgPublished) GetMessage() []byte { if x != nil { return x.Message } return nil } func (x *PubsubMsgPublished) GetMessageId() string { if x != nil { return x.MessageId } return "" } func (x *PubsubMsgPublished) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *PubsubMsgPublished) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type ServiceInit struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint64 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` DefLoc int32 `protobuf:"varint,2,opt,name=def_loc,json=defLoc,proto3" json:"def_loc,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Service string `protobuf:"bytes,5,opt,name=service,proto3" json:"service,omitempty"` Err []byte `protobuf:"bytes,6,opt,name=err,proto3" json:"err,omitempty"` ErrStack *StackTrace `protobuf:"bytes,7,opt,name=err_stack,json=errStack,proto3" json:"err_stack,omitempty"` // null if not an error unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceInit) Reset() { *x = ServiceInit{} mi := &file_encore_engine_trace_trace_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceInit) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceInit) ProtoMessage() {} func (x *ServiceInit) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceInit.ProtoReflect.Descriptor instead. func (*ServiceInit) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{8} } func (x *ServiceInit) GetGoid() uint64 { if x != nil { return x.Goid } return 0 } func (x *ServiceInit) GetDefLoc() int32 { if x != nil { return x.DefLoc } return 0 } func (x *ServiceInit) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *ServiceInit) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *ServiceInit) GetService() string { if x != nil { return x.Service } return "" } func (x *ServiceInit) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *ServiceInit) GetErrStack() *StackTrace { if x != nil { return x.ErrStack } return nil } type CacheOp struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` DefLoc int32 `protobuf:"varint,2,opt,name=def_loc,json=defLoc,proto3" json:"def_loc,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Operation string `protobuf:"bytes,5,opt,name=operation,proto3" json:"operation,omitempty"` Keys []string `protobuf:"bytes,6,rep,name=keys,proto3" json:"keys,omitempty"` Inputs [][]byte `protobuf:"bytes,7,rep,name=inputs,proto3" json:"inputs,omitempty"` Outputs [][]byte `protobuf:"bytes,8,rep,name=outputs,proto3" json:"outputs,omitempty"` Stack *StackTrace `protobuf:"bytes,9,opt,name=stack,proto3" json:"stack,omitempty"` // null if unavailable Err []byte `protobuf:"bytes,10,opt,name=err,proto3" json:"err,omitempty"` // set iff result == ERR Write bool `protobuf:"varint,11,opt,name=write,proto3" json:"write,omitempty"` Result CacheOp_Result `protobuf:"varint,12,opt,name=result,proto3,enum=encore.engine.trace.CacheOp_Result" json:"result,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheOp) Reset() { *x = CacheOp{} mi := &file_encore_engine_trace_trace_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheOp) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheOp) ProtoMessage() {} func (x *CacheOp) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheOp.ProtoReflect.Descriptor instead. func (*CacheOp) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{9} } func (x *CacheOp) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *CacheOp) GetDefLoc() int32 { if x != nil { return x.DefLoc } return 0 } func (x *CacheOp) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *CacheOp) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *CacheOp) GetOperation() string { if x != nil { return x.Operation } return "" } func (x *CacheOp) GetKeys() []string { if x != nil { return x.Keys } return nil } func (x *CacheOp) GetInputs() [][]byte { if x != nil { return x.Inputs } return nil } func (x *CacheOp) GetOutputs() [][]byte { if x != nil { return x.Outputs } return nil } func (x *CacheOp) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } func (x *CacheOp) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *CacheOp) GetWrite() bool { if x != nil { return x.Write } return false } func (x *CacheOp) GetResult() CacheOp_Result { if x != nil { return x.Result } return CacheOp_UNKNOWN } type BodyStream struct { state protoimpl.MessageState `protogen:"open.v1"` IsResponse bool `protobuf:"varint,1,opt,name=is_response,json=isResponse,proto3" json:"is_response,omitempty"` Overflowed bool `protobuf:"varint,2,opt,name=overflowed,proto3" json:"overflowed,omitempty"` Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BodyStream) Reset() { *x = BodyStream{} mi := &file_encore_engine_trace_trace_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BodyStream) String() string { return protoimpl.X.MessageStringOf(x) } func (*BodyStream) ProtoMessage() {} func (x *BodyStream) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BodyStream.ProtoReflect.Descriptor instead. func (*BodyStream) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{10} } func (x *BodyStream) GetIsResponse() bool { if x != nil { return x.IsResponse } return false } func (x *BodyStream) GetOverflowed() bool { if x != nil { return x.Overflowed } return false } func (x *BodyStream) GetData() []byte { if x != nil { return x.Data } return nil } type HTTPCall struct { state protoimpl.MessageState `protogen:"open.v1"` SpanId uint64 `protobuf:"varint,1,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` Goid uint32 `protobuf:"varint,2,opt,name=goid,proto3" json:"goid,omitempty"` StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` EndTime uint64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Method string `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"` Url string `protobuf:"bytes,6,opt,name=url,proto3" json:"url,omitempty"` StatusCode uint32 `protobuf:"varint,7,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` Err []byte `protobuf:"bytes,8,opt,name=err,proto3" json:"err,omitempty"` BodyClosedTime uint64 `protobuf:"varint,9,opt,name=body_closed_time,json=bodyClosedTime,proto3" json:"body_closed_time,omitempty"` Events []*HTTPTraceEvent `protobuf:"bytes,10,rep,name=events,proto3" json:"events,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPCall) Reset() { *x = HTTPCall{} mi := &file_encore_engine_trace_trace_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPCall) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPCall) ProtoMessage() {} func (x *HTTPCall) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPCall.ProtoReflect.Descriptor instead. func (*HTTPCall) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{11} } func (x *HTTPCall) GetSpanId() uint64 { if x != nil { return x.SpanId } return 0 } func (x *HTTPCall) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *HTTPCall) GetStartTime() uint64 { if x != nil { return x.StartTime } return 0 } func (x *HTTPCall) GetEndTime() uint64 { if x != nil { return x.EndTime } return 0 } func (x *HTTPCall) GetMethod() string { if x != nil { return x.Method } return "" } func (x *HTTPCall) GetUrl() string { if x != nil { return x.Url } return "" } func (x *HTTPCall) GetStatusCode() uint32 { if x != nil { return x.StatusCode } return 0 } func (x *HTTPCall) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *HTTPCall) GetBodyClosedTime() uint64 { if x != nil { return x.BodyClosedTime } return 0 } func (x *HTTPCall) GetEvents() []*HTTPTraceEvent { if x != nil { return x.Events } return nil } type HTTPTraceEvent struct { state protoimpl.MessageState `protogen:"open.v1"` Code HTTPTraceEventCode `protobuf:"varint,1,opt,name=code,proto3,enum=encore.engine.trace.HTTPTraceEventCode" json:"code,omitempty"` Time uint64 `protobuf:"varint,2,opt,name=time,proto3" json:"time,omitempty"` // Types that are valid to be assigned to Data: // // *HTTPTraceEvent_GetConn // *HTTPTraceEvent_GotConn // *HTTPTraceEvent_Got_1XxResponse // *HTTPTraceEvent_DnsStart // *HTTPTraceEvent_DnsDone // *HTTPTraceEvent_ConnectStart // *HTTPTraceEvent_ConnectDone // *HTTPTraceEvent_TlsHandshakeDone // *HTTPTraceEvent_WroteRequest Data isHTTPTraceEvent_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPTraceEvent) Reset() { *x = HTTPTraceEvent{} mi := &file_encore_engine_trace_trace_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPTraceEvent) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPTraceEvent) ProtoMessage() {} func (x *HTTPTraceEvent) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPTraceEvent.ProtoReflect.Descriptor instead. func (*HTTPTraceEvent) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{12} } func (x *HTTPTraceEvent) GetCode() HTTPTraceEventCode { if x != nil { return x.Code } return HTTPTraceEventCode_UNKNOWN } func (x *HTTPTraceEvent) GetTime() uint64 { if x != nil { return x.Time } return 0 } func (x *HTTPTraceEvent) GetData() isHTTPTraceEvent_Data { if x != nil { return x.Data } return nil } func (x *HTTPTraceEvent) GetGetConn() *HTTPGetConnData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_GetConn); ok { return x.GetConn } } return nil } func (x *HTTPTraceEvent) GetGotConn() *HTTPGotConnData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_GotConn); ok { return x.GotConn } } return nil } func (x *HTTPTraceEvent) GetGot_1XxResponse() *HTTPGot1XxResponseData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_Got_1XxResponse); ok { return x.Got_1XxResponse } } return nil } func (x *HTTPTraceEvent) GetDnsStart() *HTTPDNSStartData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_DnsStart); ok { return x.DnsStart } } return nil } func (x *HTTPTraceEvent) GetDnsDone() *HTTPDNSDoneData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_DnsDone); ok { return x.DnsDone } } return nil } func (x *HTTPTraceEvent) GetConnectStart() *HTTPConnectStartData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_ConnectStart); ok { return x.ConnectStart } } return nil } func (x *HTTPTraceEvent) GetConnectDone() *HTTPConnectDoneData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_ConnectDone); ok { return x.ConnectDone } } return nil } func (x *HTTPTraceEvent) GetTlsHandshakeDone() *HTTPTLSHandshakeDoneData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeDone); ok { return x.TlsHandshakeDone } } return nil } func (x *HTTPTraceEvent) GetWroteRequest() *HTTPWroteRequestData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_WroteRequest); ok { return x.WroteRequest } } return nil } type isHTTPTraceEvent_Data interface { isHTTPTraceEvent_Data() } type HTTPTraceEvent_GetConn struct { GetConn *HTTPGetConnData `protobuf:"bytes,3,opt,name=get_conn,json=getConn,proto3,oneof"` } type HTTPTraceEvent_GotConn struct { GotConn *HTTPGotConnData `protobuf:"bytes,4,opt,name=got_conn,json=gotConn,proto3,oneof"` } type HTTPTraceEvent_Got_1XxResponse struct { Got_1XxResponse *HTTPGot1XxResponseData `protobuf:"bytes,5,opt,name=got_1xx_response,json=got1xxResponse,proto3,oneof"` } type HTTPTraceEvent_DnsStart struct { DnsStart *HTTPDNSStartData `protobuf:"bytes,6,opt,name=dns_start,json=dnsStart,proto3,oneof"` } type HTTPTraceEvent_DnsDone struct { DnsDone *HTTPDNSDoneData `protobuf:"bytes,7,opt,name=dns_done,json=dnsDone,proto3,oneof"` } type HTTPTraceEvent_ConnectStart struct { ConnectStart *HTTPConnectStartData `protobuf:"bytes,8,opt,name=connect_start,json=connectStart,proto3,oneof"` } type HTTPTraceEvent_ConnectDone struct { ConnectDone *HTTPConnectDoneData `protobuf:"bytes,9,opt,name=connect_done,json=connectDone,proto3,oneof"` } type HTTPTraceEvent_TlsHandshakeDone struct { TlsHandshakeDone *HTTPTLSHandshakeDoneData `protobuf:"bytes,10,opt,name=tls_handshake_done,json=tlsHandshakeDone,proto3,oneof"` } type HTTPTraceEvent_WroteRequest struct { WroteRequest *HTTPWroteRequestData `protobuf:"bytes,11,opt,name=wrote_request,json=wroteRequest,proto3,oneof"` } func (*HTTPTraceEvent_GetConn) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_GotConn) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_Got_1XxResponse) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_DnsStart) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_DnsDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_ConnectStart) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_ConnectDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_TlsHandshakeDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_WroteRequest) isHTTPTraceEvent_Data() {} type HTTPGetConnData struct { state protoimpl.MessageState `protogen:"open.v1"` HostPort string `protobuf:"bytes,1,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGetConnData) Reset() { *x = HTTPGetConnData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGetConnData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGetConnData) ProtoMessage() {} func (x *HTTPGetConnData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGetConnData.ProtoReflect.Descriptor instead. func (*HTTPGetConnData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{13} } func (x *HTTPGetConnData) GetHostPort() string { if x != nil { return x.HostPort } return "" } type HTTPGotConnData struct { state protoimpl.MessageState `protogen:"open.v1"` Reused bool `protobuf:"varint,1,opt,name=reused,proto3" json:"reused,omitempty"` WasIdle bool `protobuf:"varint,2,opt,name=was_idle,json=wasIdle,proto3" json:"was_idle,omitempty"` IdleDurationNs int64 `protobuf:"varint,3,opt,name=idle_duration_ns,json=idleDurationNs,proto3" json:"idle_duration_ns,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGotConnData) Reset() { *x = HTTPGotConnData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGotConnData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGotConnData) ProtoMessage() {} func (x *HTTPGotConnData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGotConnData.ProtoReflect.Descriptor instead. func (*HTTPGotConnData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{14} } func (x *HTTPGotConnData) GetReused() bool { if x != nil { return x.Reused } return false } func (x *HTTPGotConnData) GetWasIdle() bool { if x != nil { return x.WasIdle } return false } func (x *HTTPGotConnData) GetIdleDurationNs() int64 { if x != nil { return x.IdleDurationNs } return 0 } type HTTPGot1XxResponseData struct { state protoimpl.MessageState `protogen:"open.v1"` Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGot1XxResponseData) Reset() { *x = HTTPGot1XxResponseData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGot1XxResponseData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGot1XxResponseData) ProtoMessage() {} func (x *HTTPGot1XxResponseData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGot1XxResponseData.ProtoReflect.Descriptor instead. func (*HTTPGot1XxResponseData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{15} } func (x *HTTPGot1XxResponseData) GetCode() int32 { if x != nil { return x.Code } return 0 } type HTTPDNSStartData struct { state protoimpl.MessageState `protogen:"open.v1"` Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPDNSStartData) Reset() { *x = HTTPDNSStartData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPDNSStartData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPDNSStartData) ProtoMessage() {} func (x *HTTPDNSStartData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPDNSStartData.ProtoReflect.Descriptor instead. func (*HTTPDNSStartData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{16} } func (x *HTTPDNSStartData) GetHost() string { if x != nil { return x.Host } return "" } type HTTPDNSDoneData struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3" json:"err,omitempty"` Addrs []*DNSAddr `protobuf:"bytes,2,rep,name=addrs,proto3" json:"addrs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPDNSDoneData) Reset() { *x = HTTPDNSDoneData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPDNSDoneData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPDNSDoneData) ProtoMessage() {} func (x *HTTPDNSDoneData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPDNSDoneData.ProtoReflect.Descriptor instead. func (*HTTPDNSDoneData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{17} } func (x *HTTPDNSDoneData) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *HTTPDNSDoneData) GetAddrs() []*DNSAddr { if x != nil { return x.Addrs } return nil } type DNSAddr struct { state protoimpl.MessageState `protogen:"open.v1"` Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DNSAddr) Reset() { *x = DNSAddr{} mi := &file_encore_engine_trace_trace_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DNSAddr) String() string { return protoimpl.X.MessageStringOf(x) } func (*DNSAddr) ProtoMessage() {} func (x *DNSAddr) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DNSAddr.ProtoReflect.Descriptor instead. func (*DNSAddr) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{18} } func (x *DNSAddr) GetIp() []byte { if x != nil { return x.Ip } return nil } type HTTPConnectStartData struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPConnectStartData) Reset() { *x = HTTPConnectStartData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPConnectStartData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPConnectStartData) ProtoMessage() {} func (x *HTTPConnectStartData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPConnectStartData.ProtoReflect.Descriptor instead. func (*HTTPConnectStartData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{19} } func (x *HTTPConnectStartData) GetNetwork() string { if x != nil { return x.Network } return "" } func (x *HTTPConnectStartData) GetAddr() string { if x != nil { return x.Addr } return "" } type HTTPConnectDoneData struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` Err []byte `protobuf:"bytes,3,opt,name=err,proto3" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPConnectDoneData) Reset() { *x = HTTPConnectDoneData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPConnectDoneData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPConnectDoneData) ProtoMessage() {} func (x *HTTPConnectDoneData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPConnectDoneData.ProtoReflect.Descriptor instead. func (*HTTPConnectDoneData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{20} } func (x *HTTPConnectDoneData) GetNetwork() string { if x != nil { return x.Network } return "" } func (x *HTTPConnectDoneData) GetAddr() string { if x != nil { return x.Addr } return "" } func (x *HTTPConnectDoneData) GetErr() []byte { if x != nil { return x.Err } return nil } type HTTPTLSHandshakeDoneData struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3" json:"err,omitempty"` TlsVersion uint32 `protobuf:"varint,2,opt,name=tls_version,json=tlsVersion,proto3" json:"tls_version,omitempty"` CipherSuite uint32 `protobuf:"varint,3,opt,name=cipher_suite,json=cipherSuite,proto3" json:"cipher_suite,omitempty"` ServerName string `protobuf:"bytes,4,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` NegotiatedProtocol string `protobuf:"bytes,5,opt,name=negotiated_protocol,json=negotiatedProtocol,proto3" json:"negotiated_protocol,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPTLSHandshakeDoneData) Reset() { *x = HTTPTLSHandshakeDoneData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPTLSHandshakeDoneData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPTLSHandshakeDoneData) ProtoMessage() {} func (x *HTTPTLSHandshakeDoneData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPTLSHandshakeDoneData.ProtoReflect.Descriptor instead. func (*HTTPTLSHandshakeDoneData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{21} } func (x *HTTPTLSHandshakeDoneData) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *HTTPTLSHandshakeDoneData) GetTlsVersion() uint32 { if x != nil { return x.TlsVersion } return 0 } func (x *HTTPTLSHandshakeDoneData) GetCipherSuite() uint32 { if x != nil { return x.CipherSuite } return 0 } func (x *HTTPTLSHandshakeDoneData) GetServerName() string { if x != nil { return x.ServerName } return "" } func (x *HTTPTLSHandshakeDoneData) GetNegotiatedProtocol() string { if x != nil { return x.NegotiatedProtocol } return "" } type HTTPWroteRequestData struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPWroteRequestData) Reset() { *x = HTTPWroteRequestData{} mi := &file_encore_engine_trace_trace_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPWroteRequestData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPWroteRequestData) ProtoMessage() {} func (x *HTTPWroteRequestData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPWroteRequestData.ProtoReflect.Descriptor instead. func (*HTTPWroteRequestData) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{22} } func (x *HTTPWroteRequestData) GetErr() []byte { if x != nil { return x.Err } return nil } type LogMessage struct { state protoimpl.MessageState `protogen:"open.v1"` SpanId uint64 `protobuf:"varint,1,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` Goid uint32 `protobuf:"varint,2,opt,name=goid,proto3" json:"goid,omitempty"` Time uint64 `protobuf:"varint,3,opt,name=time,proto3" json:"time,omitempty"` Level LogMessage_Level `protobuf:"varint,4,opt,name=level,proto3,enum=encore.engine.trace.LogMessage_Level" json:"level,omitempty"` Msg string `protobuf:"bytes,5,opt,name=msg,proto3" json:"msg,omitempty"` Fields []*LogField `protobuf:"bytes,6,rep,name=fields,proto3" json:"fields,omitempty"` Stack *StackTrace `protobuf:"bytes,7,opt,name=stack,proto3" json:"stack,omitempty"` // null if unavailable unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LogMessage) Reset() { *x = LogMessage{} mi := &file_encore_engine_trace_trace_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *LogMessage) String() string { return protoimpl.X.MessageStringOf(x) } func (*LogMessage) ProtoMessage() {} func (x *LogMessage) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LogMessage.ProtoReflect.Descriptor instead. func (*LogMessage) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{23} } func (x *LogMessage) GetSpanId() uint64 { if x != nil { return x.SpanId } return 0 } func (x *LogMessage) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *LogMessage) GetTime() uint64 { if x != nil { return x.Time } return 0 } func (x *LogMessage) GetLevel() LogMessage_Level { if x != nil { return x.Level } return LogMessage_DEBUG } func (x *LogMessage) GetMsg() string { if x != nil { return x.Msg } return "" } func (x *LogMessage) GetFields() []*LogField { if x != nil { return x.Fields } return nil } func (x *LogMessage) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type LogField struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Types that are valid to be assigned to Value: // // *LogField_ErrorWithoutStack // *LogField_ErrorWithStack // *LogField_Str // *LogField_Bool // *LogField_Time // *LogField_Dur // *LogField_Uuid // *LogField_Json // *LogField_Int // *LogField_Uint // *LogField_Float32 // *LogField_Float64 Value isLogField_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LogField) Reset() { *x = LogField{} mi := &file_encore_engine_trace_trace_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *LogField) String() string { return protoimpl.X.MessageStringOf(x) } func (*LogField) ProtoMessage() {} func (x *LogField) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LogField.ProtoReflect.Descriptor instead. func (*LogField) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{24} } func (x *LogField) GetKey() string { if x != nil { return x.Key } return "" } func (x *LogField) GetValue() isLogField_Value { if x != nil { return x.Value } return nil } func (x *LogField) GetErrorWithoutStack() string { if x != nil { if x, ok := x.Value.(*LogField_ErrorWithoutStack); ok { return x.ErrorWithoutStack } } return "" } func (x *LogField) GetErrorWithStack() *ErrWithStack { if x != nil { if x, ok := x.Value.(*LogField_ErrorWithStack); ok { return x.ErrorWithStack } } return nil } func (x *LogField) GetStr() string { if x != nil { if x, ok := x.Value.(*LogField_Str); ok { return x.Str } } return "" } func (x *LogField) GetBool() bool { if x != nil { if x, ok := x.Value.(*LogField_Bool); ok { return x.Bool } } return false } func (x *LogField) GetTime() *timestamppb.Timestamp { if x != nil { if x, ok := x.Value.(*LogField_Time); ok { return x.Time } } return nil } func (x *LogField) GetDur() int64 { if x != nil { if x, ok := x.Value.(*LogField_Dur); ok { return x.Dur } } return 0 } func (x *LogField) GetUuid() []byte { if x != nil { if x, ok := x.Value.(*LogField_Uuid); ok { return x.Uuid } } return nil } func (x *LogField) GetJson() []byte { if x != nil { if x, ok := x.Value.(*LogField_Json); ok { return x.Json } } return nil } func (x *LogField) GetInt() int64 { if x != nil { if x, ok := x.Value.(*LogField_Int); ok { return x.Int } } return 0 } func (x *LogField) GetUint() uint64 { if x != nil { if x, ok := x.Value.(*LogField_Uint); ok { return x.Uint } } return 0 } func (x *LogField) GetFloat32() float32 { if x != nil { if x, ok := x.Value.(*LogField_Float32); ok { return x.Float32 } } return 0 } func (x *LogField) GetFloat64() float64 { if x != nil { if x, ok := x.Value.(*LogField_Float64); ok { return x.Float64 } } return 0 } type isLogField_Value interface { isLogField_Value() } type LogField_ErrorWithoutStack struct { ErrorWithoutStack string `protobuf:"bytes,2,opt,name=error_without_stack,json=errorWithoutStack,proto3,oneof"` // deprecated: use error_with_stack } type LogField_ErrorWithStack struct { ErrorWithStack *ErrWithStack `protobuf:"bytes,13,opt,name=error_with_stack,json=errorWithStack,proto3,oneof"` } type LogField_Str struct { Str string `protobuf:"bytes,3,opt,name=str,proto3,oneof"` } type LogField_Bool struct { Bool bool `protobuf:"varint,4,opt,name=bool,proto3,oneof"` } type LogField_Time struct { Time *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=time,proto3,oneof"` } type LogField_Dur struct { Dur int64 `protobuf:"varint,6,opt,name=dur,proto3,oneof"` } type LogField_Uuid struct { Uuid []byte `protobuf:"bytes,7,opt,name=uuid,proto3,oneof"` } type LogField_Json struct { Json []byte `protobuf:"bytes,8,opt,name=json,proto3,oneof"` } type LogField_Int struct { Int int64 `protobuf:"varint,9,opt,name=int,proto3,oneof"` } type LogField_Uint struct { Uint uint64 `protobuf:"varint,10,opt,name=uint,proto3,oneof"` } type LogField_Float32 struct { Float32 float32 `protobuf:"fixed32,11,opt,name=float32,proto3,oneof"` } type LogField_Float64 struct { Float64 float64 `protobuf:"fixed64,12,opt,name=float64,proto3,oneof"` } func (*LogField_ErrorWithoutStack) isLogField_Value() {} func (*LogField_ErrorWithStack) isLogField_Value() {} func (*LogField_Str) isLogField_Value() {} func (*LogField_Bool) isLogField_Value() {} func (*LogField_Time) isLogField_Value() {} func (*LogField_Dur) isLogField_Value() {} func (*LogField_Uuid) isLogField_Value() {} func (*LogField_Json) isLogField_Value() {} func (*LogField_Int) isLogField_Value() {} func (*LogField_Uint) isLogField_Value() {} func (*LogField_Float32) isLogField_Value() {} func (*LogField_Float64) isLogField_Value() {} type ErrWithStack struct { state protoimpl.MessageState `protogen:"open.v1"` Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` Stack *StackTrace `protobuf:"bytes,2,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ErrWithStack) Reset() { *x = ErrWithStack{} mi := &file_encore_engine_trace_trace_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ErrWithStack) String() string { return protoimpl.X.MessageStringOf(x) } func (*ErrWithStack) ProtoMessage() {} func (x *ErrWithStack) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ErrWithStack.ProtoReflect.Descriptor instead. func (*ErrWithStack) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{25} } func (x *ErrWithStack) GetError() string { if x != nil { return x.Error } return "" } func (x *ErrWithStack) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type StackTrace struct { state protoimpl.MessageState `protogen:"open.v1"` Pcs []int64 `protobuf:"varint,1,rep,packed,name=pcs,proto3" json:"pcs,omitempty"` Frames []*StackFrame `protobuf:"bytes,2,rep,name=frames,proto3" json:"frames,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StackTrace) Reset() { *x = StackTrace{} mi := &file_encore_engine_trace_trace_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StackTrace) String() string { return protoimpl.X.MessageStringOf(x) } func (*StackTrace) ProtoMessage() {} func (x *StackTrace) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StackTrace.ProtoReflect.Descriptor instead. func (*StackTrace) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{26} } func (x *StackTrace) GetPcs() []int64 { if x != nil { return x.Pcs } return nil } func (x *StackTrace) GetFrames() []*StackFrame { if x != nil { return x.Frames } return nil } type StackFrame struct { state protoimpl.MessageState `protogen:"open.v1"` Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` Func string `protobuf:"bytes,2,opt,name=func,proto3" json:"func,omitempty"` Line int32 `protobuf:"varint,3,opt,name=line,proto3" json:"line,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StackFrame) Reset() { *x = StackFrame{} mi := &file_encore_engine_trace_trace_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StackFrame) String() string { return protoimpl.X.MessageStringOf(x) } func (*StackFrame) ProtoMessage() {} func (x *StackFrame) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace_trace_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StackFrame.ProtoReflect.Descriptor instead. func (*StackFrame) Descriptor() ([]byte, []int) { return file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{27} } func (x *StackFrame) GetFilename() string { if x != nil { return x.Filename } return "" } func (x *StackFrame) GetFunc() string { if x != nil { return x.Func } return "" } func (x *StackFrame) GetLine() int32 { if x != nil { return x.Line } return 0 } var File_encore_engine_trace_trace_proto protoreflect.FileDescriptor const file_encore_engine_trace_trace_proto_rawDesc = "" + "\n" + "\x1fencore/engine/trace/trace.proto\x12\x13encore.engine.trace\x1a\x1fgoogle/protobuf/timestamp.proto\"/\n" + "\aTraceID\x12\x12\n" + "\x04high\x18\x01 \x01(\x04R\x04high\x12\x10\n" + "\x03low\x18\x02 \x01(\x04R\x03low\"\xa2\f\n" + "\aRequest\x127\n" + "\btrace_id\x18\x01 \x01(\v2\x1c.encore.engine.trace.TraceIDR\atraceId\x12\x17\n" + "\aspan_id\x18\x02 \x01(\x04R\x06spanId\x12$\n" + "\x0eparent_span_id\x18\x03 \x01(\x04R\fparentSpanId\x12D\n" + "\x0fparent_trace_id\x18 \x01(\v2\x1c.encore.engine.trace.TraceIDR\rparentTraceId\x12\x12\n" + "\x04goid\x18\x04 \x01(\rR\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x05 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x06 \x01(\x04R\aendTime\x12\x17\n" + "\adef_loc\x18\b \x01(\x05R\x06defLoc\x12\x10\n" + "\x03err\x18\v \x01(\fR\x03err\x122\n" + "\x06events\x18\f \x03(\v2\x1a.encore.engine.trace.EventR\x06events\x125\n" + "\x04type\x18\x0e \x01(\x0e2!.encore.engine.trace.Request.TypeR\x04type\x12<\n" + "\terr_stack\x18\x0f \x01(\v2\x1f.encore.engine.trace.StackTraceR\berrStack\x12@\n" + "\vpanic_stack\x18\" \x01(\v2\x1f.encore.engine.trace.StackTraceR\n" + "panicStack\x12$\n" + "\x0eabs_start_time\x18\x10 \x01(\x04R\fabsStartTime\x12!\n" + "\fservice_name\x18\x11 \x01(\tR\vserviceName\x12#\n" + "\rendpoint_name\x18\x12 \x01(\tR\fendpointName\x12\x1d\n" + "\n" + "topic_name\x18\x13 \x01(\tR\ttopicName\x12+\n" + "\x11subscription_name\x18\x14 \x01(\tR\x10subscriptionName\x12\x1d\n" + "\n" + "message_id\x18\x15 \x01(\tR\tmessageId\x12\x18\n" + "\aattempt\x18\x16 \x01(\rR\aattempt\x12!\n" + "\fpublish_time\x18\x17 \x01(\x04R\vpublishTime\x12\x16\n" + "\x06inputs\x18\t \x03(\fR\x06inputs\x12\x18\n" + "\aoutputs\x18\n" + " \x03(\fR\aoutputs\x12\x10\n" + "\x03uid\x18\r \x01(\tR\x03uid\x12\x1f\n" + "\vhttp_method\x18\x18 \x01(\tR\n" + "httpMethod\x12\x12\n" + "\x04path\x18\x19 \x01(\tR\x04path\x12\x1f\n" + "\vpath_params\x18\x1a \x03(\tR\n" + "pathParams\x12'\n" + "\x0frequest_payload\x18\x1b \x01(\fR\x0erequestPayload\x12)\n" + "\x10response_payload\x18\x1c \x01(\fR\x0fresponsePayload\x12c\n" + "\x13raw_request_headers\x18\x1d \x03(\v23.encore.engine.trace.Request.RawRequestHeadersEntryR\x11rawRequestHeaders\x12f\n" + "\x14raw_response_headers\x18\x1e \x03(\v24.encore.engine.trace.Request.RawResponseHeadersEntryR\x12rawResponseHeaders\x12.\n" + "\x13external_request_id\x18\x1f \x01(\tR\x11externalRequestId\x126\n" + "\x17external_correlation_id\x18! \x01(\tR\x15externalCorrelationId\x1aD\n" + "\x16RawRequestHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aE\n" + "\x17RawResponseHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\")\n" + "\x04Type\x12\a\n" + "\x03RPC\x10\x00\x12\b\n" + "\x04AUTH\x10\x01\x12\x0e\n" + "\n" + "PUBSUB_MSG\x10\x02J\x04\b\a\x10\b\"\xe7\x04\n" + "\x05Event\x120\n" + "\x03rpc\x18\x01 \x01(\v2\x1c.encore.engine.trace.RPCCallH\x00R\x03rpc\x124\n" + "\x02tx\x18\x02 \x01(\v2\".encore.engine.trace.DBTransactionH\x00R\x02tx\x124\n" + "\x05query\x18\x03 \x01(\v2\x1c.encore.engine.trace.DBQueryH\x00R\x05query\x12>\n" + "\tgoroutine\x18\x04 \x01(\v2\x1e.encore.engine.trace.GoroutineH\x00R\tgoroutine\x123\n" + "\x04http\x18\x05 \x01(\v2\x1d.encore.engine.trace.HTTPCallH\x00R\x04http\x123\n" + "\x03log\x18\x06 \x01(\v2\x1f.encore.engine.trace.LogMessageH\x00R\x03log\x12M\n" + "\fpublishedMsg\x18\a \x01(\v2'.encore.engine.trace.PubsubMsgPublishedH\x00R\fpublishedMsg\x12E\n" + "\fservice_init\x18\b \x01(\v2 .encore.engine.trace.ServiceInitH\x00R\vserviceInit\x124\n" + "\x05cache\x18\t \x01(\v2\x1c.encore.engine.trace.CacheOpH\x00R\x05cache\x12B\n" + "\vbody_stream\x18\n" + " \x01(\v2\x1f.encore.engine.trace.BodyStreamH\x00R\n" + "bodyStreamB\x06\n" + "\x04data\"\xd8\x01\n" + "\aRPCCall\x12\x17\n" + "\aspan_id\x18\x01 \x01(\x04R\x06spanId\x12\x12\n" + "\x04goid\x18\x02 \x01(\rR\x04goid\x12\x17\n" + "\adef_loc\x18\x04 \x01(\x05R\x06defLoc\x12\x1d\n" + "\n" + "start_time\x18\x05 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x06 \x01(\x04R\aendTime\x12\x10\n" + "\x03err\x18\a \x01(\fR\x03err\x125\n" + "\x05stack\x18\b \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stackJ\x04\b\x03\x10\x04\"_\n" + "\tGoroutine\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTimeJ\x04\b\x02\x10\x03\"\xb2\x03\n" + "\rDBTransaction\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x04 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x05 \x01(\x04R\aendTime\x12\x10\n" + "\x03err\x18\x06 \x01(\fR\x03err\x12Q\n" + "\n" + "completion\x18\a \x01(\x0e21.encore.engine.trace.DBTransaction.CompletionTypeR\n" + "completion\x126\n" + "\aqueries\x18\b \x03(\v2\x1c.encore.engine.trace.DBQueryR\aqueries\x12@\n" + "\vbegin_stack\x18\t \x01(\v2\x1f.encore.engine.trace.StackTraceR\n" + "beginStack\x12<\n" + "\tend_stack\x18\n" + " \x01(\v2\x1f.encore.engine.trace.StackTraceR\bendStack\"*\n" + "\x0eCompletionType\x12\f\n" + "\bROLLBACK\x10\x00\x12\n" + "\n" + "\x06COMMIT\x10\x01J\x04\b\x02\x10\x03J\x04\b\x03\x10\x04\"\xbc\x01\n" + "\aDBQuery\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTime\x12\x14\n" + "\x05query\x18\x05 \x01(\fR\x05query\x12\x10\n" + "\x03err\x18\x06 \x01(\fR\x03err\x125\n" + "\x05stack\x18\a \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stackJ\x04\b\x02\x10\x03\"\xfa\x01\n" + "\x12PubsubMsgPublished\x12\x12\n" + "\x04goid\x18\x01 \x01(\x04R\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTime\x12\x14\n" + "\x05topic\x18\x05 \x01(\tR\x05topic\x12\x18\n" + "\amessage\x18\x06 \x01(\fR\amessage\x12\x1d\n" + "\n" + "message_id\x18\a \x01(\tR\tmessageId\x12\x10\n" + "\x03err\x18\b \x01(\fR\x03err\x125\n" + "\x05stack\x18\t \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stack\"\xde\x01\n" + "\vServiceInit\x12\x12\n" + "\x04goid\x18\x01 \x01(\x04R\x04goid\x12\x17\n" + "\adef_loc\x18\x02 \x01(\x05R\x06defLoc\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTime\x12\x18\n" + "\aservice\x18\x05 \x01(\tR\aservice\x12\x10\n" + "\x03err\x18\x06 \x01(\fR\x03err\x12<\n" + "\terr_stack\x18\a \x01(\v2\x1f.encore.engine.trace.StackTraceR\berrStack\"\xb7\x03\n" + "\aCacheOp\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12\x17\n" + "\adef_loc\x18\x02 \x01(\x05R\x06defLoc\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTime\x12\x1c\n" + "\toperation\x18\x05 \x01(\tR\toperation\x12\x12\n" + "\x04keys\x18\x06 \x03(\tR\x04keys\x12\x16\n" + "\x06inputs\x18\a \x03(\fR\x06inputs\x12\x18\n" + "\aoutputs\x18\b \x03(\fR\aoutputs\x125\n" + "\x05stack\x18\t \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stack\x12\x10\n" + "\x03err\x18\n" + " \x01(\fR\x03err\x12\x14\n" + "\x05write\x18\v \x01(\bR\x05write\x12;\n" + "\x06result\x18\f \x01(\x0e2#.encore.engine.trace.CacheOp.ResultR\x06result\"E\n" + "\x06Result\x12\v\n" + "\aUNKNOWN\x10\x00\x12\x06\n" + "\x02OK\x10\x01\x12\x0f\n" + "\vNO_SUCH_KEY\x10\x02\x12\f\n" + "\bCONFLICT\x10\x03\x12\a\n" + "\x03ERR\x10\x04\"a\n" + "\n" + "BodyStream\x12\x1f\n" + "\vis_response\x18\x01 \x01(\bR\n" + "isResponse\x12\x1e\n" + "\n" + "overflowed\x18\x02 \x01(\bR\n" + "overflowed\x12\x12\n" + "\x04data\x18\x03 \x01(\fR\x04data\"\xb5\x02\n" + "\bHTTPCall\x12\x17\n" + "\aspan_id\x18\x01 \x01(\x04R\x06spanId\x12\x12\n" + "\x04goid\x18\x02 \x01(\rR\x04goid\x12\x1d\n" + "\n" + "start_time\x18\x03 \x01(\x04R\tstartTime\x12\x19\n" + "\bend_time\x18\x04 \x01(\x04R\aendTime\x12\x16\n" + "\x06method\x18\x05 \x01(\tR\x06method\x12\x10\n" + "\x03url\x18\x06 \x01(\tR\x03url\x12\x1f\n" + "\vstatus_code\x18\a \x01(\rR\n" + "statusCode\x12\x10\n" + "\x03err\x18\b \x01(\fR\x03err\x12(\n" + "\x10body_closed_time\x18\t \x01(\x04R\x0ebodyClosedTime\x12;\n" + "\x06events\x18\n" + " \x03(\v2#.encore.engine.trace.HTTPTraceEventR\x06events\"\xa3\x06\n" + "\x0eHTTPTraceEvent\x12;\n" + "\x04code\x18\x01 \x01(\x0e2'.encore.engine.trace.HTTPTraceEventCodeR\x04code\x12\x12\n" + "\x04time\x18\x02 \x01(\x04R\x04time\x12A\n" + "\bget_conn\x18\x03 \x01(\v2$.encore.engine.trace.HTTPGetConnDataH\x00R\agetConn\x12A\n" + "\bgot_conn\x18\x04 \x01(\v2$.encore.engine.trace.HTTPGotConnDataH\x00R\agotConn\x12W\n" + "\x10got_1xx_response\x18\x05 \x01(\v2+.encore.engine.trace.HTTPGot1xxResponseDataH\x00R\x0egot1xxResponse\x12D\n" + "\tdns_start\x18\x06 \x01(\v2%.encore.engine.trace.HTTPDNSStartDataH\x00R\bdnsStart\x12A\n" + "\bdns_done\x18\a \x01(\v2$.encore.engine.trace.HTTPDNSDoneDataH\x00R\adnsDone\x12P\n" + "\rconnect_start\x18\b \x01(\v2).encore.engine.trace.HTTPConnectStartDataH\x00R\fconnectStart\x12M\n" + "\fconnect_done\x18\t \x01(\v2(.encore.engine.trace.HTTPConnectDoneDataH\x00R\vconnectDone\x12]\n" + "\x12tls_handshake_done\x18\n" + " \x01(\v2-.encore.engine.trace.HTTPTLSHandshakeDoneDataH\x00R\x10tlsHandshakeDone\x12P\n" + "\rwrote_request\x18\v \x01(\v2).encore.engine.trace.HTTPWroteRequestDataH\x00R\fwroteRequestB\x06\n" + "\x04data\".\n" + "\x0fHTTPGetConnData\x12\x1b\n" + "\thost_port\x18\x01 \x01(\tR\bhostPort\"n\n" + "\x0fHTTPGotConnData\x12\x16\n" + "\x06reused\x18\x01 \x01(\bR\x06reused\x12\x19\n" + "\bwas_idle\x18\x02 \x01(\bR\awasIdle\x12(\n" + "\x10idle_duration_ns\x18\x03 \x01(\x03R\x0eidleDurationNs\",\n" + "\x16HTTPGot1xxResponseData\x12\x12\n" + "\x04code\x18\x01 \x01(\x05R\x04code\"&\n" + "\x10HTTPDNSStartData\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\"W\n" + "\x0fHTTPDNSDoneData\x12\x10\n" + "\x03err\x18\x01 \x01(\fR\x03err\x122\n" + "\x05addrs\x18\x02 \x03(\v2\x1c.encore.engine.trace.DNSAddrR\x05addrs\"\x19\n" + "\aDNSAddr\x12\x0e\n" + "\x02ip\x18\x01 \x01(\fR\x02ip\"D\n" + "\x14HTTPConnectStartData\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\"U\n" + "\x13HTTPConnectDoneData\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\x12\x10\n" + "\x03err\x18\x03 \x01(\fR\x03err\"\xc2\x01\n" + "\x18HTTPTLSHandshakeDoneData\x12\x10\n" + "\x03err\x18\x01 \x01(\fR\x03err\x12\x1f\n" + "\vtls_version\x18\x02 \x01(\rR\n" + "tlsVersion\x12!\n" + "\fcipher_suite\x18\x03 \x01(\rR\vcipherSuite\x12\x1f\n" + "\vserver_name\x18\x04 \x01(\tR\n" + "serverName\x12/\n" + "\x13negotiated_protocol\x18\x05 \x01(\tR\x12negotiatedProtocol\"(\n" + "\x14HTTPWroteRequestData\x12\x10\n" + "\x03err\x18\x01 \x01(\fR\x03err\"\xc8\x02\n" + "\n" + "LogMessage\x12\x17\n" + "\aspan_id\x18\x01 \x01(\x04R\x06spanId\x12\x12\n" + "\x04goid\x18\x02 \x01(\rR\x04goid\x12\x12\n" + "\x04time\x18\x03 \x01(\x04R\x04time\x12;\n" + "\x05level\x18\x04 \x01(\x0e2%.encore.engine.trace.LogMessage.LevelR\x05level\x12\x10\n" + "\x03msg\x18\x05 \x01(\tR\x03msg\x125\n" + "\x06fields\x18\x06 \x03(\v2\x1d.encore.engine.trace.LogFieldR\x06fields\x125\n" + "\x05stack\x18\a \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stack\"<\n" + "\x05Level\x12\t\n" + "\x05DEBUG\x10\x00\x12\b\n" + "\x04INFO\x10\x01\x12\t\n" + "\x05ERROR\x10\x02\x12\b\n" + "\x04WARN\x10\x03\x12\t\n" + "\x05TRACE\x10\x04\"\xa4\x03\n" + "\bLogField\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + "\x13error_without_stack\x18\x02 \x01(\tH\x00R\x11errorWithoutStack\x12M\n" + "\x10error_with_stack\x18\r \x01(\v2!.encore.engine.trace.ErrWithStackH\x00R\x0eerrorWithStack\x12\x12\n" + "\x03str\x18\x03 \x01(\tH\x00R\x03str\x12\x14\n" + "\x04bool\x18\x04 \x01(\bH\x00R\x04bool\x120\n" + "\x04time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\x04time\x12\x12\n" + "\x03dur\x18\x06 \x01(\x03H\x00R\x03dur\x12\x14\n" + "\x04uuid\x18\a \x01(\fH\x00R\x04uuid\x12\x14\n" + "\x04json\x18\b \x01(\fH\x00R\x04json\x12\x12\n" + "\x03int\x18\t \x01(\x03H\x00R\x03int\x12\x14\n" + "\x04uint\x18\n" + " \x01(\x04H\x00R\x04uint\x12\x1a\n" + "\afloat32\x18\v \x01(\x02H\x00R\afloat32\x12\x1a\n" + "\afloat64\x18\f \x01(\x01H\x00R\afloat64B\a\n" + "\x05value\"[\n" + "\fErrWithStack\x12\x14\n" + "\x05error\x18\x01 \x01(\tR\x05error\x125\n" + "\x05stack\x18\x02 \x01(\v2\x1f.encore.engine.trace.StackTraceR\x05stack\"W\n" + "\n" + "StackTrace\x12\x10\n" + "\x03pcs\x18\x01 \x03(\x03R\x03pcs\x127\n" + "\x06frames\x18\x02 \x03(\v2\x1f.encore.engine.trace.StackFrameR\x06frames\"P\n" + "\n" + "StackFrame\x12\x1a\n" + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x12\n" + "\x04func\x18\x02 \x01(\tR\x04func\x12\x12\n" + "\x04line\x18\x03 \x01(\x05R\x04line*\xa0\x02\n" + "\x12HTTPTraceEventCode\x12\v\n" + "\aUNKNOWN\x10\x00\x12\f\n" + "\bGET_CONN\x10\x01\x12\f\n" + "\bGOT_CONN\x10\x02\x12\x1b\n" + "\x17GOT_FIRST_RESPONSE_BYTE\x10\x03\x12\x14\n" + "\x10GOT_1XX_RESPONSE\x10\x04\x12\r\n" + "\tDNS_START\x10\x05\x12\f\n" + "\bDNS_DONE\x10\x06\x12\x11\n" + "\rCONNECT_START\x10\a\x12\x10\n" + "\fCONNECT_DONE\x10\b\x12\x17\n" + "\x13TLS_HANDSHAKE_START\x10\t\x12\x16\n" + "\x12TLS_HANDSHAKE_DONE\x10\n" + "\x12\x11\n" + "\rWROTE_HEADERS\x10\v\x12\x11\n" + "\rWROTE_REQUEST\x10\f\x12\x15\n" + "\x11WAIT_100_CONTINUE\x10\rB$Z\"encr.dev/proto/encore/engine/traceb\x06proto3" var ( file_encore_engine_trace_trace_proto_rawDescOnce sync.Once file_encore_engine_trace_trace_proto_rawDescData []byte ) func file_encore_engine_trace_trace_proto_rawDescGZIP() []byte { file_encore_engine_trace_trace_proto_rawDescOnce.Do(func() { file_encore_engine_trace_trace_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_engine_trace_trace_proto_rawDesc), len(file_encore_engine_trace_trace_proto_rawDesc))) }) return file_encore_engine_trace_trace_proto_rawDescData } var file_encore_engine_trace_trace_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_encore_engine_trace_trace_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_encore_engine_trace_trace_proto_goTypes = []any{ (HTTPTraceEventCode)(0), // 0: encore.engine.trace.HTTPTraceEventCode (Request_Type)(0), // 1: encore.engine.trace.Request.Type (DBTransaction_CompletionType)(0), // 2: encore.engine.trace.DBTransaction.CompletionType (CacheOp_Result)(0), // 3: encore.engine.trace.CacheOp.Result (LogMessage_Level)(0), // 4: encore.engine.trace.LogMessage.Level (*TraceID)(nil), // 5: encore.engine.trace.TraceID (*Request)(nil), // 6: encore.engine.trace.Request (*Event)(nil), // 7: encore.engine.trace.Event (*RPCCall)(nil), // 8: encore.engine.trace.RPCCall (*Goroutine)(nil), // 9: encore.engine.trace.Goroutine (*DBTransaction)(nil), // 10: encore.engine.trace.DBTransaction (*DBQuery)(nil), // 11: encore.engine.trace.DBQuery (*PubsubMsgPublished)(nil), // 12: encore.engine.trace.PubsubMsgPublished (*ServiceInit)(nil), // 13: encore.engine.trace.ServiceInit (*CacheOp)(nil), // 14: encore.engine.trace.CacheOp (*BodyStream)(nil), // 15: encore.engine.trace.BodyStream (*HTTPCall)(nil), // 16: encore.engine.trace.HTTPCall (*HTTPTraceEvent)(nil), // 17: encore.engine.trace.HTTPTraceEvent (*HTTPGetConnData)(nil), // 18: encore.engine.trace.HTTPGetConnData (*HTTPGotConnData)(nil), // 19: encore.engine.trace.HTTPGotConnData (*HTTPGot1XxResponseData)(nil), // 20: encore.engine.trace.HTTPGot1xxResponseData (*HTTPDNSStartData)(nil), // 21: encore.engine.trace.HTTPDNSStartData (*HTTPDNSDoneData)(nil), // 22: encore.engine.trace.HTTPDNSDoneData (*DNSAddr)(nil), // 23: encore.engine.trace.DNSAddr (*HTTPConnectStartData)(nil), // 24: encore.engine.trace.HTTPConnectStartData (*HTTPConnectDoneData)(nil), // 25: encore.engine.trace.HTTPConnectDoneData (*HTTPTLSHandshakeDoneData)(nil), // 26: encore.engine.trace.HTTPTLSHandshakeDoneData (*HTTPWroteRequestData)(nil), // 27: encore.engine.trace.HTTPWroteRequestData (*LogMessage)(nil), // 28: encore.engine.trace.LogMessage (*LogField)(nil), // 29: encore.engine.trace.LogField (*ErrWithStack)(nil), // 30: encore.engine.trace.ErrWithStack (*StackTrace)(nil), // 31: encore.engine.trace.StackTrace (*StackFrame)(nil), // 32: encore.engine.trace.StackFrame nil, // 33: encore.engine.trace.Request.RawRequestHeadersEntry nil, // 34: encore.engine.trace.Request.RawResponseHeadersEntry (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp } var file_encore_engine_trace_trace_proto_depIdxs = []int32{ 5, // 0: encore.engine.trace.Request.trace_id:type_name -> encore.engine.trace.TraceID 5, // 1: encore.engine.trace.Request.parent_trace_id:type_name -> encore.engine.trace.TraceID 7, // 2: encore.engine.trace.Request.events:type_name -> encore.engine.trace.Event 1, // 3: encore.engine.trace.Request.type:type_name -> encore.engine.trace.Request.Type 31, // 4: encore.engine.trace.Request.err_stack:type_name -> encore.engine.trace.StackTrace 31, // 5: encore.engine.trace.Request.panic_stack:type_name -> encore.engine.trace.StackTrace 33, // 6: encore.engine.trace.Request.raw_request_headers:type_name -> encore.engine.trace.Request.RawRequestHeadersEntry 34, // 7: encore.engine.trace.Request.raw_response_headers:type_name -> encore.engine.trace.Request.RawResponseHeadersEntry 8, // 8: encore.engine.trace.Event.rpc:type_name -> encore.engine.trace.RPCCall 10, // 9: encore.engine.trace.Event.tx:type_name -> encore.engine.trace.DBTransaction 11, // 10: encore.engine.trace.Event.query:type_name -> encore.engine.trace.DBQuery 9, // 11: encore.engine.trace.Event.goroutine:type_name -> encore.engine.trace.Goroutine 16, // 12: encore.engine.trace.Event.http:type_name -> encore.engine.trace.HTTPCall 28, // 13: encore.engine.trace.Event.log:type_name -> encore.engine.trace.LogMessage 12, // 14: encore.engine.trace.Event.publishedMsg:type_name -> encore.engine.trace.PubsubMsgPublished 13, // 15: encore.engine.trace.Event.service_init:type_name -> encore.engine.trace.ServiceInit 14, // 16: encore.engine.trace.Event.cache:type_name -> encore.engine.trace.CacheOp 15, // 17: encore.engine.trace.Event.body_stream:type_name -> encore.engine.trace.BodyStream 31, // 18: encore.engine.trace.RPCCall.stack:type_name -> encore.engine.trace.StackTrace 2, // 19: encore.engine.trace.DBTransaction.completion:type_name -> encore.engine.trace.DBTransaction.CompletionType 11, // 20: encore.engine.trace.DBTransaction.queries:type_name -> encore.engine.trace.DBQuery 31, // 21: encore.engine.trace.DBTransaction.begin_stack:type_name -> encore.engine.trace.StackTrace 31, // 22: encore.engine.trace.DBTransaction.end_stack:type_name -> encore.engine.trace.StackTrace 31, // 23: encore.engine.trace.DBQuery.stack:type_name -> encore.engine.trace.StackTrace 31, // 24: encore.engine.trace.PubsubMsgPublished.stack:type_name -> encore.engine.trace.StackTrace 31, // 25: encore.engine.trace.ServiceInit.err_stack:type_name -> encore.engine.trace.StackTrace 31, // 26: encore.engine.trace.CacheOp.stack:type_name -> encore.engine.trace.StackTrace 3, // 27: encore.engine.trace.CacheOp.result:type_name -> encore.engine.trace.CacheOp.Result 17, // 28: encore.engine.trace.HTTPCall.events:type_name -> encore.engine.trace.HTTPTraceEvent 0, // 29: encore.engine.trace.HTTPTraceEvent.code:type_name -> encore.engine.trace.HTTPTraceEventCode 18, // 30: encore.engine.trace.HTTPTraceEvent.get_conn:type_name -> encore.engine.trace.HTTPGetConnData 19, // 31: encore.engine.trace.HTTPTraceEvent.got_conn:type_name -> encore.engine.trace.HTTPGotConnData 20, // 32: encore.engine.trace.HTTPTraceEvent.got_1xx_response:type_name -> encore.engine.trace.HTTPGot1xxResponseData 21, // 33: encore.engine.trace.HTTPTraceEvent.dns_start:type_name -> encore.engine.trace.HTTPDNSStartData 22, // 34: encore.engine.trace.HTTPTraceEvent.dns_done:type_name -> encore.engine.trace.HTTPDNSDoneData 24, // 35: encore.engine.trace.HTTPTraceEvent.connect_start:type_name -> encore.engine.trace.HTTPConnectStartData 25, // 36: encore.engine.trace.HTTPTraceEvent.connect_done:type_name -> encore.engine.trace.HTTPConnectDoneData 26, // 37: encore.engine.trace.HTTPTraceEvent.tls_handshake_done:type_name -> encore.engine.trace.HTTPTLSHandshakeDoneData 27, // 38: encore.engine.trace.HTTPTraceEvent.wrote_request:type_name -> encore.engine.trace.HTTPWroteRequestData 23, // 39: encore.engine.trace.HTTPDNSDoneData.addrs:type_name -> encore.engine.trace.DNSAddr 4, // 40: encore.engine.trace.LogMessage.level:type_name -> encore.engine.trace.LogMessage.Level 29, // 41: encore.engine.trace.LogMessage.fields:type_name -> encore.engine.trace.LogField 31, // 42: encore.engine.trace.LogMessage.stack:type_name -> encore.engine.trace.StackTrace 30, // 43: encore.engine.trace.LogField.error_with_stack:type_name -> encore.engine.trace.ErrWithStack 35, // 44: encore.engine.trace.LogField.time:type_name -> google.protobuf.Timestamp 31, // 45: encore.engine.trace.ErrWithStack.stack:type_name -> encore.engine.trace.StackTrace 32, // 46: encore.engine.trace.StackTrace.frames:type_name -> encore.engine.trace.StackFrame 47, // [47:47] is the sub-list for method output_type 47, // [47:47] is the sub-list for method input_type 47, // [47:47] is the sub-list for extension type_name 47, // [47:47] is the sub-list for extension extendee 0, // [0:47] is the sub-list for field type_name } func init() { file_encore_engine_trace_trace_proto_init() } func file_encore_engine_trace_trace_proto_init() { if File_encore_engine_trace_trace_proto != nil { return } file_encore_engine_trace_trace_proto_msgTypes[2].OneofWrappers = []any{ (*Event_Rpc)(nil), (*Event_Tx)(nil), (*Event_Query)(nil), (*Event_Goroutine)(nil), (*Event_Http)(nil), (*Event_Log)(nil), (*Event_PublishedMsg)(nil), (*Event_ServiceInit)(nil), (*Event_Cache)(nil), (*Event_BodyStream)(nil), } file_encore_engine_trace_trace_proto_msgTypes[12].OneofWrappers = []any{ (*HTTPTraceEvent_GetConn)(nil), (*HTTPTraceEvent_GotConn)(nil), (*HTTPTraceEvent_Got_1XxResponse)(nil), (*HTTPTraceEvent_DnsStart)(nil), (*HTTPTraceEvent_DnsDone)(nil), (*HTTPTraceEvent_ConnectStart)(nil), (*HTTPTraceEvent_ConnectDone)(nil), (*HTTPTraceEvent_TlsHandshakeDone)(nil), (*HTTPTraceEvent_WroteRequest)(nil), } file_encore_engine_trace_trace_proto_msgTypes[24].OneofWrappers = []any{ (*LogField_ErrorWithoutStack)(nil), (*LogField_ErrorWithStack)(nil), (*LogField_Str)(nil), (*LogField_Bool)(nil), (*LogField_Time)(nil), (*LogField_Dur)(nil), (*LogField_Uuid)(nil), (*LogField_Json)(nil), (*LogField_Int)(nil), (*LogField_Uint)(nil), (*LogField_Float32)(nil), (*LogField_Float64)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_engine_trace_trace_proto_rawDesc), len(file_encore_engine_trace_trace_proto_rawDesc)), NumEnums: 5, NumMessages: 30, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_engine_trace_trace_proto_goTypes, DependencyIndexes: file_encore_engine_trace_trace_proto_depIdxs, EnumInfos: file_encore_engine_trace_trace_proto_enumTypes, MessageInfos: file_encore_engine_trace_trace_proto_msgTypes, }.Build() File_encore_engine_trace_trace_proto = out.File file_encore_engine_trace_trace_proto_goTypes = nil file_encore_engine_trace_trace_proto_depIdxs = nil } ================================================ FILE: proto/encore/engine/trace/trace.proto ================================================ syntax = "proto3"; package encore.engine.trace; import "google/protobuf/timestamp.proto"; option go_package = "encr.dev/proto/encore/engine/trace"; message TraceID { uint64 high = 1; uint64 low = 2; } message Request { reserved 7; // call_loc TraceID trace_id = 1; uint64 span_id = 2; uint64 parent_span_id = 3; TraceID parent_trace_id = 32; uint32 goid = 4; uint64 start_time = 5; uint64 end_time = 6; int32 def_loc = 8; bytes err = 11; repeated Event events = 12; Type type = 14; StackTrace err_stack = 15; // null if unavailable StackTrace panic_stack = 34; // null if unavailable // abs_start_time is the absolute unix timestamp // (in nanosecond resolution) of when the request started. uint64 abs_start_time = 16; string service_name = 17; string endpoint_name = 18; // Fields set if Type == PUBSUB_MSG string topic_name = 19; string subscription_name = 20; string message_id = 21; uint32 attempt = 22; uint64 publish_time = 23; // Fields set if Type == RPC or AUTH repeated bytes inputs = 9; // Deprecated: use request_payload and path_params repeated bytes outputs = 10; // Deprecated: use response_payload and uid string uid = 13; string http_method = 24; string path = 25; repeated string path_params = 26; bytes request_payload = 27; bytes response_payload = 28; map raw_request_headers = 29; map raw_response_headers = 30; // external_request_id is the value of the X-Request-ID header. string external_request_id = 31; string external_correlation_id = 33; enum Type { RPC = 0; AUTH = 1; PUBSUB_MSG = 2; } } message Event { oneof data { RPCCall rpc = 1; DBTransaction tx = 2; DBQuery query = 3; Goroutine goroutine = 4; HTTPCall http = 5; LogMessage log = 6; PubsubMsgPublished publishedMsg = 7; ServiceInit service_init = 8; CacheOp cache = 9; BodyStream body_stream = 10; } } message RPCCall { reserved 3; // call_loc uint64 span_id = 1; uint32 goid = 2; int32 def_loc = 4; uint64 start_time = 5; uint64 end_time = 6; bytes err = 7; StackTrace stack = 8; // where it was called (null if unavailable) } message Goroutine { reserved 2; // call_loc uint32 goid = 1; uint64 start_time = 3; uint64 end_time = 4; } message DBTransaction { enum CompletionType { ROLLBACK = 0; COMMIT = 1; } reserved 2, 3; // start_loc, end_loc uint32 goid = 1; uint64 start_time = 4; uint64 end_time = 5; bytes err = 6; CompletionType completion = 7; repeated DBQuery queries = 8; StackTrace begin_stack = 9; // null if unavailable StackTrace end_stack = 10; // null if unavailable } message DBQuery { reserved 2; // call_loc uint32 goid = 1; uint64 start_time = 3; uint64 end_time = 4; bytes query = 5; bytes err = 6; StackTrace stack = 7; // null if unavailable } message PubsubMsgPublished { uint64 goid = 1; uint64 start_time = 3; uint64 end_time = 4; string topic = 5; bytes message = 6; string message_id = 7; bytes err = 8; StackTrace stack = 9; } message ServiceInit { uint64 goid = 1; int32 def_loc = 2; uint64 start_time = 3; uint64 end_time = 4; string service = 5; bytes err = 6; StackTrace err_stack = 7; // null if not an error } message CacheOp { uint32 goid = 1; int32 def_loc = 2; uint64 start_time = 3; uint64 end_time = 4; string operation = 5; repeated string keys = 6; repeated bytes inputs = 7; repeated bytes outputs = 8; StackTrace stack = 9; // null if unavailable bytes err = 10; // set iff result == ERR bool write = 11; Result result = 12; enum Result { UNKNOWN = 0; OK = 1; NO_SUCH_KEY = 2; CONFLICT = 3; ERR = 4; } } message BodyStream { bool is_response = 1; bool overflowed = 2; bytes data = 3; } message HTTPCall { uint64 span_id = 1; uint32 goid = 2; uint64 start_time = 3; uint64 end_time = 4; string method = 5; string url = 6; uint32 status_code = 7; bytes err = 8; uint64 body_closed_time = 9; repeated HTTPTraceEvent events = 10; } enum HTTPTraceEventCode { UNKNOWN = 0; GET_CONN = 1; GOT_CONN = 2; GOT_FIRST_RESPONSE_BYTE = 3; GOT_1XX_RESPONSE = 4; DNS_START = 5; DNS_DONE = 6; CONNECT_START = 7; CONNECT_DONE = 8; TLS_HANDSHAKE_START = 9; TLS_HANDSHAKE_DONE = 10; WROTE_HEADERS = 11; WROTE_REQUEST = 12; WAIT_100_CONTINUE = 13; } message HTTPTraceEvent { HTTPTraceEventCode code = 1; uint64 time = 2; oneof data { HTTPGetConnData get_conn = 3; HTTPGotConnData got_conn = 4; HTTPGot1xxResponseData got_1xx_response = 5; HTTPDNSStartData dns_start = 6; HTTPDNSDoneData dns_done = 7; HTTPConnectStartData connect_start = 8; HTTPConnectDoneData connect_done = 9; HTTPTLSHandshakeDoneData tls_handshake_done = 10; HTTPWroteRequestData wrote_request = 11; } } message HTTPGetConnData { string host_port = 1; } message HTTPGotConnData { bool reused = 1; bool was_idle = 2; int64 idle_duration_ns = 3; } message HTTPGot1xxResponseData { int32 code = 1; } message HTTPDNSStartData { string host = 1; } message HTTPDNSDoneData { bytes err = 1; repeated DNSAddr addrs = 2; } message DNSAddr { bytes ip = 1; } message HTTPConnectStartData { string network = 1; string addr = 2; } message HTTPConnectDoneData { string network = 1; string addr = 2; bytes err = 3; } message HTTPTLSHandshakeDoneData { bytes err = 1; uint32 tls_version = 2; uint32 cipher_suite = 3; string server_name = 4; string negotiated_protocol = 5; } message HTTPWroteRequestData { bytes err = 1; } message LogMessage { // Note: These values don't match the values used by the binary trace protocol, // as these values are stored in persisted traces and therefore must maintain // backwards compatibility. The binary trace protocol is versioned and doesn't // have the same limitations. enum Level { DEBUG = 0; INFO = 1; ERROR = 2; WARN = 3; TRACE = 4; } uint64 span_id = 1; uint32 goid = 2; uint64 time = 3; Level level = 4; string msg = 5; repeated LogField fields = 6; StackTrace stack = 7; // null if unavailable } message LogField { string key = 1; oneof value { string error_without_stack = 2; // deprecated: use error_with_stack ErrWithStack error_with_stack = 13; string str = 3; bool bool = 4; google.protobuf.Timestamp time = 5; int64 dur = 6; bytes uuid = 7; bytes json = 8; int64 int = 9; uint64 uint = 10; float float32 = 11; double float64 = 12; } } message ErrWithStack { string error = 1; StackTrace stack = 2; } message StackTrace { repeated int64 pcs = 1; repeated StackFrame frames = 2; } message StackFrame { string filename = 1; string func = 2; int32 line = 3; } ================================================ FILE: proto/encore/engine/trace/trace_util.go ================================================ package trace func (id *TraceID) IsZero() bool { return id == nil || (id.Low == 0 && id.High == 0) } ================================================ FILE: proto/encore/engine/trace2/trace2.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/engine/trace2/trace2.proto package trace2 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type HTTPTraceEventCode int32 const ( HTTPTraceEventCode_UNKNOWN HTTPTraceEventCode = 0 HTTPTraceEventCode_GET_CONN HTTPTraceEventCode = 1 HTTPTraceEventCode_GOT_CONN HTTPTraceEventCode = 2 HTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE HTTPTraceEventCode = 3 HTTPTraceEventCode_GOT_1XX_RESPONSE HTTPTraceEventCode = 4 HTTPTraceEventCode_DNS_START HTTPTraceEventCode = 5 HTTPTraceEventCode_DNS_DONE HTTPTraceEventCode = 6 HTTPTraceEventCode_CONNECT_START HTTPTraceEventCode = 7 HTTPTraceEventCode_CONNECT_DONE HTTPTraceEventCode = 8 HTTPTraceEventCode_TLS_HANDSHAKE_START HTTPTraceEventCode = 9 HTTPTraceEventCode_TLS_HANDSHAKE_DONE HTTPTraceEventCode = 10 HTTPTraceEventCode_WROTE_HEADERS HTTPTraceEventCode = 11 HTTPTraceEventCode_WROTE_REQUEST HTTPTraceEventCode = 12 HTTPTraceEventCode_WAIT_100_CONTINUE HTTPTraceEventCode = 13 HTTPTraceEventCode_CLOSED_BODY HTTPTraceEventCode = 14 ) // Enum value maps for HTTPTraceEventCode. var ( HTTPTraceEventCode_name = map[int32]string{ 0: "UNKNOWN", 1: "GET_CONN", 2: "GOT_CONN", 3: "GOT_FIRST_RESPONSE_BYTE", 4: "GOT_1XX_RESPONSE", 5: "DNS_START", 6: "DNS_DONE", 7: "CONNECT_START", 8: "CONNECT_DONE", 9: "TLS_HANDSHAKE_START", 10: "TLS_HANDSHAKE_DONE", 11: "WROTE_HEADERS", 12: "WROTE_REQUEST", 13: "WAIT_100_CONTINUE", 14: "CLOSED_BODY", } HTTPTraceEventCode_value = map[string]int32{ "UNKNOWN": 0, "GET_CONN": 1, "GOT_CONN": 2, "GOT_FIRST_RESPONSE_BYTE": 3, "GOT_1XX_RESPONSE": 4, "DNS_START": 5, "DNS_DONE": 6, "CONNECT_START": 7, "CONNECT_DONE": 8, "TLS_HANDSHAKE_START": 9, "TLS_HANDSHAKE_DONE": 10, "WROTE_HEADERS": 11, "WROTE_REQUEST": 12, "WAIT_100_CONTINUE": 13, "CLOSED_BODY": 14, } ) func (x HTTPTraceEventCode) Enum() *HTTPTraceEventCode { p := new(HTTPTraceEventCode) *p = x return p } func (x HTTPTraceEventCode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (HTTPTraceEventCode) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[0].Descriptor() } func (HTTPTraceEventCode) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[0] } func (x HTTPTraceEventCode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use HTTPTraceEventCode.Descriptor instead. func (HTTPTraceEventCode) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0} } type StatusCode int32 const ( StatusCode_STATUS_CODE_OK StatusCode = 0 StatusCode_STATUS_CODE_CANCELED StatusCode = 1 StatusCode_STATUS_CODE_UNKNOWN StatusCode = 2 StatusCode_STATUS_CODE_INVALID_ARGUMENT StatusCode = 3 StatusCode_STATUS_CODE_DEADLINE_EXCEEDED StatusCode = 4 StatusCode_STATUS_CODE_NOT_FOUND StatusCode = 5 StatusCode_STATUS_CODE_ALREADY_EXISTS StatusCode = 6 StatusCode_STATUS_CODE_PERMISSION_DENIED StatusCode = 7 StatusCode_STATUS_CODE_RESOURCE_EXHAUSTED StatusCode = 8 StatusCode_STATUS_CODE_FAILED_PRECONDITION StatusCode = 9 StatusCode_STATUS_CODE_ABORTED StatusCode = 10 StatusCode_STATUS_CODE_OUT_OF_RANGE StatusCode = 11 StatusCode_STATUS_CODE_UNIMPLEMENTED StatusCode = 12 StatusCode_STATUS_CODE_INTERNAL StatusCode = 13 StatusCode_STATUS_CODE_UNAVAILABLE StatusCode = 14 StatusCode_STATUS_CODE_DATA_LOSS StatusCode = 15 StatusCode_STATUS_CODE_UNAUTHENTICATED StatusCode = 16 ) // Enum value maps for StatusCode. var ( StatusCode_name = map[int32]string{ 0: "STATUS_CODE_OK", 1: "STATUS_CODE_CANCELED", 2: "STATUS_CODE_UNKNOWN", 3: "STATUS_CODE_INVALID_ARGUMENT", 4: "STATUS_CODE_DEADLINE_EXCEEDED", 5: "STATUS_CODE_NOT_FOUND", 6: "STATUS_CODE_ALREADY_EXISTS", 7: "STATUS_CODE_PERMISSION_DENIED", 8: "STATUS_CODE_RESOURCE_EXHAUSTED", 9: "STATUS_CODE_FAILED_PRECONDITION", 10: "STATUS_CODE_ABORTED", 11: "STATUS_CODE_OUT_OF_RANGE", 12: "STATUS_CODE_UNIMPLEMENTED", 13: "STATUS_CODE_INTERNAL", 14: "STATUS_CODE_UNAVAILABLE", 15: "STATUS_CODE_DATA_LOSS", 16: "STATUS_CODE_UNAUTHENTICATED", } StatusCode_value = map[string]int32{ "STATUS_CODE_OK": 0, "STATUS_CODE_CANCELED": 1, "STATUS_CODE_UNKNOWN": 2, "STATUS_CODE_INVALID_ARGUMENT": 3, "STATUS_CODE_DEADLINE_EXCEEDED": 4, "STATUS_CODE_NOT_FOUND": 5, "STATUS_CODE_ALREADY_EXISTS": 6, "STATUS_CODE_PERMISSION_DENIED": 7, "STATUS_CODE_RESOURCE_EXHAUSTED": 8, "STATUS_CODE_FAILED_PRECONDITION": 9, "STATUS_CODE_ABORTED": 10, "STATUS_CODE_OUT_OF_RANGE": 11, "STATUS_CODE_UNIMPLEMENTED": 12, "STATUS_CODE_INTERNAL": 13, "STATUS_CODE_UNAVAILABLE": 14, "STATUS_CODE_DATA_LOSS": 15, "STATUS_CODE_UNAUTHENTICATED": 16, } ) func (x StatusCode) Enum() *StatusCode { p := new(StatusCode) *p = x return p } func (x StatusCode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (StatusCode) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[1].Descriptor() } func (StatusCode) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[1] } func (x StatusCode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use StatusCode.Descriptor instead. func (StatusCode) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{1} } type SpanSummary_SpanType int32 const ( SpanSummary_UNKNOWN SpanSummary_SpanType = 0 SpanSummary_REQUEST SpanSummary_SpanType = 1 SpanSummary_AUTH SpanSummary_SpanType = 2 SpanSummary_PUBSUB_MESSAGE SpanSummary_SpanType = 3 SpanSummary_TEST SpanSummary_SpanType = 4 ) // Enum value maps for SpanSummary_SpanType. var ( SpanSummary_SpanType_name = map[int32]string{ 0: "UNKNOWN", 1: "REQUEST", 2: "AUTH", 3: "PUBSUB_MESSAGE", 4: "TEST", } SpanSummary_SpanType_value = map[string]int32{ "UNKNOWN": 0, "REQUEST": 1, "AUTH": 2, "PUBSUB_MESSAGE": 3, "TEST": 4, } ) func (x SpanSummary_SpanType) Enum() *SpanSummary_SpanType { p := new(SpanSummary_SpanType) *p = x return p } func (x SpanSummary_SpanType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (SpanSummary_SpanType) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[2].Descriptor() } func (SpanSummary_SpanType) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[2] } func (x SpanSummary_SpanType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use SpanSummary_SpanType.Descriptor instead. func (SpanSummary_SpanType) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0, 0} } type DBTransactionEnd_CompletionType int32 const ( DBTransactionEnd_ROLLBACK DBTransactionEnd_CompletionType = 0 DBTransactionEnd_COMMIT DBTransactionEnd_CompletionType = 1 ) // Enum value maps for DBTransactionEnd_CompletionType. var ( DBTransactionEnd_CompletionType_name = map[int32]string{ 0: "ROLLBACK", 1: "COMMIT", } DBTransactionEnd_CompletionType_value = map[string]int32{ "ROLLBACK": 0, "COMMIT": 1, } ) func (x DBTransactionEnd_CompletionType) Enum() *DBTransactionEnd_CompletionType { p := new(DBTransactionEnd_CompletionType) *p = x return p } func (x DBTransactionEnd_CompletionType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (DBTransactionEnd_CompletionType) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[3].Descriptor() } func (DBTransactionEnd_CompletionType) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[3] } func (x DBTransactionEnd_CompletionType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use DBTransactionEnd_CompletionType.Descriptor instead. func (DBTransactionEnd_CompletionType) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{20, 0} } type CacheCallEnd_Result int32 const ( CacheCallEnd_UNKNOWN CacheCallEnd_Result = 0 CacheCallEnd_OK CacheCallEnd_Result = 1 CacheCallEnd_NO_SUCH_KEY CacheCallEnd_Result = 2 CacheCallEnd_CONFLICT CacheCallEnd_Result = 3 CacheCallEnd_ERR CacheCallEnd_Result = 4 ) // Enum value maps for CacheCallEnd_Result. var ( CacheCallEnd_Result_name = map[int32]string{ 0: "UNKNOWN", 1: "OK", 2: "NO_SUCH_KEY", 3: "CONFLICT", 4: "ERR", } CacheCallEnd_Result_value = map[string]int32{ "UNKNOWN": 0, "OK": 1, "NO_SUCH_KEY": 2, "CONFLICT": 3, "ERR": 4, } ) func (x CacheCallEnd_Result) Enum() *CacheCallEnd_Result { p := new(CacheCallEnd_Result) *p = x return p } func (x CacheCallEnd_Result) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (CacheCallEnd_Result) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[4].Descriptor() } func (CacheCallEnd_Result) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[4] } func (x CacheCallEnd_Result) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use CacheCallEnd_Result.Descriptor instead. func (CacheCallEnd_Result) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{28, 0} } // Note: These values don't match the values used by the binary trace protocol, // as these values are stored in persisted traces and therefore must maintain // backwards compatibility. The binary trace protocol is versioned and doesn't // have the same limitations. type LogMessage_Level int32 const ( LogMessage_DEBUG LogMessage_Level = 0 LogMessage_INFO LogMessage_Level = 1 LogMessage_ERROR LogMessage_Level = 2 LogMessage_WARN LogMessage_Level = 3 LogMessage_TRACE LogMessage_Level = 4 ) // Enum value maps for LogMessage_Level. var ( LogMessage_Level_name = map[int32]string{ 0: "DEBUG", 1: "INFO", 2: "ERROR", 3: "WARN", 4: "TRACE", } LogMessage_Level_value = map[string]int32{ "DEBUG": 0, "INFO": 1, "ERROR": 2, "WARN": 3, "TRACE": 4, } ) func (x LogMessage_Level) Enum() *LogMessage_Level { p := new(LogMessage_Level) *p = x return p } func (x LogMessage_Level) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (LogMessage_Level) Descriptor() protoreflect.EnumDescriptor { return file_encore_engine_trace2_trace2_proto_enumTypes[5].Descriptor() } func (LogMessage_Level) Type() protoreflect.EnumType { return &file_encore_engine_trace2_trace2_proto_enumTypes[5] } func (x LogMessage_Level) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use LogMessage_Level.Descriptor instead. func (LogMessage_Level) EnumDescriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{60, 0} } // SpanSummary summarizes a span for display purposes. type SpanSummary struct { state protoimpl.MessageState `protogen:"open.v1"` TraceId string `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` SpanId string `protobuf:"bytes,2,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` Type SpanSummary_SpanType `protobuf:"varint,3,opt,name=type,proto3,enum=encore.engine.trace2.SpanSummary_SpanType" json:"type,omitempty"` IsRoot bool `protobuf:"varint,4,opt,name=is_root,json=isRoot,proto3" json:"is_root,omitempty"` // whether it's a root request IsError bool `protobuf:"varint,5,opt,name=is_error,json=isError,proto3" json:"is_error,omitempty"` // whether the request failed DeployedCommit string `protobuf:"bytes,6,opt,name=deployed_commit,json=deployedCommit,proto3" json:"deployed_commit,omitempty"` // the commit hash of the running service StartedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` DurationNanos uint64 `protobuf:"varint,8,opt,name=duration_nanos,json=durationNanos,proto3" json:"duration_nanos,omitempty"` ServiceName string `protobuf:"bytes,9,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName *string `protobuf:"bytes,10,opt,name=endpoint_name,json=endpointName,proto3,oneof" json:"endpoint_name,omitempty"` TopicName *string `protobuf:"bytes,11,opt,name=topic_name,json=topicName,proto3,oneof" json:"topic_name,omitempty"` SubscriptionName *string `protobuf:"bytes,12,opt,name=subscription_name,json=subscriptionName,proto3,oneof" json:"subscription_name,omitempty"` MessageId *string `protobuf:"bytes,13,opt,name=message_id,json=messageId,proto3,oneof" json:"message_id,omitempty"` TestSkipped *bool `protobuf:"varint,14,opt,name=test_skipped,json=testSkipped,proto3,oneof" json:"test_skipped,omitempty"` // whether the test was skipped SrcFile *string `protobuf:"bytes,15,opt,name=src_file,json=srcFile,proto3,oneof" json:"src_file,omitempty"` // the source file where the span was started (if available) SrcLine *uint32 `protobuf:"varint,16,opt,name=src_line,json=srcLine,proto3,oneof" json:"src_line,omitempty"` // the source line where the span was started (if available) ParentSpanId *string `protobuf:"bytes,17,opt,name=parent_span_id,json=parentSpanId,proto3,oneof" json:"parent_span_id,omitempty"` // parent of the span if it's a child, if this is not populated then it's a root CallerEventId *uint64 `protobuf:"varint,18,opt,name=caller_event_id,json=callerEventId,proto3,oneof" json:"caller_event_id,omitempty"` // the event id of the call that started this span unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SpanSummary) Reset() { *x = SpanSummary{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SpanSummary) String() string { return protoimpl.X.MessageStringOf(x) } func (*SpanSummary) ProtoMessage() {} func (x *SpanSummary) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SpanSummary.ProtoReflect.Descriptor instead. func (*SpanSummary) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0} } func (x *SpanSummary) GetTraceId() string { if x != nil { return x.TraceId } return "" } func (x *SpanSummary) GetSpanId() string { if x != nil { return x.SpanId } return "" } func (x *SpanSummary) GetType() SpanSummary_SpanType { if x != nil { return x.Type } return SpanSummary_UNKNOWN } func (x *SpanSummary) GetIsRoot() bool { if x != nil { return x.IsRoot } return false } func (x *SpanSummary) GetIsError() bool { if x != nil { return x.IsError } return false } func (x *SpanSummary) GetDeployedCommit() string { if x != nil { return x.DeployedCommit } return "" } func (x *SpanSummary) GetStartedAt() *timestamppb.Timestamp { if x != nil { return x.StartedAt } return nil } func (x *SpanSummary) GetDurationNanos() uint64 { if x != nil { return x.DurationNanos } return 0 } func (x *SpanSummary) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *SpanSummary) GetEndpointName() string { if x != nil && x.EndpointName != nil { return *x.EndpointName } return "" } func (x *SpanSummary) GetTopicName() string { if x != nil && x.TopicName != nil { return *x.TopicName } return "" } func (x *SpanSummary) GetSubscriptionName() string { if x != nil && x.SubscriptionName != nil { return *x.SubscriptionName } return "" } func (x *SpanSummary) GetMessageId() string { if x != nil && x.MessageId != nil { return *x.MessageId } return "" } func (x *SpanSummary) GetTestSkipped() bool { if x != nil && x.TestSkipped != nil { return *x.TestSkipped } return false } func (x *SpanSummary) GetSrcFile() string { if x != nil && x.SrcFile != nil { return *x.SrcFile } return "" } func (x *SpanSummary) GetSrcLine() uint32 { if x != nil && x.SrcLine != nil { return *x.SrcLine } return 0 } func (x *SpanSummary) GetParentSpanId() string { if x != nil && x.ParentSpanId != nil { return *x.ParentSpanId } return "" } func (x *SpanSummary) GetCallerEventId() uint64 { if x != nil && x.CallerEventId != nil { return *x.CallerEventId } return 0 } type TraceID struct { state protoimpl.MessageState `protogen:"open.v1"` High uint64 `protobuf:"varint,1,opt,name=high,proto3" json:"high,omitempty"` Low uint64 `protobuf:"varint,2,opt,name=low,proto3" json:"low,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TraceID) Reset() { *x = TraceID{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TraceID) String() string { return protoimpl.X.MessageStringOf(x) } func (*TraceID) ProtoMessage() {} func (x *TraceID) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TraceID.ProtoReflect.Descriptor instead. func (*TraceID) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{1} } func (x *TraceID) GetHigh() uint64 { if x != nil { return x.High } return 0 } func (x *TraceID) GetLow() uint64 { if x != nil { return x.Low } return 0 } type EventList struct { state protoimpl.MessageState `protogen:"open.v1"` Events []*TraceEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EventList) Reset() { *x = EventList{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EventList) String() string { return protoimpl.X.MessageStringOf(x) } func (*EventList) ProtoMessage() {} func (x *EventList) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EventList.ProtoReflect.Descriptor instead. func (*EventList) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{2} } func (x *EventList) GetEvents() []*TraceEvent { if x != nil { return x.Events } return nil } type TraceEvent struct { state protoimpl.MessageState `protogen:"open.v1"` TraceId *TraceID `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` SpanId uint64 `protobuf:"varint,2,opt,name=span_id,json=spanId,proto3" json:"span_id,omitempty"` EventId uint64 `protobuf:"varint,3,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` EventTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` // Types that are valid to be assigned to Event: // // *TraceEvent_SpanStart // *TraceEvent_SpanEnd // *TraceEvent_SpanEvent Event isTraceEvent_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TraceEvent) Reset() { *x = TraceEvent{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TraceEvent) String() string { return protoimpl.X.MessageStringOf(x) } func (*TraceEvent) ProtoMessage() {} func (x *TraceEvent) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TraceEvent.ProtoReflect.Descriptor instead. func (*TraceEvent) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{3} } func (x *TraceEvent) GetTraceId() *TraceID { if x != nil { return x.TraceId } return nil } func (x *TraceEvent) GetSpanId() uint64 { if x != nil { return x.SpanId } return 0 } func (x *TraceEvent) GetEventId() uint64 { if x != nil { return x.EventId } return 0 } func (x *TraceEvent) GetEventTime() *timestamppb.Timestamp { if x != nil { return x.EventTime } return nil } func (x *TraceEvent) GetEvent() isTraceEvent_Event { if x != nil { return x.Event } return nil } func (x *TraceEvent) GetSpanStart() *SpanStart { if x != nil { if x, ok := x.Event.(*TraceEvent_SpanStart); ok { return x.SpanStart } } return nil } func (x *TraceEvent) GetSpanEnd() *SpanEnd { if x != nil { if x, ok := x.Event.(*TraceEvent_SpanEnd); ok { return x.SpanEnd } } return nil } func (x *TraceEvent) GetSpanEvent() *SpanEvent { if x != nil { if x, ok := x.Event.(*TraceEvent_SpanEvent); ok { return x.SpanEvent } } return nil } type isTraceEvent_Event interface { isTraceEvent_Event() } type TraceEvent_SpanStart struct { SpanStart *SpanStart `protobuf:"bytes,10,opt,name=span_start,json=spanStart,proto3,oneof"` } type TraceEvent_SpanEnd struct { SpanEnd *SpanEnd `protobuf:"bytes,11,opt,name=span_end,json=spanEnd,proto3,oneof"` } type TraceEvent_SpanEvent struct { SpanEvent *SpanEvent `protobuf:"bytes,12,opt,name=span_event,json=spanEvent,proto3,oneof"` } func (*TraceEvent_SpanStart) isTraceEvent_Event() {} func (*TraceEvent_SpanEnd) isTraceEvent_Event() {} func (*TraceEvent_SpanEvent) isTraceEvent_Event() {} type SpanStart struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` ParentTraceId *TraceID `protobuf:"bytes,2,opt,name=parent_trace_id,json=parentTraceId,proto3,oneof" json:"parent_trace_id,omitempty"` ParentSpanId *uint64 `protobuf:"varint,3,opt,name=parent_span_id,json=parentSpanId,proto3,oneof" json:"parent_span_id,omitempty"` CallerEventId *uint64 `protobuf:"varint,4,opt,name=caller_event_id,json=callerEventId,proto3,oneof" json:"caller_event_id,omitempty"` ExternalCorrelationId *string `protobuf:"bytes,5,opt,name=external_correlation_id,json=externalCorrelationId,proto3,oneof" json:"external_correlation_id,omitempty"` DefLoc *uint32 `protobuf:"varint,6,opt,name=def_loc,json=defLoc,proto3,oneof" json:"def_loc,omitempty"` // Types that are valid to be assigned to Data: // // *SpanStart_Request // *SpanStart_Auth // *SpanStart_PubsubMessage // *SpanStart_Test Data isSpanStart_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SpanStart) Reset() { *x = SpanStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SpanStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*SpanStart) ProtoMessage() {} func (x *SpanStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SpanStart.ProtoReflect.Descriptor instead. func (*SpanStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{4} } func (x *SpanStart) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *SpanStart) GetParentTraceId() *TraceID { if x != nil { return x.ParentTraceId } return nil } func (x *SpanStart) GetParentSpanId() uint64 { if x != nil && x.ParentSpanId != nil { return *x.ParentSpanId } return 0 } func (x *SpanStart) GetCallerEventId() uint64 { if x != nil && x.CallerEventId != nil { return *x.CallerEventId } return 0 } func (x *SpanStart) GetExternalCorrelationId() string { if x != nil && x.ExternalCorrelationId != nil { return *x.ExternalCorrelationId } return "" } func (x *SpanStart) GetDefLoc() uint32 { if x != nil && x.DefLoc != nil { return *x.DefLoc } return 0 } func (x *SpanStart) GetData() isSpanStart_Data { if x != nil { return x.Data } return nil } func (x *SpanStart) GetRequest() *RequestSpanStart { if x != nil { if x, ok := x.Data.(*SpanStart_Request); ok { return x.Request } } return nil } func (x *SpanStart) GetAuth() *AuthSpanStart { if x != nil { if x, ok := x.Data.(*SpanStart_Auth); ok { return x.Auth } } return nil } func (x *SpanStart) GetPubsubMessage() *PubsubMessageSpanStart { if x != nil { if x, ok := x.Data.(*SpanStart_PubsubMessage); ok { return x.PubsubMessage } } return nil } func (x *SpanStart) GetTest() *TestSpanStart { if x != nil { if x, ok := x.Data.(*SpanStart_Test); ok { return x.Test } } return nil } type isSpanStart_Data interface { isSpanStart_Data() } type SpanStart_Request struct { Request *RequestSpanStart `protobuf:"bytes,10,opt,name=request,proto3,oneof"` } type SpanStart_Auth struct { Auth *AuthSpanStart `protobuf:"bytes,11,opt,name=auth,proto3,oneof"` } type SpanStart_PubsubMessage struct { PubsubMessage *PubsubMessageSpanStart `protobuf:"bytes,12,opt,name=pubsub_message,json=pubsubMessage,proto3,oneof"` } type SpanStart_Test struct { Test *TestSpanStart `protobuf:"bytes,13,opt,name=test,proto3,oneof"` } func (*SpanStart_Request) isSpanStart_Data() {} func (*SpanStart_Auth) isSpanStart_Data() {} func (*SpanStart_PubsubMessage) isSpanStart_Data() {} func (*SpanStart_Test) isSpanStart_Data() {} type SpanEnd struct { state protoimpl.MessageState `protogen:"open.v1"` DurationNanos uint64 `protobuf:"varint,1,opt,name=duration_nanos,json=durationNanos,proto3" json:"duration_nanos,omitempty"` Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof" json:"error,omitempty"` // panic_stack is the stack trace if the span ended due to a panic PanicStack *StackTrace `protobuf:"bytes,3,opt,name=panic_stack,json=panicStack,proto3,oneof" json:"panic_stack,omitempty"` ParentTraceId *TraceID `protobuf:"bytes,4,opt,name=parent_trace_id,json=parentTraceId,proto3,oneof" json:"parent_trace_id,omitempty"` ParentSpanId *uint64 `protobuf:"varint,5,opt,name=parent_span_id,json=parentSpanId,proto3,oneof" json:"parent_span_id,omitempty"` StatusCode StatusCode `protobuf:"varint,6,opt,name=status_code,json=statusCode,proto3,enum=encore.engine.trace2.StatusCode" json:"status_code,omitempty"` // Types that are valid to be assigned to Data: // // *SpanEnd_Request // *SpanEnd_Auth // *SpanEnd_PubsubMessage // *SpanEnd_Test Data isSpanEnd_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SpanEnd) Reset() { *x = SpanEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SpanEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*SpanEnd) ProtoMessage() {} func (x *SpanEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SpanEnd.ProtoReflect.Descriptor instead. func (*SpanEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{5} } func (x *SpanEnd) GetDurationNanos() uint64 { if x != nil { return x.DurationNanos } return 0 } func (x *SpanEnd) GetError() *Error { if x != nil { return x.Error } return nil } func (x *SpanEnd) GetPanicStack() *StackTrace { if x != nil { return x.PanicStack } return nil } func (x *SpanEnd) GetParentTraceId() *TraceID { if x != nil { return x.ParentTraceId } return nil } func (x *SpanEnd) GetParentSpanId() uint64 { if x != nil && x.ParentSpanId != nil { return *x.ParentSpanId } return 0 } func (x *SpanEnd) GetStatusCode() StatusCode { if x != nil { return x.StatusCode } return StatusCode_STATUS_CODE_OK } func (x *SpanEnd) GetData() isSpanEnd_Data { if x != nil { return x.Data } return nil } func (x *SpanEnd) GetRequest() *RequestSpanEnd { if x != nil { if x, ok := x.Data.(*SpanEnd_Request); ok { return x.Request } } return nil } func (x *SpanEnd) GetAuth() *AuthSpanEnd { if x != nil { if x, ok := x.Data.(*SpanEnd_Auth); ok { return x.Auth } } return nil } func (x *SpanEnd) GetPubsubMessage() *PubsubMessageSpanEnd { if x != nil { if x, ok := x.Data.(*SpanEnd_PubsubMessage); ok { return x.PubsubMessage } } return nil } func (x *SpanEnd) GetTest() *TestSpanEnd { if x != nil { if x, ok := x.Data.(*SpanEnd_Test); ok { return x.Test } } return nil } type isSpanEnd_Data interface { isSpanEnd_Data() } type SpanEnd_Request struct { Request *RequestSpanEnd `protobuf:"bytes,10,opt,name=request,proto3,oneof"` } type SpanEnd_Auth struct { Auth *AuthSpanEnd `protobuf:"bytes,11,opt,name=auth,proto3,oneof"` } type SpanEnd_PubsubMessage struct { PubsubMessage *PubsubMessageSpanEnd `protobuf:"bytes,12,opt,name=pubsub_message,json=pubsubMessage,proto3,oneof"` } type SpanEnd_Test struct { Test *TestSpanEnd `protobuf:"bytes,13,opt,name=test,proto3,oneof"` } func (*SpanEnd_Request) isSpanEnd_Data() {} func (*SpanEnd_Auth) isSpanEnd_Data() {} func (*SpanEnd_PubsubMessage) isSpanEnd_Data() {} func (*SpanEnd_Test) isSpanEnd_Data() {} type RequestSpanStart struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName string `protobuf:"bytes,2,opt,name=endpoint_name,json=endpointName,proto3" json:"endpoint_name,omitempty"` HttpMethod string `protobuf:"bytes,3,opt,name=http_method,json=httpMethod,proto3" json:"http_method,omitempty"` Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` PathParams []string `protobuf:"bytes,5,rep,name=path_params,json=pathParams,proto3" json:"path_params,omitempty"` RequestHeaders map[string]string `protobuf:"bytes,6,rep,name=request_headers,json=requestHeaders,proto3" json:"request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` RequestPayload []byte `protobuf:"bytes,7,opt,name=request_payload,json=requestPayload,proto3,oneof" json:"request_payload,omitempty"` ExtCorrelationId *string `protobuf:"bytes,8,opt,name=ext_correlation_id,json=extCorrelationId,proto3,oneof" json:"ext_correlation_id,omitempty"` Uid *string `protobuf:"bytes,9,opt,name=uid,proto3,oneof" json:"uid,omitempty"` // mocked is true if the request was handled by a mock Mocked bool `protobuf:"varint,10,opt,name=mocked,proto3" json:"mocked,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestSpanStart) Reset() { *x = RequestSpanStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RequestSpanStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*RequestSpanStart) ProtoMessage() {} func (x *RequestSpanStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RequestSpanStart.ProtoReflect.Descriptor instead. func (*RequestSpanStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{6} } func (x *RequestSpanStart) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *RequestSpanStart) GetEndpointName() string { if x != nil { return x.EndpointName } return "" } func (x *RequestSpanStart) GetHttpMethod() string { if x != nil { return x.HttpMethod } return "" } func (x *RequestSpanStart) GetPath() string { if x != nil { return x.Path } return "" } func (x *RequestSpanStart) GetPathParams() []string { if x != nil { return x.PathParams } return nil } func (x *RequestSpanStart) GetRequestHeaders() map[string]string { if x != nil { return x.RequestHeaders } return nil } func (x *RequestSpanStart) GetRequestPayload() []byte { if x != nil { return x.RequestPayload } return nil } func (x *RequestSpanStart) GetExtCorrelationId() string { if x != nil && x.ExtCorrelationId != nil { return *x.ExtCorrelationId } return "" } func (x *RequestSpanStart) GetUid() string { if x != nil && x.Uid != nil { return *x.Uid } return "" } func (x *RequestSpanStart) GetMocked() bool { if x != nil { return x.Mocked } return false } type RequestSpanEnd struct { state protoimpl.MessageState `protogen:"open.v1"` // Repeat service/endpoint name here to make it possible // to consume end events without having to look up the start. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName string `protobuf:"bytes,2,opt,name=endpoint_name,json=endpointName,proto3" json:"endpoint_name,omitempty"` HttpStatusCode uint32 `protobuf:"varint,3,opt,name=http_status_code,json=httpStatusCode,proto3" json:"http_status_code,omitempty"` ResponseHeaders map[string]string `protobuf:"bytes,4,rep,name=response_headers,json=responseHeaders,proto3" json:"response_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` ResponsePayload []byte `protobuf:"bytes,5,opt,name=response_payload,json=responsePayload,proto3,oneof" json:"response_payload,omitempty"` CallerEventId *uint64 `protobuf:"varint,6,opt,name=caller_event_id,json=callerEventId,proto3,oneof" json:"caller_event_id,omitempty"` Uid *string `protobuf:"bytes,7,opt,name=uid,proto3,oneof" json:"uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestSpanEnd) Reset() { *x = RequestSpanEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RequestSpanEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*RequestSpanEnd) ProtoMessage() {} func (x *RequestSpanEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RequestSpanEnd.ProtoReflect.Descriptor instead. func (*RequestSpanEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{7} } func (x *RequestSpanEnd) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *RequestSpanEnd) GetEndpointName() string { if x != nil { return x.EndpointName } return "" } func (x *RequestSpanEnd) GetHttpStatusCode() uint32 { if x != nil { return x.HttpStatusCode } return 0 } func (x *RequestSpanEnd) GetResponseHeaders() map[string]string { if x != nil { return x.ResponseHeaders } return nil } func (x *RequestSpanEnd) GetResponsePayload() []byte { if x != nil { return x.ResponsePayload } return nil } func (x *RequestSpanEnd) GetCallerEventId() uint64 { if x != nil && x.CallerEventId != nil { return *x.CallerEventId } return 0 } func (x *RequestSpanEnd) GetUid() string { if x != nil && x.Uid != nil { return *x.Uid } return "" } type AuthSpanStart struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName string `protobuf:"bytes,2,opt,name=endpoint_name,json=endpointName,proto3" json:"endpoint_name,omitempty"` AuthPayload []byte `protobuf:"bytes,3,opt,name=auth_payload,json=authPayload,proto3,oneof" json:"auth_payload,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AuthSpanStart) Reset() { *x = AuthSpanStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AuthSpanStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuthSpanStart) ProtoMessage() {} func (x *AuthSpanStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuthSpanStart.ProtoReflect.Descriptor instead. func (*AuthSpanStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{8} } func (x *AuthSpanStart) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *AuthSpanStart) GetEndpointName() string { if x != nil { return x.EndpointName } return "" } func (x *AuthSpanStart) GetAuthPayload() []byte { if x != nil { return x.AuthPayload } return nil } type AuthSpanEnd struct { state protoimpl.MessageState `protogen:"open.v1"` // Repeat service/endpoint name here to make it possible // to consume end events without having to look up the start. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` EndpointName string `protobuf:"bytes,2,opt,name=endpoint_name,json=endpointName,proto3" json:"endpoint_name,omitempty"` Uid string `protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"` UserData []byte `protobuf:"bytes,4,opt,name=user_data,json=userData,proto3,oneof" json:"user_data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AuthSpanEnd) Reset() { *x = AuthSpanEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AuthSpanEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuthSpanEnd) ProtoMessage() {} func (x *AuthSpanEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuthSpanEnd.ProtoReflect.Descriptor instead. func (*AuthSpanEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{9} } func (x *AuthSpanEnd) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *AuthSpanEnd) GetEndpointName() string { if x != nil { return x.EndpointName } return "" } func (x *AuthSpanEnd) GetUid() string { if x != nil { return x.Uid } return "" } func (x *AuthSpanEnd) GetUserData() []byte { if x != nil { return x.UserData } return nil } type PubsubMessageSpanStart struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` TopicName string `protobuf:"bytes,2,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` SubscriptionName string `protobuf:"bytes,3,opt,name=subscription_name,json=subscriptionName,proto3" json:"subscription_name,omitempty"` MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` Attempt uint32 `protobuf:"varint,5,opt,name=attempt,proto3" json:"attempt,omitempty"` PublishTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=publish_time,json=publishTime,proto3" json:"publish_time,omitempty"` MessagePayload []byte `protobuf:"bytes,7,opt,name=message_payload,json=messagePayload,proto3,oneof" json:"message_payload,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubsubMessageSpanStart) Reset() { *x = PubsubMessageSpanStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubsubMessageSpanStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubsubMessageSpanStart) ProtoMessage() {} func (x *PubsubMessageSpanStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubsubMessageSpanStart.ProtoReflect.Descriptor instead. func (*PubsubMessageSpanStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{10} } func (x *PubsubMessageSpanStart) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *PubsubMessageSpanStart) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *PubsubMessageSpanStart) GetSubscriptionName() string { if x != nil { return x.SubscriptionName } return "" } func (x *PubsubMessageSpanStart) GetMessageId() string { if x != nil { return x.MessageId } return "" } func (x *PubsubMessageSpanStart) GetAttempt() uint32 { if x != nil { return x.Attempt } return 0 } func (x *PubsubMessageSpanStart) GetPublishTime() *timestamppb.Timestamp { if x != nil { return x.PublishTime } return nil } func (x *PubsubMessageSpanStart) GetMessagePayload() []byte { if x != nil { return x.MessagePayload } return nil } type PubsubMessageSpanEnd struct { state protoimpl.MessageState `protogen:"open.v1"` // Repeat service/topic/subscription name here to make it possible // to consume end events without having to look up the start. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` TopicName string `protobuf:"bytes,2,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` SubscriptionName string `protobuf:"bytes,3,opt,name=subscription_name,json=subscriptionName,proto3" json:"subscription_name,omitempty"` MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubsubMessageSpanEnd) Reset() { *x = PubsubMessageSpanEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubsubMessageSpanEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubsubMessageSpanEnd) ProtoMessage() {} func (x *PubsubMessageSpanEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubsubMessageSpanEnd.ProtoReflect.Descriptor instead. func (*PubsubMessageSpanEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{11} } func (x *PubsubMessageSpanEnd) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *PubsubMessageSpanEnd) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *PubsubMessageSpanEnd) GetSubscriptionName() string { if x != nil { return x.SubscriptionName } return "" } func (x *PubsubMessageSpanEnd) GetMessageId() string { if x != nil { return x.MessageId } return "" } type TestSpanStart struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` TestName string `protobuf:"bytes,2,opt,name=test_name,json=testName,proto3" json:"test_name,omitempty"` Uid string `protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"` TestFile string `protobuf:"bytes,4,opt,name=test_file,json=testFile,proto3" json:"test_file,omitempty"` TestLine uint32 `protobuf:"varint,5,opt,name=test_line,json=testLine,proto3" json:"test_line,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TestSpanStart) Reset() { *x = TestSpanStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TestSpanStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*TestSpanStart) ProtoMessage() {} func (x *TestSpanStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TestSpanStart.ProtoReflect.Descriptor instead. func (*TestSpanStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{12} } func (x *TestSpanStart) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *TestSpanStart) GetTestName() string { if x != nil { return x.TestName } return "" } func (x *TestSpanStart) GetUid() string { if x != nil { return x.Uid } return "" } func (x *TestSpanStart) GetTestFile() string { if x != nil { return x.TestFile } return "" } func (x *TestSpanStart) GetTestLine() uint32 { if x != nil { return x.TestLine } return 0 } type TestSpanEnd struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` TestName string `protobuf:"bytes,2,opt,name=test_name,json=testName,proto3" json:"test_name,omitempty"` Failed bool `protobuf:"varint,3,opt,name=failed,proto3" json:"failed,omitempty"` Skipped bool `protobuf:"varint,4,opt,name=skipped,proto3" json:"skipped,omitempty"` Uid *string `protobuf:"bytes,5,opt,name=uid,proto3,oneof" json:"uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TestSpanEnd) Reset() { *x = TestSpanEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TestSpanEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*TestSpanEnd) ProtoMessage() {} func (x *TestSpanEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TestSpanEnd.ProtoReflect.Descriptor instead. func (*TestSpanEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{13} } func (x *TestSpanEnd) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *TestSpanEnd) GetTestName() string { if x != nil { return x.TestName } return "" } func (x *TestSpanEnd) GetFailed() bool { if x != nil { return x.Failed } return false } func (x *TestSpanEnd) GetSkipped() bool { if x != nil { return x.Skipped } return false } func (x *TestSpanEnd) GetUid() string { if x != nil && x.Uid != nil { return *x.Uid } return "" } type SpanEvent struct { state protoimpl.MessageState `protogen:"open.v1"` Goid uint32 `protobuf:"varint,1,opt,name=goid,proto3" json:"goid,omitempty"` DefLoc *uint32 `protobuf:"varint,2,opt,name=def_loc,json=defLoc,proto3,oneof" json:"def_loc,omitempty"` // correlation_event_id is the other event // this event is correlated with. CorrelationEventId *uint64 `protobuf:"varint,3,opt,name=correlation_event_id,json=correlationEventId,proto3,oneof" json:"correlation_event_id,omitempty"` // Types that are valid to be assigned to Data: // // *SpanEvent_LogMessage // *SpanEvent_BodyStream // *SpanEvent_RpcCallStart // *SpanEvent_RpcCallEnd // *SpanEvent_DbTransactionStart // *SpanEvent_DbTransactionEnd // *SpanEvent_DbQueryStart // *SpanEvent_DbQueryEnd // *SpanEvent_HttpCallStart // *SpanEvent_HttpCallEnd // *SpanEvent_PubsubPublishStart // *SpanEvent_PubsubPublishEnd // *SpanEvent_CacheCallStart // *SpanEvent_CacheCallEnd // *SpanEvent_ServiceInitStart // *SpanEvent_ServiceInitEnd // *SpanEvent_BucketObjectUploadStart // *SpanEvent_BucketObjectUploadEnd // *SpanEvent_BucketObjectDownloadStart // *SpanEvent_BucketObjectDownloadEnd // *SpanEvent_BucketObjectGetAttrsStart // *SpanEvent_BucketObjectGetAttrsEnd // *SpanEvent_BucketListObjectsStart // *SpanEvent_BucketListObjectsEnd // *SpanEvent_BucketDeleteObjectsStart // *SpanEvent_BucketDeleteObjectsEnd Data isSpanEvent_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SpanEvent) Reset() { *x = SpanEvent{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SpanEvent) String() string { return protoimpl.X.MessageStringOf(x) } func (*SpanEvent) ProtoMessage() {} func (x *SpanEvent) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SpanEvent.ProtoReflect.Descriptor instead. func (*SpanEvent) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{14} } func (x *SpanEvent) GetGoid() uint32 { if x != nil { return x.Goid } return 0 } func (x *SpanEvent) GetDefLoc() uint32 { if x != nil && x.DefLoc != nil { return *x.DefLoc } return 0 } func (x *SpanEvent) GetCorrelationEventId() uint64 { if x != nil && x.CorrelationEventId != nil { return *x.CorrelationEventId } return 0 } func (x *SpanEvent) GetData() isSpanEvent_Data { if x != nil { return x.Data } return nil } func (x *SpanEvent) GetLogMessage() *LogMessage { if x != nil { if x, ok := x.Data.(*SpanEvent_LogMessage); ok { return x.LogMessage } } return nil } func (x *SpanEvent) GetBodyStream() *BodyStream { if x != nil { if x, ok := x.Data.(*SpanEvent_BodyStream); ok { return x.BodyStream } } return nil } func (x *SpanEvent) GetRpcCallStart() *RPCCallStart { if x != nil { if x, ok := x.Data.(*SpanEvent_RpcCallStart); ok { return x.RpcCallStart } } return nil } func (x *SpanEvent) GetRpcCallEnd() *RPCCallEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_RpcCallEnd); ok { return x.RpcCallEnd } } return nil } func (x *SpanEvent) GetDbTransactionStart() *DBTransactionStart { if x != nil { if x, ok := x.Data.(*SpanEvent_DbTransactionStart); ok { return x.DbTransactionStart } } return nil } func (x *SpanEvent) GetDbTransactionEnd() *DBTransactionEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_DbTransactionEnd); ok { return x.DbTransactionEnd } } return nil } func (x *SpanEvent) GetDbQueryStart() *DBQueryStart { if x != nil { if x, ok := x.Data.(*SpanEvent_DbQueryStart); ok { return x.DbQueryStart } } return nil } func (x *SpanEvent) GetDbQueryEnd() *DBQueryEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_DbQueryEnd); ok { return x.DbQueryEnd } } return nil } func (x *SpanEvent) GetHttpCallStart() *HTTPCallStart { if x != nil { if x, ok := x.Data.(*SpanEvent_HttpCallStart); ok { return x.HttpCallStart } } return nil } func (x *SpanEvent) GetHttpCallEnd() *HTTPCallEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_HttpCallEnd); ok { return x.HttpCallEnd } } return nil } func (x *SpanEvent) GetPubsubPublishStart() *PubsubPublishStart { if x != nil { if x, ok := x.Data.(*SpanEvent_PubsubPublishStart); ok { return x.PubsubPublishStart } } return nil } func (x *SpanEvent) GetPubsubPublishEnd() *PubsubPublishEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_PubsubPublishEnd); ok { return x.PubsubPublishEnd } } return nil } func (x *SpanEvent) GetCacheCallStart() *CacheCallStart { if x != nil { if x, ok := x.Data.(*SpanEvent_CacheCallStart); ok { return x.CacheCallStart } } return nil } func (x *SpanEvent) GetCacheCallEnd() *CacheCallEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_CacheCallEnd); ok { return x.CacheCallEnd } } return nil } func (x *SpanEvent) GetServiceInitStart() *ServiceInitStart { if x != nil { if x, ok := x.Data.(*SpanEvent_ServiceInitStart); ok { return x.ServiceInitStart } } return nil } func (x *SpanEvent) GetServiceInitEnd() *ServiceInitEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_ServiceInitEnd); ok { return x.ServiceInitEnd } } return nil } func (x *SpanEvent) GetBucketObjectUploadStart() *BucketObjectUploadStart { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectUploadStart); ok { return x.BucketObjectUploadStart } } return nil } func (x *SpanEvent) GetBucketObjectUploadEnd() *BucketObjectUploadEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectUploadEnd); ok { return x.BucketObjectUploadEnd } } return nil } func (x *SpanEvent) GetBucketObjectDownloadStart() *BucketObjectDownloadStart { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectDownloadStart); ok { return x.BucketObjectDownloadStart } } return nil } func (x *SpanEvent) GetBucketObjectDownloadEnd() *BucketObjectDownloadEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectDownloadEnd); ok { return x.BucketObjectDownloadEnd } } return nil } func (x *SpanEvent) GetBucketObjectGetAttrsStart() *BucketObjectGetAttrsStart { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectGetAttrsStart); ok { return x.BucketObjectGetAttrsStart } } return nil } func (x *SpanEvent) GetBucketObjectGetAttrsEnd() *BucketObjectGetAttrsEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketObjectGetAttrsEnd); ok { return x.BucketObjectGetAttrsEnd } } return nil } func (x *SpanEvent) GetBucketListObjectsStart() *BucketListObjectsStart { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketListObjectsStart); ok { return x.BucketListObjectsStart } } return nil } func (x *SpanEvent) GetBucketListObjectsEnd() *BucketListObjectsEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketListObjectsEnd); ok { return x.BucketListObjectsEnd } } return nil } func (x *SpanEvent) GetBucketDeleteObjectsStart() *BucketDeleteObjectsStart { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketDeleteObjectsStart); ok { return x.BucketDeleteObjectsStart } } return nil } func (x *SpanEvent) GetBucketDeleteObjectsEnd() *BucketDeleteObjectsEnd { if x != nil { if x, ok := x.Data.(*SpanEvent_BucketDeleteObjectsEnd); ok { return x.BucketDeleteObjectsEnd } } return nil } type isSpanEvent_Data interface { isSpanEvent_Data() } type SpanEvent_LogMessage struct { LogMessage *LogMessage `protobuf:"bytes,10,opt,name=log_message,json=logMessage,proto3,oneof"` } type SpanEvent_BodyStream struct { BodyStream *BodyStream `protobuf:"bytes,11,opt,name=body_stream,json=bodyStream,proto3,oneof"` } type SpanEvent_RpcCallStart struct { RpcCallStart *RPCCallStart `protobuf:"bytes,12,opt,name=rpc_call_start,json=rpcCallStart,proto3,oneof"` } type SpanEvent_RpcCallEnd struct { RpcCallEnd *RPCCallEnd `protobuf:"bytes,13,opt,name=rpc_call_end,json=rpcCallEnd,proto3,oneof"` } type SpanEvent_DbTransactionStart struct { DbTransactionStart *DBTransactionStart `protobuf:"bytes,14,opt,name=db_transaction_start,json=dbTransactionStart,proto3,oneof"` } type SpanEvent_DbTransactionEnd struct { DbTransactionEnd *DBTransactionEnd `protobuf:"bytes,15,opt,name=db_transaction_end,json=dbTransactionEnd,proto3,oneof"` } type SpanEvent_DbQueryStart struct { DbQueryStart *DBQueryStart `protobuf:"bytes,16,opt,name=db_query_start,json=dbQueryStart,proto3,oneof"` } type SpanEvent_DbQueryEnd struct { DbQueryEnd *DBQueryEnd `protobuf:"bytes,17,opt,name=db_query_end,json=dbQueryEnd,proto3,oneof"` } type SpanEvent_HttpCallStart struct { HttpCallStart *HTTPCallStart `protobuf:"bytes,18,opt,name=http_call_start,json=httpCallStart,proto3,oneof"` } type SpanEvent_HttpCallEnd struct { HttpCallEnd *HTTPCallEnd `protobuf:"bytes,19,opt,name=http_call_end,json=httpCallEnd,proto3,oneof"` } type SpanEvent_PubsubPublishStart struct { PubsubPublishStart *PubsubPublishStart `protobuf:"bytes,20,opt,name=pubsub_publish_start,json=pubsubPublishStart,proto3,oneof"` } type SpanEvent_PubsubPublishEnd struct { PubsubPublishEnd *PubsubPublishEnd `protobuf:"bytes,21,opt,name=pubsub_publish_end,json=pubsubPublishEnd,proto3,oneof"` } type SpanEvent_CacheCallStart struct { CacheCallStart *CacheCallStart `protobuf:"bytes,22,opt,name=cache_call_start,json=cacheCallStart,proto3,oneof"` } type SpanEvent_CacheCallEnd struct { CacheCallEnd *CacheCallEnd `protobuf:"bytes,23,opt,name=cache_call_end,json=cacheCallEnd,proto3,oneof"` } type SpanEvent_ServiceInitStart struct { ServiceInitStart *ServiceInitStart `protobuf:"bytes,24,opt,name=service_init_start,json=serviceInitStart,proto3,oneof"` } type SpanEvent_ServiceInitEnd struct { ServiceInitEnd *ServiceInitEnd `protobuf:"bytes,25,opt,name=service_init_end,json=serviceInitEnd,proto3,oneof"` } type SpanEvent_BucketObjectUploadStart struct { BucketObjectUploadStart *BucketObjectUploadStart `protobuf:"bytes,26,opt,name=bucket_object_upload_start,json=bucketObjectUploadStart,proto3,oneof"` } type SpanEvent_BucketObjectUploadEnd struct { BucketObjectUploadEnd *BucketObjectUploadEnd `protobuf:"bytes,27,opt,name=bucket_object_upload_end,json=bucketObjectUploadEnd,proto3,oneof"` } type SpanEvent_BucketObjectDownloadStart struct { BucketObjectDownloadStart *BucketObjectDownloadStart `protobuf:"bytes,28,opt,name=bucket_object_download_start,json=bucketObjectDownloadStart,proto3,oneof"` } type SpanEvent_BucketObjectDownloadEnd struct { BucketObjectDownloadEnd *BucketObjectDownloadEnd `protobuf:"bytes,29,opt,name=bucket_object_download_end,json=bucketObjectDownloadEnd,proto3,oneof"` } type SpanEvent_BucketObjectGetAttrsStart struct { BucketObjectGetAttrsStart *BucketObjectGetAttrsStart `protobuf:"bytes,30,opt,name=bucket_object_get_attrs_start,json=bucketObjectGetAttrsStart,proto3,oneof"` } type SpanEvent_BucketObjectGetAttrsEnd struct { BucketObjectGetAttrsEnd *BucketObjectGetAttrsEnd `protobuf:"bytes,31,opt,name=bucket_object_get_attrs_end,json=bucketObjectGetAttrsEnd,proto3,oneof"` } type SpanEvent_BucketListObjectsStart struct { BucketListObjectsStart *BucketListObjectsStart `protobuf:"bytes,32,opt,name=bucket_list_objects_start,json=bucketListObjectsStart,proto3,oneof"` } type SpanEvent_BucketListObjectsEnd struct { BucketListObjectsEnd *BucketListObjectsEnd `protobuf:"bytes,33,opt,name=bucket_list_objects_end,json=bucketListObjectsEnd,proto3,oneof"` } type SpanEvent_BucketDeleteObjectsStart struct { BucketDeleteObjectsStart *BucketDeleteObjectsStart `protobuf:"bytes,34,opt,name=bucket_delete_objects_start,json=bucketDeleteObjectsStart,proto3,oneof"` } type SpanEvent_BucketDeleteObjectsEnd struct { BucketDeleteObjectsEnd *BucketDeleteObjectsEnd `protobuf:"bytes,35,opt,name=bucket_delete_objects_end,json=bucketDeleteObjectsEnd,proto3,oneof"` } func (*SpanEvent_LogMessage) isSpanEvent_Data() {} func (*SpanEvent_BodyStream) isSpanEvent_Data() {} func (*SpanEvent_RpcCallStart) isSpanEvent_Data() {} func (*SpanEvent_RpcCallEnd) isSpanEvent_Data() {} func (*SpanEvent_DbTransactionStart) isSpanEvent_Data() {} func (*SpanEvent_DbTransactionEnd) isSpanEvent_Data() {} func (*SpanEvent_DbQueryStart) isSpanEvent_Data() {} func (*SpanEvent_DbQueryEnd) isSpanEvent_Data() {} func (*SpanEvent_HttpCallStart) isSpanEvent_Data() {} func (*SpanEvent_HttpCallEnd) isSpanEvent_Data() {} func (*SpanEvent_PubsubPublishStart) isSpanEvent_Data() {} func (*SpanEvent_PubsubPublishEnd) isSpanEvent_Data() {} func (*SpanEvent_CacheCallStart) isSpanEvent_Data() {} func (*SpanEvent_CacheCallEnd) isSpanEvent_Data() {} func (*SpanEvent_ServiceInitStart) isSpanEvent_Data() {} func (*SpanEvent_ServiceInitEnd) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectUploadStart) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectUploadEnd) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectDownloadStart) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectDownloadEnd) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectGetAttrsStart) isSpanEvent_Data() {} func (*SpanEvent_BucketObjectGetAttrsEnd) isSpanEvent_Data() {} func (*SpanEvent_BucketListObjectsStart) isSpanEvent_Data() {} func (*SpanEvent_BucketListObjectsEnd) isSpanEvent_Data() {} func (*SpanEvent_BucketDeleteObjectsStart) isSpanEvent_Data() {} func (*SpanEvent_BucketDeleteObjectsEnd) isSpanEvent_Data() {} type RPCCallStart struct { state protoimpl.MessageState `protogen:"open.v1"` TargetServiceName string `protobuf:"bytes,1,opt,name=target_service_name,json=targetServiceName,proto3" json:"target_service_name,omitempty"` TargetEndpointName string `protobuf:"bytes,2,opt,name=target_endpoint_name,json=targetEndpointName,proto3" json:"target_endpoint_name,omitempty"` Stack *StackTrace `protobuf:"bytes,3,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPCCallStart) Reset() { *x = RPCCallStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPCCallStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPCCallStart) ProtoMessage() {} func (x *RPCCallStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPCCallStart.ProtoReflect.Descriptor instead. func (*RPCCallStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{15} } func (x *RPCCallStart) GetTargetServiceName() string { if x != nil { return x.TargetServiceName } return "" } func (x *RPCCallStart) GetTargetEndpointName() string { if x != nil { return x.TargetEndpointName } return "" } func (x *RPCCallStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type RPCCallEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPCCallEnd) Reset() { *x = RPCCallEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPCCallEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPCCallEnd) ProtoMessage() {} func (x *RPCCallEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPCCallEnd.ProtoReflect.Descriptor instead. func (*RPCCallEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{16} } func (x *RPCCallEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type GoroutineStart struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GoroutineStart) Reset() { *x = GoroutineStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GoroutineStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*GoroutineStart) ProtoMessage() {} func (x *GoroutineStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GoroutineStart.ProtoReflect.Descriptor instead. func (*GoroutineStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{17} } type GoroutineEnd struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GoroutineEnd) Reset() { *x = GoroutineEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GoroutineEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*GoroutineEnd) ProtoMessage() {} func (x *GoroutineEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GoroutineEnd.ProtoReflect.Descriptor instead. func (*GoroutineEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{18} } type DBTransactionStart struct { state protoimpl.MessageState `protogen:"open.v1"` Stack *StackTrace `protobuf:"bytes,1,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBTransactionStart) Reset() { *x = DBTransactionStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBTransactionStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBTransactionStart) ProtoMessage() {} func (x *DBTransactionStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBTransactionStart.ProtoReflect.Descriptor instead. func (*DBTransactionStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{19} } func (x *DBTransactionStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type DBTransactionEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Completion DBTransactionEnd_CompletionType `protobuf:"varint,1,opt,name=completion,proto3,enum=encore.engine.trace2.DBTransactionEnd_CompletionType" json:"completion,omitempty"` Stack *StackTrace `protobuf:"bytes,2,opt,name=stack,proto3" json:"stack,omitempty"` Err *Error `protobuf:"bytes,3,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBTransactionEnd) Reset() { *x = DBTransactionEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBTransactionEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBTransactionEnd) ProtoMessage() {} func (x *DBTransactionEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBTransactionEnd.ProtoReflect.Descriptor instead. func (*DBTransactionEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{20} } func (x *DBTransactionEnd) GetCompletion() DBTransactionEnd_CompletionType { if x != nil { return x.Completion } return DBTransactionEnd_ROLLBACK } func (x *DBTransactionEnd) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } func (x *DBTransactionEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type DBQueryStart struct { state protoimpl.MessageState `protogen:"open.v1"` Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` Stack *StackTrace `protobuf:"bytes,2,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBQueryStart) Reset() { *x = DBQueryStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBQueryStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBQueryStart) ProtoMessage() {} func (x *DBQueryStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBQueryStart.ProtoReflect.Descriptor instead. func (*DBQueryStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{21} } func (x *DBQueryStart) GetQuery() string { if x != nil { return x.Query } return "" } func (x *DBQueryStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type DBQueryEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBQueryEnd) Reset() { *x = DBQueryEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBQueryEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBQueryEnd) ProtoMessage() {} func (x *DBQueryEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBQueryEnd.ProtoReflect.Descriptor instead. func (*DBQueryEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{22} } func (x *DBQueryEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type PubsubPublishStart struct { state protoimpl.MessageState `protogen:"open.v1"` Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` Message []byte `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` Stack *StackTrace `protobuf:"bytes,3,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubsubPublishStart) Reset() { *x = PubsubPublishStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubsubPublishStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubsubPublishStart) ProtoMessage() {} func (x *PubsubPublishStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubsubPublishStart.ProtoReflect.Descriptor instead. func (*PubsubPublishStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{23} } func (x *PubsubPublishStart) GetTopic() string { if x != nil { return x.Topic } return "" } func (x *PubsubPublishStart) GetMessage() []byte { if x != nil { return x.Message } return nil } func (x *PubsubPublishStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type PubsubPublishEnd struct { state protoimpl.MessageState `protogen:"open.v1"` MessageId *string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3,oneof" json:"message_id,omitempty"` Err *Error `protobuf:"bytes,2,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubsubPublishEnd) Reset() { *x = PubsubPublishEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubsubPublishEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubsubPublishEnd) ProtoMessage() {} func (x *PubsubPublishEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubsubPublishEnd.ProtoReflect.Descriptor instead. func (*PubsubPublishEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{24} } func (x *PubsubPublishEnd) GetMessageId() string { if x != nil && x.MessageId != nil { return *x.MessageId } return "" } func (x *PubsubPublishEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type ServiceInitStart struct { state protoimpl.MessageState `protogen:"open.v1"` Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceInitStart) Reset() { *x = ServiceInitStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceInitStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceInitStart) ProtoMessage() {} func (x *ServiceInitStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceInitStart.ProtoReflect.Descriptor instead. func (*ServiceInitStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{25} } func (x *ServiceInitStart) GetService() string { if x != nil { return x.Service } return "" } type ServiceInitEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceInitEnd) Reset() { *x = ServiceInitEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceInitEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceInitEnd) ProtoMessage() {} func (x *ServiceInitEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceInitEnd.ProtoReflect.Descriptor instead. func (*ServiceInitEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{26} } func (x *ServiceInitEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type CacheCallStart struct { state protoimpl.MessageState `protogen:"open.v1"` Operation string `protobuf:"bytes,1,opt,name=operation,proto3" json:"operation,omitempty"` Keys []string `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` Write bool `protobuf:"varint,3,opt,name=write,proto3" json:"write,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` // TODO include more info (like inputs) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheCallStart) Reset() { *x = CacheCallStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheCallStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheCallStart) ProtoMessage() {} func (x *CacheCallStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheCallStart.ProtoReflect.Descriptor instead. func (*CacheCallStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{27} } func (x *CacheCallStart) GetOperation() string { if x != nil { return x.Operation } return "" } func (x *CacheCallStart) GetKeys() []string { if x != nil { return x.Keys } return nil } func (x *CacheCallStart) GetWrite() bool { if x != nil { return x.Write } return false } func (x *CacheCallStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type CacheCallEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Result CacheCallEnd_Result `protobuf:"varint,1,opt,name=result,proto3,enum=encore.engine.trace2.CacheCallEnd_Result" json:"result,omitempty"` Err *Error `protobuf:"bytes,2,opt,name=err,proto3,oneof" json:"err,omitempty"` // TODO include more info (like outputs) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheCallEnd) Reset() { *x = CacheCallEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheCallEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheCallEnd) ProtoMessage() {} func (x *CacheCallEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheCallEnd.ProtoReflect.Descriptor instead. func (*CacheCallEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{28} } func (x *CacheCallEnd) GetResult() CacheCallEnd_Result { if x != nil { return x.Result } return CacheCallEnd_UNKNOWN } func (x *CacheCallEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type BucketObjectUploadStart struct { state protoimpl.MessageState `protogen:"open.v1"` Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` Attrs *BucketObjectAttributes `protobuf:"bytes,3,opt,name=attrs,proto3" json:"attrs,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectUploadStart) Reset() { *x = BucketObjectUploadStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectUploadStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectUploadStart) ProtoMessage() {} func (x *BucketObjectUploadStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectUploadStart.ProtoReflect.Descriptor instead. func (*BucketObjectUploadStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{29} } func (x *BucketObjectUploadStart) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketObjectUploadStart) GetObject() string { if x != nil { return x.Object } return "" } func (x *BucketObjectUploadStart) GetAttrs() *BucketObjectAttributes { if x != nil { return x.Attrs } return nil } func (x *BucketObjectUploadStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type BucketObjectUploadEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` Size *uint64 `protobuf:"varint,2,opt,name=size,proto3,oneof" json:"size,omitempty"` Version *string `protobuf:"bytes,3,opt,name=version,proto3,oneof" json:"version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectUploadEnd) Reset() { *x = BucketObjectUploadEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectUploadEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectUploadEnd) ProtoMessage() {} func (x *BucketObjectUploadEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectUploadEnd.ProtoReflect.Descriptor instead. func (*BucketObjectUploadEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{30} } func (x *BucketObjectUploadEnd) GetErr() *Error { if x != nil { return x.Err } return nil } func (x *BucketObjectUploadEnd) GetSize() uint64 { if x != nil && x.Size != nil { return *x.Size } return 0 } func (x *BucketObjectUploadEnd) GetVersion() string { if x != nil && x.Version != nil { return *x.Version } return "" } type BucketObjectDownloadStart struct { state protoimpl.MessageState `protogen:"open.v1"` Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` Version *string `protobuf:"bytes,3,opt,name=version,proto3,oneof" json:"version,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectDownloadStart) Reset() { *x = BucketObjectDownloadStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectDownloadStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectDownloadStart) ProtoMessage() {} func (x *BucketObjectDownloadStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectDownloadStart.ProtoReflect.Descriptor instead. func (*BucketObjectDownloadStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{31} } func (x *BucketObjectDownloadStart) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketObjectDownloadStart) GetObject() string { if x != nil { return x.Object } return "" } func (x *BucketObjectDownloadStart) GetVersion() string { if x != nil && x.Version != nil { return *x.Version } return "" } func (x *BucketObjectDownloadStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type BucketObjectDownloadEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` Size *uint64 `protobuf:"varint,2,opt,name=size,proto3,oneof" json:"size,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectDownloadEnd) Reset() { *x = BucketObjectDownloadEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectDownloadEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectDownloadEnd) ProtoMessage() {} func (x *BucketObjectDownloadEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectDownloadEnd.ProtoReflect.Descriptor instead. func (*BucketObjectDownloadEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{32} } func (x *BucketObjectDownloadEnd) GetErr() *Error { if x != nil { return x.Err } return nil } func (x *BucketObjectDownloadEnd) GetSize() uint64 { if x != nil && x.Size != nil { return *x.Size } return 0 } type BucketObjectGetAttrsStart struct { state protoimpl.MessageState `protogen:"open.v1"` Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` Version *string `protobuf:"bytes,3,opt,name=version,proto3,oneof" json:"version,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectGetAttrsStart) Reset() { *x = BucketObjectGetAttrsStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectGetAttrsStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectGetAttrsStart) ProtoMessage() {} func (x *BucketObjectGetAttrsStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectGetAttrsStart.ProtoReflect.Descriptor instead. func (*BucketObjectGetAttrsStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{33} } func (x *BucketObjectGetAttrsStart) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketObjectGetAttrsStart) GetObject() string { if x != nil { return x.Object } return "" } func (x *BucketObjectGetAttrsStart) GetVersion() string { if x != nil && x.Version != nil { return *x.Version } return "" } func (x *BucketObjectGetAttrsStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type BucketObjectGetAttrsEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` Attrs *BucketObjectAttributes `protobuf:"bytes,2,opt,name=attrs,proto3,oneof" json:"attrs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectGetAttrsEnd) Reset() { *x = BucketObjectGetAttrsEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectGetAttrsEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectGetAttrsEnd) ProtoMessage() {} func (x *BucketObjectGetAttrsEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectGetAttrsEnd.ProtoReflect.Descriptor instead. func (*BucketObjectGetAttrsEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{34} } func (x *BucketObjectGetAttrsEnd) GetErr() *Error { if x != nil { return x.Err } return nil } func (x *BucketObjectGetAttrsEnd) GetAttrs() *BucketObjectAttributes { if x != nil { return x.Attrs } return nil } type BucketListObjectsStart struct { state protoimpl.MessageState `protogen:"open.v1"` Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` Prefix *string `protobuf:"bytes,2,opt,name=prefix,proto3,oneof" json:"prefix,omitempty"` Stack *StackTrace `protobuf:"bytes,3,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketListObjectsStart) Reset() { *x = BucketListObjectsStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketListObjectsStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketListObjectsStart) ProtoMessage() {} func (x *BucketListObjectsStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketListObjectsStart.ProtoReflect.Descriptor instead. func (*BucketListObjectsStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{35} } func (x *BucketListObjectsStart) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketListObjectsStart) GetPrefix() string { if x != nil && x.Prefix != nil { return *x.Prefix } return "" } func (x *BucketListObjectsStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type BucketListObjectsEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` Observed uint64 `protobuf:"varint,2,opt,name=observed,proto3" json:"observed,omitempty"` HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketListObjectsEnd) Reset() { *x = BucketListObjectsEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketListObjectsEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketListObjectsEnd) ProtoMessage() {} func (x *BucketListObjectsEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketListObjectsEnd.ProtoReflect.Descriptor instead. func (*BucketListObjectsEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{36} } func (x *BucketListObjectsEnd) GetErr() *Error { if x != nil { return x.Err } return nil } func (x *BucketListObjectsEnd) GetObserved() uint64 { if x != nil { return x.Observed } return 0 } func (x *BucketListObjectsEnd) GetHasMore() bool { if x != nil { return x.HasMore } return false } type BucketDeleteObjectsStart struct { state protoimpl.MessageState `protogen:"open.v1"` Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` Stack *StackTrace `protobuf:"bytes,2,opt,name=stack,proto3" json:"stack,omitempty"` Entries []*BucketDeleteObjectEntry `protobuf:"bytes,3,rep,name=entries,proto3" json:"entries,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketDeleteObjectsStart) Reset() { *x = BucketDeleteObjectsStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketDeleteObjectsStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketDeleteObjectsStart) ProtoMessage() {} func (x *BucketDeleteObjectsStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketDeleteObjectsStart.ProtoReflect.Descriptor instead. func (*BucketDeleteObjectsStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{37} } func (x *BucketDeleteObjectsStart) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketDeleteObjectsStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } func (x *BucketDeleteObjectsStart) GetEntries() []*BucketDeleteObjectEntry { if x != nil { return x.Entries } return nil } type BucketDeleteObjectEntry struct { state protoimpl.MessageState `protogen:"open.v1"` Object string `protobuf:"bytes,1,opt,name=object,proto3" json:"object,omitempty"` Version *string `protobuf:"bytes,2,opt,name=version,proto3,oneof" json:"version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketDeleteObjectEntry) Reset() { *x = BucketDeleteObjectEntry{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketDeleteObjectEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketDeleteObjectEntry) ProtoMessage() {} func (x *BucketDeleteObjectEntry) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketDeleteObjectEntry.ProtoReflect.Descriptor instead. func (*BucketDeleteObjectEntry) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{38} } func (x *BucketDeleteObjectEntry) GetObject() string { if x != nil { return x.Object } return "" } func (x *BucketDeleteObjectEntry) GetVersion() string { if x != nil && x.Version != nil { return *x.Version } return "" } type BucketDeleteObjectsEnd struct { state protoimpl.MessageState `protogen:"open.v1"` Err *Error `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketDeleteObjectsEnd) Reset() { *x = BucketDeleteObjectsEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketDeleteObjectsEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketDeleteObjectsEnd) ProtoMessage() {} func (x *BucketDeleteObjectsEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketDeleteObjectsEnd.ProtoReflect.Descriptor instead. func (*BucketDeleteObjectsEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{39} } func (x *BucketDeleteObjectsEnd) GetErr() *Error { if x != nil { return x.Err } return nil } type BucketObjectAttributes struct { state protoimpl.MessageState `protogen:"open.v1"` Size *uint64 `protobuf:"varint,1,opt,name=size,proto3,oneof" json:"size,omitempty"` Version *string `protobuf:"bytes,2,opt,name=version,proto3,oneof" json:"version,omitempty"` Etag *string `protobuf:"bytes,3,opt,name=etag,proto3,oneof" json:"etag,omitempty"` ContentType *string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3,oneof" json:"content_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketObjectAttributes) Reset() { *x = BucketObjectAttributes{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketObjectAttributes) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketObjectAttributes) ProtoMessage() {} func (x *BucketObjectAttributes) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketObjectAttributes.ProtoReflect.Descriptor instead. func (*BucketObjectAttributes) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{40} } func (x *BucketObjectAttributes) GetSize() uint64 { if x != nil && x.Size != nil { return *x.Size } return 0 } func (x *BucketObjectAttributes) GetVersion() string { if x != nil && x.Version != nil { return *x.Version } return "" } func (x *BucketObjectAttributes) GetEtag() string { if x != nil && x.Etag != nil { return *x.Etag } return "" } func (x *BucketObjectAttributes) GetContentType() string { if x != nil && x.ContentType != nil { return *x.ContentType } return "" } type BodyStream struct { state protoimpl.MessageState `protogen:"open.v1"` IsResponse bool `protobuf:"varint,1,opt,name=is_response,json=isResponse,proto3" json:"is_response,omitempty"` Overflowed bool `protobuf:"varint,2,opt,name=overflowed,proto3" json:"overflowed,omitempty"` Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BodyStream) Reset() { *x = BodyStream{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BodyStream) String() string { return protoimpl.X.MessageStringOf(x) } func (*BodyStream) ProtoMessage() {} func (x *BodyStream) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BodyStream.ProtoReflect.Descriptor instead. func (*BodyStream) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{41} } func (x *BodyStream) GetIsResponse() bool { if x != nil { return x.IsResponse } return false } func (x *BodyStream) GetOverflowed() bool { if x != nil { return x.Overflowed } return false } func (x *BodyStream) GetData() []byte { if x != nil { return x.Data } return nil } type HTTPCallStart struct { state protoimpl.MessageState `protogen:"open.v1"` CorrelationParentSpanId uint64 `protobuf:"varint,1,opt,name=correlation_parent_span_id,json=correlationParentSpanId,proto3" json:"correlation_parent_span_id,omitempty"` Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` // start_nanotime is used to compute timings based on the // nanotime in the HTTP trace events. StartNanotime int64 `protobuf:"varint,5,opt,name=start_nanotime,json=startNanotime,proto3" json:"start_nanotime,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPCallStart) Reset() { *x = HTTPCallStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPCallStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPCallStart) ProtoMessage() {} func (x *HTTPCallStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPCallStart.ProtoReflect.Descriptor instead. func (*HTTPCallStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{42} } func (x *HTTPCallStart) GetCorrelationParentSpanId() uint64 { if x != nil { return x.CorrelationParentSpanId } return 0 } func (x *HTTPCallStart) GetMethod() string { if x != nil { return x.Method } return "" } func (x *HTTPCallStart) GetUrl() string { if x != nil { return x.Url } return "" } func (x *HTTPCallStart) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } func (x *HTTPCallStart) GetStartNanotime() int64 { if x != nil { return x.StartNanotime } return 0 } type HTTPCallEnd struct { state protoimpl.MessageState `protogen:"open.v1"` // status_code is set if we got a HTTP response. StatusCode *uint32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3,oneof" json:"status_code,omitempty"` // err is set otherwise. Err *Error `protobuf:"bytes,2,opt,name=err,proto3,oneof" json:"err,omitempty"` // TODO these should be moved to be asynchronous via a separate event. TraceEvents []*HTTPTraceEvent `protobuf:"bytes,3,rep,name=trace_events,json=traceEvents,proto3" json:"trace_events,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPCallEnd) Reset() { *x = HTTPCallEnd{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPCallEnd) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPCallEnd) ProtoMessage() {} func (x *HTTPCallEnd) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPCallEnd.ProtoReflect.Descriptor instead. func (*HTTPCallEnd) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{43} } func (x *HTTPCallEnd) GetStatusCode() uint32 { if x != nil && x.StatusCode != nil { return *x.StatusCode } return 0 } func (x *HTTPCallEnd) GetErr() *Error { if x != nil { return x.Err } return nil } func (x *HTTPCallEnd) GetTraceEvents() []*HTTPTraceEvent { if x != nil { return x.TraceEvents } return nil } type HTTPTraceEvent struct { state protoimpl.MessageState `protogen:"open.v1"` Nanotime int64 `protobuf:"varint,1,opt,name=nanotime,proto3" json:"nanotime,omitempty"` // Types that are valid to be assigned to Data: // // *HTTPTraceEvent_GetConn // *HTTPTraceEvent_GotConn // *HTTPTraceEvent_GotFirstResponseByte // *HTTPTraceEvent_Got_1XxResponse // *HTTPTraceEvent_DnsStart // *HTTPTraceEvent_DnsDone // *HTTPTraceEvent_ConnectStart // *HTTPTraceEvent_ConnectDone // *HTTPTraceEvent_TlsHandshakeStart // *HTTPTraceEvent_TlsHandshakeDone // *HTTPTraceEvent_WroteHeaders // *HTTPTraceEvent_WroteRequest // *HTTPTraceEvent_Wait_100Continue // *HTTPTraceEvent_ClosedBody Data isHTTPTraceEvent_Data `protobuf_oneof:"data"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPTraceEvent) Reset() { *x = HTTPTraceEvent{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPTraceEvent) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPTraceEvent) ProtoMessage() {} func (x *HTTPTraceEvent) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPTraceEvent.ProtoReflect.Descriptor instead. func (*HTTPTraceEvent) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{44} } func (x *HTTPTraceEvent) GetNanotime() int64 { if x != nil { return x.Nanotime } return 0 } func (x *HTTPTraceEvent) GetData() isHTTPTraceEvent_Data { if x != nil { return x.Data } return nil } func (x *HTTPTraceEvent) GetGetConn() *HTTPGetConn { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_GetConn); ok { return x.GetConn } } return nil } func (x *HTTPTraceEvent) GetGotConn() *HTTPGotConn { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_GotConn); ok { return x.GotConn } } return nil } func (x *HTTPTraceEvent) GetGotFirstResponseByte() *HTTPGotFirstResponseByte { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_GotFirstResponseByte); ok { return x.GotFirstResponseByte } } return nil } func (x *HTTPTraceEvent) GetGot_1XxResponse() *HTTPGot1XxResponse { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_Got_1XxResponse); ok { return x.Got_1XxResponse } } return nil } func (x *HTTPTraceEvent) GetDnsStart() *HTTPDNSStart { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_DnsStart); ok { return x.DnsStart } } return nil } func (x *HTTPTraceEvent) GetDnsDone() *HTTPDNSDone { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_DnsDone); ok { return x.DnsDone } } return nil } func (x *HTTPTraceEvent) GetConnectStart() *HTTPConnectStart { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_ConnectStart); ok { return x.ConnectStart } } return nil } func (x *HTTPTraceEvent) GetConnectDone() *HTTPConnectDone { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_ConnectDone); ok { return x.ConnectDone } } return nil } func (x *HTTPTraceEvent) GetTlsHandshakeStart() *HTTPTLSHandshakeStart { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeStart); ok { return x.TlsHandshakeStart } } return nil } func (x *HTTPTraceEvent) GetTlsHandshakeDone() *HTTPTLSHandshakeDone { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeDone); ok { return x.TlsHandshakeDone } } return nil } func (x *HTTPTraceEvent) GetWroteHeaders() *HTTPWroteHeaders { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_WroteHeaders); ok { return x.WroteHeaders } } return nil } func (x *HTTPTraceEvent) GetWroteRequest() *HTTPWroteRequest { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_WroteRequest); ok { return x.WroteRequest } } return nil } func (x *HTTPTraceEvent) GetWait_100Continue() *HTTPWait100Continue { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_Wait_100Continue); ok { return x.Wait_100Continue } } return nil } func (x *HTTPTraceEvent) GetClosedBody() *HTTPClosedBodyData { if x != nil { if x, ok := x.Data.(*HTTPTraceEvent_ClosedBody); ok { return x.ClosedBody } } return nil } type isHTTPTraceEvent_Data interface { isHTTPTraceEvent_Data() } type HTTPTraceEvent_GetConn struct { GetConn *HTTPGetConn `protobuf:"bytes,2,opt,name=get_conn,json=getConn,proto3,oneof"` } type HTTPTraceEvent_GotConn struct { GotConn *HTTPGotConn `protobuf:"bytes,3,opt,name=got_conn,json=gotConn,proto3,oneof"` } type HTTPTraceEvent_GotFirstResponseByte struct { GotFirstResponseByte *HTTPGotFirstResponseByte `protobuf:"bytes,4,opt,name=got_first_response_byte,json=gotFirstResponseByte,proto3,oneof"` } type HTTPTraceEvent_Got_1XxResponse struct { Got_1XxResponse *HTTPGot1XxResponse `protobuf:"bytes,5,opt,name=got_1xx_response,json=got1xxResponse,proto3,oneof"` } type HTTPTraceEvent_DnsStart struct { DnsStart *HTTPDNSStart `protobuf:"bytes,6,opt,name=dns_start,json=dnsStart,proto3,oneof"` } type HTTPTraceEvent_DnsDone struct { DnsDone *HTTPDNSDone `protobuf:"bytes,7,opt,name=dns_done,json=dnsDone,proto3,oneof"` } type HTTPTraceEvent_ConnectStart struct { ConnectStart *HTTPConnectStart `protobuf:"bytes,8,opt,name=connect_start,json=connectStart,proto3,oneof"` } type HTTPTraceEvent_ConnectDone struct { ConnectDone *HTTPConnectDone `protobuf:"bytes,9,opt,name=connect_done,json=connectDone,proto3,oneof"` } type HTTPTraceEvent_TlsHandshakeStart struct { TlsHandshakeStart *HTTPTLSHandshakeStart `protobuf:"bytes,10,opt,name=tls_handshake_start,json=tlsHandshakeStart,proto3,oneof"` } type HTTPTraceEvent_TlsHandshakeDone struct { TlsHandshakeDone *HTTPTLSHandshakeDone `protobuf:"bytes,11,opt,name=tls_handshake_done,json=tlsHandshakeDone,proto3,oneof"` } type HTTPTraceEvent_WroteHeaders struct { WroteHeaders *HTTPWroteHeaders `protobuf:"bytes,12,opt,name=wrote_headers,json=wroteHeaders,proto3,oneof"` } type HTTPTraceEvent_WroteRequest struct { WroteRequest *HTTPWroteRequest `protobuf:"bytes,13,opt,name=wrote_request,json=wroteRequest,proto3,oneof"` } type HTTPTraceEvent_Wait_100Continue struct { Wait_100Continue *HTTPWait100Continue `protobuf:"bytes,14,opt,name=wait_100_continue,json=wait100Continue,proto3,oneof"` } type HTTPTraceEvent_ClosedBody struct { ClosedBody *HTTPClosedBodyData `protobuf:"bytes,15,opt,name=closed_body,json=closedBody,proto3,oneof"` } func (*HTTPTraceEvent_GetConn) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_GotConn) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_GotFirstResponseByte) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_Got_1XxResponse) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_DnsStart) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_DnsDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_ConnectStart) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_ConnectDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_TlsHandshakeStart) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_TlsHandshakeDone) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_WroteHeaders) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_WroteRequest) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_Wait_100Continue) isHTTPTraceEvent_Data() {} func (*HTTPTraceEvent_ClosedBody) isHTTPTraceEvent_Data() {} type HTTPGetConn struct { state protoimpl.MessageState `protogen:"open.v1"` HostPort string `protobuf:"bytes,1,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGetConn) Reset() { *x = HTTPGetConn{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGetConn) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGetConn) ProtoMessage() {} func (x *HTTPGetConn) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGetConn.ProtoReflect.Descriptor instead. func (*HTTPGetConn) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{45} } func (x *HTTPGetConn) GetHostPort() string { if x != nil { return x.HostPort } return "" } type HTTPGotConn struct { state protoimpl.MessageState `protogen:"open.v1"` Reused bool `protobuf:"varint,1,opt,name=reused,proto3" json:"reused,omitempty"` WasIdle bool `protobuf:"varint,2,opt,name=was_idle,json=wasIdle,proto3" json:"was_idle,omitempty"` IdleDurationNs int64 `protobuf:"varint,3,opt,name=idle_duration_ns,json=idleDurationNs,proto3" json:"idle_duration_ns,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGotConn) Reset() { *x = HTTPGotConn{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGotConn) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGotConn) ProtoMessage() {} func (x *HTTPGotConn) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGotConn.ProtoReflect.Descriptor instead. func (*HTTPGotConn) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{46} } func (x *HTTPGotConn) GetReused() bool { if x != nil { return x.Reused } return false } func (x *HTTPGotConn) GetWasIdle() bool { if x != nil { return x.WasIdle } return false } func (x *HTTPGotConn) GetIdleDurationNs() int64 { if x != nil { return x.IdleDurationNs } return 0 } type HTTPGotFirstResponseByte struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGotFirstResponseByte) Reset() { *x = HTTPGotFirstResponseByte{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGotFirstResponseByte) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGotFirstResponseByte) ProtoMessage() {} func (x *HTTPGotFirstResponseByte) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGotFirstResponseByte.ProtoReflect.Descriptor instead. func (*HTTPGotFirstResponseByte) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{47} } type HTTPGot1XxResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPGot1XxResponse) Reset() { *x = HTTPGot1XxResponse{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPGot1XxResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPGot1XxResponse) ProtoMessage() {} func (x *HTTPGot1XxResponse) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPGot1XxResponse.ProtoReflect.Descriptor instead. func (*HTTPGot1XxResponse) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{48} } func (x *HTTPGot1XxResponse) GetCode() int32 { if x != nil { return x.Code } return 0 } type HTTPDNSStart struct { state protoimpl.MessageState `protogen:"open.v1"` Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPDNSStart) Reset() { *x = HTTPDNSStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPDNSStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPDNSStart) ProtoMessage() {} func (x *HTTPDNSStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPDNSStart.ProtoReflect.Descriptor instead. func (*HTTPDNSStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{49} } func (x *HTTPDNSStart) GetHost() string { if x != nil { return x.Host } return "" } type HTTPDNSDone struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` Addrs []*DNSAddr `protobuf:"bytes,2,rep,name=addrs,proto3" json:"addrs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPDNSDone) Reset() { *x = HTTPDNSDone{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPDNSDone) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPDNSDone) ProtoMessage() {} func (x *HTTPDNSDone) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPDNSDone.ProtoReflect.Descriptor instead. func (*HTTPDNSDone) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{50} } func (x *HTTPDNSDone) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *HTTPDNSDone) GetAddrs() []*DNSAddr { if x != nil { return x.Addrs } return nil } type DNSAddr struct { state protoimpl.MessageState `protogen:"open.v1"` Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DNSAddr) Reset() { *x = DNSAddr{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DNSAddr) String() string { return protoimpl.X.MessageStringOf(x) } func (*DNSAddr) ProtoMessage() {} func (x *DNSAddr) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DNSAddr.ProtoReflect.Descriptor instead. func (*DNSAddr) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{51} } func (x *DNSAddr) GetIp() []byte { if x != nil { return x.Ip } return nil } type HTTPConnectStart struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPConnectStart) Reset() { *x = HTTPConnectStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPConnectStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPConnectStart) ProtoMessage() {} func (x *HTTPConnectStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPConnectStart.ProtoReflect.Descriptor instead. func (*HTTPConnectStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{52} } func (x *HTTPConnectStart) GetNetwork() string { if x != nil { return x.Network } return "" } func (x *HTTPConnectStart) GetAddr() string { if x != nil { return x.Addr } return "" } type HTTPConnectDone struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` Err []byte `protobuf:"bytes,3,opt,name=err,proto3" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPConnectDone) Reset() { *x = HTTPConnectDone{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPConnectDone) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPConnectDone) ProtoMessage() {} func (x *HTTPConnectDone) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPConnectDone.ProtoReflect.Descriptor instead. func (*HTTPConnectDone) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{53} } func (x *HTTPConnectDone) GetNetwork() string { if x != nil { return x.Network } return "" } func (x *HTTPConnectDone) GetAddr() string { if x != nil { return x.Addr } return "" } func (x *HTTPConnectDone) GetErr() []byte { if x != nil { return x.Err } return nil } type HTTPTLSHandshakeStart struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPTLSHandshakeStart) Reset() { *x = HTTPTLSHandshakeStart{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPTLSHandshakeStart) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPTLSHandshakeStart) ProtoMessage() {} func (x *HTTPTLSHandshakeStart) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPTLSHandshakeStart.ProtoReflect.Descriptor instead. func (*HTTPTLSHandshakeStart) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{54} } type HTTPTLSHandshakeDone struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` TlsVersion uint32 `protobuf:"varint,2,opt,name=tls_version,json=tlsVersion,proto3" json:"tls_version,omitempty"` CipherSuite uint32 `protobuf:"varint,3,opt,name=cipher_suite,json=cipherSuite,proto3" json:"cipher_suite,omitempty"` ServerName string `protobuf:"bytes,4,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` NegotiatedProtocol string `protobuf:"bytes,5,opt,name=negotiated_protocol,json=negotiatedProtocol,proto3" json:"negotiated_protocol,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPTLSHandshakeDone) Reset() { *x = HTTPTLSHandshakeDone{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPTLSHandshakeDone) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPTLSHandshakeDone) ProtoMessage() {} func (x *HTTPTLSHandshakeDone) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPTLSHandshakeDone.ProtoReflect.Descriptor instead. func (*HTTPTLSHandshakeDone) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{55} } func (x *HTTPTLSHandshakeDone) GetErr() []byte { if x != nil { return x.Err } return nil } func (x *HTTPTLSHandshakeDone) GetTlsVersion() uint32 { if x != nil { return x.TlsVersion } return 0 } func (x *HTTPTLSHandshakeDone) GetCipherSuite() uint32 { if x != nil { return x.CipherSuite } return 0 } func (x *HTTPTLSHandshakeDone) GetServerName() string { if x != nil { return x.ServerName } return "" } func (x *HTTPTLSHandshakeDone) GetNegotiatedProtocol() string { if x != nil { return x.NegotiatedProtocol } return "" } type HTTPWroteHeaders struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPWroteHeaders) Reset() { *x = HTTPWroteHeaders{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPWroteHeaders) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPWroteHeaders) ProtoMessage() {} func (x *HTTPWroteHeaders) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPWroteHeaders.ProtoReflect.Descriptor instead. func (*HTTPWroteHeaders) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{56} } type HTTPWroteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPWroteRequest) Reset() { *x = HTTPWroteRequest{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPWroteRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPWroteRequest) ProtoMessage() {} func (x *HTTPWroteRequest) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPWroteRequest.ProtoReflect.Descriptor instead. func (*HTTPWroteRequest) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{57} } func (x *HTTPWroteRequest) GetErr() []byte { if x != nil { return x.Err } return nil } type HTTPWait100Continue struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPWait100Continue) Reset() { *x = HTTPWait100Continue{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPWait100Continue) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPWait100Continue) ProtoMessage() {} func (x *HTTPWait100Continue) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPWait100Continue.ProtoReflect.Descriptor instead. func (*HTTPWait100Continue) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{58} } type HTTPClosedBodyData struct { state protoimpl.MessageState `protogen:"open.v1"` Err []byte `protobuf:"bytes,1,opt,name=err,proto3,oneof" json:"err,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HTTPClosedBodyData) Reset() { *x = HTTPClosedBodyData{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HTTPClosedBodyData) String() string { return protoimpl.X.MessageStringOf(x) } func (*HTTPClosedBodyData) ProtoMessage() {} func (x *HTTPClosedBodyData) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HTTPClosedBodyData.ProtoReflect.Descriptor instead. func (*HTTPClosedBodyData) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{59} } func (x *HTTPClosedBodyData) GetErr() []byte { if x != nil { return x.Err } return nil } type LogMessage struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogMessage_Level `protobuf:"varint,1,opt,name=level,proto3,enum=encore.engine.trace2.LogMessage_Level" json:"level,omitempty"` Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` Fields []*LogField `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` Stack *StackTrace `protobuf:"bytes,4,opt,name=stack,proto3" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LogMessage) Reset() { *x = LogMessage{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *LogMessage) String() string { return protoimpl.X.MessageStringOf(x) } func (*LogMessage) ProtoMessage() {} func (x *LogMessage) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LogMessage.ProtoReflect.Descriptor instead. func (*LogMessage) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{60} } func (x *LogMessage) GetLevel() LogMessage_Level { if x != nil { return x.Level } return LogMessage_DEBUG } func (x *LogMessage) GetMsg() string { if x != nil { return x.Msg } return "" } func (x *LogMessage) GetFields() []*LogField { if x != nil { return x.Fields } return nil } func (x *LogMessage) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } type LogField struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Types that are valid to be assigned to Value: // // *LogField_Error // *LogField_Str // *LogField_Bool // *LogField_Time // *LogField_Dur // *LogField_Uuid // *LogField_Json // *LogField_Int // *LogField_Uint // *LogField_Float32 // *LogField_Float64 Value isLogField_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LogField) Reset() { *x = LogField{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *LogField) String() string { return protoimpl.X.MessageStringOf(x) } func (*LogField) ProtoMessage() {} func (x *LogField) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LogField.ProtoReflect.Descriptor instead. func (*LogField) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{61} } func (x *LogField) GetKey() string { if x != nil { return x.Key } return "" } func (x *LogField) GetValue() isLogField_Value { if x != nil { return x.Value } return nil } func (x *LogField) GetError() *Error { if x != nil { if x, ok := x.Value.(*LogField_Error); ok { return x.Error } } return nil } func (x *LogField) GetStr() string { if x != nil { if x, ok := x.Value.(*LogField_Str); ok { return x.Str } } return "" } func (x *LogField) GetBool() bool { if x != nil { if x, ok := x.Value.(*LogField_Bool); ok { return x.Bool } } return false } func (x *LogField) GetTime() *timestamppb.Timestamp { if x != nil { if x, ok := x.Value.(*LogField_Time); ok { return x.Time } } return nil } func (x *LogField) GetDur() int64 { if x != nil { if x, ok := x.Value.(*LogField_Dur); ok { return x.Dur } } return 0 } func (x *LogField) GetUuid() []byte { if x != nil { if x, ok := x.Value.(*LogField_Uuid); ok { return x.Uuid } } return nil } func (x *LogField) GetJson() []byte { if x != nil { if x, ok := x.Value.(*LogField_Json); ok { return x.Json } } return nil } func (x *LogField) GetInt() int64 { if x != nil { if x, ok := x.Value.(*LogField_Int); ok { return x.Int } } return 0 } func (x *LogField) GetUint() uint64 { if x != nil { if x, ok := x.Value.(*LogField_Uint); ok { return x.Uint } } return 0 } func (x *LogField) GetFloat32() float32 { if x != nil { if x, ok := x.Value.(*LogField_Float32); ok { return x.Float32 } } return 0 } func (x *LogField) GetFloat64() float64 { if x != nil { if x, ok := x.Value.(*LogField_Float64); ok { return x.Float64 } } return 0 } type isLogField_Value interface { isLogField_Value() } type LogField_Error struct { Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof"` } type LogField_Str struct { Str string `protobuf:"bytes,3,opt,name=str,proto3,oneof"` } type LogField_Bool struct { Bool bool `protobuf:"varint,4,opt,name=bool,proto3,oneof"` } type LogField_Time struct { Time *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=time,proto3,oneof"` } type LogField_Dur struct { Dur int64 `protobuf:"varint,6,opt,name=dur,proto3,oneof"` } type LogField_Uuid struct { Uuid []byte `protobuf:"bytes,7,opt,name=uuid,proto3,oneof"` } type LogField_Json struct { Json []byte `protobuf:"bytes,8,opt,name=json,proto3,oneof"` } type LogField_Int struct { Int int64 `protobuf:"varint,9,opt,name=int,proto3,oneof"` } type LogField_Uint struct { Uint uint64 `protobuf:"varint,10,opt,name=uint,proto3,oneof"` } type LogField_Float32 struct { Float32 float32 `protobuf:"fixed32,11,opt,name=float32,proto3,oneof"` } type LogField_Float64 struct { Float64 float64 `protobuf:"fixed64,12,opt,name=float64,proto3,oneof"` } func (*LogField_Error) isLogField_Value() {} func (*LogField_Str) isLogField_Value() {} func (*LogField_Bool) isLogField_Value() {} func (*LogField_Time) isLogField_Value() {} func (*LogField_Dur) isLogField_Value() {} func (*LogField_Uuid) isLogField_Value() {} func (*LogField_Json) isLogField_Value() {} func (*LogField_Int) isLogField_Value() {} func (*LogField_Uint) isLogField_Value() {} func (*LogField_Float32) isLogField_Value() {} func (*LogField_Float64) isLogField_Value() {} type StackTrace struct { state protoimpl.MessageState `protogen:"open.v1"` Pcs []int64 `protobuf:"varint,1,rep,packed,name=pcs,proto3" json:"pcs,omitempty"` Frames []*StackFrame `protobuf:"bytes,2,rep,name=frames,proto3" json:"frames,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StackTrace) Reset() { *x = StackTrace{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StackTrace) String() string { return protoimpl.X.MessageStringOf(x) } func (*StackTrace) ProtoMessage() {} func (x *StackTrace) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StackTrace.ProtoReflect.Descriptor instead. func (*StackTrace) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{62} } func (x *StackTrace) GetPcs() []int64 { if x != nil { return x.Pcs } return nil } func (x *StackTrace) GetFrames() []*StackFrame { if x != nil { return x.Frames } return nil } type StackFrame struct { state protoimpl.MessageState `protogen:"open.v1"` Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` Func string `protobuf:"bytes,2,opt,name=func,proto3" json:"func,omitempty"` Line int32 `protobuf:"varint,3,opt,name=line,proto3" json:"line,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StackFrame) Reset() { *x = StackFrame{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StackFrame) String() string { return protoimpl.X.MessageStringOf(x) } func (*StackFrame) ProtoMessage() {} func (x *StackFrame) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StackFrame.ProtoReflect.Descriptor instead. func (*StackFrame) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{63} } func (x *StackFrame) GetFilename() string { if x != nil { return x.Filename } return "" } func (x *StackFrame) GetFunc() string { if x != nil { return x.Func } return "" } func (x *StackFrame) GetLine() int32 { if x != nil { return x.Line } return 0 } type Error struct { state protoimpl.MessageState `protogen:"open.v1"` Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` Stack *StackTrace `protobuf:"bytes,2,opt,name=stack,proto3,oneof" json:"stack,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Error) Reset() { *x = Error{} mi := &file_encore_engine_trace2_trace2_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Error) String() string { return protoimpl.X.MessageStringOf(x) } func (*Error) ProtoMessage() {} func (x *Error) ProtoReflect() protoreflect.Message { mi := &file_encore_engine_trace2_trace2_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Error.ProtoReflect.Descriptor instead. func (*Error) Descriptor() ([]byte, []int) { return file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{64} } func (x *Error) GetMsg() string { if x != nil { return x.Msg } return "" } func (x *Error) GetStack() *StackTrace { if x != nil { return x.Stack } return nil } var File_encore_engine_trace2_trace2_proto protoreflect.FileDescriptor const file_encore_engine_trace2_trace2_proto_rawDesc = "" + "\n" + "!encore/engine/trace2/trace2.proto\x12\x14encore.engine.trace2\x1a\x1fgoogle/protobuf/timestamp.proto\"\xad\a\n" + "\vSpanSummary\x12\x19\n" + "\btrace_id\x18\x01 \x01(\tR\atraceId\x12\x17\n" + "\aspan_id\x18\x02 \x01(\tR\x06spanId\x12>\n" + "\x04type\x18\x03 \x01(\x0e2*.encore.engine.trace2.SpanSummary.SpanTypeR\x04type\x12\x17\n" + "\ais_root\x18\x04 \x01(\bR\x06isRoot\x12\x19\n" + "\bis_error\x18\x05 \x01(\bR\aisError\x12'\n" + "\x0fdeployed_commit\x18\x06 \x01(\tR\x0edeployedCommit\x129\n" + "\n" + "started_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12%\n" + "\x0eduration_nanos\x18\b \x01(\x04R\rdurationNanos\x12!\n" + "\fservice_name\x18\t \x01(\tR\vserviceName\x12(\n" + "\rendpoint_name\x18\n" + " \x01(\tH\x00R\fendpointName\x88\x01\x01\x12\"\n" + "\n" + "topic_name\x18\v \x01(\tH\x01R\ttopicName\x88\x01\x01\x120\n" + "\x11subscription_name\x18\f \x01(\tH\x02R\x10subscriptionName\x88\x01\x01\x12\"\n" + "\n" + "message_id\x18\r \x01(\tH\x03R\tmessageId\x88\x01\x01\x12&\n" + "\ftest_skipped\x18\x0e \x01(\bH\x04R\vtestSkipped\x88\x01\x01\x12\x1e\n" + "\bsrc_file\x18\x0f \x01(\tH\x05R\asrcFile\x88\x01\x01\x12\x1e\n" + "\bsrc_line\x18\x10 \x01(\rH\x06R\asrcLine\x88\x01\x01\x12)\n" + "\x0eparent_span_id\x18\x11 \x01(\tH\aR\fparentSpanId\x88\x01\x01\x12+\n" + "\x0fcaller_event_id\x18\x12 \x01(\x04H\bR\rcallerEventId\x88\x01\x01\"L\n" + "\bSpanType\x12\v\n" + "\aUNKNOWN\x10\x00\x12\v\n" + "\aREQUEST\x10\x01\x12\b\n" + "\x04AUTH\x10\x02\x12\x12\n" + "\x0ePUBSUB_MESSAGE\x10\x03\x12\b\n" + "\x04TEST\x10\x04B\x10\n" + "\x0e_endpoint_nameB\r\n" + "\v_topic_nameB\x14\n" + "\x12_subscription_nameB\r\n" + "\v_message_idB\x0f\n" + "\r_test_skippedB\v\n" + "\t_src_fileB\v\n" + "\t_src_lineB\x11\n" + "\x0f_parent_span_idB\x12\n" + "\x10_caller_event_id\"/\n" + "\aTraceID\x12\x12\n" + "\x04high\x18\x01 \x01(\x04R\x04high\x12\x10\n" + "\x03low\x18\x02 \x01(\x04R\x03low\"E\n" + "\tEventList\x128\n" + "\x06events\x18\x01 \x03(\v2 .encore.engine.trace2.TraceEventR\x06events\"\xfe\x02\n" + "\n" + "TraceEvent\x128\n" + "\btrace_id\x18\x01 \x01(\v2\x1d.encore.engine.trace2.TraceIDR\atraceId\x12\x17\n" + "\aspan_id\x18\x02 \x01(\x04R\x06spanId\x12\x19\n" + "\bevent_id\x18\x03 \x01(\x04R\aeventId\x129\n" + "\n" + "event_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12@\n" + "\n" + "span_start\x18\n" + " \x01(\v2\x1f.encore.engine.trace2.SpanStartH\x00R\tspanStart\x12:\n" + "\bspan_end\x18\v \x01(\v2\x1d.encore.engine.trace2.SpanEndH\x00R\aspanEnd\x12@\n" + "\n" + "span_event\x18\f \x01(\v2\x1f.encore.engine.trace2.SpanEventH\x00R\tspanEventB\a\n" + "\x05event\"\x9a\x05\n" + "\tSpanStart\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12J\n" + "\x0fparent_trace_id\x18\x02 \x01(\v2\x1d.encore.engine.trace2.TraceIDH\x01R\rparentTraceId\x88\x01\x01\x12)\n" + "\x0eparent_span_id\x18\x03 \x01(\x04H\x02R\fparentSpanId\x88\x01\x01\x12+\n" + "\x0fcaller_event_id\x18\x04 \x01(\x04H\x03R\rcallerEventId\x88\x01\x01\x12;\n" + "\x17external_correlation_id\x18\x05 \x01(\tH\x04R\x15externalCorrelationId\x88\x01\x01\x12\x1c\n" + "\adef_loc\x18\x06 \x01(\rH\x05R\x06defLoc\x88\x01\x01\x12B\n" + "\arequest\x18\n" + " \x01(\v2&.encore.engine.trace2.RequestSpanStartH\x00R\arequest\x129\n" + "\x04auth\x18\v \x01(\v2#.encore.engine.trace2.AuthSpanStartH\x00R\x04auth\x12U\n" + "\x0epubsub_message\x18\f \x01(\v2,.encore.engine.trace2.PubsubMessageSpanStartH\x00R\rpubsubMessage\x129\n" + "\x04test\x18\r \x01(\v2#.encore.engine.trace2.TestSpanStartH\x00R\x04testB\x06\n" + "\x04dataB\x12\n" + "\x10_parent_trace_idB\x11\n" + "\x0f_parent_span_idB\x12\n" + "\x10_caller_event_idB\x1a\n" + "\x18_external_correlation_idB\n" + "\n" + "\b_def_loc\"\xbc\x05\n" + "\aSpanEnd\x12%\n" + "\x0eduration_nanos\x18\x01 \x01(\x04R\rdurationNanos\x126\n" + "\x05error\x18\x02 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x01R\x05error\x88\x01\x01\x12F\n" + "\vpanic_stack\x18\x03 \x01(\v2 .encore.engine.trace2.StackTraceH\x02R\n" + "panicStack\x88\x01\x01\x12J\n" + "\x0fparent_trace_id\x18\x04 \x01(\v2\x1d.encore.engine.trace2.TraceIDH\x03R\rparentTraceId\x88\x01\x01\x12)\n" + "\x0eparent_span_id\x18\x05 \x01(\x04H\x04R\fparentSpanId\x88\x01\x01\x12A\n" + "\vstatus_code\x18\x06 \x01(\x0e2 .encore.engine.trace2.StatusCodeR\n" + "statusCode\x12@\n" + "\arequest\x18\n" + " \x01(\v2$.encore.engine.trace2.RequestSpanEndH\x00R\arequest\x127\n" + "\x04auth\x18\v \x01(\v2!.encore.engine.trace2.AuthSpanEndH\x00R\x04auth\x12S\n" + "\x0epubsub_message\x18\f \x01(\v2*.encore.engine.trace2.PubsubMessageSpanEndH\x00R\rpubsubMessage\x127\n" + "\x04test\x18\r \x01(\v2!.encore.engine.trace2.TestSpanEndH\x00R\x04testB\x06\n" + "\x04dataB\b\n" + "\x06_errorB\x0e\n" + "\f_panic_stackB\x12\n" + "\x10_parent_trace_idB\x11\n" + "\x0f_parent_span_id\"\x9b\x04\n" + "\x10RequestSpanStart\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12#\n" + "\rendpoint_name\x18\x02 \x01(\tR\fendpointName\x12\x1f\n" + "\vhttp_method\x18\x03 \x01(\tR\n" + "httpMethod\x12\x12\n" + "\x04path\x18\x04 \x01(\tR\x04path\x12\x1f\n" + "\vpath_params\x18\x05 \x03(\tR\n" + "pathParams\x12c\n" + "\x0frequest_headers\x18\x06 \x03(\v2:.encore.engine.trace2.RequestSpanStart.RequestHeadersEntryR\x0erequestHeaders\x12,\n" + "\x0frequest_payload\x18\a \x01(\fH\x00R\x0erequestPayload\x88\x01\x01\x121\n" + "\x12ext_correlation_id\x18\b \x01(\tH\x01R\x10extCorrelationId\x88\x01\x01\x12\x15\n" + "\x03uid\x18\t \x01(\tH\x02R\x03uid\x88\x01\x01\x12\x16\n" + "\x06mocked\x18\n" + " \x01(\bR\x06mocked\x1aA\n" + "\x13RequestHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x12\n" + "\x10_request_payloadB\x15\n" + "\x13_ext_correlation_idB\x06\n" + "\x04_uid\"\xd1\x03\n" + "\x0eRequestSpanEnd\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12#\n" + "\rendpoint_name\x18\x02 \x01(\tR\fendpointName\x12(\n" + "\x10http_status_code\x18\x03 \x01(\rR\x0ehttpStatusCode\x12d\n" + "\x10response_headers\x18\x04 \x03(\v29.encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntryR\x0fresponseHeaders\x12.\n" + "\x10response_payload\x18\x05 \x01(\fH\x00R\x0fresponsePayload\x88\x01\x01\x12+\n" + "\x0fcaller_event_id\x18\x06 \x01(\x04H\x01R\rcallerEventId\x88\x01\x01\x12\x15\n" + "\x03uid\x18\a \x01(\tH\x02R\x03uid\x88\x01\x01\x1aB\n" + "\x14ResponseHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x13\n" + "\x11_response_payloadB\x12\n" + "\x10_caller_event_idB\x06\n" + "\x04_uid\"\x90\x01\n" + "\rAuthSpanStart\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12#\n" + "\rendpoint_name\x18\x02 \x01(\tR\fendpointName\x12&\n" + "\fauth_payload\x18\x03 \x01(\fH\x00R\vauthPayload\x88\x01\x01B\x0f\n" + "\r_auth_payload\"\x97\x01\n" + "\vAuthSpanEnd\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12#\n" + "\rendpoint_name\x18\x02 \x01(\tR\fendpointName\x12\x10\n" + "\x03uid\x18\x03 \x01(\tR\x03uid\x12 \n" + "\tuser_data\x18\x04 \x01(\fH\x00R\buserData\x88\x01\x01B\f\n" + "\n" + "_user_data\"\xc1\x02\n" + "\x16PubsubMessageSpanStart\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1d\n" + "\n" + "topic_name\x18\x02 \x01(\tR\ttopicName\x12+\n" + "\x11subscription_name\x18\x03 \x01(\tR\x10subscriptionName\x12\x1d\n" + "\n" + "message_id\x18\x04 \x01(\tR\tmessageId\x12\x18\n" + "\aattempt\x18\x05 \x01(\rR\aattempt\x12=\n" + "\fpublish_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vpublishTime\x12,\n" + "\x0fmessage_payload\x18\a \x01(\fH\x00R\x0emessagePayload\x88\x01\x01B\x12\n" + "\x10_message_payload\"\xa4\x01\n" + "\x14PubsubMessageSpanEnd\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1d\n" + "\n" + "topic_name\x18\x02 \x01(\tR\ttopicName\x12+\n" + "\x11subscription_name\x18\x03 \x01(\tR\x10subscriptionName\x12\x1d\n" + "\n" + "message_id\x18\x04 \x01(\tR\tmessageId\"\x9b\x01\n" + "\rTestSpanStart\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1b\n" + "\ttest_name\x18\x02 \x01(\tR\btestName\x12\x10\n" + "\x03uid\x18\x03 \x01(\tR\x03uid\x12\x1b\n" + "\ttest_file\x18\x04 \x01(\tR\btestFile\x12\x1b\n" + "\ttest_line\x18\x05 \x01(\rR\btestLine\"\x9e\x01\n" + "\vTestSpanEnd\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1b\n" + "\ttest_name\x18\x02 \x01(\tR\btestName\x12\x16\n" + "\x06failed\x18\x03 \x01(\bR\x06failed\x12\x18\n" + "\askipped\x18\x04 \x01(\bR\askipped\x12\x15\n" + "\x03uid\x18\x05 \x01(\tH\x00R\x03uid\x88\x01\x01B\x06\n" + "\x04_uid\"\xe3\x13\n" + "\tSpanEvent\x12\x12\n" + "\x04goid\x18\x01 \x01(\rR\x04goid\x12\x1c\n" + "\adef_loc\x18\x02 \x01(\rH\x01R\x06defLoc\x88\x01\x01\x125\n" + "\x14correlation_event_id\x18\x03 \x01(\x04H\x02R\x12correlationEventId\x88\x01\x01\x12C\n" + "\vlog_message\x18\n" + " \x01(\v2 .encore.engine.trace2.LogMessageH\x00R\n" + "logMessage\x12C\n" + "\vbody_stream\x18\v \x01(\v2 .encore.engine.trace2.BodyStreamH\x00R\n" + "bodyStream\x12J\n" + "\x0erpc_call_start\x18\f \x01(\v2\".encore.engine.trace2.RPCCallStartH\x00R\frpcCallStart\x12D\n" + "\frpc_call_end\x18\r \x01(\v2 .encore.engine.trace2.RPCCallEndH\x00R\n" + "rpcCallEnd\x12\\\n" + "\x14db_transaction_start\x18\x0e \x01(\v2(.encore.engine.trace2.DBTransactionStartH\x00R\x12dbTransactionStart\x12V\n" + "\x12db_transaction_end\x18\x0f \x01(\v2&.encore.engine.trace2.DBTransactionEndH\x00R\x10dbTransactionEnd\x12J\n" + "\x0edb_query_start\x18\x10 \x01(\v2\".encore.engine.trace2.DBQueryStartH\x00R\fdbQueryStart\x12D\n" + "\fdb_query_end\x18\x11 \x01(\v2 .encore.engine.trace2.DBQueryEndH\x00R\n" + "dbQueryEnd\x12M\n" + "\x0fhttp_call_start\x18\x12 \x01(\v2#.encore.engine.trace2.HTTPCallStartH\x00R\rhttpCallStart\x12G\n" + "\rhttp_call_end\x18\x13 \x01(\v2!.encore.engine.trace2.HTTPCallEndH\x00R\vhttpCallEnd\x12\\\n" + "\x14pubsub_publish_start\x18\x14 \x01(\v2(.encore.engine.trace2.PubsubPublishStartH\x00R\x12pubsubPublishStart\x12V\n" + "\x12pubsub_publish_end\x18\x15 \x01(\v2&.encore.engine.trace2.PubsubPublishEndH\x00R\x10pubsubPublishEnd\x12P\n" + "\x10cache_call_start\x18\x16 \x01(\v2$.encore.engine.trace2.CacheCallStartH\x00R\x0ecacheCallStart\x12J\n" + "\x0ecache_call_end\x18\x17 \x01(\v2\".encore.engine.trace2.CacheCallEndH\x00R\fcacheCallEnd\x12V\n" + "\x12service_init_start\x18\x18 \x01(\v2&.encore.engine.trace2.ServiceInitStartH\x00R\x10serviceInitStart\x12P\n" + "\x10service_init_end\x18\x19 \x01(\v2$.encore.engine.trace2.ServiceInitEndH\x00R\x0eserviceInitEnd\x12l\n" + "\x1abucket_object_upload_start\x18\x1a \x01(\v2-.encore.engine.trace2.BucketObjectUploadStartH\x00R\x17bucketObjectUploadStart\x12f\n" + "\x18bucket_object_upload_end\x18\x1b \x01(\v2+.encore.engine.trace2.BucketObjectUploadEndH\x00R\x15bucketObjectUploadEnd\x12r\n" + "\x1cbucket_object_download_start\x18\x1c \x01(\v2/.encore.engine.trace2.BucketObjectDownloadStartH\x00R\x19bucketObjectDownloadStart\x12l\n" + "\x1abucket_object_download_end\x18\x1d \x01(\v2-.encore.engine.trace2.BucketObjectDownloadEndH\x00R\x17bucketObjectDownloadEnd\x12s\n" + "\x1dbucket_object_get_attrs_start\x18\x1e \x01(\v2/.encore.engine.trace2.BucketObjectGetAttrsStartH\x00R\x19bucketObjectGetAttrsStart\x12m\n" + "\x1bbucket_object_get_attrs_end\x18\x1f \x01(\v2-.encore.engine.trace2.BucketObjectGetAttrsEndH\x00R\x17bucketObjectGetAttrsEnd\x12i\n" + "\x19bucket_list_objects_start\x18 \x01(\v2,.encore.engine.trace2.BucketListObjectsStartH\x00R\x16bucketListObjectsStart\x12c\n" + "\x17bucket_list_objects_end\x18! \x01(\v2*.encore.engine.trace2.BucketListObjectsEndH\x00R\x14bucketListObjectsEnd\x12o\n" + "\x1bbucket_delete_objects_start\x18\" \x01(\v2..encore.engine.trace2.BucketDeleteObjectsStartH\x00R\x18bucketDeleteObjectsStart\x12i\n" + "\x19bucket_delete_objects_end\x18# \x01(\v2,.encore.engine.trace2.BucketDeleteObjectsEndH\x00R\x16bucketDeleteObjectsEndB\x06\n" + "\x04dataB\n" + "\n" + "\b_def_locB\x17\n" + "\x15_correlation_event_id\"\xa8\x01\n" + "\fRPCCallStart\x12.\n" + "\x13target_service_name\x18\x01 \x01(\tR\x11targetServiceName\x120\n" + "\x14target_endpoint_name\x18\x02 \x01(\tR\x12targetEndpointName\x126\n" + "\x05stack\x18\x03 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"H\n" + "\n" + "RPCCallEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"\x10\n" + "\x0eGoroutineStart\"\x0e\n" + "\fGoroutineEnd\"L\n" + "\x12DBTransactionStart\x126\n" + "\x05stack\x18\x01 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"\x89\x02\n" + "\x10DBTransactionEnd\x12U\n" + "\n" + "completion\x18\x01 \x01(\x0e25.encore.engine.trace2.DBTransactionEnd.CompletionTypeR\n" + "completion\x126\n" + "\x05stack\x18\x02 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\x122\n" + "\x03err\x18\x03 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\"*\n" + "\x0eCompletionType\x12\f\n" + "\bROLLBACK\x10\x00\x12\n" + "\n" + "\x06COMMIT\x10\x01B\x06\n" + "\x04_err\"\\\n" + "\fDBQueryStart\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x126\n" + "\x05stack\x18\x02 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"H\n" + "\n" + "DBQueryEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"|\n" + "\x12PubsubPublishStart\x12\x14\n" + "\x05topic\x18\x01 \x01(\tR\x05topic\x12\x18\n" + "\amessage\x18\x02 \x01(\fR\amessage\x126\n" + "\x05stack\x18\x03 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"\x81\x01\n" + "\x10PubsubPublishEnd\x12\"\n" + "\n" + "message_id\x18\x01 \x01(\tH\x00R\tmessageId\x88\x01\x01\x122\n" + "\x03err\x18\x02 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x01R\x03err\x88\x01\x01B\r\n" + "\v_message_idB\x06\n" + "\x04_err\",\n" + "\x10ServiceInitStart\x12\x18\n" + "\aservice\x18\x01 \x01(\tR\aservice\"L\n" + "\x0eServiceInitEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"\x90\x01\n" + "\x0eCacheCallStart\x12\x1c\n" + "\toperation\x18\x01 \x01(\tR\toperation\x12\x12\n" + "\x04keys\x18\x02 \x03(\tR\x04keys\x12\x14\n" + "\x05write\x18\x03 \x01(\bR\x05write\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"\xd4\x01\n" + "\fCacheCallEnd\x12A\n" + "\x06result\x18\x01 \x01(\x0e2).encore.engine.trace2.CacheCallEnd.ResultR\x06result\x122\n" + "\x03err\x18\x02 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\"E\n" + "\x06Result\x12\v\n" + "\aUNKNOWN\x10\x00\x12\x06\n" + "\x02OK\x10\x01\x12\x0f\n" + "\vNO_SUCH_KEY\x10\x02\x12\f\n" + "\bCONFLICT\x10\x03\x12\a\n" + "\x03ERR\x10\x04B\x06\n" + "\x04_err\"\xc5\x01\n" + "\x17BucketObjectUploadStart\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x12\x16\n" + "\x06object\x18\x02 \x01(\tR\x06object\x12B\n" + "\x05attrs\x18\x03 \x01(\v2,.encore.engine.trace2.BucketObjectAttributesR\x05attrs\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"\xa0\x01\n" + "\x15BucketObjectUploadEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\x12\x17\n" + "\x04size\x18\x02 \x01(\x04H\x01R\x04size\x88\x01\x01\x12\x1d\n" + "\aversion\x18\x03 \x01(\tH\x02R\aversion\x88\x01\x01B\x06\n" + "\x04_errB\a\n" + "\x05_sizeB\n" + "\n" + "\b_version\"\xae\x01\n" + "\x19BucketObjectDownloadStart\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x12\x16\n" + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1d\n" + "\aversion\x18\x03 \x01(\tH\x00R\aversion\x88\x01\x01\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stackB\n" + "\n" + "\b_version\"w\n" + "\x17BucketObjectDownloadEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\x12\x17\n" + "\x04size\x18\x02 \x01(\x04H\x01R\x04size\x88\x01\x01B\x06\n" + "\x04_errB\a\n" + "\x05_size\"\xae\x01\n" + "\x19BucketObjectGetAttrsStart\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x12\x16\n" + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1d\n" + "\aversion\x18\x03 \x01(\tH\x00R\aversion\x88\x01\x01\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stackB\n" + "\n" + "\b_version\"\xa8\x01\n" + "\x17BucketObjectGetAttrsEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\x12G\n" + "\x05attrs\x18\x02 \x01(\v2,.encore.engine.trace2.BucketObjectAttributesH\x01R\x05attrs\x88\x01\x01B\x06\n" + "\x04_errB\b\n" + "\x06_attrs\"\x90\x01\n" + "\x16BucketListObjectsStart\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x12\x1b\n" + "\x06prefix\x18\x02 \x01(\tH\x00R\x06prefix\x88\x01\x01\x126\n" + "\x05stack\x18\x03 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stackB\t\n" + "\a_prefix\"\x89\x01\n" + "\x14BucketListObjectsEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01\x12\x1a\n" + "\bobserved\x18\x02 \x01(\x04R\bobserved\x12\x19\n" + "\bhas_more\x18\x03 \x01(\bR\ahasMoreB\x06\n" + "\x04_err\"\xb3\x01\n" + "\x18BucketDeleteObjectsStart\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x126\n" + "\x05stack\x18\x02 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\x12G\n" + "\aentries\x18\x03 \x03(\v2-.encore.engine.trace2.BucketDeleteObjectEntryR\aentries\"\\\n" + "\x17BucketDeleteObjectEntry\x12\x16\n" + "\x06object\x18\x01 \x01(\tR\x06object\x12\x1d\n" + "\aversion\x18\x02 \x01(\tH\x00R\aversion\x88\x01\x01B\n" + "\n" + "\b_version\"T\n" + "\x16BucketDeleteObjectsEnd\x122\n" + "\x03err\x18\x01 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"\xc0\x01\n" + "\x16BucketObjectAttributes\x12\x17\n" + "\x04size\x18\x01 \x01(\x04H\x00R\x04size\x88\x01\x01\x12\x1d\n" + "\aversion\x18\x02 \x01(\tH\x01R\aversion\x88\x01\x01\x12\x17\n" + "\x04etag\x18\x03 \x01(\tH\x02R\x04etag\x88\x01\x01\x12&\n" + "\fcontent_type\x18\x04 \x01(\tH\x03R\vcontentType\x88\x01\x01B\a\n" + "\x05_sizeB\n" + "\n" + "\b_versionB\a\n" + "\x05_etagB\x0f\n" + "\r_content_type\"a\n" + "\n" + "BodyStream\x12\x1f\n" + "\vis_response\x18\x01 \x01(\bR\n" + "isResponse\x12\x1e\n" + "\n" + "overflowed\x18\x02 \x01(\bR\n" + "overflowed\x12\x12\n" + "\x04data\x18\x03 \x01(\fR\x04data\"\xd5\x01\n" + "\rHTTPCallStart\x12;\n" + "\x1acorrelation_parent_span_id\x18\x01 \x01(\x04R\x17correlationParentSpanId\x12\x16\n" + "\x06method\x18\x02 \x01(\tR\x06method\x12\x10\n" + "\x03url\x18\x03 \x01(\tR\x03url\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\x12%\n" + "\x0estart_nanotime\x18\x05 \x01(\x03R\rstartNanotime\"\xc8\x01\n" + "\vHTTPCallEnd\x12$\n" + "\vstatus_code\x18\x01 \x01(\rH\x00R\n" + "statusCode\x88\x01\x01\x122\n" + "\x03err\x18\x02 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x01R\x03err\x88\x01\x01\x12G\n" + "\ftrace_events\x18\x03 \x03(\v2$.encore.engine.trace2.HTTPTraceEventR\vtraceEventsB\x0e\n" + "\f_status_codeB\x06\n" + "\x04_err\"\x90\t\n" + "\x0eHTTPTraceEvent\x12\x1a\n" + "\bnanotime\x18\x01 \x01(\x03R\bnanotime\x12>\n" + "\bget_conn\x18\x02 \x01(\v2!.encore.engine.trace2.HTTPGetConnH\x00R\agetConn\x12>\n" + "\bgot_conn\x18\x03 \x01(\v2!.encore.engine.trace2.HTTPGotConnH\x00R\agotConn\x12g\n" + "\x17got_first_response_byte\x18\x04 \x01(\v2..encore.engine.trace2.HTTPGotFirstResponseByteH\x00R\x14gotFirstResponseByte\x12T\n" + "\x10got_1xx_response\x18\x05 \x01(\v2(.encore.engine.trace2.HTTPGot1xxResponseH\x00R\x0egot1xxResponse\x12A\n" + "\tdns_start\x18\x06 \x01(\v2\".encore.engine.trace2.HTTPDNSStartH\x00R\bdnsStart\x12>\n" + "\bdns_done\x18\a \x01(\v2!.encore.engine.trace2.HTTPDNSDoneH\x00R\adnsDone\x12M\n" + "\rconnect_start\x18\b \x01(\v2&.encore.engine.trace2.HTTPConnectStartH\x00R\fconnectStart\x12J\n" + "\fconnect_done\x18\t \x01(\v2%.encore.engine.trace2.HTTPConnectDoneH\x00R\vconnectDone\x12]\n" + "\x13tls_handshake_start\x18\n" + " \x01(\v2+.encore.engine.trace2.HTTPTLSHandshakeStartH\x00R\x11tlsHandshakeStart\x12Z\n" + "\x12tls_handshake_done\x18\v \x01(\v2*.encore.engine.trace2.HTTPTLSHandshakeDoneH\x00R\x10tlsHandshakeDone\x12M\n" + "\rwrote_headers\x18\f \x01(\v2&.encore.engine.trace2.HTTPWroteHeadersH\x00R\fwroteHeaders\x12M\n" + "\rwrote_request\x18\r \x01(\v2&.encore.engine.trace2.HTTPWroteRequestH\x00R\fwroteRequest\x12W\n" + "\x11wait_100_continue\x18\x0e \x01(\v2).encore.engine.trace2.HTTPWait100ContinueH\x00R\x0fwait100Continue\x12K\n" + "\vclosed_body\x18\x0f \x01(\v2(.encore.engine.trace2.HTTPClosedBodyDataH\x00R\n" + "closedBodyB\x06\n" + "\x04data\"*\n" + "\vHTTPGetConn\x12\x1b\n" + "\thost_port\x18\x01 \x01(\tR\bhostPort\"j\n" + "\vHTTPGotConn\x12\x16\n" + "\x06reused\x18\x01 \x01(\bR\x06reused\x12\x19\n" + "\bwas_idle\x18\x02 \x01(\bR\awasIdle\x12(\n" + "\x10idle_duration_ns\x18\x03 \x01(\x03R\x0eidleDurationNs\"\x1a\n" + "\x18HTTPGotFirstResponseByte\"(\n" + "\x12HTTPGot1xxResponse\x12\x12\n" + "\x04code\x18\x01 \x01(\x05R\x04code\"\"\n" + "\fHTTPDNSStart\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\"a\n" + "\vHTTPDNSDone\x12\x15\n" + "\x03err\x18\x01 \x01(\fH\x00R\x03err\x88\x01\x01\x123\n" + "\x05addrs\x18\x02 \x03(\v2\x1d.encore.engine.trace2.DNSAddrR\x05addrsB\x06\n" + "\x04_err\"\x19\n" + "\aDNSAddr\x12\x0e\n" + "\x02ip\x18\x01 \x01(\fR\x02ip\"@\n" + "\x10HTTPConnectStart\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\"Q\n" + "\x0fHTTPConnectDone\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\x12\x10\n" + "\x03err\x18\x03 \x01(\fR\x03err\"\x17\n" + "\x15HTTPTLSHandshakeStart\"\xcb\x01\n" + "\x14HTTPTLSHandshakeDone\x12\x15\n" + "\x03err\x18\x01 \x01(\fH\x00R\x03err\x88\x01\x01\x12\x1f\n" + "\vtls_version\x18\x02 \x01(\rR\n" + "tlsVersion\x12!\n" + "\fcipher_suite\x18\x03 \x01(\rR\vcipherSuite\x12\x1f\n" + "\vserver_name\x18\x04 \x01(\tR\n" + "serverName\x12/\n" + "\x13negotiated_protocol\x18\x05 \x01(\tR\x12negotiatedProtocolB\x06\n" + "\x04_err\"\x12\n" + "\x10HTTPWroteHeaders\"1\n" + "\x10HTTPWroteRequest\x12\x15\n" + "\x03err\x18\x01 \x01(\fH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"\x15\n" + "\x13HTTPWait100Continue\"3\n" + "\x12HTTPClosedBodyData\x12\x15\n" + "\x03err\x18\x01 \x01(\fH\x00R\x03err\x88\x01\x01B\x06\n" + "\x04_err\"\x8a\x02\n" + "\n" + "LogMessage\x12<\n" + "\x05level\x18\x01 \x01(\x0e2&.encore.engine.trace2.LogMessage.LevelR\x05level\x12\x10\n" + "\x03msg\x18\x02 \x01(\tR\x03msg\x126\n" + "\x06fields\x18\x03 \x03(\v2\x1e.encore.engine.trace2.LogFieldR\x06fields\x126\n" + "\x05stack\x18\x04 \x01(\v2 .encore.engine.trace2.StackTraceR\x05stack\"<\n" + "\x05Level\x12\t\n" + "\x05DEBUG\x10\x00\x12\b\n" + "\x04INFO\x10\x01\x12\t\n" + "\x05ERROR\x10\x02\x12\b\n" + "\x04WARN\x10\x03\x12\t\n" + "\x05TRACE\x10\x04\"\xd8\x02\n" + "\bLogField\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x123\n" + "\x05error\x18\x02 \x01(\v2\x1b.encore.engine.trace2.ErrorH\x00R\x05error\x12\x12\n" + "\x03str\x18\x03 \x01(\tH\x00R\x03str\x12\x14\n" + "\x04bool\x18\x04 \x01(\bH\x00R\x04bool\x120\n" + "\x04time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\x04time\x12\x12\n" + "\x03dur\x18\x06 \x01(\x03H\x00R\x03dur\x12\x14\n" + "\x04uuid\x18\a \x01(\fH\x00R\x04uuid\x12\x14\n" + "\x04json\x18\b \x01(\fH\x00R\x04json\x12\x12\n" + "\x03int\x18\t \x01(\x03H\x00R\x03int\x12\x14\n" + "\x04uint\x18\n" + " \x01(\x04H\x00R\x04uint\x12\x1a\n" + "\afloat32\x18\v \x01(\x02H\x00R\afloat32\x12\x1a\n" + "\afloat64\x18\f \x01(\x01H\x00R\afloat64B\a\n" + "\x05value\"X\n" + "\n" + "StackTrace\x12\x10\n" + "\x03pcs\x18\x01 \x03(\x03R\x03pcs\x128\n" + "\x06frames\x18\x02 \x03(\v2 .encore.engine.trace2.StackFrameR\x06frames\"P\n" + "\n" + "StackFrame\x12\x1a\n" + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x12\n" + "\x04func\x18\x02 \x01(\tR\x04func\x12\x12\n" + "\x04line\x18\x03 \x01(\x05R\x04line\"`\n" + "\x05Error\x12\x10\n" + "\x03msg\x18\x01 \x01(\tR\x03msg\x12;\n" + "\x05stack\x18\x02 \x01(\v2 .encore.engine.trace2.StackTraceH\x00R\x05stack\x88\x01\x01B\b\n" + "\x06_stack*\xb1\x02\n" + "\x12HTTPTraceEventCode\x12\v\n" + "\aUNKNOWN\x10\x00\x12\f\n" + "\bGET_CONN\x10\x01\x12\f\n" + "\bGOT_CONN\x10\x02\x12\x1b\n" + "\x17GOT_FIRST_RESPONSE_BYTE\x10\x03\x12\x14\n" + "\x10GOT_1XX_RESPONSE\x10\x04\x12\r\n" + "\tDNS_START\x10\x05\x12\f\n" + "\bDNS_DONE\x10\x06\x12\x11\n" + "\rCONNECT_START\x10\a\x12\x10\n" + "\fCONNECT_DONE\x10\b\x12\x17\n" + "\x13TLS_HANDSHAKE_START\x10\t\x12\x16\n" + "\x12TLS_HANDSHAKE_DONE\x10\n" + "\x12\x11\n" + "\rWROTE_HEADERS\x10\v\x12\x11\n" + "\rWROTE_REQUEST\x10\f\x12\x15\n" + "\x11WAIT_100_CONTINUE\x10\r\x12\x0f\n" + "\vCLOSED_BODY\x10\x0e*\x88\x04\n" + "\n" + "StatusCode\x12\x12\n" + "\x0eSTATUS_CODE_OK\x10\x00\x12\x18\n" + "\x14STATUS_CODE_CANCELED\x10\x01\x12\x17\n" + "\x13STATUS_CODE_UNKNOWN\x10\x02\x12 \n" + "\x1cSTATUS_CODE_INVALID_ARGUMENT\x10\x03\x12!\n" + "\x1dSTATUS_CODE_DEADLINE_EXCEEDED\x10\x04\x12\x19\n" + "\x15STATUS_CODE_NOT_FOUND\x10\x05\x12\x1e\n" + "\x1aSTATUS_CODE_ALREADY_EXISTS\x10\x06\x12!\n" + "\x1dSTATUS_CODE_PERMISSION_DENIED\x10\a\x12\"\n" + "\x1eSTATUS_CODE_RESOURCE_EXHAUSTED\x10\b\x12#\n" + "\x1fSTATUS_CODE_FAILED_PRECONDITION\x10\t\x12\x17\n" + "\x13STATUS_CODE_ABORTED\x10\n" + "\x12\x1c\n" + "\x18STATUS_CODE_OUT_OF_RANGE\x10\v\x12\x1d\n" + "\x19STATUS_CODE_UNIMPLEMENTED\x10\f\x12\x18\n" + "\x14STATUS_CODE_INTERNAL\x10\r\x12\x1b\n" + "\x17STATUS_CODE_UNAVAILABLE\x10\x0e\x12\x19\n" + "\x15STATUS_CODE_DATA_LOSS\x10\x0f\x12\x1f\n" + "\x1bSTATUS_CODE_UNAUTHENTICATED\x10\x10B%Z#encr.dev/proto/encore/engine/trace2b\x06proto3" var ( file_encore_engine_trace2_trace2_proto_rawDescOnce sync.Once file_encore_engine_trace2_trace2_proto_rawDescData []byte ) func file_encore_engine_trace2_trace2_proto_rawDescGZIP() []byte { file_encore_engine_trace2_trace2_proto_rawDescOnce.Do(func() { file_encore_engine_trace2_trace2_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_engine_trace2_trace2_proto_rawDesc), len(file_encore_engine_trace2_trace2_proto_rawDesc))) }) return file_encore_engine_trace2_trace2_proto_rawDescData } var file_encore_engine_trace2_trace2_proto_enumTypes = make([]protoimpl.EnumInfo, 6) var file_encore_engine_trace2_trace2_proto_msgTypes = make([]protoimpl.MessageInfo, 67) var file_encore_engine_trace2_trace2_proto_goTypes = []any{ (HTTPTraceEventCode)(0), // 0: encore.engine.trace2.HTTPTraceEventCode (StatusCode)(0), // 1: encore.engine.trace2.StatusCode (SpanSummary_SpanType)(0), // 2: encore.engine.trace2.SpanSummary.SpanType (DBTransactionEnd_CompletionType)(0), // 3: encore.engine.trace2.DBTransactionEnd.CompletionType (CacheCallEnd_Result)(0), // 4: encore.engine.trace2.CacheCallEnd.Result (LogMessage_Level)(0), // 5: encore.engine.trace2.LogMessage.Level (*SpanSummary)(nil), // 6: encore.engine.trace2.SpanSummary (*TraceID)(nil), // 7: encore.engine.trace2.TraceID (*EventList)(nil), // 8: encore.engine.trace2.EventList (*TraceEvent)(nil), // 9: encore.engine.trace2.TraceEvent (*SpanStart)(nil), // 10: encore.engine.trace2.SpanStart (*SpanEnd)(nil), // 11: encore.engine.trace2.SpanEnd (*RequestSpanStart)(nil), // 12: encore.engine.trace2.RequestSpanStart (*RequestSpanEnd)(nil), // 13: encore.engine.trace2.RequestSpanEnd (*AuthSpanStart)(nil), // 14: encore.engine.trace2.AuthSpanStart (*AuthSpanEnd)(nil), // 15: encore.engine.trace2.AuthSpanEnd (*PubsubMessageSpanStart)(nil), // 16: encore.engine.trace2.PubsubMessageSpanStart (*PubsubMessageSpanEnd)(nil), // 17: encore.engine.trace2.PubsubMessageSpanEnd (*TestSpanStart)(nil), // 18: encore.engine.trace2.TestSpanStart (*TestSpanEnd)(nil), // 19: encore.engine.trace2.TestSpanEnd (*SpanEvent)(nil), // 20: encore.engine.trace2.SpanEvent (*RPCCallStart)(nil), // 21: encore.engine.trace2.RPCCallStart (*RPCCallEnd)(nil), // 22: encore.engine.trace2.RPCCallEnd (*GoroutineStart)(nil), // 23: encore.engine.trace2.GoroutineStart (*GoroutineEnd)(nil), // 24: encore.engine.trace2.GoroutineEnd (*DBTransactionStart)(nil), // 25: encore.engine.trace2.DBTransactionStart (*DBTransactionEnd)(nil), // 26: encore.engine.trace2.DBTransactionEnd (*DBQueryStart)(nil), // 27: encore.engine.trace2.DBQueryStart (*DBQueryEnd)(nil), // 28: encore.engine.trace2.DBQueryEnd (*PubsubPublishStart)(nil), // 29: encore.engine.trace2.PubsubPublishStart (*PubsubPublishEnd)(nil), // 30: encore.engine.trace2.PubsubPublishEnd (*ServiceInitStart)(nil), // 31: encore.engine.trace2.ServiceInitStart (*ServiceInitEnd)(nil), // 32: encore.engine.trace2.ServiceInitEnd (*CacheCallStart)(nil), // 33: encore.engine.trace2.CacheCallStart (*CacheCallEnd)(nil), // 34: encore.engine.trace2.CacheCallEnd (*BucketObjectUploadStart)(nil), // 35: encore.engine.trace2.BucketObjectUploadStart (*BucketObjectUploadEnd)(nil), // 36: encore.engine.trace2.BucketObjectUploadEnd (*BucketObjectDownloadStart)(nil), // 37: encore.engine.trace2.BucketObjectDownloadStart (*BucketObjectDownloadEnd)(nil), // 38: encore.engine.trace2.BucketObjectDownloadEnd (*BucketObjectGetAttrsStart)(nil), // 39: encore.engine.trace2.BucketObjectGetAttrsStart (*BucketObjectGetAttrsEnd)(nil), // 40: encore.engine.trace2.BucketObjectGetAttrsEnd (*BucketListObjectsStart)(nil), // 41: encore.engine.trace2.BucketListObjectsStart (*BucketListObjectsEnd)(nil), // 42: encore.engine.trace2.BucketListObjectsEnd (*BucketDeleteObjectsStart)(nil), // 43: encore.engine.trace2.BucketDeleteObjectsStart (*BucketDeleteObjectEntry)(nil), // 44: encore.engine.trace2.BucketDeleteObjectEntry (*BucketDeleteObjectsEnd)(nil), // 45: encore.engine.trace2.BucketDeleteObjectsEnd (*BucketObjectAttributes)(nil), // 46: encore.engine.trace2.BucketObjectAttributes (*BodyStream)(nil), // 47: encore.engine.trace2.BodyStream (*HTTPCallStart)(nil), // 48: encore.engine.trace2.HTTPCallStart (*HTTPCallEnd)(nil), // 49: encore.engine.trace2.HTTPCallEnd (*HTTPTraceEvent)(nil), // 50: encore.engine.trace2.HTTPTraceEvent (*HTTPGetConn)(nil), // 51: encore.engine.trace2.HTTPGetConn (*HTTPGotConn)(nil), // 52: encore.engine.trace2.HTTPGotConn (*HTTPGotFirstResponseByte)(nil), // 53: encore.engine.trace2.HTTPGotFirstResponseByte (*HTTPGot1XxResponse)(nil), // 54: encore.engine.trace2.HTTPGot1xxResponse (*HTTPDNSStart)(nil), // 55: encore.engine.trace2.HTTPDNSStart (*HTTPDNSDone)(nil), // 56: encore.engine.trace2.HTTPDNSDone (*DNSAddr)(nil), // 57: encore.engine.trace2.DNSAddr (*HTTPConnectStart)(nil), // 58: encore.engine.trace2.HTTPConnectStart (*HTTPConnectDone)(nil), // 59: encore.engine.trace2.HTTPConnectDone (*HTTPTLSHandshakeStart)(nil), // 60: encore.engine.trace2.HTTPTLSHandshakeStart (*HTTPTLSHandshakeDone)(nil), // 61: encore.engine.trace2.HTTPTLSHandshakeDone (*HTTPWroteHeaders)(nil), // 62: encore.engine.trace2.HTTPWroteHeaders (*HTTPWroteRequest)(nil), // 63: encore.engine.trace2.HTTPWroteRequest (*HTTPWait100Continue)(nil), // 64: encore.engine.trace2.HTTPWait100Continue (*HTTPClosedBodyData)(nil), // 65: encore.engine.trace2.HTTPClosedBodyData (*LogMessage)(nil), // 66: encore.engine.trace2.LogMessage (*LogField)(nil), // 67: encore.engine.trace2.LogField (*StackTrace)(nil), // 68: encore.engine.trace2.StackTrace (*StackFrame)(nil), // 69: encore.engine.trace2.StackFrame (*Error)(nil), // 70: encore.engine.trace2.Error nil, // 71: encore.engine.trace2.RequestSpanStart.RequestHeadersEntry nil, // 72: encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntry (*timestamppb.Timestamp)(nil), // 73: google.protobuf.Timestamp } var file_encore_engine_trace2_trace2_proto_depIdxs = []int32{ 2, // 0: encore.engine.trace2.SpanSummary.type:type_name -> encore.engine.trace2.SpanSummary.SpanType 73, // 1: encore.engine.trace2.SpanSummary.started_at:type_name -> google.protobuf.Timestamp 9, // 2: encore.engine.trace2.EventList.events:type_name -> encore.engine.trace2.TraceEvent 7, // 3: encore.engine.trace2.TraceEvent.trace_id:type_name -> encore.engine.trace2.TraceID 73, // 4: encore.engine.trace2.TraceEvent.event_time:type_name -> google.protobuf.Timestamp 10, // 5: encore.engine.trace2.TraceEvent.span_start:type_name -> encore.engine.trace2.SpanStart 11, // 6: encore.engine.trace2.TraceEvent.span_end:type_name -> encore.engine.trace2.SpanEnd 20, // 7: encore.engine.trace2.TraceEvent.span_event:type_name -> encore.engine.trace2.SpanEvent 7, // 8: encore.engine.trace2.SpanStart.parent_trace_id:type_name -> encore.engine.trace2.TraceID 12, // 9: encore.engine.trace2.SpanStart.request:type_name -> encore.engine.trace2.RequestSpanStart 14, // 10: encore.engine.trace2.SpanStart.auth:type_name -> encore.engine.trace2.AuthSpanStart 16, // 11: encore.engine.trace2.SpanStart.pubsub_message:type_name -> encore.engine.trace2.PubsubMessageSpanStart 18, // 12: encore.engine.trace2.SpanStart.test:type_name -> encore.engine.trace2.TestSpanStart 70, // 13: encore.engine.trace2.SpanEnd.error:type_name -> encore.engine.trace2.Error 68, // 14: encore.engine.trace2.SpanEnd.panic_stack:type_name -> encore.engine.trace2.StackTrace 7, // 15: encore.engine.trace2.SpanEnd.parent_trace_id:type_name -> encore.engine.trace2.TraceID 1, // 16: encore.engine.trace2.SpanEnd.status_code:type_name -> encore.engine.trace2.StatusCode 13, // 17: encore.engine.trace2.SpanEnd.request:type_name -> encore.engine.trace2.RequestSpanEnd 15, // 18: encore.engine.trace2.SpanEnd.auth:type_name -> encore.engine.trace2.AuthSpanEnd 17, // 19: encore.engine.trace2.SpanEnd.pubsub_message:type_name -> encore.engine.trace2.PubsubMessageSpanEnd 19, // 20: encore.engine.trace2.SpanEnd.test:type_name -> encore.engine.trace2.TestSpanEnd 71, // 21: encore.engine.trace2.RequestSpanStart.request_headers:type_name -> encore.engine.trace2.RequestSpanStart.RequestHeadersEntry 72, // 22: encore.engine.trace2.RequestSpanEnd.response_headers:type_name -> encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntry 73, // 23: encore.engine.trace2.PubsubMessageSpanStart.publish_time:type_name -> google.protobuf.Timestamp 66, // 24: encore.engine.trace2.SpanEvent.log_message:type_name -> encore.engine.trace2.LogMessage 47, // 25: encore.engine.trace2.SpanEvent.body_stream:type_name -> encore.engine.trace2.BodyStream 21, // 26: encore.engine.trace2.SpanEvent.rpc_call_start:type_name -> encore.engine.trace2.RPCCallStart 22, // 27: encore.engine.trace2.SpanEvent.rpc_call_end:type_name -> encore.engine.trace2.RPCCallEnd 25, // 28: encore.engine.trace2.SpanEvent.db_transaction_start:type_name -> encore.engine.trace2.DBTransactionStart 26, // 29: encore.engine.trace2.SpanEvent.db_transaction_end:type_name -> encore.engine.trace2.DBTransactionEnd 27, // 30: encore.engine.trace2.SpanEvent.db_query_start:type_name -> encore.engine.trace2.DBQueryStart 28, // 31: encore.engine.trace2.SpanEvent.db_query_end:type_name -> encore.engine.trace2.DBQueryEnd 48, // 32: encore.engine.trace2.SpanEvent.http_call_start:type_name -> encore.engine.trace2.HTTPCallStart 49, // 33: encore.engine.trace2.SpanEvent.http_call_end:type_name -> encore.engine.trace2.HTTPCallEnd 29, // 34: encore.engine.trace2.SpanEvent.pubsub_publish_start:type_name -> encore.engine.trace2.PubsubPublishStart 30, // 35: encore.engine.trace2.SpanEvent.pubsub_publish_end:type_name -> encore.engine.trace2.PubsubPublishEnd 33, // 36: encore.engine.trace2.SpanEvent.cache_call_start:type_name -> encore.engine.trace2.CacheCallStart 34, // 37: encore.engine.trace2.SpanEvent.cache_call_end:type_name -> encore.engine.trace2.CacheCallEnd 31, // 38: encore.engine.trace2.SpanEvent.service_init_start:type_name -> encore.engine.trace2.ServiceInitStart 32, // 39: encore.engine.trace2.SpanEvent.service_init_end:type_name -> encore.engine.trace2.ServiceInitEnd 35, // 40: encore.engine.trace2.SpanEvent.bucket_object_upload_start:type_name -> encore.engine.trace2.BucketObjectUploadStart 36, // 41: encore.engine.trace2.SpanEvent.bucket_object_upload_end:type_name -> encore.engine.trace2.BucketObjectUploadEnd 37, // 42: encore.engine.trace2.SpanEvent.bucket_object_download_start:type_name -> encore.engine.trace2.BucketObjectDownloadStart 38, // 43: encore.engine.trace2.SpanEvent.bucket_object_download_end:type_name -> encore.engine.trace2.BucketObjectDownloadEnd 39, // 44: encore.engine.trace2.SpanEvent.bucket_object_get_attrs_start:type_name -> encore.engine.trace2.BucketObjectGetAttrsStart 40, // 45: encore.engine.trace2.SpanEvent.bucket_object_get_attrs_end:type_name -> encore.engine.trace2.BucketObjectGetAttrsEnd 41, // 46: encore.engine.trace2.SpanEvent.bucket_list_objects_start:type_name -> encore.engine.trace2.BucketListObjectsStart 42, // 47: encore.engine.trace2.SpanEvent.bucket_list_objects_end:type_name -> encore.engine.trace2.BucketListObjectsEnd 43, // 48: encore.engine.trace2.SpanEvent.bucket_delete_objects_start:type_name -> encore.engine.trace2.BucketDeleteObjectsStart 45, // 49: encore.engine.trace2.SpanEvent.bucket_delete_objects_end:type_name -> encore.engine.trace2.BucketDeleteObjectsEnd 68, // 50: encore.engine.trace2.RPCCallStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 51: encore.engine.trace2.RPCCallEnd.err:type_name -> encore.engine.trace2.Error 68, // 52: encore.engine.trace2.DBTransactionStart.stack:type_name -> encore.engine.trace2.StackTrace 3, // 53: encore.engine.trace2.DBTransactionEnd.completion:type_name -> encore.engine.trace2.DBTransactionEnd.CompletionType 68, // 54: encore.engine.trace2.DBTransactionEnd.stack:type_name -> encore.engine.trace2.StackTrace 70, // 55: encore.engine.trace2.DBTransactionEnd.err:type_name -> encore.engine.trace2.Error 68, // 56: encore.engine.trace2.DBQueryStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 57: encore.engine.trace2.DBQueryEnd.err:type_name -> encore.engine.trace2.Error 68, // 58: encore.engine.trace2.PubsubPublishStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 59: encore.engine.trace2.PubsubPublishEnd.err:type_name -> encore.engine.trace2.Error 70, // 60: encore.engine.trace2.ServiceInitEnd.err:type_name -> encore.engine.trace2.Error 68, // 61: encore.engine.trace2.CacheCallStart.stack:type_name -> encore.engine.trace2.StackTrace 4, // 62: encore.engine.trace2.CacheCallEnd.result:type_name -> encore.engine.trace2.CacheCallEnd.Result 70, // 63: encore.engine.trace2.CacheCallEnd.err:type_name -> encore.engine.trace2.Error 46, // 64: encore.engine.trace2.BucketObjectUploadStart.attrs:type_name -> encore.engine.trace2.BucketObjectAttributes 68, // 65: encore.engine.trace2.BucketObjectUploadStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 66: encore.engine.trace2.BucketObjectUploadEnd.err:type_name -> encore.engine.trace2.Error 68, // 67: encore.engine.trace2.BucketObjectDownloadStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 68: encore.engine.trace2.BucketObjectDownloadEnd.err:type_name -> encore.engine.trace2.Error 68, // 69: encore.engine.trace2.BucketObjectGetAttrsStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 70: encore.engine.trace2.BucketObjectGetAttrsEnd.err:type_name -> encore.engine.trace2.Error 46, // 71: encore.engine.trace2.BucketObjectGetAttrsEnd.attrs:type_name -> encore.engine.trace2.BucketObjectAttributes 68, // 72: encore.engine.trace2.BucketListObjectsStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 73: encore.engine.trace2.BucketListObjectsEnd.err:type_name -> encore.engine.trace2.Error 68, // 74: encore.engine.trace2.BucketDeleteObjectsStart.stack:type_name -> encore.engine.trace2.StackTrace 44, // 75: encore.engine.trace2.BucketDeleteObjectsStart.entries:type_name -> encore.engine.trace2.BucketDeleteObjectEntry 70, // 76: encore.engine.trace2.BucketDeleteObjectsEnd.err:type_name -> encore.engine.trace2.Error 68, // 77: encore.engine.trace2.HTTPCallStart.stack:type_name -> encore.engine.trace2.StackTrace 70, // 78: encore.engine.trace2.HTTPCallEnd.err:type_name -> encore.engine.trace2.Error 50, // 79: encore.engine.trace2.HTTPCallEnd.trace_events:type_name -> encore.engine.trace2.HTTPTraceEvent 51, // 80: encore.engine.trace2.HTTPTraceEvent.get_conn:type_name -> encore.engine.trace2.HTTPGetConn 52, // 81: encore.engine.trace2.HTTPTraceEvent.got_conn:type_name -> encore.engine.trace2.HTTPGotConn 53, // 82: encore.engine.trace2.HTTPTraceEvent.got_first_response_byte:type_name -> encore.engine.trace2.HTTPGotFirstResponseByte 54, // 83: encore.engine.trace2.HTTPTraceEvent.got_1xx_response:type_name -> encore.engine.trace2.HTTPGot1xxResponse 55, // 84: encore.engine.trace2.HTTPTraceEvent.dns_start:type_name -> encore.engine.trace2.HTTPDNSStart 56, // 85: encore.engine.trace2.HTTPTraceEvent.dns_done:type_name -> encore.engine.trace2.HTTPDNSDone 58, // 86: encore.engine.trace2.HTTPTraceEvent.connect_start:type_name -> encore.engine.trace2.HTTPConnectStart 59, // 87: encore.engine.trace2.HTTPTraceEvent.connect_done:type_name -> encore.engine.trace2.HTTPConnectDone 60, // 88: encore.engine.trace2.HTTPTraceEvent.tls_handshake_start:type_name -> encore.engine.trace2.HTTPTLSHandshakeStart 61, // 89: encore.engine.trace2.HTTPTraceEvent.tls_handshake_done:type_name -> encore.engine.trace2.HTTPTLSHandshakeDone 62, // 90: encore.engine.trace2.HTTPTraceEvent.wrote_headers:type_name -> encore.engine.trace2.HTTPWroteHeaders 63, // 91: encore.engine.trace2.HTTPTraceEvent.wrote_request:type_name -> encore.engine.trace2.HTTPWroteRequest 64, // 92: encore.engine.trace2.HTTPTraceEvent.wait_100_continue:type_name -> encore.engine.trace2.HTTPWait100Continue 65, // 93: encore.engine.trace2.HTTPTraceEvent.closed_body:type_name -> encore.engine.trace2.HTTPClosedBodyData 57, // 94: encore.engine.trace2.HTTPDNSDone.addrs:type_name -> encore.engine.trace2.DNSAddr 5, // 95: encore.engine.trace2.LogMessage.level:type_name -> encore.engine.trace2.LogMessage.Level 67, // 96: encore.engine.trace2.LogMessage.fields:type_name -> encore.engine.trace2.LogField 68, // 97: encore.engine.trace2.LogMessage.stack:type_name -> encore.engine.trace2.StackTrace 70, // 98: encore.engine.trace2.LogField.error:type_name -> encore.engine.trace2.Error 73, // 99: encore.engine.trace2.LogField.time:type_name -> google.protobuf.Timestamp 69, // 100: encore.engine.trace2.StackTrace.frames:type_name -> encore.engine.trace2.StackFrame 68, // 101: encore.engine.trace2.Error.stack:type_name -> encore.engine.trace2.StackTrace 102, // [102:102] is the sub-list for method output_type 102, // [102:102] is the sub-list for method input_type 102, // [102:102] is the sub-list for extension type_name 102, // [102:102] is the sub-list for extension extendee 0, // [0:102] is the sub-list for field type_name } func init() { file_encore_engine_trace2_trace2_proto_init() } func file_encore_engine_trace2_trace2_proto_init() { if File_encore_engine_trace2_trace2_proto != nil { return } file_encore_engine_trace2_trace2_proto_msgTypes[0].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[3].OneofWrappers = []any{ (*TraceEvent_SpanStart)(nil), (*TraceEvent_SpanEnd)(nil), (*TraceEvent_SpanEvent)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[4].OneofWrappers = []any{ (*SpanStart_Request)(nil), (*SpanStart_Auth)(nil), (*SpanStart_PubsubMessage)(nil), (*SpanStart_Test)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[5].OneofWrappers = []any{ (*SpanEnd_Request)(nil), (*SpanEnd_Auth)(nil), (*SpanEnd_PubsubMessage)(nil), (*SpanEnd_Test)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[6].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[7].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[8].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[9].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[10].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[13].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[14].OneofWrappers = []any{ (*SpanEvent_LogMessage)(nil), (*SpanEvent_BodyStream)(nil), (*SpanEvent_RpcCallStart)(nil), (*SpanEvent_RpcCallEnd)(nil), (*SpanEvent_DbTransactionStart)(nil), (*SpanEvent_DbTransactionEnd)(nil), (*SpanEvent_DbQueryStart)(nil), (*SpanEvent_DbQueryEnd)(nil), (*SpanEvent_HttpCallStart)(nil), (*SpanEvent_HttpCallEnd)(nil), (*SpanEvent_PubsubPublishStart)(nil), (*SpanEvent_PubsubPublishEnd)(nil), (*SpanEvent_CacheCallStart)(nil), (*SpanEvent_CacheCallEnd)(nil), (*SpanEvent_ServiceInitStart)(nil), (*SpanEvent_ServiceInitEnd)(nil), (*SpanEvent_BucketObjectUploadStart)(nil), (*SpanEvent_BucketObjectUploadEnd)(nil), (*SpanEvent_BucketObjectDownloadStart)(nil), (*SpanEvent_BucketObjectDownloadEnd)(nil), (*SpanEvent_BucketObjectGetAttrsStart)(nil), (*SpanEvent_BucketObjectGetAttrsEnd)(nil), (*SpanEvent_BucketListObjectsStart)(nil), (*SpanEvent_BucketListObjectsEnd)(nil), (*SpanEvent_BucketDeleteObjectsStart)(nil), (*SpanEvent_BucketDeleteObjectsEnd)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[16].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[20].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[22].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[24].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[26].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[28].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[30].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[31].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[32].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[33].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[34].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[35].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[36].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[38].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[39].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[40].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[43].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[44].OneofWrappers = []any{ (*HTTPTraceEvent_GetConn)(nil), (*HTTPTraceEvent_GotConn)(nil), (*HTTPTraceEvent_GotFirstResponseByte)(nil), (*HTTPTraceEvent_Got_1XxResponse)(nil), (*HTTPTraceEvent_DnsStart)(nil), (*HTTPTraceEvent_DnsDone)(nil), (*HTTPTraceEvent_ConnectStart)(nil), (*HTTPTraceEvent_ConnectDone)(nil), (*HTTPTraceEvent_TlsHandshakeStart)(nil), (*HTTPTraceEvent_TlsHandshakeDone)(nil), (*HTTPTraceEvent_WroteHeaders)(nil), (*HTTPTraceEvent_WroteRequest)(nil), (*HTTPTraceEvent_Wait_100Continue)(nil), (*HTTPTraceEvent_ClosedBody)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[50].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[55].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[57].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[59].OneofWrappers = []any{} file_encore_engine_trace2_trace2_proto_msgTypes[61].OneofWrappers = []any{ (*LogField_Error)(nil), (*LogField_Str)(nil), (*LogField_Bool)(nil), (*LogField_Time)(nil), (*LogField_Dur)(nil), (*LogField_Uuid)(nil), (*LogField_Json)(nil), (*LogField_Int)(nil), (*LogField_Uint)(nil), (*LogField_Float32)(nil), (*LogField_Float64)(nil), } file_encore_engine_trace2_trace2_proto_msgTypes[64].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_engine_trace2_trace2_proto_rawDesc), len(file_encore_engine_trace2_trace2_proto_rawDesc)), NumEnums: 6, NumMessages: 67, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_engine_trace2_trace2_proto_goTypes, DependencyIndexes: file_encore_engine_trace2_trace2_proto_depIdxs, EnumInfos: file_encore_engine_trace2_trace2_proto_enumTypes, MessageInfos: file_encore_engine_trace2_trace2_proto_msgTypes, }.Build() File_encore_engine_trace2_trace2_proto = out.File file_encore_engine_trace2_trace2_proto_goTypes = nil file_encore_engine_trace2_trace2_proto_depIdxs = nil } ================================================ FILE: proto/encore/engine/trace2/trace2.proto ================================================ syntax = "proto3"; package encore.engine.trace2; import "google/protobuf/timestamp.proto"; option go_package = "encr.dev/proto/encore/engine/trace2"; // SpanSummary summarizes a span for display purposes. message SpanSummary { string trace_id = 1; string span_id = 2; SpanType type = 3; bool is_root = 4; // whether it's a root request bool is_error = 5; // whether the request failed string deployed_commit = 6; // the commit hash of the running service google.protobuf.Timestamp started_at = 7; uint64 duration_nanos = 8; string service_name = 9; optional string endpoint_name = 10; optional string topic_name = 11; optional string subscription_name = 12; optional string message_id = 13; optional bool test_skipped = 14; // whether the test was skipped optional string src_file = 15; // the source file where the span was started (if available) optional uint32 src_line = 16; // the source line where the span was started (if available) optional string parent_span_id = 17; // parent of the span if it's a child, if this is not populated then it's a root optional uint64 caller_event_id = 18; // the event id of the call that started this span enum SpanType { UNKNOWN = 0; REQUEST = 1; AUTH = 2; PUBSUB_MESSAGE = 3; TEST = 4; } } message TraceID { uint64 high = 1; uint64 low = 2; } message EventList { repeated TraceEvent events = 1; } message TraceEvent { TraceID trace_id = 1; uint64 span_id = 2; uint64 event_id = 3; google.protobuf.Timestamp event_time = 4; oneof event { SpanStart span_start = 10; SpanEnd span_end = 11; SpanEvent span_event = 12; } } message SpanStart { uint32 goid = 1; optional TraceID parent_trace_id = 2; optional uint64 parent_span_id = 3; optional uint64 caller_event_id = 4; optional string external_correlation_id = 5; optional uint32 def_loc = 6; oneof data { RequestSpanStart request = 10; AuthSpanStart auth = 11; PubsubMessageSpanStart pubsub_message = 12; TestSpanStart test = 13; } } message SpanEnd { uint64 duration_nanos = 1; optional Error error = 2; // panic_stack is the stack trace if the span ended due to a panic optional StackTrace panic_stack = 3; optional TraceID parent_trace_id = 4; optional uint64 parent_span_id = 5; StatusCode status_code = 6; oneof data { RequestSpanEnd request = 10; AuthSpanEnd auth = 11; PubsubMessageSpanEnd pubsub_message = 12; TestSpanEnd test = 13; } } message RequestSpanStart { string service_name = 1; string endpoint_name = 2; string http_method = 3; string path = 4; repeated string path_params = 5; map request_headers = 6; optional bytes request_payload = 7; optional string ext_correlation_id = 8; optional string uid = 9; // mocked is true if the request was handled by a mock bool mocked = 10; } message RequestSpanEnd { // Repeat service/endpoint name here to make it possible // to consume end events without having to look up the start. string service_name = 1; string endpoint_name = 2; uint32 http_status_code = 3; map response_headers = 4; optional bytes response_payload = 5; optional uint64 caller_event_id = 6; optional string uid = 7; } message AuthSpanStart { string service_name = 1; string endpoint_name = 2; optional bytes auth_payload = 3; } message AuthSpanEnd { // Repeat service/endpoint name here to make it possible // to consume end events without having to look up the start. string service_name = 1; string endpoint_name = 2; string uid = 3; optional bytes user_data = 4; } message PubsubMessageSpanStart { string service_name = 1; string topic_name = 2; string subscription_name = 3; string message_id = 4; uint32 attempt = 5; google.protobuf.Timestamp publish_time = 6; optional bytes message_payload = 7; } message PubsubMessageSpanEnd { // Repeat service/topic/subscription name here to make it possible // to consume end events without having to look up the start. string service_name = 1; string topic_name = 2; string subscription_name = 3; string message_id = 4; } message TestSpanStart { string service_name = 1; string test_name = 2; string uid = 3; string test_file = 4; uint32 test_line = 5; } message TestSpanEnd { string service_name = 1; string test_name = 2; bool failed = 3; bool skipped = 4; optional string uid = 5; } message SpanEvent { uint32 goid = 1; optional uint32 def_loc = 2; // correlation_event_id is the other event // this event is correlated with. optional uint64 correlation_event_id = 3; oneof data { LogMessage log_message = 10; BodyStream body_stream = 11; RPCCallStart rpc_call_start = 12; RPCCallEnd rpc_call_end = 13; DBTransactionStart db_transaction_start = 14; DBTransactionEnd db_transaction_end = 15; DBQueryStart db_query_start = 16; DBQueryEnd db_query_end = 17; HTTPCallStart http_call_start = 18; HTTPCallEnd http_call_end = 19; PubsubPublishStart pubsub_publish_start = 20; PubsubPublishEnd pubsub_publish_end = 21; CacheCallStart cache_call_start = 22; CacheCallEnd cache_call_end = 23; ServiceInitStart service_init_start = 24; ServiceInitEnd service_init_end = 25; BucketObjectUploadStart bucket_object_upload_start = 26; BucketObjectUploadEnd bucket_object_upload_end = 27; BucketObjectDownloadStart bucket_object_download_start = 28; BucketObjectDownloadEnd bucket_object_download_end = 29; BucketObjectGetAttrsStart bucket_object_get_attrs_start = 30; BucketObjectGetAttrsEnd bucket_object_get_attrs_end = 31; BucketListObjectsStart bucket_list_objects_start = 32; BucketListObjectsEnd bucket_list_objects_end = 33; BucketDeleteObjectsStart bucket_delete_objects_start = 34; BucketDeleteObjectsEnd bucket_delete_objects_end = 35; } } message RPCCallStart { string target_service_name = 1; string target_endpoint_name = 2; StackTrace stack = 3; } message RPCCallEnd { optional Error err = 1; } message GoroutineStart {} message GoroutineEnd {} message DBTransactionStart { StackTrace stack = 1; } message DBTransactionEnd { enum CompletionType { ROLLBACK = 0; COMMIT = 1; } CompletionType completion = 1; StackTrace stack = 2; optional Error err = 3; } message DBQueryStart { string query = 1; StackTrace stack = 2; } message DBQueryEnd { optional Error err = 1; } message PubsubPublishStart { string topic = 1; bytes message = 2; StackTrace stack = 3; } message PubsubPublishEnd { optional string message_id = 1; optional Error err = 2; } message ServiceInitStart { string service = 1; } message ServiceInitEnd { optional Error err = 1; } message CacheCallStart { string operation = 1; repeated string keys = 2; bool write = 3; StackTrace stack = 4; // TODO include more info (like inputs) } message CacheCallEnd { Result result = 1; optional Error err = 2; // TODO include more info (like outputs) enum Result { UNKNOWN = 0; OK = 1; NO_SUCH_KEY = 2; CONFLICT = 3; ERR = 4; } } message BucketObjectUploadStart { string bucket = 1; string object = 2; BucketObjectAttributes attrs = 3; StackTrace stack = 4; } message BucketObjectUploadEnd { optional Error err = 1; optional uint64 size = 2; optional string version = 3; } message BucketObjectDownloadStart { string bucket = 1; string object = 2; optional string version = 3; StackTrace stack = 4; } message BucketObjectDownloadEnd { optional Error err = 1; optional uint64 size = 2; } message BucketObjectGetAttrsStart { string bucket = 1; string object = 2; optional string version = 3; StackTrace stack = 4; } message BucketObjectGetAttrsEnd { optional Error err = 1; optional BucketObjectAttributes attrs = 2; } message BucketListObjectsStart { string bucket = 1; optional string prefix = 2; StackTrace stack = 3; } message BucketListObjectsEnd { optional Error err = 1; uint64 observed = 2; bool has_more = 3; } message BucketDeleteObjectsStart { string bucket = 1; StackTrace stack = 2; repeated BucketDeleteObjectEntry entries = 3; } message BucketDeleteObjectEntry { string object = 1; optional string version = 2; } message BucketDeleteObjectsEnd { optional Error err = 1; } message BucketObjectAttributes { optional uint64 size = 1; optional string version = 2; optional string etag = 3; optional string content_type = 4; } message BodyStream { bool is_response = 1; bool overflowed = 2; bytes data = 3; } message HTTPCallStart { uint64 correlation_parent_span_id = 1; string method = 2; string url = 3; StackTrace stack = 4; // start_nanotime is used to compute timings based on the // nanotime in the HTTP trace events. int64 start_nanotime = 5; } message HTTPCallEnd { // status_code is set if we got a HTTP response. optional uint32 status_code = 1; // err is set otherwise. optional Error err = 2; // TODO these should be moved to be asynchronous via a separate event. repeated HTTPTraceEvent trace_events = 3; } enum HTTPTraceEventCode { UNKNOWN = 0; GET_CONN = 1; GOT_CONN = 2; GOT_FIRST_RESPONSE_BYTE = 3; GOT_1XX_RESPONSE = 4; DNS_START = 5; DNS_DONE = 6; CONNECT_START = 7; CONNECT_DONE = 8; TLS_HANDSHAKE_START = 9; TLS_HANDSHAKE_DONE = 10; WROTE_HEADERS = 11; WROTE_REQUEST = 12; WAIT_100_CONTINUE = 13; CLOSED_BODY = 14; } message HTTPTraceEvent { int64 nanotime = 1; oneof data { HTTPGetConn get_conn = 2; HTTPGotConn got_conn = 3; HTTPGotFirstResponseByte got_first_response_byte = 4; HTTPGot1xxResponse got_1xx_response = 5; HTTPDNSStart dns_start = 6; HTTPDNSDone dns_done = 7; HTTPConnectStart connect_start = 8; HTTPConnectDone connect_done = 9; HTTPTLSHandshakeStart tls_handshake_start = 10; HTTPTLSHandshakeDone tls_handshake_done = 11; HTTPWroteHeaders wrote_headers = 12; HTTPWroteRequest wrote_request = 13; HTTPWait100Continue wait_100_continue = 14; HTTPClosedBodyData closed_body = 15; } } message HTTPGetConn { string host_port = 1; } message HTTPGotConn { bool reused = 1; bool was_idle = 2; int64 idle_duration_ns = 3; } message HTTPGotFirstResponseByte {} message HTTPGot1xxResponse { int32 code = 1; } message HTTPDNSStart { string host = 1; } message HTTPDNSDone { optional bytes err = 1; repeated DNSAddr addrs = 2; } message DNSAddr { bytes ip = 1; } message HTTPConnectStart { string network = 1; string addr = 2; } message HTTPConnectDone { string network = 1; string addr = 2; bytes err = 3; } message HTTPTLSHandshakeStart {} message HTTPTLSHandshakeDone { optional bytes err = 1; uint32 tls_version = 2; uint32 cipher_suite = 3; string server_name = 4; string negotiated_protocol = 5; } message HTTPWroteHeaders {} message HTTPWroteRequest { optional bytes err = 1; } message HTTPWait100Continue {} message HTTPClosedBodyData { optional bytes err = 1; } message LogMessage { // Note: These values don't match the values used by the binary trace protocol, // as these values are stored in persisted traces and therefore must maintain // backwards compatibility. The binary trace protocol is versioned and doesn't // have the same limitations. enum Level { DEBUG = 0; INFO = 1; ERROR = 2; WARN = 3; TRACE = 4; } Level level = 1; string msg = 2; repeated LogField fields = 3; StackTrace stack = 4; } message LogField { string key = 1; oneof value { Error error = 2; string str = 3; bool bool = 4; google.protobuf.Timestamp time = 5; int64 dur = 6; bytes uuid = 7; bytes json = 8; int64 int = 9; uint64 uint = 10; float float32 = 11; double float64 = 12; } } message StackTrace { repeated int64 pcs = 1; repeated StackFrame frames = 2; } message StackFrame { string filename = 1; string func = 2; int32 line = 3; } message Error { string msg = 1; optional StackTrace stack = 2; } enum StatusCode { STATUS_CODE_OK = 0; STATUS_CODE_CANCELED = 1; STATUS_CODE_UNKNOWN = 2; STATUS_CODE_INVALID_ARGUMENT = 3; STATUS_CODE_DEADLINE_EXCEEDED = 4; STATUS_CODE_NOT_FOUND = 5; STATUS_CODE_ALREADY_EXISTS = 6; STATUS_CODE_PERMISSION_DENIED = 7; STATUS_CODE_RESOURCE_EXHAUSTED = 8; STATUS_CODE_FAILED_PRECONDITION = 9; STATUS_CODE_ABORTED = 10; STATUS_CODE_OUT_OF_RANGE = 11; STATUS_CODE_UNIMPLEMENTED = 12; STATUS_CODE_INTERNAL = 13; STATUS_CODE_UNAVAILABLE = 14; STATUS_CODE_DATA_LOSS = 15; STATUS_CODE_UNAUTHENTICATED = 16; } ================================================ FILE: proto/encore/engine/trace2/trace_util.go ================================================ package trace2 func (id *TraceID) IsZero() bool { return id == nil || (id.Low == 0 && id.High == 0) } ================================================ FILE: proto/encore/parser/meta/v1/meta.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/parser/meta/v1/meta.proto package v1 import ( v1 "encr.dev/proto/encore/parser/schema/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Lang describes the language an application is written in. // Defaults to Go if not set. type Lang int32 const ( Lang_GO Lang = 0 Lang_TYPESCRIPT Lang = 1 ) // Enum value maps for Lang. var ( Lang_name = map[int32]string{ 0: "GO", 1: "TYPESCRIPT", } Lang_value = map[string]int32{ "GO": 0, "TYPESCRIPT": 1, } ) func (x Lang) Enum() *Lang { p := new(Lang) *p = x return p } func (x Lang) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Lang) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[0].Descriptor() } func (Lang) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[0] } func (x Lang) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Lang.Descriptor instead. func (Lang) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{0} } type BucketUsage_Operation int32 const ( BucketUsage_UNKNOWN BucketUsage_Operation = 0 // Listing objects and accessing their metadata during listing. BucketUsage_LIST_OBJECTS BucketUsage_Operation = 1 // Reading the contents of an object. BucketUsage_READ_OBJECT_CONTENTS BucketUsage_Operation = 2 // Creating or updating an object, with contents and metadata. BucketUsage_WRITE_OBJECT BucketUsage_Operation = 3 // Updating the metadata of an object, without reading or writing its contents. BucketUsage_UPDATE_OBJECT_METADATA BucketUsage_Operation = 4 // Reading the metadata of an object, or checking for its existence. BucketUsage_GET_OBJECT_METADATA BucketUsage_Operation = 5 // Deleting an object. BucketUsage_DELETE_OBJECT BucketUsage_Operation = 6 // Get an bucket/object's public url. BucketUsage_GET_PUBLIC_URL BucketUsage_Operation = 7 // Generating a signed URL to allow an external recipient to create or // update an object. BucketUsage_SIGNED_UPLOAD_URL BucketUsage_Operation = 8 // Generating a signed URL to allow an external recipient to download an object. BucketUsage_SIGNED_DOWNLOAD_URL BucketUsage_Operation = 9 ) // Enum value maps for BucketUsage_Operation. var ( BucketUsage_Operation_name = map[int32]string{ 0: "UNKNOWN", 1: "LIST_OBJECTS", 2: "READ_OBJECT_CONTENTS", 3: "WRITE_OBJECT", 4: "UPDATE_OBJECT_METADATA", 5: "GET_OBJECT_METADATA", 6: "DELETE_OBJECT", 7: "GET_PUBLIC_URL", 8: "SIGNED_UPLOAD_URL", 9: "SIGNED_DOWNLOAD_URL", } BucketUsage_Operation_value = map[string]int32{ "UNKNOWN": 0, "LIST_OBJECTS": 1, "READ_OBJECT_CONTENTS": 2, "WRITE_OBJECT": 3, "UPDATE_OBJECT_METADATA": 4, "GET_OBJECT_METADATA": 5, "DELETE_OBJECT": 6, "GET_PUBLIC_URL": 7, "SIGNED_UPLOAD_URL": 8, "SIGNED_DOWNLOAD_URL": 9, } ) func (x BucketUsage_Operation) Enum() *BucketUsage_Operation { p := new(BucketUsage_Operation) *p = x return p } func (x BucketUsage_Operation) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (BucketUsage_Operation) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[1].Descriptor() } func (BucketUsage_Operation) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[1] } func (x BucketUsage_Operation) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use BucketUsage_Operation.Descriptor instead. func (BucketUsage_Operation) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{4, 0} } type Selector_Type int32 const ( Selector_UNKNOWN Selector_Type = 0 Selector_ALL Selector_Type = 1 Selector_TAG Selector_Type = 2 // NOTE: If more types are added, update the (selector.Selector).ToProto method. ) // Enum value maps for Selector_Type. var ( Selector_Type_name = map[int32]string{ 0: "UNKNOWN", 1: "ALL", 2: "TAG", } Selector_Type_value = map[string]int32{ "UNKNOWN": 0, "ALL": 1, "TAG": 2, } ) func (x Selector_Type) Enum() *Selector_Type { p := new(Selector_Type) *p = x return p } func (x Selector_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Selector_Type) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[2].Descriptor() } func (Selector_Type) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[2] } func (x Selector_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Selector_Type.Descriptor instead. func (Selector_Type) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{5, 0} } type RPC_AccessType int32 const ( RPC_PRIVATE RPC_AccessType = 0 RPC_PUBLIC RPC_AccessType = 1 RPC_AUTH RPC_AccessType = 2 ) // Enum value maps for RPC_AccessType. var ( RPC_AccessType_name = map[int32]string{ 0: "PRIVATE", 1: "PUBLIC", 2: "AUTH", } RPC_AccessType_value = map[string]int32{ "PRIVATE": 0, "PUBLIC": 1, "AUTH": 2, } ) func (x RPC_AccessType) Enum() *RPC_AccessType { p := new(RPC_AccessType) *p = x return p } func (x RPC_AccessType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (RPC_AccessType) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[3].Descriptor() } func (RPC_AccessType) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[3] } func (x RPC_AccessType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use RPC_AccessType.Descriptor instead. func (RPC_AccessType) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 0} } type RPC_Protocol int32 const ( RPC_REGULAR RPC_Protocol = 0 RPC_RAW RPC_Protocol = 1 ) // Enum value maps for RPC_Protocol. var ( RPC_Protocol_name = map[int32]string{ 0: "REGULAR", 1: "RAW", } RPC_Protocol_value = map[string]int32{ "REGULAR": 0, "RAW": 1, } ) func (x RPC_Protocol) Enum() *RPC_Protocol { p := new(RPC_Protocol) *p = x return p } func (x RPC_Protocol) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (RPC_Protocol) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[4].Descriptor() } func (RPC_Protocol) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[4] } func (x RPC_Protocol) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use RPC_Protocol.Descriptor instead. func (RPC_Protocol) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 1} } type StaticCallNode_Package int32 const ( StaticCallNode_UNKNOWN StaticCallNode_Package = 0 StaticCallNode_SQLDB StaticCallNode_Package = 1 StaticCallNode_RLOG StaticCallNode_Package = 2 ) // Enum value maps for StaticCallNode_Package. var ( StaticCallNode_Package_name = map[int32]string{ 0: "UNKNOWN", 1: "SQLDB", 2: "RLOG", } StaticCallNode_Package_value = map[string]int32{ "UNKNOWN": 0, "SQLDB": 1, "RLOG": 2, } ) func (x StaticCallNode_Package) Enum() *StaticCallNode_Package { p := new(StaticCallNode_Package) *p = x return p } func (x StaticCallNode_Package) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (StaticCallNode_Package) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[5].Descriptor() } func (StaticCallNode_Package) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[5] } func (x StaticCallNode_Package) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use StaticCallNode_Package.Descriptor instead. func (StaticCallNode_Package) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{12, 0} } type Path_Type int32 const ( Path_URL Path_Type = 0 Path_CACHE_KEYSPACE Path_Type = 1 ) // Enum value maps for Path_Type. var ( Path_Type_name = map[int32]string{ 0: "URL", 1: "CACHE_KEYSPACE", } Path_Type_value = map[string]int32{ "URL": 0, "CACHE_KEYSPACE": 1, } ) func (x Path_Type) Enum() *Path_Type { p := new(Path_Type) *p = x return p } func (x Path_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Path_Type) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[6].Descriptor() } func (Path_Type) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[6] } func (x Path_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Path_Type.Descriptor instead. func (Path_Type) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{20, 0} } type PathSegment_SegmentType int32 const ( PathSegment_LITERAL PathSegment_SegmentType = 0 PathSegment_PARAM PathSegment_SegmentType = 1 PathSegment_WILDCARD PathSegment_SegmentType = 2 PathSegment_FALLBACK PathSegment_SegmentType = 3 ) // Enum value maps for PathSegment_SegmentType. var ( PathSegment_SegmentType_name = map[int32]string{ 0: "LITERAL", 1: "PARAM", 2: "WILDCARD", 3: "FALLBACK", } PathSegment_SegmentType_value = map[string]int32{ "LITERAL": 0, "PARAM": 1, "WILDCARD": 2, "FALLBACK": 3, } ) func (x PathSegment_SegmentType) Enum() *PathSegment_SegmentType { p := new(PathSegment_SegmentType) *p = x return p } func (x PathSegment_SegmentType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PathSegment_SegmentType) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[7].Descriptor() } func (PathSegment_SegmentType) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[7] } func (x PathSegment_SegmentType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PathSegment_SegmentType.Descriptor instead. func (PathSegment_SegmentType) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21, 0} } type PathSegment_ParamType int32 const ( PathSegment_STRING PathSegment_ParamType = 0 PathSegment_BOOL PathSegment_ParamType = 1 PathSegment_INT8 PathSegment_ParamType = 2 PathSegment_INT16 PathSegment_ParamType = 3 PathSegment_INT32 PathSegment_ParamType = 4 PathSegment_INT64 PathSegment_ParamType = 5 PathSegment_INT PathSegment_ParamType = 6 PathSegment_UINT8 PathSegment_ParamType = 7 PathSegment_UINT16 PathSegment_ParamType = 8 PathSegment_UINT32 PathSegment_ParamType = 9 PathSegment_UINT64 PathSegment_ParamType = 10 PathSegment_UINT PathSegment_ParamType = 11 PathSegment_UUID PathSegment_ParamType = 12 ) // Enum value maps for PathSegment_ParamType. var ( PathSegment_ParamType_name = map[int32]string{ 0: "STRING", 1: "BOOL", 2: "INT8", 3: "INT16", 4: "INT32", 5: "INT64", 6: "INT", 7: "UINT8", 8: "UINT16", 9: "UINT32", 10: "UINT64", 11: "UINT", 12: "UUID", } PathSegment_ParamType_value = map[string]int32{ "STRING": 0, "BOOL": 1, "INT8": 2, "INT16": 3, "INT32": 4, "INT64": 5, "INT": 6, "UINT8": 7, "UINT16": 8, "UINT32": 9, "UINT64": 10, "UINT": 11, "UUID": 12, } ) func (x PathSegment_ParamType) Enum() *PathSegment_ParamType { p := new(PathSegment_ParamType) *p = x return p } func (x PathSegment_ParamType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PathSegment_ParamType) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[8].Descriptor() } func (PathSegment_ParamType) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[8] } func (x PathSegment_ParamType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PathSegment_ParamType.Descriptor instead. func (PathSegment_ParamType) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21, 1} } type PubSubTopic_DeliveryGuarantee int32 const ( PubSubTopic_AT_LEAST_ONCE PubSubTopic_DeliveryGuarantee = 0 // All messages will be delivered to each subscription at least once PubSubTopic_EXACTLY_ONCE PubSubTopic_DeliveryGuarantee = 1 // All messages will be delivered to each subscription exactly once ) // Enum value maps for PubSubTopic_DeliveryGuarantee. var ( PubSubTopic_DeliveryGuarantee_name = map[int32]string{ 0: "AT_LEAST_ONCE", 1: "EXACTLY_ONCE", } PubSubTopic_DeliveryGuarantee_value = map[string]int32{ "AT_LEAST_ONCE": 0, "EXACTLY_ONCE": 1, } ) func (x PubSubTopic_DeliveryGuarantee) Enum() *PubSubTopic_DeliveryGuarantee { p := new(PubSubTopic_DeliveryGuarantee) *p = x return p } func (x PubSubTopic_DeliveryGuarantee) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PubSubTopic_DeliveryGuarantee) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[9].Descriptor() } func (PubSubTopic_DeliveryGuarantee) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[9] } func (x PubSubTopic_DeliveryGuarantee) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PubSubTopic_DeliveryGuarantee.Descriptor instead. func (PubSubTopic_DeliveryGuarantee) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 0} } type Metric_MetricKind int32 const ( Metric_COUNTER Metric_MetricKind = 0 Metric_GAUGE Metric_MetricKind = 1 Metric_HISTOGRAM Metric_MetricKind = 2 ) // Enum value maps for Metric_MetricKind. var ( Metric_MetricKind_name = map[int32]string{ 0: "COUNTER", 1: "GAUGE", 2: "HISTOGRAM", } Metric_MetricKind_value = map[string]int32{ "COUNTER": 0, "GAUGE": 1, "HISTOGRAM": 2, } ) func (x Metric_MetricKind) Enum() *Metric_MetricKind { p := new(Metric_MetricKind) *p = x return p } func (x Metric_MetricKind) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Metric_MetricKind) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_meta_v1_meta_proto_enumTypes[10].Descriptor() } func (Metric_MetricKind) Type() protoreflect.EnumType { return &file_encore_parser_meta_v1_meta_proto_enumTypes[10] } func (x Metric_MetricKind) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Metric_MetricKind.Descriptor instead. func (Metric_MetricKind) EnumDescriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29, 0} } // Data is the metadata associated with an app version. type Data struct { state protoimpl.MessageState `protogen:"open.v1"` ModulePath string `protobuf:"bytes,1,opt,name=module_path,json=modulePath,proto3" json:"module_path,omitempty"` // app module path AppRevision string `protobuf:"bytes,2,opt,name=app_revision,json=appRevision,proto3" json:"app_revision,omitempty"` // app revision (always the VCS revision reference) UncommittedChanges bool `protobuf:"varint,8,opt,name=uncommitted_changes,json=uncommittedChanges,proto3" json:"uncommitted_changes,omitempty"` // true if there where changes made on-top of the VCS revision Decls []*v1.Decl `protobuf:"bytes,3,rep,name=decls,proto3" json:"decls,omitempty"` Pkgs []*Package `protobuf:"bytes,4,rep,name=pkgs,proto3" json:"pkgs,omitempty"` Svcs []*Service `protobuf:"bytes,5,rep,name=svcs,proto3" json:"svcs,omitempty"` AuthHandler *AuthHandler `protobuf:"bytes,6,opt,name=auth_handler,json=authHandler,proto3,oneof" json:"auth_handler,omitempty"` // the auth handler or nil CronJobs []*CronJob `protobuf:"bytes,7,rep,name=cron_jobs,json=cronJobs,proto3" json:"cron_jobs,omitempty"` PubsubTopics []*PubSubTopic `protobuf:"bytes,9,rep,name=pubsub_topics,json=pubsubTopics,proto3" json:"pubsub_topics,omitempty"` // All the pub sub topics declared in the application Middleware []*Middleware `protobuf:"bytes,10,rep,name=middleware,proto3" json:"middleware,omitempty"` CacheClusters []*CacheCluster `protobuf:"bytes,11,rep,name=cache_clusters,json=cacheClusters,proto3" json:"cache_clusters,omitempty"` Experiments []string `protobuf:"bytes,12,rep,name=experiments,proto3" json:"experiments,omitempty"` Metrics []*Metric `protobuf:"bytes,13,rep,name=metrics,proto3" json:"metrics,omitempty"` SqlDatabases []*SQLDatabase `protobuf:"bytes,14,rep,name=sql_databases,json=sqlDatabases,proto3" json:"sql_databases,omitempty"` Gateways []*Gateway `protobuf:"bytes,15,rep,name=gateways,proto3" json:"gateways,omitempty"` Language Lang `protobuf:"varint,16,opt,name=language,proto3,enum=encore.parser.meta.v1.Lang" json:"language,omitempty"` Buckets []*Bucket `protobuf:"bytes,17,rep,name=buckets,proto3" json:"buckets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Data) Reset() { *x = Data{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Data) String() string { return protoimpl.X.MessageStringOf(x) } func (*Data) ProtoMessage() {} func (x *Data) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Data.ProtoReflect.Descriptor instead. func (*Data) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{0} } func (x *Data) GetModulePath() string { if x != nil { return x.ModulePath } return "" } func (x *Data) GetAppRevision() string { if x != nil { return x.AppRevision } return "" } func (x *Data) GetUncommittedChanges() bool { if x != nil { return x.UncommittedChanges } return false } func (x *Data) GetDecls() []*v1.Decl { if x != nil { return x.Decls } return nil } func (x *Data) GetPkgs() []*Package { if x != nil { return x.Pkgs } return nil } func (x *Data) GetSvcs() []*Service { if x != nil { return x.Svcs } return nil } func (x *Data) GetAuthHandler() *AuthHandler { if x != nil { return x.AuthHandler } return nil } func (x *Data) GetCronJobs() []*CronJob { if x != nil { return x.CronJobs } return nil } func (x *Data) GetPubsubTopics() []*PubSubTopic { if x != nil { return x.PubsubTopics } return nil } func (x *Data) GetMiddleware() []*Middleware { if x != nil { return x.Middleware } return nil } func (x *Data) GetCacheClusters() []*CacheCluster { if x != nil { return x.CacheClusters } return nil } func (x *Data) GetExperiments() []string { if x != nil { return x.Experiments } return nil } func (x *Data) GetMetrics() []*Metric { if x != nil { return x.Metrics } return nil } func (x *Data) GetSqlDatabases() []*SQLDatabase { if x != nil { return x.SqlDatabases } return nil } func (x *Data) GetGateways() []*Gateway { if x != nil { return x.Gateways } return nil } func (x *Data) GetLanguage() Lang { if x != nil { return x.Language } return Lang_GO } func (x *Data) GetBuckets() []*Bucket { if x != nil { return x.Buckets } return nil } // QualifiedName is a name of an object in a specific package. // It is never an unqualified name, even in circumstances // where a package may refer to its own objects. type QualifiedName struct { state protoimpl.MessageState `protogen:"open.v1"` Pkg string `protobuf:"bytes,1,opt,name=pkg,proto3" json:"pkg,omitempty"` // "rel/path/to/pkg" Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // ObjectName unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *QualifiedName) Reset() { *x = QualifiedName{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *QualifiedName) String() string { return protoimpl.X.MessageStringOf(x) } func (*QualifiedName) ProtoMessage() {} func (x *QualifiedName) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use QualifiedName.ProtoReflect.Descriptor instead. func (*QualifiedName) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{1} } func (x *QualifiedName) GetPkg() string { if x != nil { return x.Pkg } return "" } func (x *QualifiedName) GetName() string { if x != nil { return x.Name } return "" } type Package struct { state protoimpl.MessageState `protogen:"open.v1"` RelPath string `protobuf:"bytes,1,opt,name=rel_path,json=relPath,proto3" json:"rel_path,omitempty"` // import path relative to app root ("." for the app root itself) Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // package name as declared in Go files Doc string `protobuf:"bytes,3,opt,name=doc,proto3" json:"doc,omitempty"` // associated documentation ServiceName string `protobuf:"bytes,4,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // service name this package is a part of, if any Secrets []string `protobuf:"bytes,5,rep,name=secrets,proto3" json:"secrets,omitempty"` // secrets required by this package RpcCalls []*QualifiedName `protobuf:"bytes,6,rep,name=rpc_calls,json=rpcCalls,proto3" json:"rpc_calls,omitempty"` // RPCs called by the package TraceNodes []*TraceNode `protobuf:"bytes,7,rep,name=trace_nodes,json=traceNodes,proto3" json:"trace_nodes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Package) Reset() { *x = Package{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Package) String() string { return protoimpl.X.MessageStringOf(x) } func (*Package) ProtoMessage() {} func (x *Package) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Package.ProtoReflect.Descriptor instead. func (*Package) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{2} } func (x *Package) GetRelPath() string { if x != nil { return x.RelPath } return "" } func (x *Package) GetName() string { if x != nil { return x.Name } return "" } func (x *Package) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *Package) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *Package) GetSecrets() []string { if x != nil { return x.Secrets } return nil } func (x *Package) GetRpcCalls() []*QualifiedName { if x != nil { return x.RpcCalls } return nil } func (x *Package) GetTraceNodes() []*TraceNode { if x != nil { return x.TraceNodes } return nil } type Service struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` RelPath string `protobuf:"bytes,2,opt,name=rel_path,json=relPath,proto3" json:"rel_path,omitempty"` // import path relative to app root for the root package in the service Rpcs []*RPC `protobuf:"bytes,3,rep,name=rpcs,proto3" json:"rpcs,omitempty"` Migrations []*DBMigration `protobuf:"bytes,4,rep,name=migrations,proto3" json:"migrations,omitempty"` Databases []string `protobuf:"bytes,5,rep,name=databases,proto3" json:"databases,omitempty"` // databases this service connects to HasConfig bool `protobuf:"varint,6,opt,name=has_config,json=hasConfig,proto3" json:"has_config,omitempty"` // true if the service has uses config Buckets []*BucketUsage `protobuf:"bytes,7,rep,name=buckets,proto3" json:"buckets,omitempty"` // buckets this service uses Metrics []string `protobuf:"bytes,8,rep,name=metrics,proto3" json:"metrics,omitempty"` // metrics this service uses unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Service) Reset() { *x = Service{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Service) String() string { return protoimpl.X.MessageStringOf(x) } func (*Service) ProtoMessage() {} func (x *Service) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Service.ProtoReflect.Descriptor instead. func (*Service) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{3} } func (x *Service) GetName() string { if x != nil { return x.Name } return "" } func (x *Service) GetRelPath() string { if x != nil { return x.RelPath } return "" } func (x *Service) GetRpcs() []*RPC { if x != nil { return x.Rpcs } return nil } func (x *Service) GetMigrations() []*DBMigration { if x != nil { return x.Migrations } return nil } func (x *Service) GetDatabases() []string { if x != nil { return x.Databases } return nil } func (x *Service) GetHasConfig() bool { if x != nil { return x.HasConfig } return false } func (x *Service) GetBuckets() []*BucketUsage { if x != nil { return x.Buckets } return nil } func (x *Service) GetMetrics() []string { if x != nil { return x.Metrics } return nil } type BucketUsage struct { state protoimpl.MessageState `protogen:"open.v1"` // The encore name of the bucket. Bucket string `protobuf:"bytes,1,opt,name=bucket,proto3" json:"bucket,omitempty"` // Recorded operations. Operations []BucketUsage_Operation `protobuf:"varint,2,rep,packed,name=operations,proto3,enum=encore.parser.meta.v1.BucketUsage_Operation" json:"operations,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketUsage) Reset() { *x = BucketUsage{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketUsage) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketUsage) ProtoMessage() {} func (x *BucketUsage) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketUsage.ProtoReflect.Descriptor instead. func (*BucketUsage) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{4} } func (x *BucketUsage) GetBucket() string { if x != nil { return x.Bucket } return "" } func (x *BucketUsage) GetOperations() []BucketUsage_Operation { if x != nil { return x.Operations } return nil } type Selector struct { state protoimpl.MessageState `protogen:"open.v1"` Type Selector_Type `protobuf:"varint,1,opt,name=type,proto3,enum=encore.parser.meta.v1.Selector_Type" json:"type,omitempty"` Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Selector) Reset() { *x = Selector{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Selector) String() string { return protoimpl.X.MessageStringOf(x) } func (*Selector) ProtoMessage() {} func (x *Selector) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Selector.ProtoReflect.Descriptor instead. func (*Selector) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{5} } func (x *Selector) GetType() Selector_Type { if x != nil { return x.Type } return Selector_UNKNOWN } func (x *Selector) GetValue() string { if x != nil { return x.Value } return "" } type RPC struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // name of the RPC endpoint Doc *string `protobuf:"bytes,2,opt,name=doc,proto3,oneof" json:"doc,omitempty"` // associated documentation ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // the service the RPC belongs to. AccessType RPC_AccessType `protobuf:"varint,4,opt,name=access_type,json=accessType,proto3,enum=encore.parser.meta.v1.RPC_AccessType" json:"access_type,omitempty"` // how can the RPC be accessed? RequestSchema *v1.Type `protobuf:"bytes,5,opt,name=request_schema,json=requestSchema,proto3,oneof" json:"request_schema,omitempty"` // request schema, or nil ResponseSchema *v1.Type `protobuf:"bytes,6,opt,name=response_schema,json=responseSchema,proto3,oneof" json:"response_schema,omitempty"` // response schema, or nil Proto RPC_Protocol `protobuf:"varint,7,opt,name=proto,proto3,enum=encore.parser.meta.v1.RPC_Protocol" json:"proto,omitempty"` Loc *v1.Loc `protobuf:"bytes,8,opt,name=loc,proto3" json:"loc,omitempty"` Path *Path `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"` HttpMethods []string `protobuf:"bytes,10,rep,name=http_methods,json=httpMethods,proto3" json:"http_methods,omitempty"` Tags []*Selector `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty"` // sensitive reports whether the whole payload is sensitive. // If true, none of the request/response payload will be traced. Sensitive bool `protobuf:"varint,12,opt,name=sensitive,proto3" json:"sensitive,omitempty"` // Whether the endpoint can be called without auth parameters. AllowUnauthenticated bool `protobuf:"varint,13,opt,name=allow_unauthenticated,json=allowUnauthenticated,proto3" json:"allow_unauthenticated,omitempty"` // Whether the endpoint is exposed to the public, keyed by gateway. Expose map[string]*RPC_ExposeOptions `protobuf:"bytes,14,rep,name=expose,proto3" json:"expose,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // The maximum size of the request body in bytes. // If not set, defaults to no limit. BodyLimit *uint64 `protobuf:"varint,15,opt,name=body_limit,json=bodyLimit,proto3,oneof" json:"body_limit,omitempty"` // If the endpoint is streaming StreamingRequest bool `protobuf:"varint,16,opt,name=streaming_request,json=streamingRequest,proto3" json:"streaming_request,omitempty"` StreamingResponse bool `protobuf:"varint,17,opt,name=streaming_response,json=streamingResponse,proto3" json:"streaming_response,omitempty"` HandshakeSchema *v1.Type `protobuf:"bytes,18,opt,name=handshake_schema,json=handshakeSchema,proto3,oneof" json:"handshake_schema,omitempty"` // handshake schema, or nil // If the endpoint serves static assets. StaticAssets *RPC_StaticAssets `protobuf:"bytes,19,opt,name=static_assets,json=staticAssets,proto3,oneof" json:"static_assets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPC) Reset() { *x = RPC{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPC) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPC) ProtoMessage() {} func (x *RPC) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPC.ProtoReflect.Descriptor instead. func (*RPC) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6} } func (x *RPC) GetName() string { if x != nil { return x.Name } return "" } func (x *RPC) GetDoc() string { if x != nil && x.Doc != nil { return *x.Doc } return "" } func (x *RPC) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *RPC) GetAccessType() RPC_AccessType { if x != nil { return x.AccessType } return RPC_PRIVATE } func (x *RPC) GetRequestSchema() *v1.Type { if x != nil { return x.RequestSchema } return nil } func (x *RPC) GetResponseSchema() *v1.Type { if x != nil { return x.ResponseSchema } return nil } func (x *RPC) GetProto() RPC_Protocol { if x != nil { return x.Proto } return RPC_REGULAR } func (x *RPC) GetLoc() *v1.Loc { if x != nil { return x.Loc } return nil } func (x *RPC) GetPath() *Path { if x != nil { return x.Path } return nil } func (x *RPC) GetHttpMethods() []string { if x != nil { return x.HttpMethods } return nil } func (x *RPC) GetTags() []*Selector { if x != nil { return x.Tags } return nil } func (x *RPC) GetSensitive() bool { if x != nil { return x.Sensitive } return false } func (x *RPC) GetAllowUnauthenticated() bool { if x != nil { return x.AllowUnauthenticated } return false } func (x *RPC) GetExpose() map[string]*RPC_ExposeOptions { if x != nil { return x.Expose } return nil } func (x *RPC) GetBodyLimit() uint64 { if x != nil && x.BodyLimit != nil { return *x.BodyLimit } return 0 } func (x *RPC) GetStreamingRequest() bool { if x != nil { return x.StreamingRequest } return false } func (x *RPC) GetStreamingResponse() bool { if x != nil { return x.StreamingResponse } return false } func (x *RPC) GetHandshakeSchema() *v1.Type { if x != nil { return x.HandshakeSchema } return nil } func (x *RPC) GetStaticAssets() *RPC_StaticAssets { if x != nil { return x.StaticAssets } return nil } type AuthHandler struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Doc string `protobuf:"bytes,2,opt,name=doc,proto3" json:"doc,omitempty"` PkgPath string `protobuf:"bytes,3,opt,name=pkg_path,json=pkgPath,proto3" json:"pkg_path,omitempty"` // package (service) import path PkgName string `protobuf:"bytes,4,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` // package (service) name Loc *v1.Loc `protobuf:"bytes,5,opt,name=loc,proto3" json:"loc,omitempty"` AuthData *v1.Type `protobuf:"bytes,6,opt,name=auth_data,json=authData,proto3,oneof" json:"auth_data,omitempty"` // custom auth data, or nil Params *v1.Type `protobuf:"bytes,7,opt,name=params,proto3,oneof" json:"params,omitempty"` // builtin string or named type ServiceName string `protobuf:"bytes,8,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AuthHandler) Reset() { *x = AuthHandler{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AuthHandler) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuthHandler) ProtoMessage() {} func (x *AuthHandler) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuthHandler.ProtoReflect.Descriptor instead. func (*AuthHandler) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{7} } func (x *AuthHandler) GetName() string { if x != nil { return x.Name } return "" } func (x *AuthHandler) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *AuthHandler) GetPkgPath() string { if x != nil { return x.PkgPath } return "" } func (x *AuthHandler) GetPkgName() string { if x != nil { return x.PkgName } return "" } func (x *AuthHandler) GetLoc() *v1.Loc { if x != nil { return x.Loc } return nil } func (x *AuthHandler) GetAuthData() *v1.Type { if x != nil { return x.AuthData } return nil } func (x *AuthHandler) GetParams() *v1.Type { if x != nil { return x.Params } return nil } func (x *AuthHandler) GetServiceName() string { if x != nil { return x.ServiceName } return "" } type Middleware struct { state protoimpl.MessageState `protogen:"open.v1"` Name *QualifiedName `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Doc string `protobuf:"bytes,2,opt,name=doc,proto3" json:"doc,omitempty"` Loc *v1.Loc `protobuf:"bytes,3,opt,name=loc,proto3" json:"loc,omitempty"` Global bool `protobuf:"varint,4,opt,name=global,proto3" json:"global,omitempty"` ServiceName *string `protobuf:"bytes,5,opt,name=service_name,json=serviceName,proto3,oneof" json:"service_name,omitempty"` // nil if global Target []*Selector `protobuf:"bytes,6,rep,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Middleware) Reset() { *x = Middleware{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Middleware) String() string { return protoimpl.X.MessageStringOf(x) } func (*Middleware) ProtoMessage() {} func (x *Middleware) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Middleware.ProtoReflect.Descriptor instead. func (*Middleware) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{8} } func (x *Middleware) GetName() *QualifiedName { if x != nil { return x.Name } return nil } func (x *Middleware) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *Middleware) GetLoc() *v1.Loc { if x != nil { return x.Loc } return nil } func (x *Middleware) GetGlobal() bool { if x != nil { return x.Global } return false } func (x *Middleware) GetServiceName() string { if x != nil && x.ServiceName != nil { return *x.ServiceName } return "" } func (x *Middleware) GetTarget() []*Selector { if x != nil { return x.Target } return nil } type TraceNode struct { state protoimpl.MessageState `protogen:"open.v1"` Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Filepath string `protobuf:"bytes,2,opt,name=filepath,proto3" json:"filepath,omitempty"` // slash-separated, relative to app root StartPos int32 `protobuf:"varint,4,opt,name=start_pos,json=startPos,proto3" json:"start_pos,omitempty"` EndPos int32 `protobuf:"varint,5,opt,name=end_pos,json=endPos,proto3" json:"end_pos,omitempty"` SrcLineStart int32 `protobuf:"varint,6,opt,name=src_line_start,json=srcLineStart,proto3" json:"src_line_start,omitempty"` SrcLineEnd int32 `protobuf:"varint,7,opt,name=src_line_end,json=srcLineEnd,proto3" json:"src_line_end,omitempty"` SrcColStart int32 `protobuf:"varint,8,opt,name=src_col_start,json=srcColStart,proto3" json:"src_col_start,omitempty"` SrcColEnd int32 `protobuf:"varint,9,opt,name=src_col_end,json=srcColEnd,proto3" json:"src_col_end,omitempty"` // Types that are valid to be assigned to Context: // // *TraceNode_RpcDef // *TraceNode_RpcCall // *TraceNode_StaticCall // *TraceNode_AuthHandlerDef // *TraceNode_PubsubTopicDef // *TraceNode_PubsubPublish // *TraceNode_PubsubSubscriber // *TraceNode_ServiceInit // *TraceNode_MiddlewareDef // *TraceNode_CacheKeyspace Context isTraceNode_Context `protobuf_oneof:"context"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TraceNode) Reset() { *x = TraceNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TraceNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*TraceNode) ProtoMessage() {} func (x *TraceNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TraceNode.ProtoReflect.Descriptor instead. func (*TraceNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{9} } func (x *TraceNode) GetId() int32 { if x != nil { return x.Id } return 0 } func (x *TraceNode) GetFilepath() string { if x != nil { return x.Filepath } return "" } func (x *TraceNode) GetStartPos() int32 { if x != nil { return x.StartPos } return 0 } func (x *TraceNode) GetEndPos() int32 { if x != nil { return x.EndPos } return 0 } func (x *TraceNode) GetSrcLineStart() int32 { if x != nil { return x.SrcLineStart } return 0 } func (x *TraceNode) GetSrcLineEnd() int32 { if x != nil { return x.SrcLineEnd } return 0 } func (x *TraceNode) GetSrcColStart() int32 { if x != nil { return x.SrcColStart } return 0 } func (x *TraceNode) GetSrcColEnd() int32 { if x != nil { return x.SrcColEnd } return 0 } func (x *TraceNode) GetContext() isTraceNode_Context { if x != nil { return x.Context } return nil } func (x *TraceNode) GetRpcDef() *RPCDefNode { if x != nil { if x, ok := x.Context.(*TraceNode_RpcDef); ok { return x.RpcDef } } return nil } func (x *TraceNode) GetRpcCall() *RPCCallNode { if x != nil { if x, ok := x.Context.(*TraceNode_RpcCall); ok { return x.RpcCall } } return nil } func (x *TraceNode) GetStaticCall() *StaticCallNode { if x != nil { if x, ok := x.Context.(*TraceNode_StaticCall); ok { return x.StaticCall } } return nil } func (x *TraceNode) GetAuthHandlerDef() *AuthHandlerDefNode { if x != nil { if x, ok := x.Context.(*TraceNode_AuthHandlerDef); ok { return x.AuthHandlerDef } } return nil } func (x *TraceNode) GetPubsubTopicDef() *PubSubTopicDefNode { if x != nil { if x, ok := x.Context.(*TraceNode_PubsubTopicDef); ok { return x.PubsubTopicDef } } return nil } func (x *TraceNode) GetPubsubPublish() *PubSubPublishNode { if x != nil { if x, ok := x.Context.(*TraceNode_PubsubPublish); ok { return x.PubsubPublish } } return nil } func (x *TraceNode) GetPubsubSubscriber() *PubSubSubscriberNode { if x != nil { if x, ok := x.Context.(*TraceNode_PubsubSubscriber); ok { return x.PubsubSubscriber } } return nil } func (x *TraceNode) GetServiceInit() *ServiceInitNode { if x != nil { if x, ok := x.Context.(*TraceNode_ServiceInit); ok { return x.ServiceInit } } return nil } func (x *TraceNode) GetMiddlewareDef() *MiddlewareDefNode { if x != nil { if x, ok := x.Context.(*TraceNode_MiddlewareDef); ok { return x.MiddlewareDef } } return nil } func (x *TraceNode) GetCacheKeyspace() *CacheKeyspaceDefNode { if x != nil { if x, ok := x.Context.(*TraceNode_CacheKeyspace); ok { return x.CacheKeyspace } } return nil } type isTraceNode_Context interface { isTraceNode_Context() } type TraceNode_RpcDef struct { RpcDef *RPCDefNode `protobuf:"bytes,10,opt,name=rpc_def,json=rpcDef,proto3,oneof"` } type TraceNode_RpcCall struct { RpcCall *RPCCallNode `protobuf:"bytes,11,opt,name=rpc_call,json=rpcCall,proto3,oneof"` } type TraceNode_StaticCall struct { StaticCall *StaticCallNode `protobuf:"bytes,12,opt,name=static_call,json=staticCall,proto3,oneof"` } type TraceNode_AuthHandlerDef struct { AuthHandlerDef *AuthHandlerDefNode `protobuf:"bytes,13,opt,name=auth_handler_def,json=authHandlerDef,proto3,oneof"` } type TraceNode_PubsubTopicDef struct { PubsubTopicDef *PubSubTopicDefNode `protobuf:"bytes,14,opt,name=pubsub_topic_def,json=pubsubTopicDef,proto3,oneof"` } type TraceNode_PubsubPublish struct { PubsubPublish *PubSubPublishNode `protobuf:"bytes,15,opt,name=pubsub_publish,json=pubsubPublish,proto3,oneof"` } type TraceNode_PubsubSubscriber struct { PubsubSubscriber *PubSubSubscriberNode `protobuf:"bytes,16,opt,name=pubsub_subscriber,json=pubsubSubscriber,proto3,oneof"` } type TraceNode_ServiceInit struct { ServiceInit *ServiceInitNode `protobuf:"bytes,17,opt,name=service_init,json=serviceInit,proto3,oneof"` } type TraceNode_MiddlewareDef struct { MiddlewareDef *MiddlewareDefNode `protobuf:"bytes,18,opt,name=middleware_def,json=middlewareDef,proto3,oneof"` } type TraceNode_CacheKeyspace struct { CacheKeyspace *CacheKeyspaceDefNode `protobuf:"bytes,19,opt,name=cache_keyspace,json=cacheKeyspace,proto3,oneof"` } func (*TraceNode_RpcDef) isTraceNode_Context() {} func (*TraceNode_RpcCall) isTraceNode_Context() {} func (*TraceNode_StaticCall) isTraceNode_Context() {} func (*TraceNode_AuthHandlerDef) isTraceNode_Context() {} func (*TraceNode_PubsubTopicDef) isTraceNode_Context() {} func (*TraceNode_PubsubPublish) isTraceNode_Context() {} func (*TraceNode_PubsubSubscriber) isTraceNode_Context() {} func (*TraceNode_ServiceInit) isTraceNode_Context() {} func (*TraceNode_MiddlewareDef) isTraceNode_Context() {} func (*TraceNode_CacheKeyspace) isTraceNode_Context() {} type RPCDefNode struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` RpcName string `protobuf:"bytes,2,opt,name=rpc_name,json=rpcName,proto3" json:"rpc_name,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPCDefNode) Reset() { *x = RPCDefNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPCDefNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPCDefNode) ProtoMessage() {} func (x *RPCDefNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPCDefNode.ProtoReflect.Descriptor instead. func (*RPCDefNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{10} } func (x *RPCDefNode) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *RPCDefNode) GetRpcName() string { if x != nil { return x.RpcName } return "" } func (x *RPCDefNode) GetContext() string { if x != nil { return x.Context } return "" } type RPCCallNode struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` RpcName string `protobuf:"bytes,2,opt,name=rpc_name,json=rpcName,proto3" json:"rpc_name,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPCCallNode) Reset() { *x = RPCCallNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPCCallNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPCCallNode) ProtoMessage() {} func (x *RPCCallNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPCCallNode.ProtoReflect.Descriptor instead. func (*RPCCallNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{11} } func (x *RPCCallNode) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *RPCCallNode) GetRpcName() string { if x != nil { return x.RpcName } return "" } func (x *RPCCallNode) GetContext() string { if x != nil { return x.Context } return "" } type StaticCallNode struct { state protoimpl.MessageState `protogen:"open.v1"` Package StaticCallNode_Package `protobuf:"varint,1,opt,name=package,proto3,enum=encore.parser.meta.v1.StaticCallNode_Package" json:"package,omitempty"` Func string `protobuf:"bytes,2,opt,name=func,proto3" json:"func,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StaticCallNode) Reset() { *x = StaticCallNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StaticCallNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*StaticCallNode) ProtoMessage() {} func (x *StaticCallNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use StaticCallNode.ProtoReflect.Descriptor instead. func (*StaticCallNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{12} } func (x *StaticCallNode) GetPackage() StaticCallNode_Package { if x != nil { return x.Package } return StaticCallNode_UNKNOWN } func (x *StaticCallNode) GetFunc() string { if x != nil { return x.Func } return "" } func (x *StaticCallNode) GetContext() string { if x != nil { return x.Context } return "" } type AuthHandlerDefNode struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AuthHandlerDefNode) Reset() { *x = AuthHandlerDefNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AuthHandlerDefNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuthHandlerDefNode) ProtoMessage() {} func (x *AuthHandlerDefNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuthHandlerDefNode.ProtoReflect.Descriptor instead. func (*AuthHandlerDefNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{13} } func (x *AuthHandlerDefNode) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *AuthHandlerDefNode) GetName() string { if x != nil { return x.Name } return "" } func (x *AuthHandlerDefNode) GetContext() string { if x != nil { return x.Context } return "" } type PubSubTopicDefNode struct { state protoimpl.MessageState `protogen:"open.v1"` TopicName string `protobuf:"bytes,1,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` Context string `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopicDefNode) Reset() { *x = PubSubTopicDefNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopicDefNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopicDefNode) ProtoMessage() {} func (x *PubSubTopicDefNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopicDefNode.ProtoReflect.Descriptor instead. func (*PubSubTopicDefNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{14} } func (x *PubSubTopicDefNode) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *PubSubTopicDefNode) GetContext() string { if x != nil { return x.Context } return "" } type PubSubPublishNode struct { state protoimpl.MessageState `protogen:"open.v1"` TopicName string `protobuf:"bytes,1,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` Context string `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubPublishNode) Reset() { *x = PubSubPublishNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubPublishNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubPublishNode) ProtoMessage() {} func (x *PubSubPublishNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubPublishNode.ProtoReflect.Descriptor instead. func (*PubSubPublishNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{15} } func (x *PubSubPublishNode) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *PubSubPublishNode) GetContext() string { if x != nil { return x.Context } return "" } type PubSubSubscriberNode struct { state protoimpl.MessageState `protogen:"open.v1"` TopicName string `protobuf:"bytes,1,opt,name=topic_name,json=topicName,proto3" json:"topic_name,omitempty"` SubscriberName string `protobuf:"bytes,2,opt,name=subscriber_name,json=subscriberName,proto3" json:"subscriber_name,omitempty"` ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` Context string `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubSubscriberNode) Reset() { *x = PubSubSubscriberNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubSubscriberNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubSubscriberNode) ProtoMessage() {} func (x *PubSubSubscriberNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubSubscriberNode.ProtoReflect.Descriptor instead. func (*PubSubSubscriberNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{16} } func (x *PubSubSubscriberNode) GetTopicName() string { if x != nil { return x.TopicName } return "" } func (x *PubSubSubscriberNode) GetSubscriberName() string { if x != nil { return x.SubscriberName } return "" } func (x *PubSubSubscriberNode) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *PubSubSubscriberNode) GetContext() string { if x != nil { return x.Context } return "" } type ServiceInitNode struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` SetupFuncName string `protobuf:"bytes,2,opt,name=setup_func_name,json=setupFuncName,proto3" json:"setup_func_name,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceInitNode) Reset() { *x = ServiceInitNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceInitNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceInitNode) ProtoMessage() {} func (x *ServiceInitNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceInitNode.ProtoReflect.Descriptor instead. func (*ServiceInitNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{17} } func (x *ServiceInitNode) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *ServiceInitNode) GetSetupFuncName() string { if x != nil { return x.SetupFuncName } return "" } func (x *ServiceInitNode) GetContext() string { if x != nil { return x.Context } return "" } type MiddlewareDefNode struct { state protoimpl.MessageState `protogen:"open.v1"` PkgRelPath string `protobuf:"bytes,1,opt,name=pkg_rel_path,json=pkgRelPath,proto3" json:"pkg_rel_path,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Context string `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` Target []*Selector `protobuf:"bytes,4,rep,name=target,proto3" json:"target,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MiddlewareDefNode) Reset() { *x = MiddlewareDefNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MiddlewareDefNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*MiddlewareDefNode) ProtoMessage() {} func (x *MiddlewareDefNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MiddlewareDefNode.ProtoReflect.Descriptor instead. func (*MiddlewareDefNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{18} } func (x *MiddlewareDefNode) GetPkgRelPath() string { if x != nil { return x.PkgRelPath } return "" } func (x *MiddlewareDefNode) GetName() string { if x != nil { return x.Name } return "" } func (x *MiddlewareDefNode) GetContext() string { if x != nil { return x.Context } return "" } func (x *MiddlewareDefNode) GetTarget() []*Selector { if x != nil { return x.Target } return nil } type CacheKeyspaceDefNode struct { state protoimpl.MessageState `protogen:"open.v1"` PkgRelPath string `protobuf:"bytes,1,opt,name=pkg_rel_path,json=pkgRelPath,proto3" json:"pkg_rel_path,omitempty"` VarName string `protobuf:"bytes,2,opt,name=var_name,json=varName,proto3" json:"var_name,omitempty"` ClusterName string `protobuf:"bytes,3,opt,name=cluster_name,json=clusterName,proto3" json:"cluster_name,omitempty"` Context string `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheKeyspaceDefNode) Reset() { *x = CacheKeyspaceDefNode{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheKeyspaceDefNode) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheKeyspaceDefNode) ProtoMessage() {} func (x *CacheKeyspaceDefNode) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheKeyspaceDefNode.ProtoReflect.Descriptor instead. func (*CacheKeyspaceDefNode) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{19} } func (x *CacheKeyspaceDefNode) GetPkgRelPath() string { if x != nil { return x.PkgRelPath } return "" } func (x *CacheKeyspaceDefNode) GetVarName() string { if x != nil { return x.VarName } return "" } func (x *CacheKeyspaceDefNode) GetClusterName() string { if x != nil { return x.ClusterName } return "" } func (x *CacheKeyspaceDefNode) GetContext() string { if x != nil { return x.Context } return "" } type Path struct { state protoimpl.MessageState `protogen:"open.v1"` Segments []*PathSegment `protobuf:"bytes,1,rep,name=segments,proto3" json:"segments,omitempty"` Type Path_Type `protobuf:"varint,2,opt,name=type,proto3,enum=encore.parser.meta.v1.Path_Type" json:"type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Path) Reset() { *x = Path{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Path) String() string { return protoimpl.X.MessageStringOf(x) } func (*Path) ProtoMessage() {} func (x *Path) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Path.ProtoReflect.Descriptor instead. func (*Path) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{20} } func (x *Path) GetSegments() []*PathSegment { if x != nil { return x.Segments } return nil } func (x *Path) GetType() Path_Type { if x != nil { return x.Type } return Path_URL } type PathSegment struct { state protoimpl.MessageState `protogen:"open.v1"` Type PathSegment_SegmentType `protobuf:"varint,1,opt,name=type,proto3,enum=encore.parser.meta.v1.PathSegment_SegmentType" json:"type,omitempty"` Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` ValueType PathSegment_ParamType `protobuf:"varint,3,opt,name=value_type,json=valueType,proto3,enum=encore.parser.meta.v1.PathSegment_ParamType" json:"value_type,omitempty"` Validation *v1.ValidationExpr `protobuf:"bytes,4,opt,name=validation,proto3,oneof" json:"validation,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PathSegment) Reset() { *x = PathSegment{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PathSegment) String() string { return protoimpl.X.MessageStringOf(x) } func (*PathSegment) ProtoMessage() {} func (x *PathSegment) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PathSegment.ProtoReflect.Descriptor instead. func (*PathSegment) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21} } func (x *PathSegment) GetType() PathSegment_SegmentType { if x != nil { return x.Type } return PathSegment_LITERAL } func (x *PathSegment) GetValue() string { if x != nil { return x.Value } return "" } func (x *PathSegment) GetValueType() PathSegment_ParamType { if x != nil { return x.ValueType } return PathSegment_STRING } func (x *PathSegment) GetValidation() *v1.ValidationExpr { if x != nil { return x.Validation } return nil } type Gateway struct { state protoimpl.MessageState `protogen:"open.v1"` EncoreName string `protobuf:"bytes,1,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // Spec is the configuration for the gateway, if it's explicitly defined. Explicit *Gateway_Explicit `protobuf:"bytes,2,opt,name=explicit,proto3,oneof" json:"explicit,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Gateway) Reset() { *x = Gateway{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Gateway) String() string { return protoimpl.X.MessageStringOf(x) } func (*Gateway) ProtoMessage() {} func (x *Gateway) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Gateway.ProtoReflect.Descriptor instead. func (*Gateway) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{22} } func (x *Gateway) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *Gateway) GetExplicit() *Gateway_Explicit { if x != nil { return x.Explicit } return nil } type CronJob struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` Doc *string `protobuf:"bytes,3,opt,name=doc,proto3,oneof" json:"doc,omitempty"` Schedule string `protobuf:"bytes,4,opt,name=schedule,proto3" json:"schedule,omitempty"` Endpoint *QualifiedName `protobuf:"bytes,5,opt,name=endpoint,proto3" json:"endpoint,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CronJob) Reset() { *x = CronJob{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CronJob) String() string { return protoimpl.X.MessageStringOf(x) } func (*CronJob) ProtoMessage() {} func (x *CronJob) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CronJob.ProtoReflect.Descriptor instead. func (*CronJob) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{23} } func (x *CronJob) GetId() string { if x != nil { return x.Id } return "" } func (x *CronJob) GetTitle() string { if x != nil { return x.Title } return "" } func (x *CronJob) GetDoc() string { if x != nil && x.Doc != nil { return *x.Doc } return "" } func (x *CronJob) GetSchedule() string { if x != nil { return x.Schedule } return "" } func (x *CronJob) GetEndpoint() *QualifiedName { if x != nil { return x.Endpoint } return nil } type SQLDatabase struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Doc *string `protobuf:"bytes,2,opt,name=doc,proto3,oneof" json:"doc,omitempty"` // migration_rel_path is the slash-separated path to the migrations, // relative to the main module's root directory. MigrationRelPath *string `protobuf:"bytes,3,opt,name=migration_rel_path,json=migrationRelPath,proto3,oneof" json:"migration_rel_path,omitempty"` Migrations []*DBMigration `protobuf:"bytes,4,rep,name=migrations,proto3" json:"migrations,omitempty"` AllowNonSequentialMigrations bool `protobuf:"varint,5,opt,name=allow_non_sequential_migrations,json=allowNonSequentialMigrations,proto3" json:"allow_non_sequential_migrations,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLDatabase) Reset() { *x = SQLDatabase{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLDatabase) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLDatabase) ProtoMessage() {} func (x *SQLDatabase) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLDatabase.ProtoReflect.Descriptor instead. func (*SQLDatabase) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{24} } func (x *SQLDatabase) GetName() string { if x != nil { return x.Name } return "" } func (x *SQLDatabase) GetDoc() string { if x != nil && x.Doc != nil { return *x.Doc } return "" } func (x *SQLDatabase) GetMigrationRelPath() string { if x != nil && x.MigrationRelPath != nil { return *x.MigrationRelPath } return "" } func (x *SQLDatabase) GetMigrations() []*DBMigration { if x != nil { return x.Migrations } return nil } func (x *SQLDatabase) GetAllowNonSequentialMigrations() bool { if x != nil { return x.AllowNonSequentialMigrations } return false } type DBMigration struct { state protoimpl.MessageState `protogen:"open.v1"` Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` // filename Number uint64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"` // migration number Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` // descriptive name unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DBMigration) Reset() { *x = DBMigration{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DBMigration) String() string { return protoimpl.X.MessageStringOf(x) } func (*DBMigration) ProtoMessage() {} func (x *DBMigration) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DBMigration.ProtoReflect.Descriptor instead. func (*DBMigration) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{25} } func (x *DBMigration) GetFilename() string { if x != nil { return x.Filename } return "" } func (x *DBMigration) GetNumber() uint64 { if x != nil { return x.Number } return 0 } func (x *DBMigration) GetDescription() string { if x != nil { return x.Description } return "" } type Bucket struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Doc *string `protobuf:"bytes,2,opt,name=doc,proto3,oneof" json:"doc,omitempty"` Versioned bool `protobuf:"varint,3,opt,name=versioned,proto3" json:"versioned,omitempty"` Public bool `protobuf:"varint,4,opt,name=public,proto3" json:"public,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Bucket) Reset() { *x = Bucket{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Bucket) String() string { return protoimpl.X.MessageStringOf(x) } func (*Bucket) ProtoMessage() {} func (x *Bucket) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Bucket.ProtoReflect.Descriptor instead. func (*Bucket) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{26} } func (x *Bucket) GetName() string { if x != nil { return x.Name } return "" } func (x *Bucket) GetDoc() string { if x != nil && x.Doc != nil { return *x.Doc } return "" } func (x *Bucket) GetVersioned() bool { if x != nil { return x.Versioned } return false } func (x *Bucket) GetPublic() bool { if x != nil { return x.Public } return false } type PubSubTopic struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The pub sub topic name (unique per application) Doc *string `protobuf:"bytes,2,opt,name=doc,proto3,oneof" json:"doc,omitempty"` // The documentation for the topic MessageType *v1.Type `protobuf:"bytes,3,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` // The type of the message DeliveryGuarantee PubSubTopic_DeliveryGuarantee `protobuf:"varint,4,opt,name=delivery_guarantee,json=deliveryGuarantee,proto3,enum=encore.parser.meta.v1.PubSubTopic_DeliveryGuarantee" json:"delivery_guarantee,omitempty"` // The delivery guarantee for the topic OrderingKey string `protobuf:"bytes,5,opt,name=ordering_key,json=orderingKey,proto3" json:"ordering_key,omitempty"` // The field used to group messages; if empty, the topic is not ordered Publishers []*PubSubTopic_Publisher `protobuf:"bytes,6,rep,name=publishers,proto3" json:"publishers,omitempty"` // The publishers for this topic Subscriptions []*PubSubTopic_Subscription `protobuf:"bytes,7,rep,name=subscriptions,proto3" json:"subscriptions,omitempty"` // The subscriptions to the topic unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic) Reset() { *x = PubSubTopic{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic) ProtoMessage() {} func (x *PubSubTopic) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic.ProtoReflect.Descriptor instead. func (*PubSubTopic) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27} } func (x *PubSubTopic) GetName() string { if x != nil { return x.Name } return "" } func (x *PubSubTopic) GetDoc() string { if x != nil && x.Doc != nil { return *x.Doc } return "" } func (x *PubSubTopic) GetMessageType() *v1.Type { if x != nil { return x.MessageType } return nil } func (x *PubSubTopic) GetDeliveryGuarantee() PubSubTopic_DeliveryGuarantee { if x != nil { return x.DeliveryGuarantee } return PubSubTopic_AT_LEAST_ONCE } func (x *PubSubTopic) GetOrderingKey() string { if x != nil { return x.OrderingKey } return "" } func (x *PubSubTopic) GetPublishers() []*PubSubTopic_Publisher { if x != nil { return x.Publishers } return nil } func (x *PubSubTopic) GetSubscriptions() []*PubSubTopic_Subscription { if x != nil { return x.Subscriptions } return nil } type CacheCluster struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The pub sub topic name (unique per application) Doc string `protobuf:"bytes,2,opt,name=doc,proto3" json:"doc,omitempty"` // The documentation for the topic Keyspaces []*CacheCluster_Keyspace `protobuf:"bytes,3,rep,name=keyspaces,proto3" json:"keyspaces,omitempty"` // The publishers for this topic EvictionPolicy string `protobuf:"bytes,4,opt,name=eviction_policy,json=evictionPolicy,proto3" json:"eviction_policy,omitempty"` // redis eviction policy unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheCluster) Reset() { *x = CacheCluster{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheCluster) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheCluster) ProtoMessage() {} func (x *CacheCluster) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheCluster.ProtoReflect.Descriptor instead. func (*CacheCluster) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{28} } func (x *CacheCluster) GetName() string { if x != nil { return x.Name } return "" } func (x *CacheCluster) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *CacheCluster) GetKeyspaces() []*CacheCluster_Keyspace { if x != nil { return x.Keyspaces } return nil } func (x *CacheCluster) GetEvictionPolicy() string { if x != nil { return x.EvictionPolicy } return "" } type Metric struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // the name of the metric ValueType v1.Builtin `protobuf:"varint,2,opt,name=value_type,json=valueType,proto3,enum=encore.parser.schema.v1.Builtin" json:"value_type,omitempty"` Doc string `protobuf:"bytes,3,opt,name=doc,proto3" json:"doc,omitempty"` // the doc string Kind Metric_MetricKind `protobuf:"varint,4,opt,name=kind,proto3,enum=encore.parser.meta.v1.Metric_MetricKind" json:"kind,omitempty"` ServiceName *string `protobuf:"bytes,5,opt,name=service_name,json=serviceName,proto3,oneof" json:"service_name,omitempty"` // the service the metric is exclusive to, if any. Labels []*Metric_Label `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Metric) Reset() { *x = Metric{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Metric) String() string { return protoimpl.X.MessageStringOf(x) } func (*Metric) ProtoMessage() {} func (x *Metric) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Metric.ProtoReflect.Descriptor instead. func (*Metric) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29} } func (x *Metric) GetName() string { if x != nil { return x.Name } return "" } func (x *Metric) GetValueType() v1.Builtin { if x != nil { return x.ValueType } return v1.Builtin(0) } func (x *Metric) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *Metric) GetKind() Metric_MetricKind { if x != nil { return x.Kind } return Metric_COUNTER } func (x *Metric) GetServiceName() string { if x != nil && x.ServiceName != nil { return *x.ServiceName } return "" } func (x *Metric) GetLabels() []*Metric_Label { if x != nil { return x.Labels } return nil } type RPC_ExposeOptions struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPC_ExposeOptions) Reset() { *x = RPC_ExposeOptions{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPC_ExposeOptions) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPC_ExposeOptions) ProtoMessage() {} func (x *RPC_ExposeOptions) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPC_ExposeOptions.ProtoReflect.Descriptor instead. func (*RPC_ExposeOptions) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 1} } type RPC_StaticAssets struct { state protoimpl.MessageState `protogen:"open.v1"` // dir_rel_path is the slash-separated path to the static files directory, // relative to the app root. DirRelPath string `protobuf:"bytes,1,opt,name=dir_rel_path,json=dirRelPath,proto3" json:"dir_rel_path,omitempty"` // not_found_rel_path is the relative path to the file to serve when the requested // file is not found. It is relative to the files_rel_path directory. NotFoundRelPath *string `protobuf:"bytes,2,opt,name=not_found_rel_path,json=notFoundRelPath,proto3,oneof" json:"not_found_rel_path,omitempty"` NotFoundStatus *uint32 `protobuf:"varint,3,opt,name=not_found_status,json=notFoundStatus,proto3,oneof" json:"not_found_status,omitempty"` Headers map[string]*RPC_StaticAssets_HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPC_StaticAssets) Reset() { *x = RPC_StaticAssets{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPC_StaticAssets) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPC_StaticAssets) ProtoMessage() {} func (x *RPC_StaticAssets) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPC_StaticAssets.ProtoReflect.Descriptor instead. func (*RPC_StaticAssets) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 2} } func (x *RPC_StaticAssets) GetDirRelPath() string { if x != nil { return x.DirRelPath } return "" } func (x *RPC_StaticAssets) GetNotFoundRelPath() string { if x != nil && x.NotFoundRelPath != nil { return *x.NotFoundRelPath } return "" } func (x *RPC_StaticAssets) GetNotFoundStatus() uint32 { if x != nil && x.NotFoundStatus != nil { return *x.NotFoundStatus } return 0 } func (x *RPC_StaticAssets) GetHeaders() map[string]*RPC_StaticAssets_HeaderValues { if x != nil { return x.Headers } return nil } // Custom HTTP headers to apply to all static files served. // Each header can have multiple values. type RPC_StaticAssets_HeaderValues struct { state protoimpl.MessageState `protogen:"open.v1"` Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RPC_StaticAssets_HeaderValues) Reset() { *x = RPC_StaticAssets_HeaderValues{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RPC_StaticAssets_HeaderValues) String() string { return protoimpl.X.MessageStringOf(x) } func (*RPC_StaticAssets_HeaderValues) ProtoMessage() {} func (x *RPC_StaticAssets_HeaderValues) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RPC_StaticAssets_HeaderValues.ProtoReflect.Descriptor instead. func (*RPC_StaticAssets_HeaderValues) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 2, 0} } func (x *RPC_StaticAssets_HeaderValues) GetValues() []string { if x != nil { return x.Values } return nil } type Gateway_Explicit struct { state protoimpl.MessageState `protogen:"open.v1"` // The service name this gateway belongs to. ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` AuthHandler *AuthHandler `protobuf:"bytes,2,opt,name=auth_handler,json=authHandler,proto3,oneof" json:"auth_handler,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Gateway_Explicit) Reset() { *x = Gateway_Explicit{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Gateway_Explicit) String() string { return protoimpl.X.MessageStringOf(x) } func (*Gateway_Explicit) ProtoMessage() {} func (x *Gateway_Explicit) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Gateway_Explicit.ProtoReflect.Descriptor instead. func (*Gateway_Explicit) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{22, 0} } func (x *Gateway_Explicit) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *Gateway_Explicit) GetAuthHandler() *AuthHandler { if x != nil { return x.AuthHandler } return nil } type PubSubTopic_Publisher struct { state protoimpl.MessageState `protogen:"open.v1"` ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // The service the publisher is in unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic_Publisher) Reset() { *x = PubSubTopic_Publisher{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic_Publisher) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic_Publisher) ProtoMessage() {} func (x *PubSubTopic_Publisher) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic_Publisher.ProtoReflect.Descriptor instead. func (*PubSubTopic_Publisher) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 0} } func (x *PubSubTopic_Publisher) GetServiceName() string { if x != nil { return x.ServiceName } return "" } type PubSubTopic_Subscription struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The unique name of the subscription for this topic ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // The service that the subscriber is in AckDeadline int64 `protobuf:"varint,3,opt,name=ack_deadline,json=ackDeadline,proto3" json:"ack_deadline,omitempty"` // How long has a consumer got to process and ack a message in nanoseconds MessageRetention int64 `protobuf:"varint,4,opt,name=message_retention,json=messageRetention,proto3" json:"message_retention,omitempty"` // How long is an undelivered message kept in nanoseconds RetryPolicy *PubSubTopic_RetryPolicy `protobuf:"bytes,5,opt,name=retry_policy,json=retryPolicy,proto3" json:"retry_policy,omitempty"` // The retry policy for the subscription // How many messages each instance can process concurrently. // If not set, the default is provider-specific. MaxConcurrency *int32 `protobuf:"varint,6,opt,name=max_concurrency,json=maxConcurrency,proto3,oneof" json:"max_concurrency,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic_Subscription) Reset() { *x = PubSubTopic_Subscription{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic_Subscription) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic_Subscription) ProtoMessage() {} func (x *PubSubTopic_Subscription) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic_Subscription.ProtoReflect.Descriptor instead. func (*PubSubTopic_Subscription) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 1} } func (x *PubSubTopic_Subscription) GetName() string { if x != nil { return x.Name } return "" } func (x *PubSubTopic_Subscription) GetServiceName() string { if x != nil { return x.ServiceName } return "" } func (x *PubSubTopic_Subscription) GetAckDeadline() int64 { if x != nil { return x.AckDeadline } return 0 } func (x *PubSubTopic_Subscription) GetMessageRetention() int64 { if x != nil { return x.MessageRetention } return 0 } func (x *PubSubTopic_Subscription) GetRetryPolicy() *PubSubTopic_RetryPolicy { if x != nil { return x.RetryPolicy } return nil } func (x *PubSubTopic_Subscription) GetMaxConcurrency() int32 { if x != nil && x.MaxConcurrency != nil { return *x.MaxConcurrency } return 0 } type PubSubTopic_RetryPolicy struct { state protoimpl.MessageState `protogen:"open.v1"` MinBackoff int64 `protobuf:"varint,1,opt,name=min_backoff,json=minBackoff,proto3" json:"min_backoff,omitempty"` // min backoff in nanoseconds MaxBackoff int64 `protobuf:"varint,2,opt,name=max_backoff,json=maxBackoff,proto3" json:"max_backoff,omitempty"` // max backoff in nanoseconds MaxRetries int64 `protobuf:"varint,3,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"` // max number of retries unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic_RetryPolicy) Reset() { *x = PubSubTopic_RetryPolicy{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic_RetryPolicy) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic_RetryPolicy) ProtoMessage() {} func (x *PubSubTopic_RetryPolicy) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic_RetryPolicy.ProtoReflect.Descriptor instead. func (*PubSubTopic_RetryPolicy) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 2} } func (x *PubSubTopic_RetryPolicy) GetMinBackoff() int64 { if x != nil { return x.MinBackoff } return 0 } func (x *PubSubTopic_RetryPolicy) GetMaxBackoff() int64 { if x != nil { return x.MaxBackoff } return 0 } func (x *PubSubTopic_RetryPolicy) GetMaxRetries() int64 { if x != nil { return x.MaxRetries } return 0 } type CacheCluster_Keyspace struct { state protoimpl.MessageState `protogen:"open.v1"` KeyType *v1.Type `protobuf:"bytes,1,opt,name=key_type,json=keyType,proto3" json:"key_type,omitempty"` ValueType *v1.Type `protobuf:"bytes,2,opt,name=value_type,json=valueType,proto3" json:"value_type,omitempty"` Service string `protobuf:"bytes,3,opt,name=service,proto3" json:"service,omitempty"` Doc string `protobuf:"bytes,4,opt,name=doc,proto3" json:"doc,omitempty"` PathPattern *Path `protobuf:"bytes,5,opt,name=path_pattern,json=pathPattern,proto3" json:"path_pattern,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CacheCluster_Keyspace) Reset() { *x = CacheCluster_Keyspace{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CacheCluster_Keyspace) String() string { return protoimpl.X.MessageStringOf(x) } func (*CacheCluster_Keyspace) ProtoMessage() {} func (x *CacheCluster_Keyspace) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CacheCluster_Keyspace.ProtoReflect.Descriptor instead. func (*CacheCluster_Keyspace) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{28, 0} } func (x *CacheCluster_Keyspace) GetKeyType() *v1.Type { if x != nil { return x.KeyType } return nil } func (x *CacheCluster_Keyspace) GetValueType() *v1.Type { if x != nil { return x.ValueType } return nil } func (x *CacheCluster_Keyspace) GetService() string { if x != nil { return x.Service } return "" } func (x *CacheCluster_Keyspace) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *CacheCluster_Keyspace) GetPathPattern() *Path { if x != nil { return x.PathPattern } return nil } type Metric_Label struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Type v1.Builtin `protobuf:"varint,2,opt,name=type,proto3,enum=encore.parser.schema.v1.Builtin" json:"type,omitempty"` Doc string `protobuf:"bytes,3,opt,name=doc,proto3" json:"doc,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Metric_Label) Reset() { *x = Metric_Label{} mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Metric_Label) String() string { return protoimpl.X.MessageStringOf(x) } func (*Metric_Label) ProtoMessage() {} func (x *Metric_Label) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_meta_v1_meta_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Metric_Label.ProtoReflect.Descriptor instead. func (*Metric_Label) Descriptor() ([]byte, []int) { return file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29, 0} } func (x *Metric_Label) GetKey() string { if x != nil { return x.Key } return "" } func (x *Metric_Label) GetType() v1.Builtin { if x != nil { return x.Type } return v1.Builtin(0) } func (x *Metric_Label) GetDoc() string { if x != nil { return x.Doc } return "" } var File_encore_parser_meta_v1_meta_proto protoreflect.FileDescriptor const file_encore_parser_meta_v1_meta_proto_rawDesc = "" + "\n" + " encore/parser/meta/v1/meta.proto\x12\x15encore.parser.meta.v1\x1a$encore/parser/schema/v1/schema.proto\"\xdc\a\n" + "\x04Data\x12\x1f\n" + "\vmodule_path\x18\x01 \x01(\tR\n" + "modulePath\x12!\n" + "\fapp_revision\x18\x02 \x01(\tR\vappRevision\x12/\n" + "\x13uncommitted_changes\x18\b \x01(\bR\x12uncommittedChanges\x123\n" + "\x05decls\x18\x03 \x03(\v2\x1d.encore.parser.schema.v1.DeclR\x05decls\x122\n" + "\x04pkgs\x18\x04 \x03(\v2\x1e.encore.parser.meta.v1.PackageR\x04pkgs\x122\n" + "\x04svcs\x18\x05 \x03(\v2\x1e.encore.parser.meta.v1.ServiceR\x04svcs\x12J\n" + "\fauth_handler\x18\x06 \x01(\v2\".encore.parser.meta.v1.AuthHandlerH\x00R\vauthHandler\x88\x01\x01\x12;\n" + "\tcron_jobs\x18\a \x03(\v2\x1e.encore.parser.meta.v1.CronJobR\bcronJobs\x12G\n" + "\rpubsub_topics\x18\t \x03(\v2\".encore.parser.meta.v1.PubSubTopicR\fpubsubTopics\x12A\n" + "\n" + "middleware\x18\n" + " \x03(\v2!.encore.parser.meta.v1.MiddlewareR\n" + "middleware\x12J\n" + "\x0ecache_clusters\x18\v \x03(\v2#.encore.parser.meta.v1.CacheClusterR\rcacheClusters\x12 \n" + "\vexperiments\x18\f \x03(\tR\vexperiments\x127\n" + "\ametrics\x18\r \x03(\v2\x1d.encore.parser.meta.v1.MetricR\ametrics\x12G\n" + "\rsql_databases\x18\x0e \x03(\v2\".encore.parser.meta.v1.SQLDatabaseR\fsqlDatabases\x12:\n" + "\bgateways\x18\x0f \x03(\v2\x1e.encore.parser.meta.v1.GatewayR\bgateways\x127\n" + "\blanguage\x18\x10 \x01(\x0e2\x1b.encore.parser.meta.v1.LangR\blanguage\x127\n" + "\abuckets\x18\x11 \x03(\v2\x1d.encore.parser.meta.v1.BucketR\abucketsB\x0f\n" + "\r_auth_handler\"5\n" + "\rQualifiedName\x12\x10\n" + "\x03pkg\x18\x01 \x01(\tR\x03pkg\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\"\x8d\x02\n" + "\aPackage\x12\x19\n" + "\brel_path\x18\x01 \x01(\tR\arelPath\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" + "\x03doc\x18\x03 \x01(\tR\x03doc\x12!\n" + "\fservice_name\x18\x04 \x01(\tR\vserviceName\x12\x18\n" + "\asecrets\x18\x05 \x03(\tR\asecrets\x12A\n" + "\trpc_calls\x18\x06 \x03(\v2$.encore.parser.meta.v1.QualifiedNameR\brpcCalls\x12A\n" + "\vtrace_nodes\x18\a \x03(\v2 .encore.parser.meta.v1.TraceNodeR\n" + "traceNodes\"\xc1\x02\n" + "\aService\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\brel_path\x18\x02 \x01(\tR\arelPath\x12.\n" + "\x04rpcs\x18\x03 \x03(\v2\x1a.encore.parser.meta.v1.RPCR\x04rpcs\x12B\n" + "\n" + "migrations\x18\x04 \x03(\v2\".encore.parser.meta.v1.DBMigrationR\n" + "migrations\x12\x1c\n" + "\tdatabases\x18\x05 \x03(\tR\tdatabases\x12\x1d\n" + "\n" + "has_config\x18\x06 \x01(\bR\thasConfig\x12<\n" + "\abuckets\x18\a \x03(\v2\".encore.parser.meta.v1.BucketUsageR\abuckets\x12\x18\n" + "\ametrics\x18\b \x03(\tR\ametrics\"\xd8\x02\n" + "\vBucketUsage\x12\x16\n" + "\x06bucket\x18\x01 \x01(\tR\x06bucket\x12L\n" + "\n" + "operations\x18\x02 \x03(\x0e2,.encore.parser.meta.v1.BucketUsage.OperationR\n" + "operations\"\xe2\x01\n" + "\tOperation\x12\v\n" + "\aUNKNOWN\x10\x00\x12\x10\n" + "\fLIST_OBJECTS\x10\x01\x12\x18\n" + "\x14READ_OBJECT_CONTENTS\x10\x02\x12\x10\n" + "\fWRITE_OBJECT\x10\x03\x12\x1a\n" + "\x16UPDATE_OBJECT_METADATA\x10\x04\x12\x17\n" + "\x13GET_OBJECT_METADATA\x10\x05\x12\x11\n" + "\rDELETE_OBJECT\x10\x06\x12\x12\n" + "\x0eGET_PUBLIC_URL\x10\a\x12\x15\n" + "\x11SIGNED_UPLOAD_URL\x10\b\x12\x17\n" + "\x13SIGNED_DOWNLOAD_URL\x10\t\"\x81\x01\n" + "\bSelector\x128\n" + "\x04type\x18\x01 \x01(\x0e2$.encore.parser.meta.v1.Selector.TypeR\x04type\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\"%\n" + "\x04Type\x12\v\n" + "\aUNKNOWN\x10\x00\x12\a\n" + "\x03ALL\x10\x01\x12\a\n" + "\x03TAG\x10\x02\"\xb4\r\n" + "\x03RPC\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x15\n" + "\x03doc\x18\x02 \x01(\tH\x00R\x03doc\x88\x01\x01\x12!\n" + "\fservice_name\x18\x03 \x01(\tR\vserviceName\x12F\n" + "\vaccess_type\x18\x04 \x01(\x0e2%.encore.parser.meta.v1.RPC.AccessTypeR\n" + "accessType\x12I\n" + "\x0erequest_schema\x18\x05 \x01(\v2\x1d.encore.parser.schema.v1.TypeH\x01R\rrequestSchema\x88\x01\x01\x12K\n" + "\x0fresponse_schema\x18\x06 \x01(\v2\x1d.encore.parser.schema.v1.TypeH\x02R\x0eresponseSchema\x88\x01\x01\x129\n" + "\x05proto\x18\a \x01(\x0e2#.encore.parser.meta.v1.RPC.ProtocolR\x05proto\x12.\n" + "\x03loc\x18\b \x01(\v2\x1c.encore.parser.schema.v1.LocR\x03loc\x12/\n" + "\x04path\x18\t \x01(\v2\x1b.encore.parser.meta.v1.PathR\x04path\x12!\n" + "\fhttp_methods\x18\n" + " \x03(\tR\vhttpMethods\x123\n" + "\x04tags\x18\v \x03(\v2\x1f.encore.parser.meta.v1.SelectorR\x04tags\x12\x1c\n" + "\tsensitive\x18\f \x01(\bR\tsensitive\x123\n" + "\x15allow_unauthenticated\x18\r \x01(\bR\x14allowUnauthenticated\x12>\n" + "\x06expose\x18\x0e \x03(\v2&.encore.parser.meta.v1.RPC.ExposeEntryR\x06expose\x12\"\n" + "\n" + "body_limit\x18\x0f \x01(\x04H\x03R\tbodyLimit\x88\x01\x01\x12+\n" + "\x11streaming_request\x18\x10 \x01(\bR\x10streamingRequest\x12-\n" + "\x12streaming_response\x18\x11 \x01(\bR\x11streamingResponse\x12M\n" + "\x10handshake_schema\x18\x12 \x01(\v2\x1d.encore.parser.schema.v1.TypeH\x04R\x0fhandshakeSchema\x88\x01\x01\x12Q\n" + "\rstatic_assets\x18\x13 \x01(\v2'.encore.parser.meta.v1.RPC.StaticAssetsH\x05R\fstaticAssets\x88\x01\x01\x1ac\n" + "\vExposeEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12>\n" + "\x05value\x18\x02 \x01(\v2(.encore.parser.meta.v1.RPC.ExposeOptionsR\x05value:\x028\x01\x1a\x0f\n" + "\rExposeOptions\x1a\xa7\x03\n" + "\fStaticAssets\x12 \n" + "\fdir_rel_path\x18\x01 \x01(\tR\n" + "dirRelPath\x120\n" + "\x12not_found_rel_path\x18\x02 \x01(\tH\x00R\x0fnotFoundRelPath\x88\x01\x01\x12-\n" + "\x10not_found_status\x18\x03 \x01(\rH\x01R\x0enotFoundStatus\x88\x01\x01\x12N\n" + "\aheaders\x18\x04 \x03(\v24.encore.parser.meta.v1.RPC.StaticAssets.HeadersEntryR\aheaders\x1a&\n" + "\fHeaderValues\x12\x16\n" + "\x06values\x18\x01 \x03(\tR\x06values\x1ap\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12J\n" + "\x05value\x18\x02 \x01(\v24.encore.parser.meta.v1.RPC.StaticAssets.HeaderValuesR\x05value:\x028\x01B\x15\n" + "\x13_not_found_rel_pathB\x13\n" + "\x11_not_found_status\"/\n" + "\n" + "AccessType\x12\v\n" + "\aPRIVATE\x10\x00\x12\n" + "\n" + "\x06PUBLIC\x10\x01\x12\b\n" + "\x04AUTH\x10\x02\" \n" + "\bProtocol\x12\v\n" + "\aREGULAR\x10\x00\x12\a\n" + "\x03RAW\x10\x01B\x06\n" + "\x04_docB\x11\n" + "\x0f_request_schemaB\x12\n" + "\x10_response_schemaB\r\n" + "\v_body_limitB\x13\n" + "\x11_handshake_schemaB\x10\n" + "\x0e_static_assets\"\xd2\x02\n" + "\vAuthHandler\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03doc\x18\x02 \x01(\tR\x03doc\x12\x19\n" + "\bpkg_path\x18\x03 \x01(\tR\apkgPath\x12\x19\n" + "\bpkg_name\x18\x04 \x01(\tR\apkgName\x12.\n" + "\x03loc\x18\x05 \x01(\v2\x1c.encore.parser.schema.v1.LocR\x03loc\x12?\n" + "\tauth_data\x18\x06 \x01(\v2\x1d.encore.parser.schema.v1.TypeH\x00R\bauthData\x88\x01\x01\x12:\n" + "\x06params\x18\a \x01(\v2\x1d.encore.parser.schema.v1.TypeH\x01R\x06params\x88\x01\x01\x12!\n" + "\fservice_name\x18\b \x01(\tR\vserviceNameB\f\n" + "\n" + "_auth_dataB\t\n" + "\a_params\"\x92\x02\n" + "\n" + "Middleware\x128\n" + "\x04name\x18\x01 \x01(\v2$.encore.parser.meta.v1.QualifiedNameR\x04name\x12\x10\n" + "\x03doc\x18\x02 \x01(\tR\x03doc\x12.\n" + "\x03loc\x18\x03 \x01(\v2\x1c.encore.parser.schema.v1.LocR\x03loc\x12\x16\n" + "\x06global\x18\x04 \x01(\bR\x06global\x12&\n" + "\fservice_name\x18\x05 \x01(\tH\x00R\vserviceName\x88\x01\x01\x127\n" + "\x06target\x18\x06 \x03(\v2\x1f.encore.parser.meta.v1.SelectorR\x06targetB\x0f\n" + "\r_service_name\"\xa0\b\n" + "\tTraceNode\x12\x0e\n" + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x1a\n" + "\bfilepath\x18\x02 \x01(\tR\bfilepath\x12\x1b\n" + "\tstart_pos\x18\x04 \x01(\x05R\bstartPos\x12\x17\n" + "\aend_pos\x18\x05 \x01(\x05R\x06endPos\x12$\n" + "\x0esrc_line_start\x18\x06 \x01(\x05R\fsrcLineStart\x12 \n" + "\fsrc_line_end\x18\a \x01(\x05R\n" + "srcLineEnd\x12\"\n" + "\rsrc_col_start\x18\b \x01(\x05R\vsrcColStart\x12\x1e\n" + "\vsrc_col_end\x18\t \x01(\x05R\tsrcColEnd\x12<\n" + "\arpc_def\x18\n" + " \x01(\v2!.encore.parser.meta.v1.RPCDefNodeH\x00R\x06rpcDef\x12?\n" + "\brpc_call\x18\v \x01(\v2\".encore.parser.meta.v1.RPCCallNodeH\x00R\arpcCall\x12H\n" + "\vstatic_call\x18\f \x01(\v2%.encore.parser.meta.v1.StaticCallNodeH\x00R\n" + "staticCall\x12U\n" + "\x10auth_handler_def\x18\r \x01(\v2).encore.parser.meta.v1.AuthHandlerDefNodeH\x00R\x0eauthHandlerDef\x12U\n" + "\x10pubsub_topic_def\x18\x0e \x01(\v2).encore.parser.meta.v1.PubSubTopicDefNodeH\x00R\x0epubsubTopicDef\x12Q\n" + "\x0epubsub_publish\x18\x0f \x01(\v2(.encore.parser.meta.v1.PubSubPublishNodeH\x00R\rpubsubPublish\x12Z\n" + "\x11pubsub_subscriber\x18\x10 \x01(\v2+.encore.parser.meta.v1.PubSubSubscriberNodeH\x00R\x10pubsubSubscriber\x12K\n" + "\fservice_init\x18\x11 \x01(\v2&.encore.parser.meta.v1.ServiceInitNodeH\x00R\vserviceInit\x12Q\n" + "\x0emiddleware_def\x18\x12 \x01(\v2(.encore.parser.meta.v1.MiddlewareDefNodeH\x00R\rmiddlewareDef\x12T\n" + "\x0ecache_keyspace\x18\x13 \x01(\v2+.encore.parser.meta.v1.CacheKeyspaceDefNodeH\x00R\rcacheKeyspaceB\t\n" + "\acontext\"d\n" + "\n" + "RPCDefNode\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x19\n" + "\brpc_name\x18\x02 \x01(\tR\arpcName\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\"e\n" + "\vRPCCallNode\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x19\n" + "\brpc_name\x18\x02 \x01(\tR\arpcName\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\"\xb4\x01\n" + "\x0eStaticCallNode\x12G\n" + "\apackage\x18\x01 \x01(\x0e2-.encore.parser.meta.v1.StaticCallNode.PackageR\apackage\x12\x12\n" + "\x04func\x18\x02 \x01(\tR\x04func\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\"+\n" + "\aPackage\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05SQLDB\x10\x01\x12\b\n" + "\x04RLOG\x10\x02\"e\n" + "\x12AuthHandlerDefNode\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\"M\n" + "\x12PubSubTopicDefNode\x12\x1d\n" + "\n" + "topic_name\x18\x01 \x01(\tR\ttopicName\x12\x18\n" + "\acontext\x18\x02 \x01(\tR\acontext\"L\n" + "\x11PubSubPublishNode\x12\x1d\n" + "\n" + "topic_name\x18\x01 \x01(\tR\ttopicName\x12\x18\n" + "\acontext\x18\x02 \x01(\tR\acontext\"\x9b\x01\n" + "\x14PubSubSubscriberNode\x12\x1d\n" + "\n" + "topic_name\x18\x01 \x01(\tR\ttopicName\x12'\n" + "\x0fsubscriber_name\x18\x02 \x01(\tR\x0esubscriberName\x12!\n" + "\fservice_name\x18\x03 \x01(\tR\vserviceName\x12\x18\n" + "\acontext\x18\x04 \x01(\tR\acontext\"v\n" + "\x0fServiceInitNode\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12&\n" + "\x0fsetup_func_name\x18\x02 \x01(\tR\rsetupFuncName\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\"\x9c\x01\n" + "\x11MiddlewareDefNode\x12 \n" + "\fpkg_rel_path\x18\x01 \x01(\tR\n" + "pkgRelPath\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + "\acontext\x18\x03 \x01(\tR\acontext\x127\n" + "\x06target\x18\x04 \x03(\v2\x1f.encore.parser.meta.v1.SelectorR\x06target\"\x90\x01\n" + "\x14CacheKeyspaceDefNode\x12 \n" + "\fpkg_rel_path\x18\x01 \x01(\tR\n" + "pkgRelPath\x12\x19\n" + "\bvar_name\x18\x02 \x01(\tR\avarName\x12!\n" + "\fcluster_name\x18\x03 \x01(\tR\vclusterName\x12\x18\n" + "\acontext\x18\x04 \x01(\tR\acontext\"\xa1\x01\n" + "\x04Path\x12>\n" + "\bsegments\x18\x01 \x03(\v2\".encore.parser.meta.v1.PathSegmentR\bsegments\x124\n" + "\x04type\x18\x02 \x01(\x0e2 .encore.parser.meta.v1.Path.TypeR\x04type\"#\n" + "\x04Type\x12\a\n" + "\x03URL\x10\x00\x12\x12\n" + "\x0eCACHE_KEYSPACE\x10\x01\"\xef\x03\n" + "\vPathSegment\x12B\n" + "\x04type\x18\x01 \x01(\x0e2..encore.parser.meta.v1.PathSegment.SegmentTypeR\x04type\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\x12K\n" + "\n" + "value_type\x18\x03 \x01(\x0e2,.encore.parser.meta.v1.PathSegment.ParamTypeR\tvalueType\x12L\n" + "\n" + "validation\x18\x04 \x01(\v2'.encore.parser.schema.v1.ValidationExprH\x00R\n" + "validation\x88\x01\x01\"A\n" + "\vSegmentType\x12\v\n" + "\aLITERAL\x10\x00\x12\t\n" + "\x05PARAM\x10\x01\x12\f\n" + "\bWILDCARD\x10\x02\x12\f\n" + "\bFALLBACK\x10\x03\"\x98\x01\n" + "\tParamType\x12\n" + "\n" + "\x06STRING\x10\x00\x12\b\n" + "\x04BOOL\x10\x01\x12\b\n" + "\x04INT8\x10\x02\x12\t\n" + "\x05INT16\x10\x03\x12\t\n" + "\x05INT32\x10\x04\x12\t\n" + "\x05INT64\x10\x05\x12\a\n" + "\x03INT\x10\x06\x12\t\n" + "\x05UINT8\x10\a\x12\n" + "\n" + "\x06UINT16\x10\b\x12\n" + "\n" + "\x06UINT32\x10\t\x12\n" + "\n" + "\x06UINT64\x10\n" + "\x12\b\n" + "\x04UINT\x10\v\x12\b\n" + "\x04UUID\x10\fB\r\n" + "\v_validation\"\x8e\x02\n" + "\aGateway\x12\x1f\n" + "\vencore_name\x18\x01 \x01(\tR\n" + "encoreName\x12H\n" + "\bexplicit\x18\x02 \x01(\v2'.encore.parser.meta.v1.Gateway.ExplicitH\x00R\bexplicit\x88\x01\x01\x1a\x8a\x01\n" + "\bExplicit\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12J\n" + "\fauth_handler\x18\x02 \x01(\v2\".encore.parser.meta.v1.AuthHandlerH\x00R\vauthHandler\x88\x01\x01B\x0f\n" + "\r_auth_handlerB\v\n" + "\t_explicit\"\xac\x01\n" + "\aCronJob\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x15\n" + "\x03doc\x18\x03 \x01(\tH\x00R\x03doc\x88\x01\x01\x12\x1a\n" + "\bschedule\x18\x04 \x01(\tR\bschedule\x12@\n" + "\bendpoint\x18\x05 \x01(\v2$.encore.parser.meta.v1.QualifiedNameR\bendpointB\x06\n" + "\x04_doc\"\x95\x02\n" + "\vSQLDatabase\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x15\n" + "\x03doc\x18\x02 \x01(\tH\x00R\x03doc\x88\x01\x01\x121\n" + "\x12migration_rel_path\x18\x03 \x01(\tH\x01R\x10migrationRelPath\x88\x01\x01\x12B\n" + "\n" + "migrations\x18\x04 \x03(\v2\".encore.parser.meta.v1.DBMigrationR\n" + "migrations\x12E\n" + "\x1fallow_non_sequential_migrations\x18\x05 \x01(\bR\x1callowNonSequentialMigrationsB\x06\n" + "\x04_docB\x15\n" + "\x13_migration_rel_path\"c\n" + "\vDBMigration\x12\x1a\n" + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x16\n" + "\x06number\x18\x02 \x01(\x04R\x06number\x12 \n" + "\vdescription\x18\x03 \x01(\tR\vdescription\"q\n" + "\x06Bucket\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x15\n" + "\x03doc\x18\x02 \x01(\tH\x00R\x03doc\x88\x01\x01\x12\x1c\n" + "\tversioned\x18\x03 \x01(\bR\tversioned\x12\x16\n" + "\x06public\x18\x04 \x01(\bR\x06publicB\x06\n" + "\x04_doc\"\xb8\a\n" + "\vPubSubTopic\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x15\n" + "\x03doc\x18\x02 \x01(\tH\x00R\x03doc\x88\x01\x01\x12@\n" + "\fmessage_type\x18\x03 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\vmessageType\x12c\n" + "\x12delivery_guarantee\x18\x04 \x01(\x0e24.encore.parser.meta.v1.PubSubTopic.DeliveryGuaranteeR\x11deliveryGuarantee\x12!\n" + "\fordering_key\x18\x05 \x01(\tR\vorderingKey\x12L\n" + "\n" + "publishers\x18\x06 \x03(\v2,.encore.parser.meta.v1.PubSubTopic.PublisherR\n" + "publishers\x12U\n" + "\rsubscriptions\x18\a \x03(\v2/.encore.parser.meta.v1.PubSubTopic.SubscriptionR\rsubscriptions\x1a.\n" + "\tPublisher\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x1a\xaa\x02\n" + "\fSubscription\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12!\n" + "\fservice_name\x18\x02 \x01(\tR\vserviceName\x12!\n" + "\fack_deadline\x18\x03 \x01(\x03R\vackDeadline\x12+\n" + "\x11message_retention\x18\x04 \x01(\x03R\x10messageRetention\x12Q\n" + "\fretry_policy\x18\x05 \x01(\v2..encore.parser.meta.v1.PubSubTopic.RetryPolicyR\vretryPolicy\x12,\n" + "\x0fmax_concurrency\x18\x06 \x01(\x05H\x00R\x0emaxConcurrency\x88\x01\x01B\x12\n" + "\x10_max_concurrency\x1ap\n" + "\vRetryPolicy\x12\x1f\n" + "\vmin_backoff\x18\x01 \x01(\x03R\n" + "minBackoff\x12\x1f\n" + "\vmax_backoff\x18\x02 \x01(\x03R\n" + "maxBackoff\x12\x1f\n" + "\vmax_retries\x18\x03 \x01(\x03R\n" + "maxRetries\"8\n" + "\x11DeliveryGuarantee\x12\x11\n" + "\rAT_LEAST_ONCE\x10\x00\x12\x10\n" + "\fEXACTLY_ONCE\x10\x01B\x06\n" + "\x04_doc\"\x9a\x03\n" + "\fCacheCluster\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03doc\x18\x02 \x01(\tR\x03doc\x12J\n" + "\tkeyspaces\x18\x03 \x03(\v2,.encore.parser.meta.v1.CacheCluster.KeyspaceR\tkeyspaces\x12'\n" + "\x0feviction_policy\x18\x04 \x01(\tR\x0eevictionPolicy\x1a\xee\x01\n" + "\bKeyspace\x128\n" + "\bkey_type\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\akeyType\x12<\n" + "\n" + "value_type\x18\x02 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\tvalueType\x12\x18\n" + "\aservice\x18\x03 \x01(\tR\aservice\x12\x10\n" + "\x03doc\x18\x04 \x01(\tR\x03doc\x12>\n" + "\fpath_pattern\x18\x05 \x01(\v2\x1b.encore.parser.meta.v1.PathR\vpathPattern\"\xbb\x03\n" + "\x06Metric\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12?\n" + "\n" + "value_type\x18\x02 \x01(\x0e2 .encore.parser.schema.v1.BuiltinR\tvalueType\x12\x10\n" + "\x03doc\x18\x03 \x01(\tR\x03doc\x12<\n" + "\x04kind\x18\x04 \x01(\x0e2(.encore.parser.meta.v1.Metric.MetricKindR\x04kind\x12&\n" + "\fservice_name\x18\x05 \x01(\tH\x00R\vserviceName\x88\x01\x01\x12;\n" + "\x06labels\x18\x06 \x03(\v2#.encore.parser.meta.v1.Metric.LabelR\x06labels\x1aa\n" + "\x05Label\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x124\n" + "\x04type\x18\x02 \x01(\x0e2 .encore.parser.schema.v1.BuiltinR\x04type\x12\x10\n" + "\x03doc\x18\x03 \x01(\tR\x03doc\"3\n" + "\n" + "MetricKind\x12\v\n" + "\aCOUNTER\x10\x00\x12\t\n" + "\x05GAUGE\x10\x01\x12\r\n" + "\tHISTOGRAM\x10\x02B\x0f\n" + "\r_service_name*\x1e\n" + "\x04Lang\x12\x06\n" + "\x02GO\x10\x00\x12\x0e\n" + "\n" + "TYPESCRIPT\x10\x01B&Z$encr.dev/proto/encore/parser/meta/v1b\x06proto3" var ( file_encore_parser_meta_v1_meta_proto_rawDescOnce sync.Once file_encore_parser_meta_v1_meta_proto_rawDescData []byte ) func file_encore_parser_meta_v1_meta_proto_rawDescGZIP() []byte { file_encore_parser_meta_v1_meta_proto_rawDescOnce.Do(func() { file_encore_parser_meta_v1_meta_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_parser_meta_v1_meta_proto_rawDesc), len(file_encore_parser_meta_v1_meta_proto_rawDesc))) }) return file_encore_parser_meta_v1_meta_proto_rawDescData } var file_encore_parser_meta_v1_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 11) var file_encore_parser_meta_v1_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_encore_parser_meta_v1_meta_proto_goTypes = []any{ (Lang)(0), // 0: encore.parser.meta.v1.Lang (BucketUsage_Operation)(0), // 1: encore.parser.meta.v1.BucketUsage.Operation (Selector_Type)(0), // 2: encore.parser.meta.v1.Selector.Type (RPC_AccessType)(0), // 3: encore.parser.meta.v1.RPC.AccessType (RPC_Protocol)(0), // 4: encore.parser.meta.v1.RPC.Protocol (StaticCallNode_Package)(0), // 5: encore.parser.meta.v1.StaticCallNode.Package (Path_Type)(0), // 6: encore.parser.meta.v1.Path.Type (PathSegment_SegmentType)(0), // 7: encore.parser.meta.v1.PathSegment.SegmentType (PathSegment_ParamType)(0), // 8: encore.parser.meta.v1.PathSegment.ParamType (PubSubTopic_DeliveryGuarantee)(0), // 9: encore.parser.meta.v1.PubSubTopic.DeliveryGuarantee (Metric_MetricKind)(0), // 10: encore.parser.meta.v1.Metric.MetricKind (*Data)(nil), // 11: encore.parser.meta.v1.Data (*QualifiedName)(nil), // 12: encore.parser.meta.v1.QualifiedName (*Package)(nil), // 13: encore.parser.meta.v1.Package (*Service)(nil), // 14: encore.parser.meta.v1.Service (*BucketUsage)(nil), // 15: encore.parser.meta.v1.BucketUsage (*Selector)(nil), // 16: encore.parser.meta.v1.Selector (*RPC)(nil), // 17: encore.parser.meta.v1.RPC (*AuthHandler)(nil), // 18: encore.parser.meta.v1.AuthHandler (*Middleware)(nil), // 19: encore.parser.meta.v1.Middleware (*TraceNode)(nil), // 20: encore.parser.meta.v1.TraceNode (*RPCDefNode)(nil), // 21: encore.parser.meta.v1.RPCDefNode (*RPCCallNode)(nil), // 22: encore.parser.meta.v1.RPCCallNode (*StaticCallNode)(nil), // 23: encore.parser.meta.v1.StaticCallNode (*AuthHandlerDefNode)(nil), // 24: encore.parser.meta.v1.AuthHandlerDefNode (*PubSubTopicDefNode)(nil), // 25: encore.parser.meta.v1.PubSubTopicDefNode (*PubSubPublishNode)(nil), // 26: encore.parser.meta.v1.PubSubPublishNode (*PubSubSubscriberNode)(nil), // 27: encore.parser.meta.v1.PubSubSubscriberNode (*ServiceInitNode)(nil), // 28: encore.parser.meta.v1.ServiceInitNode (*MiddlewareDefNode)(nil), // 29: encore.parser.meta.v1.MiddlewareDefNode (*CacheKeyspaceDefNode)(nil), // 30: encore.parser.meta.v1.CacheKeyspaceDefNode (*Path)(nil), // 31: encore.parser.meta.v1.Path (*PathSegment)(nil), // 32: encore.parser.meta.v1.PathSegment (*Gateway)(nil), // 33: encore.parser.meta.v1.Gateway (*CronJob)(nil), // 34: encore.parser.meta.v1.CronJob (*SQLDatabase)(nil), // 35: encore.parser.meta.v1.SQLDatabase (*DBMigration)(nil), // 36: encore.parser.meta.v1.DBMigration (*Bucket)(nil), // 37: encore.parser.meta.v1.Bucket (*PubSubTopic)(nil), // 38: encore.parser.meta.v1.PubSubTopic (*CacheCluster)(nil), // 39: encore.parser.meta.v1.CacheCluster (*Metric)(nil), // 40: encore.parser.meta.v1.Metric nil, // 41: encore.parser.meta.v1.RPC.ExposeEntry (*RPC_ExposeOptions)(nil), // 42: encore.parser.meta.v1.RPC.ExposeOptions (*RPC_StaticAssets)(nil), // 43: encore.parser.meta.v1.RPC.StaticAssets (*RPC_StaticAssets_HeaderValues)(nil), // 44: encore.parser.meta.v1.RPC.StaticAssets.HeaderValues nil, // 45: encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry (*Gateway_Explicit)(nil), // 46: encore.parser.meta.v1.Gateway.Explicit (*PubSubTopic_Publisher)(nil), // 47: encore.parser.meta.v1.PubSubTopic.Publisher (*PubSubTopic_Subscription)(nil), // 48: encore.parser.meta.v1.PubSubTopic.Subscription (*PubSubTopic_RetryPolicy)(nil), // 49: encore.parser.meta.v1.PubSubTopic.RetryPolicy (*CacheCluster_Keyspace)(nil), // 50: encore.parser.meta.v1.CacheCluster.Keyspace (*Metric_Label)(nil), // 51: encore.parser.meta.v1.Metric.Label (*v1.Decl)(nil), // 52: encore.parser.schema.v1.Decl (*v1.Type)(nil), // 53: encore.parser.schema.v1.Type (*v1.Loc)(nil), // 54: encore.parser.schema.v1.Loc (*v1.ValidationExpr)(nil), // 55: encore.parser.schema.v1.ValidationExpr (v1.Builtin)(0), // 56: encore.parser.schema.v1.Builtin } var file_encore_parser_meta_v1_meta_proto_depIdxs = []int32{ 52, // 0: encore.parser.meta.v1.Data.decls:type_name -> encore.parser.schema.v1.Decl 13, // 1: encore.parser.meta.v1.Data.pkgs:type_name -> encore.parser.meta.v1.Package 14, // 2: encore.parser.meta.v1.Data.svcs:type_name -> encore.parser.meta.v1.Service 18, // 3: encore.parser.meta.v1.Data.auth_handler:type_name -> encore.parser.meta.v1.AuthHandler 34, // 4: encore.parser.meta.v1.Data.cron_jobs:type_name -> encore.parser.meta.v1.CronJob 38, // 5: encore.parser.meta.v1.Data.pubsub_topics:type_name -> encore.parser.meta.v1.PubSubTopic 19, // 6: encore.parser.meta.v1.Data.middleware:type_name -> encore.parser.meta.v1.Middleware 39, // 7: encore.parser.meta.v1.Data.cache_clusters:type_name -> encore.parser.meta.v1.CacheCluster 40, // 8: encore.parser.meta.v1.Data.metrics:type_name -> encore.parser.meta.v1.Metric 35, // 9: encore.parser.meta.v1.Data.sql_databases:type_name -> encore.parser.meta.v1.SQLDatabase 33, // 10: encore.parser.meta.v1.Data.gateways:type_name -> encore.parser.meta.v1.Gateway 0, // 11: encore.parser.meta.v1.Data.language:type_name -> encore.parser.meta.v1.Lang 37, // 12: encore.parser.meta.v1.Data.buckets:type_name -> encore.parser.meta.v1.Bucket 12, // 13: encore.parser.meta.v1.Package.rpc_calls:type_name -> encore.parser.meta.v1.QualifiedName 20, // 14: encore.parser.meta.v1.Package.trace_nodes:type_name -> encore.parser.meta.v1.TraceNode 17, // 15: encore.parser.meta.v1.Service.rpcs:type_name -> encore.parser.meta.v1.RPC 36, // 16: encore.parser.meta.v1.Service.migrations:type_name -> encore.parser.meta.v1.DBMigration 15, // 17: encore.parser.meta.v1.Service.buckets:type_name -> encore.parser.meta.v1.BucketUsage 1, // 18: encore.parser.meta.v1.BucketUsage.operations:type_name -> encore.parser.meta.v1.BucketUsage.Operation 2, // 19: encore.parser.meta.v1.Selector.type:type_name -> encore.parser.meta.v1.Selector.Type 3, // 20: encore.parser.meta.v1.RPC.access_type:type_name -> encore.parser.meta.v1.RPC.AccessType 53, // 21: encore.parser.meta.v1.RPC.request_schema:type_name -> encore.parser.schema.v1.Type 53, // 22: encore.parser.meta.v1.RPC.response_schema:type_name -> encore.parser.schema.v1.Type 4, // 23: encore.parser.meta.v1.RPC.proto:type_name -> encore.parser.meta.v1.RPC.Protocol 54, // 24: encore.parser.meta.v1.RPC.loc:type_name -> encore.parser.schema.v1.Loc 31, // 25: encore.parser.meta.v1.RPC.path:type_name -> encore.parser.meta.v1.Path 16, // 26: encore.parser.meta.v1.RPC.tags:type_name -> encore.parser.meta.v1.Selector 41, // 27: encore.parser.meta.v1.RPC.expose:type_name -> encore.parser.meta.v1.RPC.ExposeEntry 53, // 28: encore.parser.meta.v1.RPC.handshake_schema:type_name -> encore.parser.schema.v1.Type 43, // 29: encore.parser.meta.v1.RPC.static_assets:type_name -> encore.parser.meta.v1.RPC.StaticAssets 54, // 30: encore.parser.meta.v1.AuthHandler.loc:type_name -> encore.parser.schema.v1.Loc 53, // 31: encore.parser.meta.v1.AuthHandler.auth_data:type_name -> encore.parser.schema.v1.Type 53, // 32: encore.parser.meta.v1.AuthHandler.params:type_name -> encore.parser.schema.v1.Type 12, // 33: encore.parser.meta.v1.Middleware.name:type_name -> encore.parser.meta.v1.QualifiedName 54, // 34: encore.parser.meta.v1.Middleware.loc:type_name -> encore.parser.schema.v1.Loc 16, // 35: encore.parser.meta.v1.Middleware.target:type_name -> encore.parser.meta.v1.Selector 21, // 36: encore.parser.meta.v1.TraceNode.rpc_def:type_name -> encore.parser.meta.v1.RPCDefNode 22, // 37: encore.parser.meta.v1.TraceNode.rpc_call:type_name -> encore.parser.meta.v1.RPCCallNode 23, // 38: encore.parser.meta.v1.TraceNode.static_call:type_name -> encore.parser.meta.v1.StaticCallNode 24, // 39: encore.parser.meta.v1.TraceNode.auth_handler_def:type_name -> encore.parser.meta.v1.AuthHandlerDefNode 25, // 40: encore.parser.meta.v1.TraceNode.pubsub_topic_def:type_name -> encore.parser.meta.v1.PubSubTopicDefNode 26, // 41: encore.parser.meta.v1.TraceNode.pubsub_publish:type_name -> encore.parser.meta.v1.PubSubPublishNode 27, // 42: encore.parser.meta.v1.TraceNode.pubsub_subscriber:type_name -> encore.parser.meta.v1.PubSubSubscriberNode 28, // 43: encore.parser.meta.v1.TraceNode.service_init:type_name -> encore.parser.meta.v1.ServiceInitNode 29, // 44: encore.parser.meta.v1.TraceNode.middleware_def:type_name -> encore.parser.meta.v1.MiddlewareDefNode 30, // 45: encore.parser.meta.v1.TraceNode.cache_keyspace:type_name -> encore.parser.meta.v1.CacheKeyspaceDefNode 5, // 46: encore.parser.meta.v1.StaticCallNode.package:type_name -> encore.parser.meta.v1.StaticCallNode.Package 16, // 47: encore.parser.meta.v1.MiddlewareDefNode.target:type_name -> encore.parser.meta.v1.Selector 32, // 48: encore.parser.meta.v1.Path.segments:type_name -> encore.parser.meta.v1.PathSegment 6, // 49: encore.parser.meta.v1.Path.type:type_name -> encore.parser.meta.v1.Path.Type 7, // 50: encore.parser.meta.v1.PathSegment.type:type_name -> encore.parser.meta.v1.PathSegment.SegmentType 8, // 51: encore.parser.meta.v1.PathSegment.value_type:type_name -> encore.parser.meta.v1.PathSegment.ParamType 55, // 52: encore.parser.meta.v1.PathSegment.validation:type_name -> encore.parser.schema.v1.ValidationExpr 46, // 53: encore.parser.meta.v1.Gateway.explicit:type_name -> encore.parser.meta.v1.Gateway.Explicit 12, // 54: encore.parser.meta.v1.CronJob.endpoint:type_name -> encore.parser.meta.v1.QualifiedName 36, // 55: encore.parser.meta.v1.SQLDatabase.migrations:type_name -> encore.parser.meta.v1.DBMigration 53, // 56: encore.parser.meta.v1.PubSubTopic.message_type:type_name -> encore.parser.schema.v1.Type 9, // 57: encore.parser.meta.v1.PubSubTopic.delivery_guarantee:type_name -> encore.parser.meta.v1.PubSubTopic.DeliveryGuarantee 47, // 58: encore.parser.meta.v1.PubSubTopic.publishers:type_name -> encore.parser.meta.v1.PubSubTopic.Publisher 48, // 59: encore.parser.meta.v1.PubSubTopic.subscriptions:type_name -> encore.parser.meta.v1.PubSubTopic.Subscription 50, // 60: encore.parser.meta.v1.CacheCluster.keyspaces:type_name -> encore.parser.meta.v1.CacheCluster.Keyspace 56, // 61: encore.parser.meta.v1.Metric.value_type:type_name -> encore.parser.schema.v1.Builtin 10, // 62: encore.parser.meta.v1.Metric.kind:type_name -> encore.parser.meta.v1.Metric.MetricKind 51, // 63: encore.parser.meta.v1.Metric.labels:type_name -> encore.parser.meta.v1.Metric.Label 42, // 64: encore.parser.meta.v1.RPC.ExposeEntry.value:type_name -> encore.parser.meta.v1.RPC.ExposeOptions 45, // 65: encore.parser.meta.v1.RPC.StaticAssets.headers:type_name -> encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry 44, // 66: encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry.value:type_name -> encore.parser.meta.v1.RPC.StaticAssets.HeaderValues 18, // 67: encore.parser.meta.v1.Gateway.Explicit.auth_handler:type_name -> encore.parser.meta.v1.AuthHandler 49, // 68: encore.parser.meta.v1.PubSubTopic.Subscription.retry_policy:type_name -> encore.parser.meta.v1.PubSubTopic.RetryPolicy 53, // 69: encore.parser.meta.v1.CacheCluster.Keyspace.key_type:type_name -> encore.parser.schema.v1.Type 53, // 70: encore.parser.meta.v1.CacheCluster.Keyspace.value_type:type_name -> encore.parser.schema.v1.Type 31, // 71: encore.parser.meta.v1.CacheCluster.Keyspace.path_pattern:type_name -> encore.parser.meta.v1.Path 56, // 72: encore.parser.meta.v1.Metric.Label.type:type_name -> encore.parser.schema.v1.Builtin 73, // [73:73] is the sub-list for method output_type 73, // [73:73] is the sub-list for method input_type 73, // [73:73] is the sub-list for extension type_name 73, // [73:73] is the sub-list for extension extendee 0, // [0:73] is the sub-list for field type_name } func init() { file_encore_parser_meta_v1_meta_proto_init() } func file_encore_parser_meta_v1_meta_proto_init() { if File_encore_parser_meta_v1_meta_proto != nil { return } file_encore_parser_meta_v1_meta_proto_msgTypes[0].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[6].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[7].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[8].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[9].OneofWrappers = []any{ (*TraceNode_RpcDef)(nil), (*TraceNode_RpcCall)(nil), (*TraceNode_StaticCall)(nil), (*TraceNode_AuthHandlerDef)(nil), (*TraceNode_PubsubTopicDef)(nil), (*TraceNode_PubsubPublish)(nil), (*TraceNode_PubsubSubscriber)(nil), (*TraceNode_ServiceInit)(nil), (*TraceNode_MiddlewareDef)(nil), (*TraceNode_CacheKeyspace)(nil), } file_encore_parser_meta_v1_meta_proto_msgTypes[21].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[22].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[23].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[24].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[26].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[27].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[29].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[32].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[35].OneofWrappers = []any{} file_encore_parser_meta_v1_meta_proto_msgTypes[37].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_parser_meta_v1_meta_proto_rawDesc), len(file_encore_parser_meta_v1_meta_proto_rawDesc)), NumEnums: 11, NumMessages: 41, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_parser_meta_v1_meta_proto_goTypes, DependencyIndexes: file_encore_parser_meta_v1_meta_proto_depIdxs, EnumInfos: file_encore_parser_meta_v1_meta_proto_enumTypes, MessageInfos: file_encore_parser_meta_v1_meta_proto_msgTypes, }.Build() File_encore_parser_meta_v1_meta_proto = out.File file_encore_parser_meta_v1_meta_proto_goTypes = nil file_encore_parser_meta_v1_meta_proto_depIdxs = nil } ================================================ FILE: proto/encore/parser/meta/v1/meta.pb.ts ================================================ /* eslint-disable */ import type { Loc, Type, Builtin, Decl, } from "../../../../encore/parser/schema/v1/schema.pb"; export const protobufPackage = "encore.parser.meta.v1"; /** Data is the metadata associated with an app version. */ export interface Data { /** app module path */ module_path: string; /** app revision (always the VCS revision reference) */ app_revision: string; /** true if there where changes made on-top of the VCS revision */ uncommitted_changes: boolean; decls: Decl[]; pkgs: Package[]; svcs: Service[]; /** the auth handler or nil */ auth_handler?: AuthHandler | undefined; cron_jobs: CronJob[]; /** All the pub sub topics declared in the application */ pubsub_topics: PubSubTopic[]; middleware: Middleware[]; cache_clusters: CacheCluster[]; experiments: string[]; metrics: Metric[]; sql_databases: SQLDatabase[]; } /** * QualifiedName is a name of an object in a specific package. * It is never an unqualified name, even in circumstances * where a package may refer to its own objects. */ export interface QualifiedName { /** "rel/path/to/pkg" */ pkg: string; /** ObjectName */ name: string; } export interface Package { /** import path relative to app root ("." for the app root itself) */ rel_path: string; /** package name as declared in Go files */ name: string; /** associated documentation */ doc: string; /** service name this package is a part of, if any */ service_name: string; /** secrets required by this package */ secrets: string[]; /** RPCs called by the package */ rpc_calls: QualifiedName[]; trace_nodes: TraceNode[]; } export interface Service { name: string; /** import path relative to app root for the root package in the service */ rel_path: string; rpcs: RPC[]; migrations: DBMigration[]; /** databases this service connects to */ databases: string[]; /** true if the service has uses config */ has_config: boolean; } export interface Selector { type: Selector_Type; value: string; } export enum Selector_Type { UNKNOWN = "UNKNOWN", ALL = "ALL", /** TAG - NOTE: If more types are added, update the (selector.Selector).ToProto method. */ TAG = "TAG", UNRECOGNIZED = "UNRECOGNIZED", } export interface RPC { /** name of the RPC endpoint */ name: string; /** associated documentation */ doc: string; /** the service the RPC belongs to. */ service_name: string; /** how can the RPC be accessed? */ access_type: RPC_AccessType; /** request schema, or nil */ request_schema?: Type | undefined; /** response schema, or nil */ response_schema?: Type | undefined; proto: RPC_Protocol; loc: Loc; path: Path; http_methods: string[]; tags: Selector[]; } export enum RPC_AccessType { PRIVATE = "PRIVATE", PUBLIC = "PUBLIC", AUTH = "AUTH", UNRECOGNIZED = "UNRECOGNIZED", } export enum RPC_Protocol { REGULAR = "REGULAR", RAW = "RAW", UNRECOGNIZED = "UNRECOGNIZED", } export interface AuthHandler { name: string; doc: string; /** package (service) import path */ pkg_path: string; /** package (service) name */ pkg_name: string; loc: Loc; /** custom auth data, or nil */ auth_data?: Type | undefined; /** builtin string or named type */ params?: Type | undefined; } export interface Middleware { name: QualifiedName; doc: string; loc: Loc; global: boolean; /** nil if global */ service_name?: string | undefined; target: Selector[]; } export interface TraceNode { id: number; /** slash-separated, relative to app root */ filepath: string; start_pos: number; end_pos: number; src_line_start: number; src_line_end: number; src_col_start: number; src_col_end: number; rpc_def: RPCDefNode | undefined; rpc_call: RPCCallNode | undefined; static_call: StaticCallNode | undefined; auth_handler_def: AuthHandlerDefNode | undefined; pubsub_topic_def: PubSubTopicDefNode | undefined; pubsub_publish: PubSubPublishNode | undefined; pubsub_subscriber: PubSubSubscriberNode | undefined; service_init: ServiceInitNode | undefined; middleware_def: MiddlewareDefNode | undefined; cache_keyspace: CacheKeyspaceDefNode | undefined; } export interface RPCDefNode { service_name: string; rpc_name: string; context: string; } export interface RPCCallNode { service_name: string; rpc_name: string; context: string; } export interface StaticCallNode { package: StaticCallNode_Package; func: string; context: string; } export enum StaticCallNode_Package { UNKNOWN = "UNKNOWN", SQLDB = "SQLDB", RLOG = "RLOG", UNRECOGNIZED = "UNRECOGNIZED", } export interface AuthHandlerDefNode { service_name: string; name: string; context: string; } export interface PubSubTopicDefNode { topic_name: string; context: string; } export interface PubSubPublishNode { topic_name: string; context: string; } export interface PubSubSubscriberNode { topic_name: string; subscriber_name: string; service_name: string; context: string; } export interface ServiceInitNode { service_name: string; setup_func_name: string; context: string; } export interface MiddlewareDefNode { pkg_rel_path: string; name: string; context: string; target: Selector[]; } export interface CacheKeyspaceDefNode { pkg_rel_path: string; var_name: string; cluster_name: string; context: string; } export interface Path { segments: PathSegment[]; type: Path_Type; } export enum Path_Type { URL = "URL", CACHE_KEYSPACE = "CACHE_KEYSPACE", UNRECOGNIZED = "UNRECOGNIZED", } export interface PathSegment { type: PathSegment_SegmentType; value: string; value_type: PathSegment_ParamType; } export enum PathSegment_SegmentType { LITERAL = "LITERAL", PARAM = "PARAM", WILDCARD = "WILDCARD", FALLBACK = "FALLBACK", UNRECOGNIZED = "UNRECOGNIZED", } export enum PathSegment_ParamType { STRING = "STRING", BOOL = "BOOL", INT8 = "INT8", INT16 = "INT16", INT32 = "INT32", INT64 = "INT64", INT = "INT", UINT8 = "UINT8", UINT16 = "UINT16", UINT32 = "UINT32", UINT64 = "UINT64", UINT = "UINT", UUID = "UUID", UNRECOGNIZED = "UNRECOGNIZED", } export interface CronJob { id: string; title: string; doc: string; schedule: string; endpoint: QualifiedName; } export interface SQLDatabase { name: string; doc: string; /** * migration_rel_path is the slash-separated path to the migrations, * relative to the main module's root directory. */ migration_rel_path: string; migrations: DBMigration[]; } export interface DBMigration { /** filename */ filename: string; /** migration number */ number: number; /** descriptive name */ description: string; } export interface PubSubTopic { /** The pub sub topic name (unique per application) */ name: string; /** The documentation for the topic */ doc: string; /** The type of the message */ message_type: Type; /** The delivery guarantee for the topic */ delivery_guarantee: PubSubTopic_DeliveryGuarantee; /** The field used to group messages; if empty, the topic is not ordered */ ordering_key: string; /** The publishers for this topic */ publishers: PubSubTopic_Publisher[]; /** The subscriptions to the topic */ subscriptions: PubSubTopic_Subscription[]; } export enum PubSubTopic_DeliveryGuarantee { /** AT_LEAST_ONCE - All messages will be delivered to each subscription at least once */ AT_LEAST_ONCE = "AT_LEAST_ONCE", /** EXACTLY_ONCE - All messages will be delivered to each subscription exactly once */ EXACTLY_ONCE = "EXACTLY_ONCE", UNRECOGNIZED = "UNRECOGNIZED", } export interface PubSubTopic_Publisher { /** The service the publisher is in */ service_name: string; } export interface PubSubTopic_Subscription { /** The unique name of the subscription for this topic */ name: string; /** The service that the subscriber is in */ service_name: string; /** How long has a consumer got to process and ack a message in nanoseconds */ ack_deadline: number; /** How long is an undelivered message kept in nanoseconds */ message_retention: number; /** The retry policy for the subscription */ retry_policy: PubSubTopic_RetryPolicy; } export interface PubSubTopic_RetryPolicy { /** min backoff in nanoseconds */ min_backoff: number; /** max backoff in nanoseconds */ max_backoff: number; /** max number of retries */ max_retries: number; } export interface CacheCluster { /** The pub sub topic name (unique per application) */ name: string; /** The documentation for the topic */ doc: string; /** The publishers for this topic */ keyspaces: CacheCluster_Keyspace[]; /** redis eviction policy */ eviction_policy: string; } export interface CacheCluster_Keyspace { key_type: Type; value_type: Type; service: string; doc: string; path_pattern: Path; } export interface Metric { /** the name of the metric */ name: string; value_type: Builtin; /** the doc string */ doc: string; kind: Metric_MetricKind; /** the service the metric is exclusive to, if any. */ service_name?: string | undefined; labels: Metric_Label[]; } export enum Metric_MetricKind { COUNTER = "COUNTER", GAUGE = "GAUGE", HISTOGRAM = "HISTOGRAM", UNRECOGNIZED = "UNRECOGNIZED", } export interface Metric_Label { key: string; type: Builtin; doc: string; } ================================================ FILE: proto/encore/parser/meta/v1/meta.proto ================================================ syntax = "proto3"; package encore.parser.meta.v1; import "encore/parser/schema/v1/schema.proto"; option go_package = "encr.dev/proto/encore/parser/meta/v1"; // Data is the metadata associated with an app version. message Data { string module_path = 1; // app module path string app_revision = 2; // app revision (always the VCS revision reference) bool uncommitted_changes = 8; // true if there where changes made on-top of the VCS revision repeated schema.v1.Decl decls = 3; repeated Package pkgs = 4; repeated Service svcs = 5; optional AuthHandler auth_handler = 6; // the auth handler or nil repeated CronJob cron_jobs = 7; repeated PubSubTopic pubsub_topics = 9; // All the pub sub topics declared in the application repeated Middleware middleware = 10; repeated CacheCluster cache_clusters = 11; repeated string experiments = 12; repeated Metric metrics = 13; repeated SQLDatabase sql_databases = 14; repeated Gateway gateways = 15; Lang language = 16; repeated Bucket buckets = 17; } // Lang describes the language an application is written in. // Defaults to Go if not set. enum Lang { GO = 0; TYPESCRIPT = 1; } // QualifiedName is a name of an object in a specific package. // It is never an unqualified name, even in circumstances // where a package may refer to its own objects. message QualifiedName { string pkg = 1; // "rel/path/to/pkg" string name = 2; // ObjectName } message Package { string rel_path = 1; // import path relative to app root ("." for the app root itself) string name = 2; // package name as declared in Go files string doc = 3; // associated documentation string service_name = 4; // service name this package is a part of, if any repeated string secrets = 5; // secrets required by this package repeated QualifiedName rpc_calls = 6; // RPCs called by the package repeated TraceNode trace_nodes = 7; } message Service { string name = 1; string rel_path = 2; // import path relative to app root for the root package in the service repeated RPC rpcs = 3; repeated DBMigration migrations = 4; repeated string databases = 5; // databases this service connects to bool has_config = 6; // true if the service has uses config repeated BucketUsage buckets = 7; // buckets this service uses repeated string metrics = 8; // metrics this service uses } message BucketUsage { // The encore name of the bucket. string bucket = 1; // Recorded operations. repeated Operation operations = 2; enum Operation { UNKNOWN = 0; // Listing objects and accessing their metadata during listing. LIST_OBJECTS = 1; // Reading the contents of an object. READ_OBJECT_CONTENTS = 2; // Creating or updating an object, with contents and metadata. WRITE_OBJECT = 3; // Updating the metadata of an object, without reading or writing its contents. UPDATE_OBJECT_METADATA = 4; // Reading the metadata of an object, or checking for its existence. GET_OBJECT_METADATA = 5; // Deleting an object. DELETE_OBJECT = 6; // Get an bucket/object's public url. GET_PUBLIC_URL = 7; // Generating a signed URL to allow an external recipient to create or // update an object. SIGNED_UPLOAD_URL = 8; // Generating a signed URL to allow an external recipient to download an object. SIGNED_DOWNLOAD_URL = 9; } } message Selector { enum Type { UNKNOWN = 0; ALL = 1; TAG = 2; // NOTE: If more types are added, update the (selector.Selector).ToProto method. } Type type = 1; string value = 2; } message RPC { string name = 1; // name of the RPC endpoint optional string doc = 2; // associated documentation string service_name = 3; // the service the RPC belongs to. AccessType access_type = 4; // how can the RPC be accessed? optional schema.v1.Type request_schema = 5; // request schema, or nil optional schema.v1.Type response_schema = 6; // response schema, or nil Protocol proto = 7; schema.v1.Loc loc = 8; Path path = 9; repeated string http_methods = 10; repeated Selector tags = 11; // sensitive reports whether the whole payload is sensitive. // If true, none of the request/response payload will be traced. bool sensitive = 12; // Whether the endpoint can be called without auth parameters. bool allow_unauthenticated = 13; // Whether the endpoint is exposed to the public, keyed by gateway. map expose = 14; // The maximum size of the request body in bytes. // If not set, defaults to no limit. optional uint64 body_limit = 15; // If the endpoint is streaming bool streaming_request = 16; bool streaming_response = 17; optional schema.v1.Type handshake_schema = 18; // handshake schema, or nil // If the endpoint serves static assets. optional StaticAssets static_assets = 19; enum AccessType { PRIVATE = 0; PUBLIC = 1; AUTH = 2; } enum Protocol { REGULAR = 0; RAW = 1; } message ExposeOptions {} message StaticAssets { // dir_rel_path is the slash-separated path to the static files directory, // relative to the app root. string dir_rel_path = 1; // not_found_rel_path is the relative path to the file to serve when the requested // file is not found. It is relative to the files_rel_path directory. optional string not_found_rel_path = 2; optional uint32 not_found_status = 3; // Custom HTTP headers to apply to all static files served. // Each header can have multiple values. message HeaderValues { repeated string values = 1; } map headers = 4; } } message AuthHandler { string name = 1; string doc = 2; string pkg_path = 3; // package (service) import path string pkg_name = 4; // package (service) name schema.v1.Loc loc = 5; optional schema.v1.Type auth_data = 6; // custom auth data, or nil optional schema.v1.Type params = 7; // builtin string or named type string service_name = 8; } message Middleware { QualifiedName name = 1; string doc = 2; schema.v1.Loc loc = 3; bool global = 4; optional string service_name = 5; // nil if global repeated Selector target = 6; } message TraceNode { int32 id = 1; string filepath = 2; // slash-separated, relative to app root int32 start_pos = 4; int32 end_pos = 5; int32 src_line_start = 6; int32 src_line_end = 7; int32 src_col_start = 8; int32 src_col_end = 9; oneof context { RPCDefNode rpc_def = 10; RPCCallNode rpc_call = 11; StaticCallNode static_call = 12; AuthHandlerDefNode auth_handler_def = 13; PubSubTopicDefNode pubsub_topic_def = 14; PubSubPublishNode pubsub_publish = 15; PubSubSubscriberNode pubsub_subscriber = 16; ServiceInitNode service_init = 17; MiddlewareDefNode middleware_def = 18; CacheKeyspaceDefNode cache_keyspace = 19; } } message RPCDefNode { string service_name = 1; string rpc_name = 2; string context = 3; } message RPCCallNode { string service_name = 1; string rpc_name = 2; string context = 3; } message StaticCallNode { enum Package { UNKNOWN = 0; SQLDB = 1; RLOG = 2; } Package package = 1; string func = 2; string context = 3; } message AuthHandlerDefNode { string service_name = 1; string name = 2; string context = 3; } message PubSubTopicDefNode { string topic_name = 1; string context = 2; } message PubSubPublishNode { string topic_name = 1; string context = 2; } message PubSubSubscriberNode { string topic_name = 1; string subscriber_name = 2; string service_name = 3; string context = 4; } message ServiceInitNode { string service_name = 1; string setup_func_name = 2; string context = 3; } message MiddlewareDefNode { string pkg_rel_path = 1; string name = 2; string context = 3; repeated Selector target = 4; } message CacheKeyspaceDefNode { string pkg_rel_path = 1; string var_name = 2; string cluster_name = 3; string context = 4; } message Path { enum Type { URL = 0; CACHE_KEYSPACE = 1; } repeated PathSegment segments = 1; Type type = 2; } message PathSegment { enum SegmentType { LITERAL = 0; PARAM = 1; WILDCARD = 2; FALLBACK = 3; } enum ParamType { STRING = 0; BOOL = 1; INT8 = 2; INT16 = 3; INT32 = 4; INT64 = 5; INT = 6; UINT8 = 7; UINT16 = 8; UINT32 = 9; UINT64 = 10; UINT = 11; UUID = 12; } SegmentType type = 1; string value = 2; ParamType value_type = 3; optional schema.v1.ValidationExpr validation = 4; } message Gateway { string encore_name = 1; // Spec is the configuration for the gateway, if it's explicitly defined. optional Explicit explicit = 2; message Explicit { // The service name this gateway belongs to. string service_name = 1; optional AuthHandler auth_handler = 2; } } message CronJob { string id = 1; string title = 2; optional string doc = 3; string schedule = 4; QualifiedName endpoint = 5; } message SQLDatabase { string name = 1; optional string doc = 2; // migration_rel_path is the slash-separated path to the migrations, // relative to the main module's root directory. optional string migration_rel_path = 3; repeated DBMigration migrations = 4; bool allow_non_sequential_migrations = 5; } message DBMigration { string filename = 1; // filename uint64 number = 2; // migration number string description = 3; // descriptive name } message Bucket { string name = 1; optional string doc = 2; bool versioned = 3; bool public = 4; } message PubSubTopic { string name = 1; // The pub sub topic name (unique per application) optional string doc = 2; // The documentation for the topic schema.v1.Type message_type = 3; // The type of the message DeliveryGuarantee delivery_guarantee = 4; // The delivery guarantee for the topic string ordering_key = 5; // The field used to group messages; if empty, the topic is not ordered repeated Publisher publishers = 6; // The publishers for this topic repeated Subscription subscriptions = 7; // The subscriptions to the topic message Publisher { string service_name = 1; // The service the publisher is in } message Subscription { string name = 1; // The unique name of the subscription for this topic string service_name = 2; // The service that the subscriber is in int64 ack_deadline = 3; // How long has a consumer got to process and ack a message in nanoseconds int64 message_retention = 4; // How long is an undelivered message kept in nanoseconds RetryPolicy retry_policy = 5; // The retry policy for the subscription // How many messages each instance can process concurrently. // If not set, the default is provider-specific. optional int32 max_concurrency = 6; } message RetryPolicy { int64 min_backoff = 1; // min backoff in nanoseconds int64 max_backoff = 2; // max backoff in nanoseconds int64 max_retries = 3; // max number of retries } enum DeliveryGuarantee { AT_LEAST_ONCE = 0; // All messages will be delivered to each subscription at least once EXACTLY_ONCE = 1; // All messages will be delivered to each subscription exactly once } } message CacheCluster { string name = 1; // The pub sub topic name (unique per application) string doc = 2; // The documentation for the topic repeated Keyspace keyspaces = 3; // The publishers for this topic string eviction_policy = 4; // redis eviction policy message Keyspace { schema.v1.Type key_type = 1; schema.v1.Type value_type = 2; string service = 3; string doc = 4; Path path_pattern = 5; } } message Metric { string name = 1; // the name of the metric schema.v1.Builtin value_type = 2; string doc = 3; // the doc string MetricKind kind = 4; optional string service_name = 5; // the service the metric is exclusive to, if any. repeated Label labels = 6; enum MetricKind { COUNTER = 0; GAUGE = 1; HISTOGRAM = 2; } message Label { string key = 1; schema.v1.Builtin type = 2; string doc = 3; } } ================================================ FILE: proto/encore/parser/schema/v1/schema.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/parser/schema/v1/schema.proto package v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling // directly, rather than needing to understand the full implementation details of how the type is structured. type Builtin int32 const ( // Inbuilt Go Types Builtin_ANY Builtin = 0 Builtin_BOOL Builtin = 1 Builtin_INT8 Builtin = 2 Builtin_INT16 Builtin = 3 Builtin_INT32 Builtin = 4 Builtin_INT64 Builtin = 5 Builtin_UINT8 Builtin = 6 Builtin_UINT16 Builtin = 7 Builtin_UINT32 Builtin = 8 Builtin_UINT64 Builtin = 9 Builtin_FLOAT32 Builtin = 10 Builtin_FLOAT64 Builtin = 11 Builtin_STRING Builtin = 12 Builtin_BYTES Builtin = 13 // Additional Encore Types Builtin_TIME Builtin = 14 Builtin_UUID Builtin = 15 Builtin_JSON Builtin = 16 Builtin_USER_ID Builtin = 17 Builtin_INT Builtin = 18 Builtin_UINT Builtin = 19 Builtin_DECIMAL Builtin = 20 ) // Enum value maps for Builtin. var ( Builtin_name = map[int32]string{ 0: "ANY", 1: "BOOL", 2: "INT8", 3: "INT16", 4: "INT32", 5: "INT64", 6: "UINT8", 7: "UINT16", 8: "UINT32", 9: "UINT64", 10: "FLOAT32", 11: "FLOAT64", 12: "STRING", 13: "BYTES", 14: "TIME", 15: "UUID", 16: "JSON", 17: "USER_ID", 18: "INT", 19: "UINT", 20: "DECIMAL", } Builtin_value = map[string]int32{ "ANY": 0, "BOOL": 1, "INT8": 2, "INT16": 3, "INT32": 4, "INT64": 5, "UINT8": 6, "UINT16": 7, "UINT32": 8, "UINT64": 9, "FLOAT32": 10, "FLOAT64": 11, "STRING": 12, "BYTES": 13, "TIME": 14, "UUID": 15, "JSON": 16, "USER_ID": 17, "INT": 18, "UINT": 19, "DECIMAL": 20, } ) func (x Builtin) Enum() *Builtin { p := new(Builtin) *p = x return p } func (x Builtin) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Builtin) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_schema_v1_schema_proto_enumTypes[0].Descriptor() } func (Builtin) Type() protoreflect.EnumType { return &file_encore_parser_schema_v1_schema_proto_enumTypes[0] } func (x Builtin) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Builtin.Descriptor instead. func (Builtin) EnumDescriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{0} } type ValidationRule_Is int32 const ( ValidationRule_UNKNOWN ValidationRule_Is = 0 ValidationRule_EMAIL ValidationRule_Is = 1 ValidationRule_URL ValidationRule_Is = 2 ) // Enum value maps for ValidationRule_Is. var ( ValidationRule_Is_name = map[int32]string{ 0: "UNKNOWN", 1: "EMAIL", 2: "URL", } ValidationRule_Is_value = map[string]int32{ "UNKNOWN": 0, "EMAIL": 1, "URL": 2, } ) func (x ValidationRule_Is) Enum() *ValidationRule_Is { p := new(ValidationRule_Is) *p = x return p } func (x ValidationRule_Is) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ValidationRule_Is) Descriptor() protoreflect.EnumDescriptor { return file_encore_parser_schema_v1_schema_proto_enumTypes[1].Descriptor() } func (ValidationRule_Is) Type() protoreflect.EnumType { return &file_encore_parser_schema_v1_schema_proto_enumTypes[1] } func (x ValidationRule_Is) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ValidationRule_Is.Descriptor instead. func (ValidationRule_Is) EnumDescriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{1, 0} } // Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one, // thing from our list of meta types. // // A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the // structures looking for any uses of the TypeParameterPtr type type Type struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Typ: // // *Type_Named // *Type_Struct // *Type_Map // *Type_List // *Type_Builtin // *Type_Pointer // *Type_Union // *Type_Literal // *Type_Option // *Type_TypeParameter // *Type_Config Typ isType_Typ `protobuf_oneof:"typ"` Validation *ValidationExpr `protobuf:"bytes,15,opt,name=validation,proto3,oneof" json:"validation,omitempty"` // The validation expression for this type unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Type) Reset() { *x = Type{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Type) String() string { return protoimpl.X.MessageStringOf(x) } func (*Type) ProtoMessage() {} func (x *Type) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Type.ProtoReflect.Descriptor instead. func (*Type) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{0} } func (x *Type) GetTyp() isType_Typ { if x != nil { return x.Typ } return nil } func (x *Type) GetNamed() *Named { if x != nil { if x, ok := x.Typ.(*Type_Named); ok { return x.Named } } return nil } func (x *Type) GetStruct() *Struct { if x != nil { if x, ok := x.Typ.(*Type_Struct); ok { return x.Struct } } return nil } func (x *Type) GetMap() *Map { if x != nil { if x, ok := x.Typ.(*Type_Map); ok { return x.Map } } return nil } func (x *Type) GetList() *List { if x != nil { if x, ok := x.Typ.(*Type_List); ok { return x.List } } return nil } func (x *Type) GetBuiltin() Builtin { if x != nil { if x, ok := x.Typ.(*Type_Builtin); ok { return x.Builtin } } return Builtin_ANY } func (x *Type) GetPointer() *Pointer { if x != nil { if x, ok := x.Typ.(*Type_Pointer); ok { return x.Pointer } } return nil } func (x *Type) GetUnion() *Union { if x != nil { if x, ok := x.Typ.(*Type_Union); ok { return x.Union } } return nil } func (x *Type) GetLiteral() *Literal { if x != nil { if x, ok := x.Typ.(*Type_Literal); ok { return x.Literal } } return nil } func (x *Type) GetOption() *Option { if x != nil { if x, ok := x.Typ.(*Type_Option); ok { return x.Option } } return nil } func (x *Type) GetTypeParameter() *TypeParameterRef { if x != nil { if x, ok := x.Typ.(*Type_TypeParameter); ok { return x.TypeParameter } } return nil } func (x *Type) GetConfig() *ConfigValue { if x != nil { if x, ok := x.Typ.(*Type_Config); ok { return x.Config } } return nil } func (x *Type) GetValidation() *ValidationExpr { if x != nil { return x.Validation } return nil } type isType_Typ interface { isType_Typ() } type Type_Named struct { // Concrete / non-parameterized Types Named *Named `protobuf:"bytes,1,opt,name=named,proto3,oneof"` // A "named" type (https://tip.golang.org/ref/spec#Types) } type Type_Struct struct { Struct *Struct `protobuf:"bytes,2,opt,name=struct,proto3,oneof"` // The type is a struct definition } type Type_Map struct { Map *Map `protobuf:"bytes,3,opt,name=map,proto3,oneof"` // The type is a map } type Type_List struct { List *List `protobuf:"bytes,4,opt,name=list,proto3,oneof"` // The type is a slice } type Type_Builtin struct { Builtin Builtin `protobuf:"varint,5,opt,name=builtin,proto3,enum=encore.parser.schema.v1.Builtin,oneof"` // The type is one of the base built in types within Go } type Type_Pointer struct { Pointer *Pointer `protobuf:"bytes,8,opt,name=pointer,proto3,oneof"` // The type is a pointer } type Type_Union struct { Union *Union `protobuf:"bytes,9,opt,name=union,proto3,oneof"` // The type is a union } type Type_Literal struct { Literal *Literal `protobuf:"bytes,10,opt,name=literal,proto3,oneof"` // The type is a literal } type Type_Option struct { Option *Option `protobuf:"bytes,11,opt,name=option,proto3,oneof"` // The type is an option type } type Type_TypeParameter struct { // Abstract Types TypeParameter *TypeParameterRef `protobuf:"bytes,6,opt,name=type_parameter,json=typeParameter,proto3,oneof"` // This is placeholder for a unknown type within the declaration block } type Type_Config struct { // Encore Special Types Config *ConfigValue `protobuf:"bytes,7,opt,name=config,proto3,oneof"` // This value is a config value } func (*Type_Named) isType_Typ() {} func (*Type_Struct) isType_Typ() {} func (*Type_Map) isType_Typ() {} func (*Type_List) isType_Typ() {} func (*Type_Builtin) isType_Typ() {} func (*Type_Pointer) isType_Typ() {} func (*Type_Union) isType_Typ() {} func (*Type_Literal) isType_Typ() {} func (*Type_Option) isType_Typ() {} func (*Type_TypeParameter) isType_Typ() {} func (*Type_Config) isType_Typ() {} type ValidationRule struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Rule: // // *ValidationRule_MinLen // *ValidationRule_MaxLen // *ValidationRule_MinVal // *ValidationRule_MaxVal // *ValidationRule_StartsWith // *ValidationRule_EndsWith // *ValidationRule_MatchesRegexp // *ValidationRule_Is_ Rule isValidationRule_Rule `protobuf_oneof:"rule"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ValidationRule) Reset() { *x = ValidationRule{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ValidationRule) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidationRule) ProtoMessage() {} func (x *ValidationRule) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidationRule.ProtoReflect.Descriptor instead. func (*ValidationRule) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{1} } func (x *ValidationRule) GetRule() isValidationRule_Rule { if x != nil { return x.Rule } return nil } func (x *ValidationRule) GetMinLen() uint64 { if x != nil { if x, ok := x.Rule.(*ValidationRule_MinLen); ok { return x.MinLen } } return 0 } func (x *ValidationRule) GetMaxLen() uint64 { if x != nil { if x, ok := x.Rule.(*ValidationRule_MaxLen); ok { return x.MaxLen } } return 0 } func (x *ValidationRule) GetMinVal() float64 { if x != nil { if x, ok := x.Rule.(*ValidationRule_MinVal); ok { return x.MinVal } } return 0 } func (x *ValidationRule) GetMaxVal() float64 { if x != nil { if x, ok := x.Rule.(*ValidationRule_MaxVal); ok { return x.MaxVal } } return 0 } func (x *ValidationRule) GetStartsWith() string { if x != nil { if x, ok := x.Rule.(*ValidationRule_StartsWith); ok { return x.StartsWith } } return "" } func (x *ValidationRule) GetEndsWith() string { if x != nil { if x, ok := x.Rule.(*ValidationRule_EndsWith); ok { return x.EndsWith } } return "" } func (x *ValidationRule) GetMatchesRegexp() string { if x != nil { if x, ok := x.Rule.(*ValidationRule_MatchesRegexp); ok { return x.MatchesRegexp } } return "" } func (x *ValidationRule) GetIs() ValidationRule_Is { if x != nil { if x, ok := x.Rule.(*ValidationRule_Is_); ok { return x.Is } } return ValidationRule_UNKNOWN } type isValidationRule_Rule interface { isValidationRule_Rule() } type ValidationRule_MinLen struct { MinLen uint64 `protobuf:"varint,1,opt,name=min_len,json=minLen,proto3,oneof"` } type ValidationRule_MaxLen struct { MaxLen uint64 `protobuf:"varint,2,opt,name=max_len,json=maxLen,proto3,oneof"` } type ValidationRule_MinVal struct { MinVal float64 `protobuf:"fixed64,3,opt,name=min_val,json=minVal,proto3,oneof"` } type ValidationRule_MaxVal struct { MaxVal float64 `protobuf:"fixed64,4,opt,name=max_val,json=maxVal,proto3,oneof"` } type ValidationRule_StartsWith struct { StartsWith string `protobuf:"bytes,5,opt,name=starts_with,json=startsWith,proto3,oneof"` } type ValidationRule_EndsWith struct { EndsWith string `protobuf:"bytes,6,opt,name=ends_with,json=endsWith,proto3,oneof"` } type ValidationRule_MatchesRegexp struct { MatchesRegexp string `protobuf:"bytes,7,opt,name=matches_regexp,json=matchesRegexp,proto3,oneof"` } type ValidationRule_Is_ struct { Is ValidationRule_Is `protobuf:"varint,8,opt,name=is,proto3,enum=encore.parser.schema.v1.ValidationRule_Is,oneof"` } func (*ValidationRule_MinLen) isValidationRule_Rule() {} func (*ValidationRule_MaxLen) isValidationRule_Rule() {} func (*ValidationRule_MinVal) isValidationRule_Rule() {} func (*ValidationRule_MaxVal) isValidationRule_Rule() {} func (*ValidationRule_StartsWith) isValidationRule_Rule() {} func (*ValidationRule_EndsWith) isValidationRule_Rule() {} func (*ValidationRule_MatchesRegexp) isValidationRule_Rule() {} func (*ValidationRule_Is_) isValidationRule_Rule() {} type ValidationExpr struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Expr: // // *ValidationExpr_Rule // *ValidationExpr_And_ // *ValidationExpr_Or_ Expr isValidationExpr_Expr `protobuf_oneof:"expr"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ValidationExpr) Reset() { *x = ValidationExpr{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ValidationExpr) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidationExpr) ProtoMessage() {} func (x *ValidationExpr) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidationExpr.ProtoReflect.Descriptor instead. func (*ValidationExpr) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2} } func (x *ValidationExpr) GetExpr() isValidationExpr_Expr { if x != nil { return x.Expr } return nil } func (x *ValidationExpr) GetRule() *ValidationRule { if x != nil { if x, ok := x.Expr.(*ValidationExpr_Rule); ok { return x.Rule } } return nil } func (x *ValidationExpr) GetAnd() *ValidationExpr_And { if x != nil { if x, ok := x.Expr.(*ValidationExpr_And_); ok { return x.And } } return nil } func (x *ValidationExpr) GetOr() *ValidationExpr_Or { if x != nil { if x, ok := x.Expr.(*ValidationExpr_Or_); ok { return x.Or } } return nil } type isValidationExpr_Expr interface { isValidationExpr_Expr() } type ValidationExpr_Rule struct { Rule *ValidationRule `protobuf:"bytes,1,opt,name=rule,proto3,oneof"` } type ValidationExpr_And_ struct { And *ValidationExpr_And `protobuf:"bytes,2,opt,name=and,proto3,oneof"` } type ValidationExpr_Or_ struct { Or *ValidationExpr_Or `protobuf:"bytes,3,opt,name=or,proto3,oneof"` } func (*ValidationExpr_Rule) isValidationExpr_Expr() {} func (*ValidationExpr_And_) isValidationExpr_Expr() {} func (*ValidationExpr_Or_) isValidationExpr_Expr() {} // TypeParameterRef is a reference to a `TypeParameter` within a declaration block type TypeParameterRef struct { state protoimpl.MessageState `protogen:"open.v1"` DeclId uint32 `protobuf:"varint,1,opt,name=decl_id,json=declId,proto3" json:"decl_id,omitempty"` // The ID of the declaration block ParamIdx uint32 `protobuf:"varint,2,opt,name=param_idx,json=paramIdx,proto3" json:"param_idx,omitempty"` // The index of the type parameter within the declaration block unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TypeParameterRef) Reset() { *x = TypeParameterRef{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TypeParameterRef) String() string { return protoimpl.X.MessageStringOf(x) } func (*TypeParameterRef) ProtoMessage() {} func (x *TypeParameterRef) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TypeParameterRef.ProtoReflect.Descriptor instead. func (*TypeParameterRef) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{3} } func (x *TypeParameterRef) GetDeclId() uint32 { if x != nil { return x.DeclId } return 0 } func (x *TypeParameterRef) GetParamIdx() uint32 { if x != nil { return x.ParamIdx } return 0 } // Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is // concrete when there are zero type parameters assigned. // // For example the Go Code: // ```go // // Set[A] represents our set type // type Set[A any] = map[A]struct{} // ``` // // Would become: // ```go // // _ = &Decl{ // id: 1, // name: "Set", // type: &Type{ // typ_map: &Map{ // key: &Type { typ_type_parameter: ... reference to "A" type parameter below ... }, // value: &Type { typ_struct: ... empty struct type ... }, // }, // }, // typeParameters: []*TypeParameter{ { name: "A" } }, // doc: "Set[A] represents our set type", // loc: &Loc { ... }, // } // // ``` type Decl struct { state protoimpl.MessageState `protogen:"open.v1"` Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // A internal ID which we can refer to this declaration by Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The name of the type as assigned in the code Type *Type `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` // The underlying type of this declaration TypeParams []*TypeParameter `protobuf:"bytes,6,rep,name=type_params,json=typeParams,proto3" json:"type_params,omitempty"` // Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here) Doc string `protobuf:"bytes,4,opt,name=doc,proto3" json:"doc,omitempty"` // The comment block on the type Loc *Loc `protobuf:"bytes,5,opt,name=loc,proto3" json:"loc,omitempty"` // The location of the declaration within the project unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Decl) Reset() { *x = Decl{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Decl) String() string { return protoimpl.X.MessageStringOf(x) } func (*Decl) ProtoMessage() {} func (x *Decl) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Decl.ProtoReflect.Descriptor instead. func (*Decl) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{4} } func (x *Decl) GetId() uint32 { if x != nil { return x.Id } return 0 } func (x *Decl) GetName() string { if x != nil { return x.Name } return "" } func (x *Decl) GetType() *Type { if x != nil { return x.Type } return nil } func (x *Decl) GetTypeParams() []*TypeParameter { if x != nil { return x.TypeParams } return nil } func (x *Decl) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *Decl) GetLoc() *Loc { if x != nil { return x.Loc } return nil } // TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is // replaced with a type argument upon instantiation of the parameterized function or type. type TypeParameter struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The identifier given to the type parameter unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TypeParameter) Reset() { *x = TypeParameter{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TypeParameter) String() string { return protoimpl.X.MessageStringOf(x) } func (*TypeParameter) ProtoMessage() {} func (x *TypeParameter) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TypeParameter.ProtoReflect.Descriptor instead. func (*TypeParameter) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{5} } func (x *TypeParameter) GetName() string { if x != nil { return x.Name } return "" } // Loc is the location of a declaration within the code base type Loc struct { state protoimpl.MessageState `protogen:"open.v1"` PkgPath string `protobuf:"bytes,1,opt,name=pkg_path,json=pkgPath,proto3" json:"pkg_path,omitempty"` // The package path within the repo (i.e. `users/signup`) PkgName string `protobuf:"bytes,2,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` // The package name (i.e. `signup`) Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"` // The file name (i.e. `signup.go`) StartPos int32 `protobuf:"varint,4,opt,name=start_pos,json=startPos,proto3" json:"start_pos,omitempty"` // The starting index within the file for this node EndPos int32 `protobuf:"varint,5,opt,name=end_pos,json=endPos,proto3" json:"end_pos,omitempty"` // The ending index within the file for this node SrcLineStart int32 `protobuf:"varint,6,opt,name=src_line_start,json=srcLineStart,proto3" json:"src_line_start,omitempty"` // The starting line within the file for this node SrcLineEnd int32 `protobuf:"varint,7,opt,name=src_line_end,json=srcLineEnd,proto3" json:"src_line_end,omitempty"` // The ending line within the file for this node SrcColStart int32 `protobuf:"varint,8,opt,name=src_col_start,json=srcColStart,proto3" json:"src_col_start,omitempty"` // The starting column on the starting line for this node SrcColEnd int32 `protobuf:"varint,9,opt,name=src_col_end,json=srcColEnd,proto3" json:"src_col_end,omitempty"` // The ending column on the ending line for this node unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Loc) Reset() { *x = Loc{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Loc) String() string { return protoimpl.X.MessageStringOf(x) } func (*Loc) ProtoMessage() {} func (x *Loc) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Loc.ProtoReflect.Descriptor instead. func (*Loc) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{6} } func (x *Loc) GetPkgPath() string { if x != nil { return x.PkgPath } return "" } func (x *Loc) GetPkgName() string { if x != nil { return x.PkgName } return "" } func (x *Loc) GetFilename() string { if x != nil { return x.Filename } return "" } func (x *Loc) GetStartPos() int32 { if x != nil { return x.StartPos } return 0 } func (x *Loc) GetEndPos() int32 { if x != nil { return x.EndPos } return 0 } func (x *Loc) GetSrcLineStart() int32 { if x != nil { return x.SrcLineStart } return 0 } func (x *Loc) GetSrcLineEnd() int32 { if x != nil { return x.SrcLineEnd } return 0 } func (x *Loc) GetSrcColStart() int32 { if x != nil { return x.SrcColStart } return 0 } func (x *Loc) GetSrcColEnd() int32 { if x != nil { return x.SrcColEnd } return 0 } // Named references declaration block by name type Named struct { state protoimpl.MessageState `protogen:"open.v1"` Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // The `Decl.id` this name refers to TypeArguments []*Type `protobuf:"bytes,2,rep,name=type_arguments,json=typeArguments,proto3" json:"type_arguments,omitempty"` // The type arguments used to instantiate this parameterised declaration unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Named) Reset() { *x = Named{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Named) String() string { return protoimpl.X.MessageStringOf(x) } func (*Named) ProtoMessage() {} func (x *Named) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Named.ProtoReflect.Descriptor instead. func (*Named) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{7} } func (x *Named) GetId() uint32 { if x != nil { return x.Id } return 0 } func (x *Named) GetTypeArguments() []*Type { if x != nil { return x.TypeArguments } return nil } // Struct contains a list of fields which make up the struct type Struct struct { state protoimpl.MessageState `protogen:"open.v1"` Fields []*Field `protobuf:"bytes,1,rep,name=fields,proto3" json:"fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Struct) Reset() { *x = Struct{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Struct) String() string { return protoimpl.X.MessageStringOf(x) } func (*Struct) ProtoMessage() {} func (x *Struct) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Struct.ProtoReflect.Descriptor instead. func (*Struct) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{8} } func (x *Struct) GetFields() []*Field { if x != nil { return x.Fields } return nil } // Field represents a field within a struct type Field struct { state protoimpl.MessageState `protogen:"open.v1"` Typ *Type `protobuf:"bytes,1,opt,name=typ,proto3" json:"typ,omitempty"` // The type of the field Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The name of the field Doc string `protobuf:"bytes,3,opt,name=doc,proto3" json:"doc,omitempty"` // The comment for the field JsonName string `protobuf:"bytes,4,opt,name=json_name,json=jsonName,proto3" json:"json_name,omitempty"` // The optional json name if it's different from the field name. (The value "-" indicates to omit the field.) Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` // Whether the field is optional. QueryStringName string `protobuf:"bytes,6,opt,name=query_string_name,json=queryStringName,proto3" json:"query_string_name,omitempty"` // The query string name to use in GET/HEAD/DELETE requests. (The value "-" indicates to omit the field.) RawTag string `protobuf:"bytes,7,opt,name=raw_tag,json=rawTag,proto3" json:"raw_tag,omitempty"` // The original Go struct tag; should not be parsed individually Tags []*Tag `protobuf:"bytes,8,rep,name=tags,proto3" json:"tags,omitempty"` // Parsed go struct tags. Used for marshalling hints Wire *WireSpec `protobuf:"bytes,9,opt,name=wire,proto3,oneof" json:"wire,omitempty"` // The explicitly set wire location of the field. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Field) Reset() { *x = Field{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Field) String() string { return protoimpl.X.MessageStringOf(x) } func (*Field) ProtoMessage() {} func (x *Field) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Field.ProtoReflect.Descriptor instead. func (*Field) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{9} } func (x *Field) GetTyp() *Type { if x != nil { return x.Typ } return nil } func (x *Field) GetName() string { if x != nil { return x.Name } return "" } func (x *Field) GetDoc() string { if x != nil { return x.Doc } return "" } func (x *Field) GetJsonName() string { if x != nil { return x.JsonName } return "" } func (x *Field) GetOptional() bool { if x != nil { return x.Optional } return false } func (x *Field) GetQueryStringName() string { if x != nil { return x.QueryStringName } return "" } func (x *Field) GetRawTag() string { if x != nil { return x.RawTag } return "" } func (x *Field) GetTags() []*Tag { if x != nil { return x.Tags } return nil } func (x *Field) GetWire() *WireSpec { if x != nil { return x.Wire } return nil } // WireLocation provides information about how a field should be encoded on the wire. type WireSpec struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Location: // // *WireSpec_Header_ // *WireSpec_Query_ // *WireSpec_Cookie_ // *WireSpec_HttpStatus_ Location isWireSpec_Location `protobuf_oneof:"location"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WireSpec) Reset() { *x = WireSpec{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WireSpec) String() string { return protoimpl.X.MessageStringOf(x) } func (*WireSpec) ProtoMessage() {} func (x *WireSpec) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WireSpec.ProtoReflect.Descriptor instead. func (*WireSpec) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10} } func (x *WireSpec) GetLocation() isWireSpec_Location { if x != nil { return x.Location } return nil } func (x *WireSpec) GetHeader() *WireSpec_Header { if x != nil { if x, ok := x.Location.(*WireSpec_Header_); ok { return x.Header } } return nil } func (x *WireSpec) GetQuery() *WireSpec_Query { if x != nil { if x, ok := x.Location.(*WireSpec_Query_); ok { return x.Query } } return nil } func (x *WireSpec) GetCookie() *WireSpec_Cookie { if x != nil { if x, ok := x.Location.(*WireSpec_Cookie_); ok { return x.Cookie } } return nil } func (x *WireSpec) GetHttpStatus() *WireSpec_HttpStatus { if x != nil { if x, ok := x.Location.(*WireSpec_HttpStatus_); ok { return x.HttpStatus } } return nil } type isWireSpec_Location interface { isWireSpec_Location() } type WireSpec_Header_ struct { Header *WireSpec_Header `protobuf:"bytes,1,opt,name=header,proto3,oneof"` } type WireSpec_Query_ struct { Query *WireSpec_Query `protobuf:"bytes,2,opt,name=query,proto3,oneof"` } type WireSpec_Cookie_ struct { Cookie *WireSpec_Cookie `protobuf:"bytes,3,opt,name=cookie,proto3,oneof"` } type WireSpec_HttpStatus_ struct { HttpStatus *WireSpec_HttpStatus `protobuf:"bytes,4,opt,name=http_status,json=httpStatus,proto3,oneof"` } func (*WireSpec_Header_) isWireSpec_Location() {} func (*WireSpec_Query_) isWireSpec_Location() {} func (*WireSpec_Cookie_) isWireSpec_Location() {} func (*WireSpec_HttpStatus_) isWireSpec_Location() {} type Tag struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // The tag key (e.g. json, query, header ...) Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The tag name (e.g. first_name, firstName, ...) Options []string `protobuf:"bytes,3,rep,name=options,proto3" json:"options,omitempty"` // Key Options (e.g. omitempty, optional ...) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Tag) Reset() { *x = Tag{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Tag) String() string { return protoimpl.X.MessageStringOf(x) } func (*Tag) ProtoMessage() {} func (x *Tag) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Tag.ProtoReflect.Descriptor instead. func (*Tag) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{11} } func (x *Tag) GetKey() string { if x != nil { return x.Key } return "" } func (x *Tag) GetName() string { if x != nil { return x.Name } return "" } func (x *Tag) GetOptions() []string { if x != nil { return x.Options } return nil } // Map represents a map Type type Map struct { state protoimpl.MessageState `protogen:"open.v1"` Key *Type `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // The type of the key for this map Value *Type `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // The type of the value of this map unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Map) Reset() { *x = Map{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Map) String() string { return protoimpl.X.MessageStringOf(x) } func (*Map) ProtoMessage() {} func (x *Map) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Map.ProtoReflect.Descriptor instead. func (*Map) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{12} } func (x *Map) GetKey() *Type { if x != nil { return x.Key } return nil } func (x *Map) GetValue() *Type { if x != nil { return x.Value } return nil } // List represents a list type (array or slice) type List struct { state protoimpl.MessageState `protogen:"open.v1"` Elem *Type `protobuf:"bytes,1,opt,name=elem,proto3" json:"elem,omitempty"` // The type of the elements in the list unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *List) Reset() { *x = List{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *List) String() string { return protoimpl.X.MessageStringOf(x) } func (*List) ProtoMessage() {} func (x *List) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use List.ProtoReflect.Descriptor instead. func (*List) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{13} } func (x *List) GetElem() *Type { if x != nil { return x.Elem } return nil } // Pointer represents a pointer to a base type type Pointer struct { state protoimpl.MessageState `protogen:"open.v1"` Base *Type `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` // The type of the pointer unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Pointer) Reset() { *x = Pointer{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Pointer) String() string { return protoimpl.X.MessageStringOf(x) } func (*Pointer) ProtoMessage() {} func (x *Pointer) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Pointer.ProtoReflect.Descriptor instead. func (*Pointer) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{14} } func (x *Pointer) GetBase() *Type { if x != nil { return x.Base } return nil } // Option represents an option type. type Option struct { state protoimpl.MessageState `protogen:"open.v1"` Value *Type `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // The value it may contain. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Option) Reset() { *x = Option{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Option) String() string { return protoimpl.X.MessageStringOf(x) } func (*Option) ProtoMessage() {} func (x *Option) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Option.ProtoReflect.Descriptor instead. func (*Option) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{15} } func (x *Option) GetValue() *Type { if x != nil { return x.Value } return nil } // Union represents a union type. type Union struct { state protoimpl.MessageState `protogen:"open.v1"` Types []*Type `protobuf:"bytes,1,rep,name=types,proto3" json:"types,omitempty"` // The types that make up the union unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Union) Reset() { *x = Union{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Union) String() string { return protoimpl.X.MessageStringOf(x) } func (*Union) ProtoMessage() {} func (x *Union) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Union.ProtoReflect.Descriptor instead. func (*Union) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{16} } func (x *Union) GetTypes() []*Type { if x != nil { return x.Types } return nil } // Literal represents a literal value. type Literal struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Value: // // *Literal_Str // *Literal_Boolean // *Literal_Int // *Literal_Float // *Literal_Null Value isLiteral_Value `protobuf_oneof:"value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Literal) Reset() { *x = Literal{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Literal) String() string { return protoimpl.X.MessageStringOf(x) } func (*Literal) ProtoMessage() {} func (x *Literal) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Literal.ProtoReflect.Descriptor instead. func (*Literal) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{17} } func (x *Literal) GetValue() isLiteral_Value { if x != nil { return x.Value } return nil } func (x *Literal) GetStr() string { if x != nil { if x, ok := x.Value.(*Literal_Str); ok { return x.Str } } return "" } func (x *Literal) GetBoolean() bool { if x != nil { if x, ok := x.Value.(*Literal_Boolean); ok { return x.Boolean } } return false } func (x *Literal) GetInt() int64 { if x != nil { if x, ok := x.Value.(*Literal_Int); ok { return x.Int } } return 0 } func (x *Literal) GetFloat() float64 { if x != nil { if x, ok := x.Value.(*Literal_Float); ok { return x.Float } } return 0 } func (x *Literal) GetNull() bool { if x != nil { if x, ok := x.Value.(*Literal_Null); ok { return x.Null } } return false } type isLiteral_Value interface { isLiteral_Value() } type Literal_Str struct { Str string `protobuf:"bytes,1,opt,name=str,proto3,oneof"` } type Literal_Boolean struct { Boolean bool `protobuf:"varint,2,opt,name=boolean,proto3,oneof"` } type Literal_Int struct { Int int64 `protobuf:"varint,3,opt,name=int,proto3,oneof"` } type Literal_Float struct { Float float64 `protobuf:"fixed64,4,opt,name=float,proto3,oneof"` } type Literal_Null struct { Null bool `protobuf:"varint,5,opt,name=null,proto3,oneof"` } func (*Literal_Str) isLiteral_Value() {} func (*Literal_Boolean) isLiteral_Value() {} func (*Literal_Int) isLiteral_Value() {} func (*Literal_Float) isLiteral_Value() {} func (*Literal_Null) isLiteral_Value() {} // ConfigValue represents a config value wrapper. type ConfigValue struct { state protoimpl.MessageState `protogen:"open.v1"` Elem *Type `protobuf:"bytes,1,opt,name=elem,proto3" json:"elem,omitempty"` // The type of the config value IsValuesList bool `protobuf:"varint,2,opt,name=IsValuesList,proto3" json:"IsValuesList,omitempty"` // Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ConfigValue) Reset() { *x = ConfigValue{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ConfigValue) String() string { return protoimpl.X.MessageStringOf(x) } func (*ConfigValue) ProtoMessage() {} func (x *ConfigValue) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ConfigValue.ProtoReflect.Descriptor instead. func (*ConfigValue) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{18} } func (x *ConfigValue) GetElem() *Type { if x != nil { return x.Elem } return nil } func (x *ConfigValue) GetIsValuesList() bool { if x != nil { return x.IsValuesList } return false } type ValidationExpr_And struct { state protoimpl.MessageState `protogen:"open.v1"` Exprs []*ValidationExpr `protobuf:"bytes,1,rep,name=exprs,proto3" json:"exprs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ValidationExpr_And) Reset() { *x = ValidationExpr_And{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ValidationExpr_And) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidationExpr_And) ProtoMessage() {} func (x *ValidationExpr_And) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidationExpr_And.ProtoReflect.Descriptor instead. func (*ValidationExpr_And) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2, 0} } func (x *ValidationExpr_And) GetExprs() []*ValidationExpr { if x != nil { return x.Exprs } return nil } type ValidationExpr_Or struct { state protoimpl.MessageState `protogen:"open.v1"` Exprs []*ValidationExpr `protobuf:"bytes,1,rep,name=exprs,proto3" json:"exprs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ValidationExpr_Or) Reset() { *x = ValidationExpr_Or{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ValidationExpr_Or) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidationExpr_Or) ProtoMessage() {} func (x *ValidationExpr_Or) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidationExpr_Or.ProtoReflect.Descriptor instead. func (*ValidationExpr_Or) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2, 1} } func (x *ValidationExpr_Or) GetExprs() []*ValidationExpr { if x != nil { return x.Exprs } return nil } type WireSpec_Header struct { state protoimpl.MessageState `protogen:"open.v1"` // The explicitly specified header name. // If empty, the name of the field is used. Name *string `protobuf:"bytes,1,opt,name=name,proto3,oneof" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WireSpec_Header) Reset() { *x = WireSpec_Header{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WireSpec_Header) String() string { return protoimpl.X.MessageStringOf(x) } func (*WireSpec_Header) ProtoMessage() {} func (x *WireSpec_Header) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WireSpec_Header.ProtoReflect.Descriptor instead. func (*WireSpec_Header) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 0} } func (x *WireSpec_Header) GetName() string { if x != nil && x.Name != nil { return *x.Name } return "" } type WireSpec_Query struct { state protoimpl.MessageState `protogen:"open.v1"` // The explicitly specified query string name. // If empty, the name of the field is used. Name *string `protobuf:"bytes,1,opt,name=name,proto3,oneof" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WireSpec_Query) Reset() { *x = WireSpec_Query{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WireSpec_Query) String() string { return protoimpl.X.MessageStringOf(x) } func (*WireSpec_Query) ProtoMessage() {} func (x *WireSpec_Query) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WireSpec_Query.ProtoReflect.Descriptor instead. func (*WireSpec_Query) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 1} } func (x *WireSpec_Query) GetName() string { if x != nil && x.Name != nil { return *x.Name } return "" } type WireSpec_Cookie struct { state protoimpl.MessageState `protogen:"open.v1"` // The explicitly specified cookie string name. // If empty, the name of the field is used. Name *string `protobuf:"bytes,1,opt,name=name,proto3,oneof" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WireSpec_Cookie) Reset() { *x = WireSpec_Cookie{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WireSpec_Cookie) String() string { return protoimpl.X.MessageStringOf(x) } func (*WireSpec_Cookie) ProtoMessage() {} func (x *WireSpec_Cookie) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WireSpec_Cookie.ProtoReflect.Descriptor instead. func (*WireSpec_Cookie) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 2} } func (x *WireSpec_Cookie) GetName() string { if x != nil && x.Name != nil { return *x.Name } return "" } type WireSpec_HttpStatus struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WireSpec_HttpStatus) Reset() { *x = WireSpec_HttpStatus{} mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *WireSpec_HttpStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*WireSpec_HttpStatus) ProtoMessage() {} func (x *WireSpec_HttpStatus) ProtoReflect() protoreflect.Message { mi := &file_encore_parser_schema_v1_schema_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use WireSpec_HttpStatus.ProtoReflect.Descriptor instead. func (*WireSpec_HttpStatus) Descriptor() ([]byte, []int) { return file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 3} } var File_encore_parser_schema_v1_schema_proto protoreflect.FileDescriptor const file_encore_parser_schema_v1_schema_proto_rawDesc = "" + "\n" + "$encore/parser/schema/v1/schema.proto\x12\x17encore.parser.schema.v1\"\x85\x06\n" + "\x04Type\x126\n" + "\x05named\x18\x01 \x01(\v2\x1e.encore.parser.schema.v1.NamedH\x00R\x05named\x129\n" + "\x06struct\x18\x02 \x01(\v2\x1f.encore.parser.schema.v1.StructH\x00R\x06struct\x120\n" + "\x03map\x18\x03 \x01(\v2\x1c.encore.parser.schema.v1.MapH\x00R\x03map\x123\n" + "\x04list\x18\x04 \x01(\v2\x1d.encore.parser.schema.v1.ListH\x00R\x04list\x12<\n" + "\abuiltin\x18\x05 \x01(\x0e2 .encore.parser.schema.v1.BuiltinH\x00R\abuiltin\x12<\n" + "\apointer\x18\b \x01(\v2 .encore.parser.schema.v1.PointerH\x00R\apointer\x126\n" + "\x05union\x18\t \x01(\v2\x1e.encore.parser.schema.v1.UnionH\x00R\x05union\x12<\n" + "\aliteral\x18\n" + " \x01(\v2 .encore.parser.schema.v1.LiteralH\x00R\aliteral\x129\n" + "\x06option\x18\v \x01(\v2\x1f.encore.parser.schema.v1.OptionH\x00R\x06option\x12R\n" + "\x0etype_parameter\x18\x06 \x01(\v2).encore.parser.schema.v1.TypeParameterRefH\x00R\rtypeParameter\x12>\n" + "\x06config\x18\a \x01(\v2$.encore.parser.schema.v1.ConfigValueH\x00R\x06config\x12L\n" + "\n" + "validation\x18\x0f \x01(\v2'.encore.parser.schema.v1.ValidationExprH\x01R\n" + "validation\x88\x01\x01B\x05\n" + "\x03typB\r\n" + "\v_validation\"\xd4\x02\n" + "\x0eValidationRule\x12\x19\n" + "\amin_len\x18\x01 \x01(\x04H\x00R\x06minLen\x12\x19\n" + "\amax_len\x18\x02 \x01(\x04H\x00R\x06maxLen\x12\x19\n" + "\amin_val\x18\x03 \x01(\x01H\x00R\x06minVal\x12\x19\n" + "\amax_val\x18\x04 \x01(\x01H\x00R\x06maxVal\x12!\n" + "\vstarts_with\x18\x05 \x01(\tH\x00R\n" + "startsWith\x12\x1d\n" + "\tends_with\x18\x06 \x01(\tH\x00R\bendsWith\x12'\n" + "\x0ematches_regexp\x18\a \x01(\tH\x00R\rmatchesRegexp\x12<\n" + "\x02is\x18\b \x01(\x0e2*.encore.parser.schema.v1.ValidationRule.IsH\x00R\x02is\"%\n" + "\x02Is\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05EMAIL\x10\x01\x12\a\n" + "\x03URL\x10\x02B\x06\n" + "\x04rule\"\xe1\x02\n" + "\x0eValidationExpr\x12=\n" + "\x04rule\x18\x01 \x01(\v2'.encore.parser.schema.v1.ValidationRuleH\x00R\x04rule\x12?\n" + "\x03and\x18\x02 \x01(\v2+.encore.parser.schema.v1.ValidationExpr.AndH\x00R\x03and\x12<\n" + "\x02or\x18\x03 \x01(\v2*.encore.parser.schema.v1.ValidationExpr.OrH\x00R\x02or\x1aD\n" + "\x03And\x12=\n" + "\x05exprs\x18\x01 \x03(\v2'.encore.parser.schema.v1.ValidationExprR\x05exprs\x1aC\n" + "\x02Or\x12=\n" + "\x05exprs\x18\x01 \x03(\v2'.encore.parser.schema.v1.ValidationExprR\x05exprsB\x06\n" + "\x04expr\"H\n" + "\x10TypeParameterRef\x12\x17\n" + "\adecl_id\x18\x01 \x01(\rR\x06declId\x12\x1b\n" + "\tparam_idx\x18\x02 \x01(\rR\bparamIdx\"\xe8\x01\n" + "\x04Decl\x12\x0e\n" + "\x02id\x18\x01 \x01(\rR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x121\n" + "\x04type\x18\x03 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x04type\x12G\n" + "\vtype_params\x18\x06 \x03(\v2&.encore.parser.schema.v1.TypeParameterR\n" + "typeParams\x12\x10\n" + "\x03doc\x18\x04 \x01(\tR\x03doc\x12.\n" + "\x03loc\x18\x05 \x01(\v2\x1c.encore.parser.schema.v1.LocR\x03loc\"#\n" + "\rTypeParameter\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"\x99\x02\n" + "\x03Loc\x12\x19\n" + "\bpkg_path\x18\x01 \x01(\tR\apkgPath\x12\x19\n" + "\bpkg_name\x18\x02 \x01(\tR\apkgName\x12\x1a\n" + "\bfilename\x18\x03 \x01(\tR\bfilename\x12\x1b\n" + "\tstart_pos\x18\x04 \x01(\x05R\bstartPos\x12\x17\n" + "\aend_pos\x18\x05 \x01(\x05R\x06endPos\x12$\n" + "\x0esrc_line_start\x18\x06 \x01(\x05R\fsrcLineStart\x12 \n" + "\fsrc_line_end\x18\a \x01(\x05R\n" + "srcLineEnd\x12\"\n" + "\rsrc_col_start\x18\b \x01(\x05R\vsrcColStart\x12\x1e\n" + "\vsrc_col_end\x18\t \x01(\x05R\tsrcColEnd\"]\n" + "\x05Named\x12\x0e\n" + "\x02id\x18\x01 \x01(\rR\x02id\x12D\n" + "\x0etype_arguments\x18\x02 \x03(\v2\x1d.encore.parser.schema.v1.TypeR\rtypeArguments\"@\n" + "\x06Struct\x126\n" + "\x06fields\x18\x01 \x03(\v2\x1e.encore.parser.schema.v1.FieldR\x06fields\"\xd3\x02\n" + "\x05Field\x12/\n" + "\x03typ\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x03typ\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" + "\x03doc\x18\x03 \x01(\tR\x03doc\x12\x1b\n" + "\tjson_name\x18\x04 \x01(\tR\bjsonName\x12\x1a\n" + "\boptional\x18\x05 \x01(\bR\boptional\x12*\n" + "\x11query_string_name\x18\x06 \x01(\tR\x0fqueryStringName\x12\x17\n" + "\araw_tag\x18\a \x01(\tR\x06rawTag\x120\n" + "\x04tags\x18\b \x03(\v2\x1c.encore.parser.schema.v1.TagR\x04tags\x12:\n" + "\x04wire\x18\t \x01(\v2!.encore.parser.schema.v1.WireSpecH\x00R\x04wire\x88\x01\x01B\a\n" + "\x05_wire\"\xc1\x03\n" + "\bWireSpec\x12B\n" + "\x06header\x18\x01 \x01(\v2(.encore.parser.schema.v1.WireSpec.HeaderH\x00R\x06header\x12?\n" + "\x05query\x18\x02 \x01(\v2'.encore.parser.schema.v1.WireSpec.QueryH\x00R\x05query\x12B\n" + "\x06cookie\x18\x03 \x01(\v2(.encore.parser.schema.v1.WireSpec.CookieH\x00R\x06cookie\x12O\n" + "\vhttp_status\x18\x04 \x01(\v2,.encore.parser.schema.v1.WireSpec.HttpStatusH\x00R\n" + "httpStatus\x1a*\n" + "\x06Header\x12\x17\n" + "\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01B\a\n" + "\x05_name\x1a)\n" + "\x05Query\x12\x17\n" + "\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01B\a\n" + "\x05_name\x1a*\n" + "\x06Cookie\x12\x17\n" + "\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01B\a\n" + "\x05_name\x1a\f\n" + "\n" + "HttpStatusB\n" + "\n" + "\blocation\"E\n" + "\x03Tag\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + "\aoptions\x18\x03 \x03(\tR\aoptions\"k\n" + "\x03Map\x12/\n" + "\x03key\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x03key\x123\n" + "\x05value\x18\x02 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x05value\"9\n" + "\x04List\x121\n" + "\x04elem\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x04elem\"<\n" + "\aPointer\x121\n" + "\x04base\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x04base\"=\n" + "\x06Option\x123\n" + "\x05value\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x05value\"<\n" + "\x05Union\x123\n" + "\x05types\x18\x01 \x03(\v2\x1d.encore.parser.schema.v1.TypeR\x05types\"\x84\x01\n" + "\aLiteral\x12\x12\n" + "\x03str\x18\x01 \x01(\tH\x00R\x03str\x12\x1a\n" + "\aboolean\x18\x02 \x01(\bH\x00R\aboolean\x12\x12\n" + "\x03int\x18\x03 \x01(\x03H\x00R\x03int\x12\x16\n" + "\x05float\x18\x04 \x01(\x01H\x00R\x05float\x12\x14\n" + "\x04null\x18\x05 \x01(\bH\x00R\x04nullB\a\n" + "\x05value\"d\n" + "\vConfigValue\x121\n" + "\x04elem\x18\x01 \x01(\v2\x1d.encore.parser.schema.v1.TypeR\x04elem\x12\"\n" + "\fIsValuesList\x18\x02 \x01(\bR\fIsValuesList*\xf2\x01\n" + "\aBuiltin\x12\a\n" + "\x03ANY\x10\x00\x12\b\n" + "\x04BOOL\x10\x01\x12\b\n" + "\x04INT8\x10\x02\x12\t\n" + "\x05INT16\x10\x03\x12\t\n" + "\x05INT32\x10\x04\x12\t\n" + "\x05INT64\x10\x05\x12\t\n" + "\x05UINT8\x10\x06\x12\n" + "\n" + "\x06UINT16\x10\a\x12\n" + "\n" + "\x06UINT32\x10\b\x12\n" + "\n" + "\x06UINT64\x10\t\x12\v\n" + "\aFLOAT32\x10\n" + "\x12\v\n" + "\aFLOAT64\x10\v\x12\n" + "\n" + "\x06STRING\x10\f\x12\t\n" + "\x05BYTES\x10\r\x12\b\n" + "\x04TIME\x10\x0e\x12\b\n" + "\x04UUID\x10\x0f\x12\b\n" + "\x04JSON\x10\x10\x12\v\n" + "\aUSER_ID\x10\x11\x12\a\n" + "\x03INT\x10\x12\x12\b\n" + "\x04UINT\x10\x13\x12\v\n" + "\aDECIMAL\x10\x14B(Z&encr.dev/proto/encore/parser/schema/v1b\x06proto3" var ( file_encore_parser_schema_v1_schema_proto_rawDescOnce sync.Once file_encore_parser_schema_v1_schema_proto_rawDescData []byte ) func file_encore_parser_schema_v1_schema_proto_rawDescGZIP() []byte { file_encore_parser_schema_v1_schema_proto_rawDescOnce.Do(func() { file_encore_parser_schema_v1_schema_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_parser_schema_v1_schema_proto_rawDesc), len(file_encore_parser_schema_v1_schema_proto_rawDesc))) }) return file_encore_parser_schema_v1_schema_proto_rawDescData } var file_encore_parser_schema_v1_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_encore_parser_schema_v1_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_encore_parser_schema_v1_schema_proto_goTypes = []any{ (Builtin)(0), // 0: encore.parser.schema.v1.Builtin (ValidationRule_Is)(0), // 1: encore.parser.schema.v1.ValidationRule.Is (*Type)(nil), // 2: encore.parser.schema.v1.Type (*ValidationRule)(nil), // 3: encore.parser.schema.v1.ValidationRule (*ValidationExpr)(nil), // 4: encore.parser.schema.v1.ValidationExpr (*TypeParameterRef)(nil), // 5: encore.parser.schema.v1.TypeParameterRef (*Decl)(nil), // 6: encore.parser.schema.v1.Decl (*TypeParameter)(nil), // 7: encore.parser.schema.v1.TypeParameter (*Loc)(nil), // 8: encore.parser.schema.v1.Loc (*Named)(nil), // 9: encore.parser.schema.v1.Named (*Struct)(nil), // 10: encore.parser.schema.v1.Struct (*Field)(nil), // 11: encore.parser.schema.v1.Field (*WireSpec)(nil), // 12: encore.parser.schema.v1.WireSpec (*Tag)(nil), // 13: encore.parser.schema.v1.Tag (*Map)(nil), // 14: encore.parser.schema.v1.Map (*List)(nil), // 15: encore.parser.schema.v1.List (*Pointer)(nil), // 16: encore.parser.schema.v1.Pointer (*Option)(nil), // 17: encore.parser.schema.v1.Option (*Union)(nil), // 18: encore.parser.schema.v1.Union (*Literal)(nil), // 19: encore.parser.schema.v1.Literal (*ConfigValue)(nil), // 20: encore.parser.schema.v1.ConfigValue (*ValidationExpr_And)(nil), // 21: encore.parser.schema.v1.ValidationExpr.And (*ValidationExpr_Or)(nil), // 22: encore.parser.schema.v1.ValidationExpr.Or (*WireSpec_Header)(nil), // 23: encore.parser.schema.v1.WireSpec.Header (*WireSpec_Query)(nil), // 24: encore.parser.schema.v1.WireSpec.Query (*WireSpec_Cookie)(nil), // 25: encore.parser.schema.v1.WireSpec.Cookie (*WireSpec_HttpStatus)(nil), // 26: encore.parser.schema.v1.WireSpec.HttpStatus } var file_encore_parser_schema_v1_schema_proto_depIdxs = []int32{ 9, // 0: encore.parser.schema.v1.Type.named:type_name -> encore.parser.schema.v1.Named 10, // 1: encore.parser.schema.v1.Type.struct:type_name -> encore.parser.schema.v1.Struct 14, // 2: encore.parser.schema.v1.Type.map:type_name -> encore.parser.schema.v1.Map 15, // 3: encore.parser.schema.v1.Type.list:type_name -> encore.parser.schema.v1.List 0, // 4: encore.parser.schema.v1.Type.builtin:type_name -> encore.parser.schema.v1.Builtin 16, // 5: encore.parser.schema.v1.Type.pointer:type_name -> encore.parser.schema.v1.Pointer 18, // 6: encore.parser.schema.v1.Type.union:type_name -> encore.parser.schema.v1.Union 19, // 7: encore.parser.schema.v1.Type.literal:type_name -> encore.parser.schema.v1.Literal 17, // 8: encore.parser.schema.v1.Type.option:type_name -> encore.parser.schema.v1.Option 5, // 9: encore.parser.schema.v1.Type.type_parameter:type_name -> encore.parser.schema.v1.TypeParameterRef 20, // 10: encore.parser.schema.v1.Type.config:type_name -> encore.parser.schema.v1.ConfigValue 4, // 11: encore.parser.schema.v1.Type.validation:type_name -> encore.parser.schema.v1.ValidationExpr 1, // 12: encore.parser.schema.v1.ValidationRule.is:type_name -> encore.parser.schema.v1.ValidationRule.Is 3, // 13: encore.parser.schema.v1.ValidationExpr.rule:type_name -> encore.parser.schema.v1.ValidationRule 21, // 14: encore.parser.schema.v1.ValidationExpr.and:type_name -> encore.parser.schema.v1.ValidationExpr.And 22, // 15: encore.parser.schema.v1.ValidationExpr.or:type_name -> encore.parser.schema.v1.ValidationExpr.Or 2, // 16: encore.parser.schema.v1.Decl.type:type_name -> encore.parser.schema.v1.Type 7, // 17: encore.parser.schema.v1.Decl.type_params:type_name -> encore.parser.schema.v1.TypeParameter 8, // 18: encore.parser.schema.v1.Decl.loc:type_name -> encore.parser.schema.v1.Loc 2, // 19: encore.parser.schema.v1.Named.type_arguments:type_name -> encore.parser.schema.v1.Type 11, // 20: encore.parser.schema.v1.Struct.fields:type_name -> encore.parser.schema.v1.Field 2, // 21: encore.parser.schema.v1.Field.typ:type_name -> encore.parser.schema.v1.Type 13, // 22: encore.parser.schema.v1.Field.tags:type_name -> encore.parser.schema.v1.Tag 12, // 23: encore.parser.schema.v1.Field.wire:type_name -> encore.parser.schema.v1.WireSpec 23, // 24: encore.parser.schema.v1.WireSpec.header:type_name -> encore.parser.schema.v1.WireSpec.Header 24, // 25: encore.parser.schema.v1.WireSpec.query:type_name -> encore.parser.schema.v1.WireSpec.Query 25, // 26: encore.parser.schema.v1.WireSpec.cookie:type_name -> encore.parser.schema.v1.WireSpec.Cookie 26, // 27: encore.parser.schema.v1.WireSpec.http_status:type_name -> encore.parser.schema.v1.WireSpec.HttpStatus 2, // 28: encore.parser.schema.v1.Map.key:type_name -> encore.parser.schema.v1.Type 2, // 29: encore.parser.schema.v1.Map.value:type_name -> encore.parser.schema.v1.Type 2, // 30: encore.parser.schema.v1.List.elem:type_name -> encore.parser.schema.v1.Type 2, // 31: encore.parser.schema.v1.Pointer.base:type_name -> encore.parser.schema.v1.Type 2, // 32: encore.parser.schema.v1.Option.value:type_name -> encore.parser.schema.v1.Type 2, // 33: encore.parser.schema.v1.Union.types:type_name -> encore.parser.schema.v1.Type 2, // 34: encore.parser.schema.v1.ConfigValue.elem:type_name -> encore.parser.schema.v1.Type 4, // 35: encore.parser.schema.v1.ValidationExpr.And.exprs:type_name -> encore.parser.schema.v1.ValidationExpr 4, // 36: encore.parser.schema.v1.ValidationExpr.Or.exprs:type_name -> encore.parser.schema.v1.ValidationExpr 37, // [37:37] is the sub-list for method output_type 37, // [37:37] is the sub-list for method input_type 37, // [37:37] is the sub-list for extension type_name 37, // [37:37] is the sub-list for extension extendee 0, // [0:37] is the sub-list for field type_name } func init() { file_encore_parser_schema_v1_schema_proto_init() } func file_encore_parser_schema_v1_schema_proto_init() { if File_encore_parser_schema_v1_schema_proto != nil { return } file_encore_parser_schema_v1_schema_proto_msgTypes[0].OneofWrappers = []any{ (*Type_Named)(nil), (*Type_Struct)(nil), (*Type_Map)(nil), (*Type_List)(nil), (*Type_Builtin)(nil), (*Type_Pointer)(nil), (*Type_Union)(nil), (*Type_Literal)(nil), (*Type_Option)(nil), (*Type_TypeParameter)(nil), (*Type_Config)(nil), } file_encore_parser_schema_v1_schema_proto_msgTypes[1].OneofWrappers = []any{ (*ValidationRule_MinLen)(nil), (*ValidationRule_MaxLen)(nil), (*ValidationRule_MinVal)(nil), (*ValidationRule_MaxVal)(nil), (*ValidationRule_StartsWith)(nil), (*ValidationRule_EndsWith)(nil), (*ValidationRule_MatchesRegexp)(nil), (*ValidationRule_Is_)(nil), } file_encore_parser_schema_v1_schema_proto_msgTypes[2].OneofWrappers = []any{ (*ValidationExpr_Rule)(nil), (*ValidationExpr_And_)(nil), (*ValidationExpr_Or_)(nil), } file_encore_parser_schema_v1_schema_proto_msgTypes[9].OneofWrappers = []any{} file_encore_parser_schema_v1_schema_proto_msgTypes[10].OneofWrappers = []any{ (*WireSpec_Header_)(nil), (*WireSpec_Query_)(nil), (*WireSpec_Cookie_)(nil), (*WireSpec_HttpStatus_)(nil), } file_encore_parser_schema_v1_schema_proto_msgTypes[17].OneofWrappers = []any{ (*Literal_Str)(nil), (*Literal_Boolean)(nil), (*Literal_Int)(nil), (*Literal_Float)(nil), (*Literal_Null)(nil), } file_encore_parser_schema_v1_schema_proto_msgTypes[21].OneofWrappers = []any{} file_encore_parser_schema_v1_schema_proto_msgTypes[22].OneofWrappers = []any{} file_encore_parser_schema_v1_schema_proto_msgTypes[23].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_parser_schema_v1_schema_proto_rawDesc), len(file_encore_parser_schema_v1_schema_proto_rawDesc)), NumEnums: 2, NumMessages: 25, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_parser_schema_v1_schema_proto_goTypes, DependencyIndexes: file_encore_parser_schema_v1_schema_proto_depIdxs, EnumInfos: file_encore_parser_schema_v1_schema_proto_enumTypes, MessageInfos: file_encore_parser_schema_v1_schema_proto_msgTypes, }.Build() File_encore_parser_schema_v1_schema_proto = out.File file_encore_parser_schema_v1_schema_proto_goTypes = nil file_encore_parser_schema_v1_schema_proto_depIdxs = nil } ================================================ FILE: proto/encore/parser/schema/v1/schema.pb.ts ================================================ /* eslint-disable */ export const protobufPackage = "encore.parser.schema.v1"; /** * Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling * directly, rather than needing to understand the full implementation details of how the type is structured. */ export enum Builtin { /** ANY - Inbuilt Go Types */ ANY = "ANY", BOOL = "BOOL", INT8 = "INT8", INT16 = "INT16", INT32 = "INT32", INT64 = "INT64", UINT8 = "UINT8", UINT16 = "UINT16", UINT32 = "UINT32", UINT64 = "UINT64", FLOAT32 = "FLOAT32", FLOAT64 = "FLOAT64", STRING = "STRING", BYTES = "BYTES", /** TIME - Additional Encore Types */ TIME = "TIME", UUID = "UUID", JSON = "JSON", USER_ID = "USER_ID", INT = "INT", UINT = "UINT", UNRECOGNIZED = "UNRECOGNIZED", } /** * Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one, * thing from our list of meta types. * * A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the * structures looking for any uses of the TypeParameterPtr type */ export interface Type { /** Concrete / non-parameterized Types */ named: Named | undefined; /** The type is a struct definition */ struct: Struct | undefined; /** The type is a map */ map: Map | undefined; /** The type is a slice */ list: List | undefined; /** The type is one of the base built in types within Go */ builtin: Builtin | undefined; /** The type is a pointer */ pointer: Pointer | undefined; /** Abstract Types */ type_parameter: TypeParameterRef | undefined; /** Encore Special Types */ config: ConfigValue | undefined; } /** TypeParameterRef is a reference to a `TypeParameter` within a declaration block */ export interface TypeParameterRef { /** The ID of the declaration block */ decl_id: number; /** The index of the type parameter within the declaration block */ param_idx: number; } /** * Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is * concrete when there are zero type parameters assigned. * * For example the Go Code: * ``` * // Set[A] represents our set type * type Set[A any] = map[A]struct{} * ``` * * Would become: * ``` * _ = &Decl{ * id: 1, * name: "Set", * type: &Type{ * typ_map: &Map{ * key: &Type { typ_type_parameter: ... reference to "A" type parameter below ... }, * value: &Type { typ_struct: ... empty struct type ... }, * }, * }, * typeParameters: []*TypeParameter{ { name: "A" } }, * doc: "Set[A] represents our set type", * loc: &Loc { ... }, * } * ``` */ export interface Decl { /** A internal ID which we can refer to this declaration by */ id: number; /** The name of the type as assigned in the code */ name: string; /** The underlying type of this declaration */ type: Type; /** Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here) */ type_params: TypeParameter[]; /** The comment block on the type */ doc: string; /** The location of the declaration within the project */ loc: Loc; } /** * TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is * replaced with a type argument upon instantiation of the parameterized function or type. */ export interface TypeParameter { /** The identifier given to the type parameter */ name: string; } /** Loc is the location of a declaration within the code base */ export interface Loc { /** The package path within the repo (i.e. `users/signup`) */ pkg_path: string; /** The package name (i.e. `signup`) */ pkg_name: string; /** The file name (i.e. `signup.go`) */ filename: string; /** The starting index within the file for this node */ start_pos: number; /** The ending index within the file for this node */ end_pos: number; /** The starting line within the file for this node */ src_line_start: number; /** The ending line within the file for this node */ src_line_end: number; /** The starting column on the starting line for this node */ src_col_start: number; /** The ending column on the ending line for this node */ src_col_end: number; } /** Named references declaration block by name */ export interface Named { /** The `Decl.id` this name refers to */ id: number; /** The type arguments used to instantiate this parameterised declaration */ type_arguments: Type[]; } /** Struct contains a list of fields which make up the struct */ export interface Struct { fields: Field[]; } /** Field represents a field within a struct */ export interface Field { /** The type of the field */ typ: Type; /** The name of the field */ name: string; /** The comment for the field */ doc: string; /** The optional json name if it's different from the field name. (The value "-" indicates to omit the field.) */ json_name: string; /** Whether the field is optional. */ optional: boolean; /** The query string name to use in GET/HEAD/DELETE requests. (The value "-" indicates to omit the field.) */ query_string_name: string; /** The original Go struct tag; should not be parsed individually */ raw_tag: string; /** Parsed go struct tags. Used for marshalling hints */ tags: Tag[]; } export interface Tag { /** The tag key (e.g. json, query, header ...) */ key: string; /** The tag name (e.g. first_name, firstName, ...) */ name: string; /** Key Options (e.g. omitempty, optional ...) */ options: string[]; } /** Map represents a map Type */ export interface Map { /** The type of the key for this map */ key: Type; /** The type of the value of this map */ value: Type; } /** List represents a list type (array or slice) */ export interface List { /** The type of the elements in the list */ elem: Type; } /** Pointer represents a pointer to a base type */ export interface Pointer { /** The type of the pointer */ base: Type; } /** ConfigValue represents a config value wrapper. */ export interface ConfigValue { /** The type of the config value */ elem: Type; /** Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]` */ IsValuesList: boolean; } ================================================ FILE: proto/encore/parser/schema/v1/schema.proto ================================================ syntax = "proto3"; package encore.parser.schema.v1; option go_package = "encr.dev/proto/encore/parser/schema/v1"; // Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one, // thing from our list of meta types. // // A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the // structures looking for any uses of the TypeParameterPtr type message Type { oneof typ { /* Concrete / non-parameterized Types */ Named named = 1; // A "named" type (https://tip.golang.org/ref/spec#Types) Struct struct = 2; // The type is a struct definition Map map = 3; // The type is a map List list = 4; // The type is a slice Builtin builtin = 5; // The type is one of the base built in types within Go Pointer pointer = 8; // The type is a pointer Union union = 9; // The type is a union Literal literal = 10; // The type is a literal Option option = 11; // The type is an option type /* Abstract Types */ TypeParameterRef type_parameter = 6; // This is placeholder for a unknown type within the declaration block /* Encore Special Types */ ConfigValue config = 7; // This value is a config value } optional ValidationExpr validation = 15; // The validation expression for this type } message ValidationRule { oneof rule { uint64 min_len = 1; uint64 max_len = 2; double min_val = 3; double max_val = 4; string starts_with = 5; string ends_with = 6; string matches_regexp = 7; Is is = 8; } enum Is { UNKNOWN = 0; EMAIL = 1; URL = 2; } } message ValidationExpr { oneof expr { ValidationRule rule = 1; And and = 2; Or or = 3; } message And { repeated ValidationExpr exprs = 1; } message Or { repeated ValidationExpr exprs = 1; } } // TypeParameterRef is a reference to a `TypeParameter` within a declaration block message TypeParameterRef { uint32 decl_id = 1; // The ID of the declaration block uint32 param_idx = 2; // The index of the type parameter within the declaration block } // Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is // concrete when there are zero type parameters assigned. // // For example the Go Code: // ```go // // Set[A] represents our set type // type Set[A any] = map[A]struct{} // ``` // // Would become: // ```go // _ = &Decl{ // id: 1, // name: "Set", // type: &Type{ // typ_map: &Map{ // key: &Type { typ_type_parameter: ... reference to "A" type parameter below ... }, // value: &Type { typ_struct: ... empty struct type ... }, // }, // }, // typeParameters: []*TypeParameter{ { name: "A" } }, // doc: "Set[A] represents our set type", // loc: &Loc { ... }, // } // ``` message Decl { uint32 id = 1; // A internal ID which we can refer to this declaration by string name = 2; // The name of the type as assigned in the code Type type = 3; // The underlying type of this declaration repeated TypeParameter type_params = 6; // Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here) string doc = 4; // The comment block on the type Loc loc = 5; // The location of the declaration within the project } // TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is // replaced with a type argument upon instantiation of the parameterized function or type. message TypeParameter { string name = 1; // The identifier given to the type parameter } // Loc is the location of a declaration within the code base message Loc { string pkg_path = 1; // The package path within the repo (i.e. `users/signup`) string pkg_name = 2; // The package name (i.e. `signup`) string filename = 3; // The file name (i.e. `signup.go`) int32 start_pos = 4; // The starting index within the file for this node int32 end_pos = 5; // The ending index within the file for this node int32 src_line_start = 6; // The starting line within the file for this node int32 src_line_end = 7; // The ending line within the file for this node int32 src_col_start = 8; // The starting column on the starting line for this node int32 src_col_end = 9; // The ending column on the ending line for this node } // Named references declaration block by name message Named { uint32 id = 1; // The `Decl.id` this name refers to repeated Type type_arguments = 2; // The type arguments used to instantiate this parameterised declaration } // Struct contains a list of fields which make up the struct message Struct { repeated Field fields = 1; } // Field represents a field within a struct message Field { Type typ = 1; // The type of the field string name = 2; // The name of the field string doc = 3; // The comment for the field string json_name = 4; // The optional json name if it's different from the field name. (The value "-" indicates to omit the field.) bool optional = 5; // Whether the field is optional. string query_string_name = 6; // The query string name to use in GET/HEAD/DELETE requests. (The value "-" indicates to omit the field.) string raw_tag = 7; // The original Go struct tag; should not be parsed individually repeated Tag tags = 8; // Parsed go struct tags. Used for marshalling hints optional WireSpec wire = 9; // The explicitly set wire location of the field. } // WireLocation provides information about how a field should be encoded on the wire. message WireSpec { oneof location { Header header = 1; Query query = 2; Cookie cookie = 3; HttpStatus http_status = 4; } message Header { // The explicitly specified header name. // If empty, the name of the field is used. optional string name = 1; } message Query { // The explicitly specified query string name. // If empty, the name of the field is used. optional string name = 1; } message Cookie { // The explicitly specified cookie string name. // If empty, the name of the field is used. optional string name = 1; } message HttpStatus { // HttpStatus fields don't have a name parameter // as they represent the HTTP status code itself } } message Tag { string key = 1; // The tag key (e.g. json, query, header ...) string name = 2; // The tag name (e.g. first_name, firstName, ...) repeated string options = 3; // Key Options (e.g. omitempty, optional ...) } // Map represents a map Type message Map { Type key = 1; // The type of the key for this map Type value = 2; // The type of the value of this map } // List represents a list type (array or slice) message List { Type elem = 1; // The type of the elements in the list } // Pointer represents a pointer to a base type message Pointer { Type base = 1; // The type of the pointer } // Option represents an option type. message Option { Type value = 1; // The value it may contain. } // Union represents a union type. message Union { repeated Type types = 1; // The types that make up the union } // Literal represents a literal value. message Literal { oneof value { string str = 1; bool boolean = 2; int64 int = 3; double float = 4; bool null = 5; } } // ConfigValue represents a config value wrapper. message ConfigValue { Type elem = 1; // The type of the config value bool IsValuesList = 2; // Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]` } // Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling // directly, rather than needing to understand the full implementation details of how the type is structured. enum Builtin { /* Inbuilt Go Types */ ANY = 0; BOOL = 1; INT8 = 2; INT16 = 3; INT32 = 4; INT64 = 5; UINT8 = 6; UINT16 = 7; UINT32 = 8; UINT64 = 9; FLOAT32 = 10; FLOAT64 = 11; STRING = 12; BYTES = 13; /* Additional Encore Types */ TIME = 14; UUID = 15; JSON = 16; USER_ID = 17; INT = 18; UINT = 19; DECIMAL = 20; } ================================================ FILE: proto/encore/parser/schema/v1/walk.go ================================================ package v1 import ( "fmt" "reflect" ) // Walk will perform a depth first walk of all schema nodes starting at node, calling visitor for each schema type found. // // If visitor returns false, the walk will be aborted. func Walk(decls []*Decl, node any, visitor func(node any) error) error { namedChain := make([]uint32, 0, 10) return walk(decls, node, visitor, namedChain) } func walk(decls []*Decl, node any, visitor func(node any) error, namedChain []uint32) error { // Check the visitor against the node type if err := visitor(node); err != nil { return err } switch node := node.(type) { case *Type: switch v := node.Typ.(type) { case *Type_Named: return walk(decls, v.Named, visitor, namedChain) case *Type_Struct: return walk(decls, v.Struct, visitor, namedChain) case *Type_Map: return walk(decls, v.Map, visitor, namedChain) case *Type_List: return walk(decls, v.List, visitor, namedChain) case *Type_Builtin: return walk(decls, v.Builtin, visitor, namedChain) case *Type_Pointer: return walk(decls, v.Pointer, visitor, namedChain) case *Type_Option: return walk(decls, v.Option, visitor, namedChain) case *Type_TypeParameter: return walk(decls, v.TypeParameter, visitor, namedChain) case *Type_Literal: return walk(decls, v.Literal, visitor, namedChain) case *Type_Union: return walk(decls, v.Union, visitor, namedChain) case *Type_Config: return walk(decls, v.Config, visitor, namedChain) default: panic(fmt.Sprintf("unknown type encountered: %+v", reflect.TypeOf(v))) } case *Decl: return walk(decls, decls[node.Id].Type, visitor, namedChain) case *Named: for _, typ := range node.TypeArguments { if err := walk(decls, typ, visitor, namedChain); err != nil { return err } } // Have we already visited this named type? for i := len(namedChain) - 1; i >= 0; i-- { if namedChain[i] == node.Id { return nil } } namedChain = append(namedChain, node.Id) return walk(decls, decls[node.Id].Type, visitor, namedChain) case *Struct: for _, field := range node.Fields { if err := walk(decls, field.Typ, visitor, namedChain); err != nil { return err } } case *Union: for _, typ := range node.Types { if err := walk(decls, typ, visitor, namedChain); err != nil { return err } } case *Map: if err := walk(decls, node.Key, visitor, namedChain); err != nil { return err } return walk(decls, node.Value, visitor, namedChain) case *List: return walk(decls, node.Elem, visitor, namedChain) case Builtin: return nil case *Pointer: return walk(decls, node.Base, visitor, namedChain) case *Option: return walk(decls, node.Value, visitor, namedChain) case *TypeParameterRef: return nil case *Literal: return nil case *ConfigValue: return walk(decls, node.Elem, visitor, namedChain) default: panic(fmt.Sprintf("unsupported node type encountered during walk: %+v", reflect.TypeOf(node))) } return nil } ================================================ FILE: proto/encore/parser/schema/v1/walk_test.go ================================================ package v1 import "testing" // TestWalk_RecursiveDataStructure tests that Walk gracefully handles // recursive and mutually recursive data structures. func TestWalk_RecursiveDataStructure(t *testing.T) { selfRecursive := &Decl{ Id: 0, Type: &Type{ Typ: &Type_Named{ Named: &Named{ Id: 0, }, }, }, } mutualRecursiveOne := &Decl{ Id: 1, Type: &Type{ Typ: &Type_Struct{ Struct: &Struct{ Fields: []*Field{ { Typ: &Type{Typ: &Type_Named{ Named: &Named{Id: 1}, }}, }, }, }, }, }, } mutualRecursiveTwo := &Decl{ Id: 1, Type: &Type{ Typ: &Type_Struct{ Struct: &Struct{ Fields: []*Field{ { Typ: &Type{Typ: &Type_Named{ Named: &Named{Id: 0}, }}, }, }, }, }, }, } tests := []struct { name string decls []*Decl node any }{ { name: "self_recursive", decls: []*Decl{selfRecursive}, node: selfRecursive.Type, }, { name: "mutual_recursive", decls: []*Decl{mutualRecursiveOne, mutualRecursiveTwo}, node: mutualRecursiveOne.Type, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { visitor := func(node any) error { return nil } if err := Walk(tt.decls, tt.node, visitor); err != nil { t.Fatal(err) } }) } } ================================================ FILE: proto/encore/runtime/v1/infra.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/runtime/v1/infra.proto package runtimev1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type ServerKind int32 const ( ServerKind_SERVER_KIND_UNSPECIFIED ServerKind = 0 ServerKind_SERVER_KIND_PRIMARY ServerKind = 1 // A hot-standby (a read replica designed to take over write traffic // at a moment's notice). ServerKind_SERVER_KIND_HOT_STANDBY ServerKind = 2 // A read-replica. ServerKind_SERVER_KIND_READ_REPLICA ServerKind = 3 ) // Enum value maps for ServerKind. var ( ServerKind_name = map[int32]string{ 0: "SERVER_KIND_UNSPECIFIED", 1: "SERVER_KIND_PRIMARY", 2: "SERVER_KIND_HOT_STANDBY", 3: "SERVER_KIND_READ_REPLICA", } ServerKind_value = map[string]int32{ "SERVER_KIND_UNSPECIFIED": 0, "SERVER_KIND_PRIMARY": 1, "SERVER_KIND_HOT_STANDBY": 2, "SERVER_KIND_READ_REPLICA": 3, } ) func (x ServerKind) Enum() *ServerKind { p := new(ServerKind) *p = x return p } func (x ServerKind) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ServerKind) Descriptor() protoreflect.EnumDescriptor { return file_encore_runtime_v1_infra_proto_enumTypes[0].Descriptor() } func (ServerKind) Type() protoreflect.EnumType { return &file_encore_runtime_v1_infra_proto_enumTypes[0] } func (x ServerKind) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ServerKind.Descriptor instead. func (ServerKind) EnumDescriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0} } type PubSubTopic_DeliveryGuarantee int32 const ( PubSubTopic_DELIVERY_GUARANTEE_UNSPECIFIED PubSubTopic_DeliveryGuarantee = 0 PubSubTopic_DELIVERY_GUARANTEE_AT_LEAST_ONCE PubSubTopic_DeliveryGuarantee = 1 // All messages will be delivered to each subscription at least once PubSubTopic_DELIVERY_GUARANTEE_EXACTLY_ONCE PubSubTopic_DeliveryGuarantee = 2 // All messages will be delivered to each subscription exactly once ) // Enum value maps for PubSubTopic_DeliveryGuarantee. var ( PubSubTopic_DeliveryGuarantee_name = map[int32]string{ 0: "DELIVERY_GUARANTEE_UNSPECIFIED", 1: "DELIVERY_GUARANTEE_AT_LEAST_ONCE", 2: "DELIVERY_GUARANTEE_EXACTLY_ONCE", } PubSubTopic_DeliveryGuarantee_value = map[string]int32{ "DELIVERY_GUARANTEE_UNSPECIFIED": 0, "DELIVERY_GUARANTEE_AT_LEAST_ONCE": 1, "DELIVERY_GUARANTEE_EXACTLY_ONCE": 2, } ) func (x PubSubTopic_DeliveryGuarantee) Enum() *PubSubTopic_DeliveryGuarantee { p := new(PubSubTopic_DeliveryGuarantee) *p = x return p } func (x PubSubTopic_DeliveryGuarantee) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PubSubTopic_DeliveryGuarantee) Descriptor() protoreflect.EnumDescriptor { return file_encore_runtime_v1_infra_proto_enumTypes[1].Descriptor() } func (PubSubTopic_DeliveryGuarantee) Type() protoreflect.EnumType { return &file_encore_runtime_v1_infra_proto_enumTypes[1] } func (x PubSubTopic_DeliveryGuarantee) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PubSubTopic_DeliveryGuarantee.Descriptor instead. func (PubSubTopic_DeliveryGuarantee) EnumDescriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15, 0} } type Infrastructure struct { state protoimpl.MessageState `protogen:"open.v1"` Resources *Infrastructure_Resources `protobuf:"bytes,1,opt,name=resources,proto3" json:"resources,omitempty"` Credentials *Infrastructure_Credentials `protobuf:"bytes,2,opt,name=credentials,proto3" json:"credentials,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Infrastructure) Reset() { *x = Infrastructure{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Infrastructure) String() string { return protoimpl.X.MessageStringOf(x) } func (*Infrastructure) ProtoMessage() {} func (x *Infrastructure) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Infrastructure.ProtoReflect.Descriptor instead. func (*Infrastructure) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0} } func (x *Infrastructure) GetResources() *Infrastructure_Resources { if x != nil { return x.Resources } return nil } func (x *Infrastructure) GetCredentials() *Infrastructure_Credentials { if x != nil { return x.Credentials } return nil } type SQLCluster struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this cluster. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Servers []*SQLServer `protobuf:"bytes,2,rep,name=servers,proto3" json:"servers,omitempty"` Databases []*SQLDatabase `protobuf:"bytes,3,rep,name=databases,proto3" json:"databases,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLCluster) Reset() { *x = SQLCluster{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLCluster) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLCluster) ProtoMessage() {} func (x *SQLCluster) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLCluster.ProtoReflect.Descriptor instead. func (*SQLCluster) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{1} } func (x *SQLCluster) GetRid() string { if x != nil { return x.Rid } return "" } func (x *SQLCluster) GetServers() []*SQLServer { if x != nil { return x.Servers } return nil } func (x *SQLCluster) GetDatabases() []*SQLDatabase { if x != nil { return x.Databases } return nil } type TLSConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // Server CA Cert PEM to use for verifying the server's certificate. ServerCaCert *string `protobuf:"bytes,1,opt,name=server_ca_cert,json=serverCaCert,proto3,oneof" json:"server_ca_cert,omitempty"` // If true, skips hostname verification when connecting. // If invalid hostnames are trusted, *any* valid certificate for *any* site will be trusted for use. // This introduces significant vulnerabilities, and should only be used as a last resort. DisableTlsHostnameVerification bool `protobuf:"varint,2,opt,name=disable_tls_hostname_verification,json=disableTlsHostnameVerification,proto3" json:"disable_tls_hostname_verification,omitempty"` // If true, skips CA cert validation when connecting. // This introduces significant vulnerabilities, and should only be used as a last resort. DisableCaValidation bool `protobuf:"varint,3,opt,name=disable_ca_validation,json=disableCaValidation,proto3" json:"disable_ca_validation,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TLSConfig) Reset() { *x = TLSConfig{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TLSConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*TLSConfig) ProtoMessage() {} func (x *TLSConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TLSConfig.ProtoReflect.Descriptor instead. func (*TLSConfig) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{2} } func (x *TLSConfig) GetServerCaCert() string { if x != nil && x.ServerCaCert != nil { return *x.ServerCaCert } return "" } func (x *TLSConfig) GetDisableTlsHostnameVerification() bool { if x != nil { return x.DisableTlsHostnameVerification } return false } func (x *TLSConfig) GetDisableCaValidation() bool { if x != nil { return x.DisableCaValidation } return false } type SQLServer struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this server. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // Host is the host to connect to. // Valid formats are "hostname", "hostname:port", and "/path/to/unix.socket". Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` Kind ServerKind `protobuf:"varint,3,opt,name=kind,proto3,enum=encore.runtime.v1.ServerKind" json:"kind,omitempty"` // TLS configuration to use when connecting. TlsConfig *TLSConfig `protobuf:"bytes,4,opt,name=tls_config,json=tlsConfig,proto3,oneof" json:"tls_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLServer) Reset() { *x = SQLServer{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLServer) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLServer) ProtoMessage() {} func (x *SQLServer) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLServer.ProtoReflect.Descriptor instead. func (*SQLServer) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{3} } func (x *SQLServer) GetRid() string { if x != nil { return x.Rid } return "" } func (x *SQLServer) GetHost() string { if x != nil { return x.Host } return "" } func (x *SQLServer) GetKind() ServerKind { if x != nil { return x.Kind } return ServerKind_SERVER_KIND_UNSPECIFIED } func (x *SQLServer) GetTlsConfig() *TLSConfig { if x != nil { return x.TlsConfig } return nil } type ClientCert struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this certificate. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Cert string `protobuf:"bytes,2,opt,name=cert,proto3" json:"cert,omitempty"` Key *SecretData `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ClientCert) Reset() { *x = ClientCert{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ClientCert) String() string { return protoimpl.X.MessageStringOf(x) } func (*ClientCert) ProtoMessage() {} func (x *ClientCert) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ClientCert.ProtoReflect.Descriptor instead. func (*ClientCert) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{4} } func (x *ClientCert) GetRid() string { if x != nil { return x.Rid } return "" } func (x *ClientCert) GetCert() string { if x != nil { return x.Cert } return "" } func (x *ClientCert) GetKey() *SecretData { if x != nil { return x.Key } return nil } type SQLRole struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this role. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` Password *SecretData `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` // The client cert to use to authenticate, if any. ClientCertRid *string `protobuf:"bytes,4,opt,name=client_cert_rid,json=clientCertRid,proto3,oneof" json:"client_cert_rid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLRole) Reset() { *x = SQLRole{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLRole) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLRole) ProtoMessage() {} func (x *SQLRole) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLRole.ProtoReflect.Descriptor instead. func (*SQLRole) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{5} } func (x *SQLRole) GetRid() string { if x != nil { return x.Rid } return "" } func (x *SQLRole) GetUsername() string { if x != nil { return x.Username } return "" } func (x *SQLRole) GetPassword() *SecretData { if x != nil { return x.Password } return nil } func (x *SQLRole) GetClientCertRid() string { if x != nil && x.ClientCertRid != nil { return *x.ClientCertRid } return "" } type SQLDatabase struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this database. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The physical name of the database in the cluster. CloudName string `protobuf:"bytes,3,opt,name=cloud_name,json=cloudName,proto3" json:"cloud_name,omitempty"` // Connection pools to use for connecting to the database. ConnPools []*SQLConnectionPool `protobuf:"bytes,4,rep,name=conn_pools,json=connPools,proto3" json:"conn_pools,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLDatabase) Reset() { *x = SQLDatabase{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLDatabase) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLDatabase) ProtoMessage() {} func (x *SQLDatabase) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLDatabase.ProtoReflect.Descriptor instead. func (*SQLDatabase) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{6} } func (x *SQLDatabase) GetRid() string { if x != nil { return x.Rid } return "" } func (x *SQLDatabase) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *SQLDatabase) GetCloudName() string { if x != nil { return x.CloudName } return "" } func (x *SQLDatabase) GetConnPools() []*SQLConnectionPool { if x != nil { return x.ConnPools } return nil } type SQLConnectionPool struct { state protoimpl.MessageState `protogen:"open.v1"` // Whether this connection pool is for read-only servers. IsReadonly bool `protobuf:"varint,1,opt,name=is_readonly,json=isReadonly,proto3" json:"is_readonly,omitempty"` // The role to use to authenticate. RoleRid string `protobuf:"bytes,2,opt,name=role_rid,json=roleRid,proto3" json:"role_rid,omitempty"` // The minimum and maximum number of connections to use. MinConnections int32 `protobuf:"varint,3,opt,name=min_connections,json=minConnections,proto3" json:"min_connections,omitempty"` MaxConnections int32 `protobuf:"varint,4,opt,name=max_connections,json=maxConnections,proto3" json:"max_connections,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SQLConnectionPool) Reset() { *x = SQLConnectionPool{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SQLConnectionPool) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLConnectionPool) ProtoMessage() {} func (x *SQLConnectionPool) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLConnectionPool.ProtoReflect.Descriptor instead. func (*SQLConnectionPool) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{7} } func (x *SQLConnectionPool) GetIsReadonly() bool { if x != nil { return x.IsReadonly } return false } func (x *SQLConnectionPool) GetRoleRid() string { if x != nil { return x.RoleRid } return "" } func (x *SQLConnectionPool) GetMinConnections() int32 { if x != nil { return x.MinConnections } return 0 } func (x *SQLConnectionPool) GetMaxConnections() int32 { if x != nil { return x.MaxConnections } return 0 } type RedisCluster struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this cluster. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Servers []*RedisServer `protobuf:"bytes,2,rep,name=servers,proto3" json:"servers,omitempty"` Databases []*RedisDatabase `protobuf:"bytes,3,rep,name=databases,proto3" json:"databases,omitempty"` // If true, the runtime will use an in-memory Redis implementation // instead of connecting to the configured servers. InMemory bool `protobuf:"varint,4,opt,name=in_memory,json=inMemory,proto3" json:"in_memory,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisCluster) Reset() { *x = RedisCluster{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisCluster) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisCluster) ProtoMessage() {} func (x *RedisCluster) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisCluster.ProtoReflect.Descriptor instead. func (*RedisCluster) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{8} } func (x *RedisCluster) GetRid() string { if x != nil { return x.Rid } return "" } func (x *RedisCluster) GetServers() []*RedisServer { if x != nil { return x.Servers } return nil } func (x *RedisCluster) GetDatabases() []*RedisDatabase { if x != nil { return x.Databases } return nil } func (x *RedisCluster) GetInMemory() bool { if x != nil { return x.InMemory } return false } type RedisServer struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this server. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // Host is the host to connect to. // Valid formats are "hostname", "hostname:port", and "/path/to/unix.socket". Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` Kind ServerKind `protobuf:"varint,3,opt,name=kind,proto3,enum=encore.runtime.v1.ServerKind" json:"kind,omitempty"` // TLS configuration to use when connecting. // If nil, TLS is not used. TlsConfig *TLSConfig `protobuf:"bytes,4,opt,name=tls_config,json=tlsConfig,proto3,oneof" json:"tls_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisServer) Reset() { *x = RedisServer{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisServer) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisServer) ProtoMessage() {} func (x *RedisServer) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisServer.ProtoReflect.Descriptor instead. func (*RedisServer) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{9} } func (x *RedisServer) GetRid() string { if x != nil { return x.Rid } return "" } func (x *RedisServer) GetHost() string { if x != nil { return x.Host } return "" } func (x *RedisServer) GetKind() ServerKind { if x != nil { return x.Kind } return ServerKind_SERVER_KIND_UNSPECIFIED } func (x *RedisServer) GetTlsConfig() *TLSConfig { if x != nil { return x.TlsConfig } return nil } type RedisConnectionPool struct { state protoimpl.MessageState `protogen:"open.v1"` // Whether this connection pool is for read-only servers. IsReadonly bool `protobuf:"varint,1,opt,name=is_readonly,json=isReadonly,proto3" json:"is_readonly,omitempty"` // The role to use to authenticate. RoleRid string `protobuf:"bytes,2,opt,name=role_rid,json=roleRid,proto3" json:"role_rid,omitempty"` // The minimum and maximum number of connections to use. MinConnections int32 `protobuf:"varint,3,opt,name=min_connections,json=minConnections,proto3" json:"min_connections,omitempty"` MaxConnections int32 `protobuf:"varint,4,opt,name=max_connections,json=maxConnections,proto3" json:"max_connections,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisConnectionPool) Reset() { *x = RedisConnectionPool{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisConnectionPool) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisConnectionPool) ProtoMessage() {} func (x *RedisConnectionPool) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisConnectionPool.ProtoReflect.Descriptor instead. func (*RedisConnectionPool) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{10} } func (x *RedisConnectionPool) GetIsReadonly() bool { if x != nil { return x.IsReadonly } return false } func (x *RedisConnectionPool) GetRoleRid() string { if x != nil { return x.RoleRid } return "" } func (x *RedisConnectionPool) GetMinConnections() int32 { if x != nil { return x.MinConnections } return 0 } func (x *RedisConnectionPool) GetMaxConnections() int32 { if x != nil { return x.MaxConnections } return 0 } type RedisRole struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this role. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The client cert to use to authenticate, if any. ClientCertRid *string `protobuf:"bytes,2,opt,name=client_cert_rid,json=clientCertRid,proto3,oneof" json:"client_cert_rid,omitempty"` // How to authenticate with Redis. // If unset, no authentication is used. // // Types that are valid to be assigned to Auth: // // *RedisRole_Acl // *RedisRole_AuthString Auth isRedisRole_Auth `protobuf_oneof:"auth"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisRole) Reset() { *x = RedisRole{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisRole) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisRole) ProtoMessage() {} func (x *RedisRole) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisRole.ProtoReflect.Descriptor instead. func (*RedisRole) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{11} } func (x *RedisRole) GetRid() string { if x != nil { return x.Rid } return "" } func (x *RedisRole) GetClientCertRid() string { if x != nil && x.ClientCertRid != nil { return *x.ClientCertRid } return "" } func (x *RedisRole) GetAuth() isRedisRole_Auth { if x != nil { return x.Auth } return nil } func (x *RedisRole) GetAcl() *RedisRole_AuthACL { if x != nil { if x, ok := x.Auth.(*RedisRole_Acl); ok { return x.Acl } } return nil } func (x *RedisRole) GetAuthString() *SecretData { if x != nil { if x, ok := x.Auth.(*RedisRole_AuthString); ok { return x.AuthString } } return nil } type isRedisRole_Auth interface { isRedisRole_Auth() } type RedisRole_Acl struct { Acl *RedisRole_AuthACL `protobuf:"bytes,10,opt,name=acl,proto3,oneof"` // Redis ACL } type RedisRole_AuthString struct { AuthString *SecretData `protobuf:"bytes,11,opt,name=auth_string,json=authString,proto3,oneof"` // Redis AUTH string } func (*RedisRole_Acl) isRedisRole_Auth() {} func (*RedisRole_AuthString) isRedisRole_Auth() {} type RedisDatabase struct { state protoimpl.MessageState `protogen:"open.v1"` // Unique resource id for this database. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the database. EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The database index to use, [0-15]. DatabaseIdx int32 `protobuf:"varint,3,opt,name=database_idx,json=databaseIdx,proto3" json:"database_idx,omitempty"` // KeyPrefix specifies a prefix to add to all cache keys // for this database. It exists to enable multiple cache clusters // to use the same physical Redis database for local development // without having to coordinate and persist database index ids. KeyPrefix *string `protobuf:"bytes,4,opt,name=key_prefix,json=keyPrefix,proto3,oneof" json:"key_prefix,omitempty"` // Connection pools to use for connecting to the database. ConnPools []*RedisConnectionPool `protobuf:"bytes,5,rep,name=conn_pools,json=connPools,proto3" json:"conn_pools,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisDatabase) Reset() { *x = RedisDatabase{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisDatabase) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisDatabase) ProtoMessage() {} func (x *RedisDatabase) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisDatabase.ProtoReflect.Descriptor instead. func (*RedisDatabase) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{12} } func (x *RedisDatabase) GetRid() string { if x != nil { return x.Rid } return "" } func (x *RedisDatabase) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *RedisDatabase) GetDatabaseIdx() int32 { if x != nil { return x.DatabaseIdx } return 0 } func (x *RedisDatabase) GetKeyPrefix() string { if x != nil && x.KeyPrefix != nil { return *x.KeyPrefix } return "" } func (x *RedisDatabase) GetConnPools() []*RedisConnectionPool { if x != nil { return x.ConnPools } return nil } type AppSecret struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this secret. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the secret. EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The secret data. Data *SecretData `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AppSecret) Reset() { *x = AppSecret{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *AppSecret) String() string { return protoimpl.X.MessageStringOf(x) } func (*AppSecret) ProtoMessage() {} func (x *AppSecret) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AppSecret.ProtoReflect.Descriptor instead. func (*AppSecret) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{13} } func (x *AppSecret) GetRid() string { if x != nil { return x.Rid } return "" } func (x *AppSecret) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *AppSecret) GetData() *SecretData { if x != nil { return x.Data } return nil } type PubSubCluster struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this cluster. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Topics []*PubSubTopic `protobuf:"bytes,2,rep,name=topics,proto3" json:"topics,omitempty"` Subscriptions []*PubSubSubscription `protobuf:"bytes,3,rep,name=subscriptions,proto3" json:"subscriptions,omitempty"` // Types that are valid to be assigned to Provider: // // *PubSubCluster_Encore // *PubSubCluster_Aws // *PubSubCluster_Gcp // *PubSubCluster_Azure // *PubSubCluster_Nsq Provider isPubSubCluster_Provider `protobuf_oneof:"provider"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster) Reset() { *x = PubSubCluster{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster) ProtoMessage() {} func (x *PubSubCluster) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster.ProtoReflect.Descriptor instead. func (*PubSubCluster) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14} } func (x *PubSubCluster) GetRid() string { if x != nil { return x.Rid } return "" } func (x *PubSubCluster) GetTopics() []*PubSubTopic { if x != nil { return x.Topics } return nil } func (x *PubSubCluster) GetSubscriptions() []*PubSubSubscription { if x != nil { return x.Subscriptions } return nil } func (x *PubSubCluster) GetProvider() isPubSubCluster_Provider { if x != nil { return x.Provider } return nil } func (x *PubSubCluster) GetEncore() *PubSubCluster_EncoreCloud { if x != nil { if x, ok := x.Provider.(*PubSubCluster_Encore); ok { return x.Encore } } return nil } func (x *PubSubCluster) GetAws() *PubSubCluster_AWSSqsSns { if x != nil { if x, ok := x.Provider.(*PubSubCluster_Aws); ok { return x.Aws } } return nil } func (x *PubSubCluster) GetGcp() *PubSubCluster_GCPPubSub { if x != nil { if x, ok := x.Provider.(*PubSubCluster_Gcp); ok { return x.Gcp } } return nil } func (x *PubSubCluster) GetAzure() *PubSubCluster_AzureServiceBus { if x != nil { if x, ok := x.Provider.(*PubSubCluster_Azure); ok { return x.Azure } } return nil } func (x *PubSubCluster) GetNsq() *PubSubCluster_NSQ { if x != nil { if x, ok := x.Provider.(*PubSubCluster_Nsq); ok { return x.Nsq } } return nil } type isPubSubCluster_Provider interface { isPubSubCluster_Provider() } type PubSubCluster_Encore struct { Encore *PubSubCluster_EncoreCloud `protobuf:"bytes,5,opt,name=encore,proto3,oneof"` } type PubSubCluster_Aws struct { Aws *PubSubCluster_AWSSqsSns `protobuf:"bytes,6,opt,name=aws,proto3,oneof"` } type PubSubCluster_Gcp struct { Gcp *PubSubCluster_GCPPubSub `protobuf:"bytes,7,opt,name=gcp,proto3,oneof"` } type PubSubCluster_Azure struct { Azure *PubSubCluster_AzureServiceBus `protobuf:"bytes,8,opt,name=azure,proto3,oneof"` } type PubSubCluster_Nsq struct { Nsq *PubSubCluster_NSQ `protobuf:"bytes,9,opt,name=nsq,proto3,oneof"` } func (*PubSubCluster_Encore) isPubSubCluster_Provider() {} func (*PubSubCluster_Aws) isPubSubCluster_Provider() {} func (*PubSubCluster_Gcp) isPubSubCluster_Provider() {} func (*PubSubCluster_Azure) isPubSubCluster_Provider() {} func (*PubSubCluster_Nsq) isPubSubCluster_Provider() {} type PubSubTopic struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this topic. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the topic. EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The cloud name of the topic. CloudName string `protobuf:"bytes,3,opt,name=cloud_name,json=cloudName,proto3" json:"cloud_name,omitempty"` // The delivery guarantee. DeliveryGuarantee PubSubTopic_DeliveryGuarantee `protobuf:"varint,4,opt,name=delivery_guarantee,json=deliveryGuarantee,proto3,enum=encore.runtime.v1.PubSubTopic_DeliveryGuarantee" json:"delivery_guarantee,omitempty"` // Optional ordering attribute. Specifies the attribute name // to use for message ordering. OrderingAttr *string `protobuf:"bytes,5,opt,name=ordering_attr,json=orderingAttr,proto3,oneof" json:"ordering_attr,omitempty"` // Provider-specific configuration. // Not all providers require this, but it must always be set // for the providers that are present. // // Types that are valid to be assigned to ProviderConfig: // // *PubSubTopic_GcpConfig ProviderConfig isPubSubTopic_ProviderConfig `protobuf_oneof:"provider_config"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic) Reset() { *x = PubSubTopic{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic) ProtoMessage() {} func (x *PubSubTopic) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic.ProtoReflect.Descriptor instead. func (*PubSubTopic) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15} } func (x *PubSubTopic) GetRid() string { if x != nil { return x.Rid } return "" } func (x *PubSubTopic) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *PubSubTopic) GetCloudName() string { if x != nil { return x.CloudName } return "" } func (x *PubSubTopic) GetDeliveryGuarantee() PubSubTopic_DeliveryGuarantee { if x != nil { return x.DeliveryGuarantee } return PubSubTopic_DELIVERY_GUARANTEE_UNSPECIFIED } func (x *PubSubTopic) GetOrderingAttr() string { if x != nil && x.OrderingAttr != nil { return *x.OrderingAttr } return "" } func (x *PubSubTopic) GetProviderConfig() isPubSubTopic_ProviderConfig { if x != nil { return x.ProviderConfig } return nil } func (x *PubSubTopic) GetGcpConfig() *PubSubTopic_GCPConfig { if x != nil { if x, ok := x.ProviderConfig.(*PubSubTopic_GcpConfig); ok { return x.GcpConfig } } return nil } type isPubSubTopic_ProviderConfig interface { isPubSubTopic_ProviderConfig() } type PubSubTopic_GcpConfig struct { GcpConfig *PubSubTopic_GCPConfig `protobuf:"bytes,10,opt,name=gcp_config,json=gcpConfig,proto3,oneof"` // Null: no provider-specific configuration. } func (*PubSubTopic_GcpConfig) isPubSubTopic_ProviderConfig() {} type PubSubSubscription struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this subscription. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the topic this subscription is for. TopicEncoreName string `protobuf:"bytes,2,opt,name=topic_encore_name,json=topicEncoreName,proto3" json:"topic_encore_name,omitempty"` // The encore name of the subscription. SubscriptionEncoreName string `protobuf:"bytes,3,opt,name=subscription_encore_name,json=subscriptionEncoreName,proto3" json:"subscription_encore_name,omitempty"` // The cloud name of the subscription. TopicCloudName string `protobuf:"bytes,4,opt,name=topic_cloud_name,json=topicCloudName,proto3" json:"topic_cloud_name,omitempty"` // The cloud name of the subscription. SubscriptionCloudName string `protobuf:"bytes,5,opt,name=subscription_cloud_name,json=subscriptionCloudName,proto3" json:"subscription_cloud_name,omitempty"` // If true the application will not actively subscribe but wait // for incoming messages to be pushed to it. PushOnly bool `protobuf:"varint,6,opt,name=push_only,json=pushOnly,proto3" json:"push_only,omitempty"` // Subscription-specific provider configuration. // Not all providers require this, but it must always be set // for the providers that are present. // // Types that are valid to be assigned to ProviderConfig: // // *PubSubSubscription_GcpConfig ProviderConfig isPubSubSubscription_ProviderConfig `protobuf_oneof:"provider_config"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubSubscription) Reset() { *x = PubSubSubscription{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubSubscription) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubSubscription) ProtoMessage() {} func (x *PubSubSubscription) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubSubscription.ProtoReflect.Descriptor instead. func (*PubSubSubscription) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{16} } func (x *PubSubSubscription) GetRid() string { if x != nil { return x.Rid } return "" } func (x *PubSubSubscription) GetTopicEncoreName() string { if x != nil { return x.TopicEncoreName } return "" } func (x *PubSubSubscription) GetSubscriptionEncoreName() string { if x != nil { return x.SubscriptionEncoreName } return "" } func (x *PubSubSubscription) GetTopicCloudName() string { if x != nil { return x.TopicCloudName } return "" } func (x *PubSubSubscription) GetSubscriptionCloudName() string { if x != nil { return x.SubscriptionCloudName } return "" } func (x *PubSubSubscription) GetPushOnly() bool { if x != nil { return x.PushOnly } return false } func (x *PubSubSubscription) GetProviderConfig() isPubSubSubscription_ProviderConfig { if x != nil { return x.ProviderConfig } return nil } func (x *PubSubSubscription) GetGcpConfig() *PubSubSubscription_GCPConfig { if x != nil { if x, ok := x.ProviderConfig.(*PubSubSubscription_GcpConfig); ok { return x.GcpConfig } } return nil } type isPubSubSubscription_ProviderConfig interface { isPubSubSubscription_ProviderConfig() } type PubSubSubscription_GcpConfig struct { GcpConfig *PubSubSubscription_GCPConfig `protobuf:"bytes,10,opt,name=gcp_config,json=gcpConfig,proto3,oneof"` // Null: no provider-specific configuration. } func (*PubSubSubscription_GcpConfig) isPubSubSubscription_ProviderConfig() {} type BucketCluster struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this cluster. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` Buckets []*Bucket `protobuf:"bytes,2,rep,name=buckets,proto3" json:"buckets,omitempty"` // Types that are valid to be assigned to Provider: // // *BucketCluster_S3_ // *BucketCluster_Gcs Provider isBucketCluster_Provider `protobuf_oneof:"provider"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketCluster) Reset() { *x = BucketCluster{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketCluster) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketCluster) ProtoMessage() {} func (x *BucketCluster) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketCluster.ProtoReflect.Descriptor instead. func (*BucketCluster) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17} } func (x *BucketCluster) GetRid() string { if x != nil { return x.Rid } return "" } func (x *BucketCluster) GetBuckets() []*Bucket { if x != nil { return x.Buckets } return nil } func (x *BucketCluster) GetProvider() isBucketCluster_Provider { if x != nil { return x.Provider } return nil } func (x *BucketCluster) GetS3() *BucketCluster_S3 { if x != nil { if x, ok := x.Provider.(*BucketCluster_S3_); ok { return x.S3 } } return nil } func (x *BucketCluster) GetGcs() *BucketCluster_GCS { if x != nil { if x, ok := x.Provider.(*BucketCluster_Gcs); ok { return x.Gcs } } return nil } type isBucketCluster_Provider interface { isBucketCluster_Provider() } type BucketCluster_S3_ struct { S3 *BucketCluster_S3 `protobuf:"bytes,10,opt,name=s3,proto3,oneof"` } type BucketCluster_Gcs struct { Gcs *BucketCluster_GCS `protobuf:"bytes,11,opt,name=gcs,proto3,oneof"` } func (*BucketCluster_S3_) isBucketCluster_Provider() {} func (*BucketCluster_Gcs) isBucketCluster_Provider() {} type Bucket struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this bucket. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the bucket. EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The cloud name of the bucket. CloudName string `protobuf:"bytes,3,opt,name=cloud_name,json=cloudName,proto3" json:"cloud_name,omitempty"` // Optional key prefix to prepend to all bucket keys. // // Note: make sure it ends with a slash ("/") if you want // to group objects within a certain folder. KeyPrefix *string `protobuf:"bytes,4,opt,name=key_prefix,json=keyPrefix,proto3,oneof" json:"key_prefix,omitempty"` // Public base URL for accessing objects in this bucket. // Must be set for public buckets. PublicBaseUrl *string `protobuf:"bytes,5,opt,name=public_base_url,json=publicBaseUrl,proto3,oneof" json:"public_base_url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Bucket) Reset() { *x = Bucket{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Bucket) String() string { return protoimpl.X.MessageStringOf(x) } func (*Bucket) ProtoMessage() {} func (x *Bucket) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Bucket.ProtoReflect.Descriptor instead. func (*Bucket) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{18} } func (x *Bucket) GetRid() string { if x != nil { return x.Rid } return "" } func (x *Bucket) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *Bucket) GetCloudName() string { if x != nil { return x.CloudName } return "" } func (x *Bucket) GetKeyPrefix() string { if x != nil && x.KeyPrefix != nil { return *x.KeyPrefix } return "" } func (x *Bucket) GetPublicBaseUrl() string { if x != nil && x.PublicBaseUrl != nil { return *x.PublicBaseUrl } return "" } type Gateway struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique id for this resource. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // The encore name of the gateway. EncoreName string `protobuf:"bytes,2,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The base url for reaching this gateway, for returning to the application // via e.g. the metadata APIs. BaseUrl string `protobuf:"bytes,3,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` // The hostnames this gateway accepts requests for. Hostnames []string `protobuf:"bytes,4,rep,name=hostnames,proto3" json:"hostnames,omitempty"` // CORS is the CORS configuration for this gateway. Cors *Gateway_CORS `protobuf:"bytes,5,opt,name=cors,proto3" json:"cors,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Gateway) Reset() { *x = Gateway{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Gateway) String() string { return protoimpl.X.MessageStringOf(x) } func (*Gateway) ProtoMessage() {} func (x *Gateway) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Gateway.ProtoReflect.Descriptor instead. func (*Gateway) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19} } func (x *Gateway) GetRid() string { if x != nil { return x.Rid } return "" } func (x *Gateway) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *Gateway) GetBaseUrl() string { if x != nil { return x.BaseUrl } return "" } func (x *Gateway) GetHostnames() []string { if x != nil { return x.Hostnames } return nil } func (x *Gateway) GetCors() *Gateway_CORS { if x != nil { return x.Cors } return nil } type Infrastructure_Credentials struct { state protoimpl.MessageState `protogen:"open.v1"` ClientCerts []*ClientCert `protobuf:"bytes,1,rep,name=client_certs,json=clientCerts,proto3" json:"client_certs,omitempty"` SqlRoles []*SQLRole `protobuf:"bytes,2,rep,name=sql_roles,json=sqlRoles,proto3" json:"sql_roles,omitempty"` RedisRoles []*RedisRole `protobuf:"bytes,3,rep,name=redis_roles,json=redisRoles,proto3" json:"redis_roles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Infrastructure_Credentials) Reset() { *x = Infrastructure_Credentials{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Infrastructure_Credentials) String() string { return protoimpl.X.MessageStringOf(x) } func (*Infrastructure_Credentials) ProtoMessage() {} func (x *Infrastructure_Credentials) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Infrastructure_Credentials.ProtoReflect.Descriptor instead. func (*Infrastructure_Credentials) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0, 0} } func (x *Infrastructure_Credentials) GetClientCerts() []*ClientCert { if x != nil { return x.ClientCerts } return nil } func (x *Infrastructure_Credentials) GetSqlRoles() []*SQLRole { if x != nil { return x.SqlRoles } return nil } func (x *Infrastructure_Credentials) GetRedisRoles() []*RedisRole { if x != nil { return x.RedisRoles } return nil } type Infrastructure_Resources struct { state protoimpl.MessageState `protogen:"open.v1"` Gateways []*Gateway `protobuf:"bytes,1,rep,name=gateways,proto3" json:"gateways,omitempty"` SqlClusters []*SQLCluster `protobuf:"bytes,2,rep,name=sql_clusters,json=sqlClusters,proto3" json:"sql_clusters,omitempty"` PubsubClusters []*PubSubCluster `protobuf:"bytes,3,rep,name=pubsub_clusters,json=pubsubClusters,proto3" json:"pubsub_clusters,omitempty"` RedisClusters []*RedisCluster `protobuf:"bytes,4,rep,name=redis_clusters,json=redisClusters,proto3" json:"redis_clusters,omitempty"` AppSecrets []*AppSecret `protobuf:"bytes,5,rep,name=app_secrets,json=appSecrets,proto3" json:"app_secrets,omitempty"` BucketClusters []*BucketCluster `protobuf:"bytes,6,rep,name=bucket_clusters,json=bucketClusters,proto3" json:"bucket_clusters,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Infrastructure_Resources) Reset() { *x = Infrastructure_Resources{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Infrastructure_Resources) String() string { return protoimpl.X.MessageStringOf(x) } func (*Infrastructure_Resources) ProtoMessage() {} func (x *Infrastructure_Resources) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Infrastructure_Resources.ProtoReflect.Descriptor instead. func (*Infrastructure_Resources) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0, 1} } func (x *Infrastructure_Resources) GetGateways() []*Gateway { if x != nil { return x.Gateways } return nil } func (x *Infrastructure_Resources) GetSqlClusters() []*SQLCluster { if x != nil { return x.SqlClusters } return nil } func (x *Infrastructure_Resources) GetPubsubClusters() []*PubSubCluster { if x != nil { return x.PubsubClusters } return nil } func (x *Infrastructure_Resources) GetRedisClusters() []*RedisCluster { if x != nil { return x.RedisClusters } return nil } func (x *Infrastructure_Resources) GetAppSecrets() []*AppSecret { if x != nil { return x.AppSecrets } return nil } func (x *Infrastructure_Resources) GetBucketClusters() []*BucketCluster { if x != nil { return x.BucketClusters } return nil } type RedisRole_AuthACL struct { state protoimpl.MessageState `protogen:"open.v1"` Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` Password *SecretData `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RedisRole_AuthACL) Reset() { *x = RedisRole_AuthACL{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RedisRole_AuthACL) String() string { return protoimpl.X.MessageStringOf(x) } func (*RedisRole_AuthACL) ProtoMessage() {} func (x *RedisRole_AuthACL) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RedisRole_AuthACL.ProtoReflect.Descriptor instead. func (*RedisRole_AuthACL) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{11, 0} } func (x *RedisRole_AuthACL) GetUsername() string { if x != nil { return x.Username } return "" } func (x *RedisRole_AuthACL) GetPassword() *SecretData { if x != nil { return x.Password } return nil } type PubSubCluster_EncoreCloud struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster_EncoreCloud) Reset() { *x = PubSubCluster_EncoreCloud{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster_EncoreCloud) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster_EncoreCloud) ProtoMessage() {} func (x *PubSubCluster_EncoreCloud) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster_EncoreCloud.ProtoReflect.Descriptor instead. func (*PubSubCluster_EncoreCloud) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 0} } type PubSubCluster_AWSSqsSns struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster_AWSSqsSns) Reset() { *x = PubSubCluster_AWSSqsSns{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster_AWSSqsSns) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster_AWSSqsSns) ProtoMessage() {} func (x *PubSubCluster_AWSSqsSns) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster_AWSSqsSns.ProtoReflect.Descriptor instead. func (*PubSubCluster_AWSSqsSns) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 1} } type PubSubCluster_GCPPubSub struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster_GCPPubSub) Reset() { *x = PubSubCluster_GCPPubSub{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster_GCPPubSub) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster_GCPPubSub) ProtoMessage() {} func (x *PubSubCluster_GCPPubSub) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster_GCPPubSub.ProtoReflect.Descriptor instead. func (*PubSubCluster_GCPPubSub) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 2} } type PubSubCluster_NSQ struct { state protoimpl.MessageState `protogen:"open.v1"` // The hosts to connect to NSQ. Must be non-empty. Hosts []string `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster_NSQ) Reset() { *x = PubSubCluster_NSQ{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster_NSQ) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster_NSQ) ProtoMessage() {} func (x *PubSubCluster_NSQ) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster_NSQ.ProtoReflect.Descriptor instead. func (*PubSubCluster_NSQ) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 3} } func (x *PubSubCluster_NSQ) GetHosts() []string { if x != nil { return x.Hosts } return nil } type PubSubCluster_AzureServiceBus struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubCluster_AzureServiceBus) Reset() { *x = PubSubCluster_AzureServiceBus{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubCluster_AzureServiceBus) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubCluster_AzureServiceBus) ProtoMessage() {} func (x *PubSubCluster_AzureServiceBus) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubCluster_AzureServiceBus.ProtoReflect.Descriptor instead. func (*PubSubCluster_AzureServiceBus) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 4} } func (x *PubSubCluster_AzureServiceBus) GetNamespace() string { if x != nil { return x.Namespace } return "" } type PubSubTopic_GCPConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // The GCP project id where the topic exists. ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubTopic_GCPConfig) Reset() { *x = PubSubTopic_GCPConfig{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubTopic_GCPConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubTopic_GCPConfig) ProtoMessage() {} func (x *PubSubTopic_GCPConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubTopic_GCPConfig.ProtoReflect.Descriptor instead. func (*PubSubTopic_GCPConfig) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15, 0} } func (x *PubSubTopic_GCPConfig) GetProjectId() string { if x != nil { return x.ProjectId } return "" } type PubSubSubscription_GCPConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // The GCP project id where the subscription exists. ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` // The service account used to authenticate messages being delivered over push. // If unset, pushes are rejected. PushServiceAccount *string `protobuf:"bytes,2,opt,name=push_service_account,json=pushServiceAccount,proto3,oneof" json:"push_service_account,omitempty"` // The audience to use when validating JWTs delivered over push. // If set, the JWT audience claim must match. If unset, any JWT audience is allowed. PushJwtAudience *string `protobuf:"bytes,3,opt,name=push_jwt_audience,json=pushJwtAudience,proto3,oneof" json:"push_jwt_audience,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PubSubSubscription_GCPConfig) Reset() { *x = PubSubSubscription_GCPConfig{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *PubSubSubscription_GCPConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*PubSubSubscription_GCPConfig) ProtoMessage() {} func (x *PubSubSubscription_GCPConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use PubSubSubscription_GCPConfig.ProtoReflect.Descriptor instead. func (*PubSubSubscription_GCPConfig) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{16, 0} } func (x *PubSubSubscription_GCPConfig) GetProjectId() string { if x != nil { return x.ProjectId } return "" } func (x *PubSubSubscription_GCPConfig) GetPushServiceAccount() string { if x != nil && x.PushServiceAccount != nil { return *x.PushServiceAccount } return "" } func (x *PubSubSubscription_GCPConfig) GetPushJwtAudience() string { if x != nil && x.PushJwtAudience != nil { return *x.PushJwtAudience } return "" } type BucketCluster_S3 struct { state protoimpl.MessageState `protogen:"open.v1"` // Region to connect to. Region string `protobuf:"bytes,1,opt,name=region,proto3" json:"region,omitempty"` // Endpoint override, if any. Must be specified if using a non-standard AWS region. Endpoint *string `protobuf:"bytes,2,opt,name=endpoint,proto3,oneof" json:"endpoint,omitempty"` // Set these to use explicit credentials for this bucket, // as opposed to resolving using AWS's default credential chain. AccessKeyId *string `protobuf:"bytes,3,opt,name=access_key_id,json=accessKeyId,proto3,oneof" json:"access_key_id,omitempty"` SecretAccessKey *SecretData `protobuf:"bytes,4,opt,name=secret_access_key,json=secretAccessKey,proto3,oneof" json:"secret_access_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketCluster_S3) Reset() { *x = BucketCluster_S3{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketCluster_S3) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketCluster_S3) ProtoMessage() {} func (x *BucketCluster_S3) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketCluster_S3.ProtoReflect.Descriptor instead. func (*BucketCluster_S3) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 0} } func (x *BucketCluster_S3) GetRegion() string { if x != nil { return x.Region } return "" } func (x *BucketCluster_S3) GetEndpoint() string { if x != nil && x.Endpoint != nil { return *x.Endpoint } return "" } func (x *BucketCluster_S3) GetAccessKeyId() string { if x != nil && x.AccessKeyId != nil { return *x.AccessKeyId } return "" } func (x *BucketCluster_S3) GetSecretAccessKey() *SecretData { if x != nil { return x.SecretAccessKey } return nil } type BucketCluster_GCS struct { state protoimpl.MessageState `protogen:"open.v1"` // Endpoint override, if any. Defaults to https://storage.googleapis.com if unset. Endpoint *string `protobuf:"bytes,1,opt,name=endpoint,proto3,oneof" json:"endpoint,omitempty"` // Whether to connect anonymously or if a service account should be resolved. Anonymous bool `protobuf:"varint,2,opt,name=anonymous,proto3" json:"anonymous,omitempty"` // Additional options for signed URLs when running in local dev mode. // Only use with anonymous mode. LocalSign *BucketCluster_GCS_LocalSignOptions `protobuf:"bytes,3,opt,name=local_sign,json=localSign,proto3,oneof" json:"local_sign,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketCluster_GCS) Reset() { *x = BucketCluster_GCS{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketCluster_GCS) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketCluster_GCS) ProtoMessage() {} func (x *BucketCluster_GCS) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketCluster_GCS.ProtoReflect.Descriptor instead. func (*BucketCluster_GCS) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 1} } func (x *BucketCluster_GCS) GetEndpoint() string { if x != nil && x.Endpoint != nil { return *x.Endpoint } return "" } func (x *BucketCluster_GCS) GetAnonymous() bool { if x != nil { return x.Anonymous } return false } func (x *BucketCluster_GCS) GetLocalSign() *BucketCluster_GCS_LocalSignOptions { if x != nil { return x.LocalSign } return nil } type BucketCluster_GCS_LocalSignOptions struct { state protoimpl.MessageState `protogen:"open.v1"` // Base prefix to use for presigned URLs. BaseUrl string `protobuf:"bytes,1,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` // Use these credentials to sign local URLs. Only pass dummy credentials // here, no actual secrets. AccessId string `protobuf:"bytes,2,opt,name=access_id,json=accessId,proto3" json:"access_id,omitempty"` PrivateKey string `protobuf:"bytes,3,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BucketCluster_GCS_LocalSignOptions) Reset() { *x = BucketCluster_GCS_LocalSignOptions{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *BucketCluster_GCS_LocalSignOptions) String() string { return protoimpl.X.MessageStringOf(x) } func (*BucketCluster_GCS_LocalSignOptions) ProtoMessage() {} func (x *BucketCluster_GCS_LocalSignOptions) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BucketCluster_GCS_LocalSignOptions.ProtoReflect.Descriptor instead. func (*BucketCluster_GCS_LocalSignOptions) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 1, 0} } func (x *BucketCluster_GCS_LocalSignOptions) GetBaseUrl() string { if x != nil { return x.BaseUrl } return "" } func (x *BucketCluster_GCS_LocalSignOptions) GetAccessId() string { if x != nil { return x.AccessId } return "" } func (x *BucketCluster_GCS_LocalSignOptions) GetPrivateKey() string { if x != nil { return x.PrivateKey } return "" } // CORS describes the CORS configuration for a gateway. type Gateway_CORS struct { state protoimpl.MessageState `protogen:"open.v1"` Debug bool `protobuf:"varint,1,opt,name=debug,proto3" json:"debug,omitempty"` // If true, causes Encore to respond to OPTIONS requests // without setting Access-Control-Allow-Credentials: true. DisableCredentials bool `protobuf:"varint,2,opt,name=disable_credentials,json=disableCredentials,proto3" json:"disable_credentials,omitempty"` // Specifies the allowed origins for requests that include credentials. // If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // If disable_credentials is true this field is not used. // // Types that are valid to be assigned to AllowedOriginsWithCredentials: // // *Gateway_CORS_AllowedOrigins // *Gateway_CORS_UnsafeAllowAllOriginsWithCredentials AllowedOriginsWithCredentials isGateway_CORS_AllowedOriginsWithCredentials `protobuf_oneof:"allowed_origins_with_credentials"` // Specifies the allowed origins for requests // that don't include credentials. // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). AllowedOriginsWithoutCredentials *Gateway_CORSAllowedOrigins `protobuf:"bytes,5,opt,name=allowed_origins_without_credentials,json=allowedOriginsWithoutCredentials,proto3" json:"allowed_origins_without_credentials,omitempty"` // Specifies extra headers to allow, beyond // the default set always recognized by Encore. // As a special case, if the list contains "*" all headers are allowed. ExtraAllowedHeaders []string `protobuf:"bytes,6,rep,name=extra_allowed_headers,json=extraAllowedHeaders,proto3" json:"extra_allowed_headers,omitempty"` // Specifies extra headers to expose, beyond // the default set always recognized by Encore. // As a special case, if the list contains "*" all headers are allowed. ExtraExposedHeaders []string `protobuf:"bytes,7,rep,name=extra_exposed_headers,json=extraExposedHeaders,proto3" json:"extra_exposed_headers,omitempty"` // If true, allows requests to Encore apps running // on private networks from websites. // See: https://wicg.github.io/private-network-access/ AllowPrivateNetworkAccess bool `protobuf:"varint,8,opt,name=allow_private_network_access,json=allowPrivateNetworkAccess,proto3" json:"allow_private_network_access,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Gateway_CORS) Reset() { *x = Gateway_CORS{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Gateway_CORS) String() string { return protoimpl.X.MessageStringOf(x) } func (*Gateway_CORS) ProtoMessage() {} func (x *Gateway_CORS) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Gateway_CORS.ProtoReflect.Descriptor instead. func (*Gateway_CORS) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19, 0} } func (x *Gateway_CORS) GetDebug() bool { if x != nil { return x.Debug } return false } func (x *Gateway_CORS) GetDisableCredentials() bool { if x != nil { return x.DisableCredentials } return false } func (x *Gateway_CORS) GetAllowedOriginsWithCredentials() isGateway_CORS_AllowedOriginsWithCredentials { if x != nil { return x.AllowedOriginsWithCredentials } return nil } func (x *Gateway_CORS) GetAllowedOrigins() *Gateway_CORSAllowedOrigins { if x != nil { if x, ok := x.AllowedOriginsWithCredentials.(*Gateway_CORS_AllowedOrigins); ok { return x.AllowedOrigins } } return nil } func (x *Gateway_CORS) GetUnsafeAllowAllOriginsWithCredentials() bool { if x != nil { if x, ok := x.AllowedOriginsWithCredentials.(*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials); ok { return x.UnsafeAllowAllOriginsWithCredentials } } return false } func (x *Gateway_CORS) GetAllowedOriginsWithoutCredentials() *Gateway_CORSAllowedOrigins { if x != nil { return x.AllowedOriginsWithoutCredentials } return nil } func (x *Gateway_CORS) GetExtraAllowedHeaders() []string { if x != nil { return x.ExtraAllowedHeaders } return nil } func (x *Gateway_CORS) GetExtraExposedHeaders() []string { if x != nil { return x.ExtraExposedHeaders } return nil } func (x *Gateway_CORS) GetAllowPrivateNetworkAccess() bool { if x != nil { return x.AllowPrivateNetworkAccess } return false } type isGateway_CORS_AllowedOriginsWithCredentials interface { isGateway_CORS_AllowedOriginsWithCredentials() } type Gateway_CORS_AllowedOrigins struct { AllowedOrigins *Gateway_CORSAllowedOrigins `protobuf:"bytes,3,opt,name=allowed_origins,json=allowedOrigins,proto3,oneof"` } type Gateway_CORS_UnsafeAllowAllOriginsWithCredentials struct { UnsafeAllowAllOriginsWithCredentials bool `protobuf:"varint,4,opt,name=unsafe_allow_all_origins_with_credentials,json=unsafeAllowAllOriginsWithCredentials,proto3,oneof"` } func (*Gateway_CORS_AllowedOrigins) isGateway_CORS_AllowedOriginsWithCredentials() {} func (*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials) isGateway_CORS_AllowedOriginsWithCredentials() { } type Gateway_CORSAllowedOrigins struct { state protoimpl.MessageState `protogen:"open.v1"` // The list of allowed origins. // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). // // The string "*" allows all origins, except for requests with credentials; // use CORS.unsafe_allow_unsafe_all_origins_with_credentials for that. AllowedOrigins []string `protobuf:"bytes,1,rep,name=allowed_origins,json=allowedOrigins,proto3" json:"allowed_origins,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Gateway_CORSAllowedOrigins) Reset() { *x = Gateway_CORSAllowedOrigins{} mi := &file_encore_runtime_v1_infra_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Gateway_CORSAllowedOrigins) String() string { return protoimpl.X.MessageStringOf(x) } func (*Gateway_CORSAllowedOrigins) ProtoMessage() {} func (x *Gateway_CORSAllowedOrigins) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_infra_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Gateway_CORSAllowedOrigins.ProtoReflect.Descriptor instead. func (*Gateway_CORSAllowedOrigins) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19, 1} } func (x *Gateway_CORSAllowedOrigins) GetAllowedOrigins() []string { if x != nil { return x.AllowedOrigins } return nil } var File_encore_runtime_v1_infra_proto protoreflect.FileDescriptor const file_encore_runtime_v1_infra_proto_rawDesc = "" + "\n" + "\x1dencore/runtime/v1/infra.proto\x12\x11encore.runtime.v1\x1a\"encore/runtime/v1/secretdata.proto\"\x9b\x06\n" + "\x0eInfrastructure\x12I\n" + "\tresources\x18\x01 \x01(\v2+.encore.runtime.v1.Infrastructure.ResourcesR\tresources\x12O\n" + "\vcredentials\x18\x02 \x01(\v2-.encore.runtime.v1.Infrastructure.CredentialsR\vcredentials\x1a\xc7\x01\n" + "\vCredentials\x12@\n" + "\fclient_certs\x18\x01 \x03(\v2\x1d.encore.runtime.v1.ClientCertR\vclientCerts\x127\n" + "\tsql_roles\x18\x02 \x03(\v2\x1a.encore.runtime.v1.SQLRoleR\bsqlRoles\x12=\n" + "\vredis_roles\x18\x03 \x03(\v2\x1c.encore.runtime.v1.RedisRoleR\n" + "redisRoles\x1a\xa2\x03\n" + "\tResources\x126\n" + "\bgateways\x18\x01 \x03(\v2\x1a.encore.runtime.v1.GatewayR\bgateways\x12@\n" + "\fsql_clusters\x18\x02 \x03(\v2\x1d.encore.runtime.v1.SQLClusterR\vsqlClusters\x12I\n" + "\x0fpubsub_clusters\x18\x03 \x03(\v2 .encore.runtime.v1.PubSubClusterR\x0epubsubClusters\x12F\n" + "\x0eredis_clusters\x18\x04 \x03(\v2\x1f.encore.runtime.v1.RedisClusterR\rredisClusters\x12=\n" + "\vapp_secrets\x18\x05 \x03(\v2\x1c.encore.runtime.v1.AppSecretR\n" + "appSecrets\x12I\n" + "\x0fbucket_clusters\x18\x06 \x03(\v2 .encore.runtime.v1.BucketClusterR\x0ebucketClusters\"\x94\x01\n" + "\n" + "SQLCluster\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x126\n" + "\aservers\x18\x02 \x03(\v2\x1c.encore.runtime.v1.SQLServerR\aservers\x12<\n" + "\tdatabases\x18\x03 \x03(\v2\x1e.encore.runtime.v1.SQLDatabaseR\tdatabases\"\xc8\x01\n" + "\tTLSConfig\x12)\n" + "\x0eserver_ca_cert\x18\x01 \x01(\tH\x00R\fserverCaCert\x88\x01\x01\x12I\n" + "!disable_tls_hostname_verification\x18\x02 \x01(\bR\x1edisableTlsHostnameVerification\x122\n" + "\x15disable_ca_validation\x18\x03 \x01(\bR\x13disableCaValidationB\x11\n" + "\x0f_server_ca_cert\"\xb5\x01\n" + "\tSQLServer\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x12\n" + "\x04host\x18\x02 \x01(\tR\x04host\x121\n" + "\x04kind\x18\x03 \x01(\x0e2\x1d.encore.runtime.v1.ServerKindR\x04kind\x12@\n" + "\n" + "tls_config\x18\x04 \x01(\v2\x1c.encore.runtime.v1.TLSConfigH\x00R\ttlsConfig\x88\x01\x01B\r\n" + "\v_tls_config\"c\n" + "\n" + "ClientCert\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x12\n" + "\x04cert\x18\x02 \x01(\tR\x04cert\x12/\n" + "\x03key\x18\x03 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\x03key\"\xb3\x01\n" + "\aSQLRole\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1a\n" + "\busername\x18\x02 \x01(\tR\busername\x129\n" + "\bpassword\x18\x03 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\bpassword\x12+\n" + "\x0fclient_cert_rid\x18\x04 \x01(\tH\x00R\rclientCertRid\x88\x01\x01B\x12\n" + "\x10_client_cert_rid\"\xa4\x01\n" + "\vSQLDatabase\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x12\x1d\n" + "\n" + "cloud_name\x18\x03 \x01(\tR\tcloudName\x12C\n" + "\n" + "conn_pools\x18\x04 \x03(\v2$.encore.runtime.v1.SQLConnectionPoolR\tconnPools\"\xa1\x01\n" + "\x11SQLConnectionPool\x12\x1f\n" + "\vis_readonly\x18\x01 \x01(\bR\n" + "isReadonly\x12\x19\n" + "\brole_rid\x18\x02 \x01(\tR\aroleRid\x12'\n" + "\x0fmin_connections\x18\x03 \x01(\x05R\x0eminConnections\x12'\n" + "\x0fmax_connections\x18\x04 \x01(\x05R\x0emaxConnections\"\xb7\x01\n" + "\fRedisCluster\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x128\n" + "\aservers\x18\x02 \x03(\v2\x1e.encore.runtime.v1.RedisServerR\aservers\x12>\n" + "\tdatabases\x18\x03 \x03(\v2 .encore.runtime.v1.RedisDatabaseR\tdatabases\x12\x1b\n" + "\tin_memory\x18\x04 \x01(\bR\binMemory\"\xb7\x01\n" + "\vRedisServer\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x12\n" + "\x04host\x18\x02 \x01(\tR\x04host\x121\n" + "\x04kind\x18\x03 \x01(\x0e2\x1d.encore.runtime.v1.ServerKindR\x04kind\x12@\n" + "\n" + "tls_config\x18\x04 \x01(\v2\x1c.encore.runtime.v1.TLSConfigH\x00R\ttlsConfig\x88\x01\x01B\r\n" + "\v_tls_config\"\xa3\x01\n" + "\x13RedisConnectionPool\x12\x1f\n" + "\vis_readonly\x18\x01 \x01(\bR\n" + "isReadonly\x12\x19\n" + "\brole_rid\x18\x02 \x01(\tR\aroleRid\x12'\n" + "\x0fmin_connections\x18\x03 \x01(\x05R\x0eminConnections\x12'\n" + "\x0fmax_connections\x18\x04 \x01(\x05R\x0emaxConnections\"\xc4\x02\n" + "\tRedisRole\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12+\n" + "\x0fclient_cert_rid\x18\x02 \x01(\tH\x01R\rclientCertRid\x88\x01\x01\x128\n" + "\x03acl\x18\n" + " \x01(\v2$.encore.runtime.v1.RedisRole.AuthACLH\x00R\x03acl\x12@\n" + "\vauth_string\x18\v \x01(\v2\x1d.encore.runtime.v1.SecretDataH\x00R\n" + "authString\x1a`\n" + "\aAuthACL\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x129\n" + "\bpassword\x18\x02 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\bpasswordB\x06\n" + "\x04authB\x12\n" + "\x10_client_cert_rid\"\xdf\x01\n" + "\rRedisDatabase\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x12!\n" + "\fdatabase_idx\x18\x03 \x01(\x05R\vdatabaseIdx\x12\"\n" + "\n" + "key_prefix\x18\x04 \x01(\tH\x00R\tkeyPrefix\x88\x01\x01\x12E\n" + "\n" + "conn_pools\x18\x05 \x03(\v2&.encore.runtime.v1.RedisConnectionPoolR\tconnPoolsB\r\n" + "\v_key_prefix\"q\n" + "\tAppSecret\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x121\n" + "\x04data\x18\x03 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\x04data\"\xf5\x04\n" + "\rPubSubCluster\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x126\n" + "\x06topics\x18\x02 \x03(\v2\x1e.encore.runtime.v1.PubSubTopicR\x06topics\x12K\n" + "\rsubscriptions\x18\x03 \x03(\v2%.encore.runtime.v1.PubSubSubscriptionR\rsubscriptions\x12F\n" + "\x06encore\x18\x05 \x01(\v2,.encore.runtime.v1.PubSubCluster.EncoreCloudH\x00R\x06encore\x12>\n" + "\x03aws\x18\x06 \x01(\v2*.encore.runtime.v1.PubSubCluster.AWSSqsSnsH\x00R\x03aws\x12>\n" + "\x03gcp\x18\a \x01(\v2*.encore.runtime.v1.PubSubCluster.GCPPubSubH\x00R\x03gcp\x12H\n" + "\x05azure\x18\b \x01(\v20.encore.runtime.v1.PubSubCluster.AzureServiceBusH\x00R\x05azure\x128\n" + "\x03nsq\x18\t \x01(\v2$.encore.runtime.v1.PubSubCluster.NSQH\x00R\x03nsq\x1a\r\n" + "\vEncoreCloud\x1a\v\n" + "\tAWSSqsSns\x1a\v\n" + "\tGCPPubSub\x1a\x1b\n" + "\x03NSQ\x12\x14\n" + "\x05hosts\x18\x01 \x03(\tR\x05hosts\x1a/\n" + "\x0fAzureServiceBus\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespaceB\n" + "\n" + "\bprovider\"\x8b\x04\n" + "\vPubSubTopic\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x12\x1d\n" + "\n" + "cloud_name\x18\x03 \x01(\tR\tcloudName\x12_\n" + "\x12delivery_guarantee\x18\x04 \x01(\x0e20.encore.runtime.v1.PubSubTopic.DeliveryGuaranteeR\x11deliveryGuarantee\x12(\n" + "\rordering_attr\x18\x05 \x01(\tH\x01R\forderingAttr\x88\x01\x01\x12I\n" + "\n" + "gcp_config\x18\n" + " \x01(\v2(.encore.runtime.v1.PubSubTopic.GCPConfigH\x00R\tgcpConfig\x1a*\n" + "\tGCPConfig\x12\x1d\n" + "\n" + "project_id\x18\x01 \x01(\tR\tprojectId\"\x82\x01\n" + "\x11DeliveryGuarantee\x12\"\n" + "\x1eDELIVERY_GUARANTEE_UNSPECIFIED\x10\x00\x12$\n" + " DELIVERY_GUARANTEE_AT_LEAST_ONCE\x10\x01\x12#\n" + "\x1fDELIVERY_GUARANTEE_EXACTLY_ONCE\x10\x02B\x11\n" + "\x0fprovider_configB\x10\n" + "\x0e_ordering_attr\"\xb4\x04\n" + "\x12PubSubSubscription\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12*\n" + "\x11topic_encore_name\x18\x02 \x01(\tR\x0ftopicEncoreName\x128\n" + "\x18subscription_encore_name\x18\x03 \x01(\tR\x16subscriptionEncoreName\x12(\n" + "\x10topic_cloud_name\x18\x04 \x01(\tR\x0etopicCloudName\x126\n" + "\x17subscription_cloud_name\x18\x05 \x01(\tR\x15subscriptionCloudName\x12\x1b\n" + "\tpush_only\x18\x06 \x01(\bR\bpushOnly\x12P\n" + "\n" + "gcp_config\x18\n" + " \x01(\v2/.encore.runtime.v1.PubSubSubscription.GCPConfigH\x00R\tgcpConfig\x1a\xc1\x01\n" + "\tGCPConfig\x12\x1d\n" + "\n" + "project_id\x18\x01 \x01(\tR\tprojectId\x125\n" + "\x14push_service_account\x18\x02 \x01(\tH\x00R\x12pushServiceAccount\x88\x01\x01\x12/\n" + "\x11push_jwt_audience\x18\x03 \x01(\tH\x01R\x0fpushJwtAudience\x88\x01\x01B\x17\n" + "\x15_push_service_accountB\x14\n" + "\x12_push_jwt_audienceB\x11\n" + "\x0fprovider_config\"\xec\x05\n" + "\rBucketCluster\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x123\n" + "\abuckets\x18\x02 \x03(\v2\x19.encore.runtime.v1.BucketR\abuckets\x125\n" + "\x02s3\x18\n" + " \x01(\v2#.encore.runtime.v1.BucketCluster.S3H\x00R\x02s3\x128\n" + "\x03gcs\x18\v \x01(\v2$.encore.runtime.v1.BucketCluster.GCSH\x00R\x03gcs\x1a\xeb\x01\n" + "\x02S3\x12\x16\n" + "\x06region\x18\x01 \x01(\tR\x06region\x12\x1f\n" + "\bendpoint\x18\x02 \x01(\tH\x00R\bendpoint\x88\x01\x01\x12'\n" + "\raccess_key_id\x18\x03 \x01(\tH\x01R\vaccessKeyId\x88\x01\x01\x12N\n" + "\x11secret_access_key\x18\x04 \x01(\v2\x1d.encore.runtime.v1.SecretDataH\x02R\x0fsecretAccessKey\x88\x01\x01B\v\n" + "\t_endpointB\x10\n" + "\x0e_access_key_idB\x14\n" + "\x12_secret_access_key\x1a\xa8\x02\n" + "\x03GCS\x12\x1f\n" + "\bendpoint\x18\x01 \x01(\tH\x00R\bendpoint\x88\x01\x01\x12\x1c\n" + "\tanonymous\x18\x02 \x01(\bR\tanonymous\x12Y\n" + "\n" + "local_sign\x18\x03 \x01(\v25.encore.runtime.v1.BucketCluster.GCS.LocalSignOptionsH\x01R\tlocalSign\x88\x01\x01\x1ak\n" + "\x10LocalSignOptions\x12\x19\n" + "\bbase_url\x18\x01 \x01(\tR\abaseUrl\x12\x1b\n" + "\taccess_id\x18\x02 \x01(\tR\baccessId\x12\x1f\n" + "\vprivate_key\x18\x03 \x01(\tR\n" + "privateKeyB\v\n" + "\t_endpointB\r\n" + "\v_local_signB\n" + "\n" + "\bprovider\"\xce\x01\n" + "\x06Bucket\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x12\x1d\n" + "\n" + "cloud_name\x18\x03 \x01(\tR\tcloudName\x12\"\n" + "\n" + "key_prefix\x18\x04 \x01(\tH\x00R\tkeyPrefix\x88\x01\x01\x12+\n" + "\x0fpublic_base_url\x18\x05 \x01(\tH\x01R\rpublicBaseUrl\x88\x01\x01B\r\n" + "\v_key_prefixB\x12\n" + "\x10_public_base_url\"\xb9\x06\n" + "\aGateway\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1f\n" + "\vencore_name\x18\x02 \x01(\tR\n" + "encoreName\x12\x19\n" + "\bbase_url\x18\x03 \x01(\tR\abaseUrl\x12\x1c\n" + "\thostnames\x18\x04 \x03(\tR\thostnames\x123\n" + "\x04cors\x18\x05 \x01(\v2\x1f.encore.runtime.v1.Gateway.CORSR\x04cors\x1a\xcd\x04\n" + "\x04CORS\x12\x14\n" + "\x05debug\x18\x01 \x01(\bR\x05debug\x12/\n" + "\x13disable_credentials\x18\x02 \x01(\bR\x12disableCredentials\x12X\n" + "\x0fallowed_origins\x18\x03 \x01(\v2-.encore.runtime.v1.Gateway.CORSAllowedOriginsH\x00R\x0eallowedOrigins\x12Y\n" + ")unsafe_allow_all_origins_with_credentials\x18\x04 \x01(\bH\x00R$unsafeAllowAllOriginsWithCredentials\x12|\n" + "#allowed_origins_without_credentials\x18\x05 \x01(\v2-.encore.runtime.v1.Gateway.CORSAllowedOriginsR allowedOriginsWithoutCredentials\x122\n" + "\x15extra_allowed_headers\x18\x06 \x03(\tR\x13extraAllowedHeaders\x122\n" + "\x15extra_exposed_headers\x18\a \x03(\tR\x13extraExposedHeaders\x12?\n" + "\x1callow_private_network_access\x18\b \x01(\bR\x19allowPrivateNetworkAccessB\"\n" + " allowed_origins_with_credentials\x1a=\n" + "\x12CORSAllowedOrigins\x12'\n" + "\x0fallowed_origins\x18\x01 \x03(\tR\x0eallowedOrigins*}\n" + "\n" + "ServerKind\x12\x1b\n" + "\x17SERVER_KIND_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13SERVER_KIND_PRIMARY\x10\x01\x12\x1b\n" + "\x17SERVER_KIND_HOT_STANDBY\x10\x02\x12\x1c\n" + "\x18SERVER_KIND_READ_REPLICA\x10\x03B,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\x06proto3" var ( file_encore_runtime_v1_infra_proto_rawDescOnce sync.Once file_encore_runtime_v1_infra_proto_rawDescData []byte ) func file_encore_runtime_v1_infra_proto_rawDescGZIP() []byte { file_encore_runtime_v1_infra_proto_rawDescOnce.Do(func() { file_encore_runtime_v1_infra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_infra_proto_rawDesc), len(file_encore_runtime_v1_infra_proto_rawDesc))) }) return file_encore_runtime_v1_infra_proto_rawDescData } var file_encore_runtime_v1_infra_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_encore_runtime_v1_infra_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_encore_runtime_v1_infra_proto_goTypes = []any{ (ServerKind)(0), // 0: encore.runtime.v1.ServerKind (PubSubTopic_DeliveryGuarantee)(0), // 1: encore.runtime.v1.PubSubTopic.DeliveryGuarantee (*Infrastructure)(nil), // 2: encore.runtime.v1.Infrastructure (*SQLCluster)(nil), // 3: encore.runtime.v1.SQLCluster (*TLSConfig)(nil), // 4: encore.runtime.v1.TLSConfig (*SQLServer)(nil), // 5: encore.runtime.v1.SQLServer (*ClientCert)(nil), // 6: encore.runtime.v1.ClientCert (*SQLRole)(nil), // 7: encore.runtime.v1.SQLRole (*SQLDatabase)(nil), // 8: encore.runtime.v1.SQLDatabase (*SQLConnectionPool)(nil), // 9: encore.runtime.v1.SQLConnectionPool (*RedisCluster)(nil), // 10: encore.runtime.v1.RedisCluster (*RedisServer)(nil), // 11: encore.runtime.v1.RedisServer (*RedisConnectionPool)(nil), // 12: encore.runtime.v1.RedisConnectionPool (*RedisRole)(nil), // 13: encore.runtime.v1.RedisRole (*RedisDatabase)(nil), // 14: encore.runtime.v1.RedisDatabase (*AppSecret)(nil), // 15: encore.runtime.v1.AppSecret (*PubSubCluster)(nil), // 16: encore.runtime.v1.PubSubCluster (*PubSubTopic)(nil), // 17: encore.runtime.v1.PubSubTopic (*PubSubSubscription)(nil), // 18: encore.runtime.v1.PubSubSubscription (*BucketCluster)(nil), // 19: encore.runtime.v1.BucketCluster (*Bucket)(nil), // 20: encore.runtime.v1.Bucket (*Gateway)(nil), // 21: encore.runtime.v1.Gateway (*Infrastructure_Credentials)(nil), // 22: encore.runtime.v1.Infrastructure.Credentials (*Infrastructure_Resources)(nil), // 23: encore.runtime.v1.Infrastructure.Resources (*RedisRole_AuthACL)(nil), // 24: encore.runtime.v1.RedisRole.AuthACL (*PubSubCluster_EncoreCloud)(nil), // 25: encore.runtime.v1.PubSubCluster.EncoreCloud (*PubSubCluster_AWSSqsSns)(nil), // 26: encore.runtime.v1.PubSubCluster.AWSSqsSns (*PubSubCluster_GCPPubSub)(nil), // 27: encore.runtime.v1.PubSubCluster.GCPPubSub (*PubSubCluster_NSQ)(nil), // 28: encore.runtime.v1.PubSubCluster.NSQ (*PubSubCluster_AzureServiceBus)(nil), // 29: encore.runtime.v1.PubSubCluster.AzureServiceBus (*PubSubTopic_GCPConfig)(nil), // 30: encore.runtime.v1.PubSubTopic.GCPConfig (*PubSubSubscription_GCPConfig)(nil), // 31: encore.runtime.v1.PubSubSubscription.GCPConfig (*BucketCluster_S3)(nil), // 32: encore.runtime.v1.BucketCluster.S3 (*BucketCluster_GCS)(nil), // 33: encore.runtime.v1.BucketCluster.GCS (*BucketCluster_GCS_LocalSignOptions)(nil), // 34: encore.runtime.v1.BucketCluster.GCS.LocalSignOptions (*Gateway_CORS)(nil), // 35: encore.runtime.v1.Gateway.CORS (*Gateway_CORSAllowedOrigins)(nil), // 36: encore.runtime.v1.Gateway.CORSAllowedOrigins (*SecretData)(nil), // 37: encore.runtime.v1.SecretData } var file_encore_runtime_v1_infra_proto_depIdxs = []int32{ 23, // 0: encore.runtime.v1.Infrastructure.resources:type_name -> encore.runtime.v1.Infrastructure.Resources 22, // 1: encore.runtime.v1.Infrastructure.credentials:type_name -> encore.runtime.v1.Infrastructure.Credentials 5, // 2: encore.runtime.v1.SQLCluster.servers:type_name -> encore.runtime.v1.SQLServer 8, // 3: encore.runtime.v1.SQLCluster.databases:type_name -> encore.runtime.v1.SQLDatabase 0, // 4: encore.runtime.v1.SQLServer.kind:type_name -> encore.runtime.v1.ServerKind 4, // 5: encore.runtime.v1.SQLServer.tls_config:type_name -> encore.runtime.v1.TLSConfig 37, // 6: encore.runtime.v1.ClientCert.key:type_name -> encore.runtime.v1.SecretData 37, // 7: encore.runtime.v1.SQLRole.password:type_name -> encore.runtime.v1.SecretData 9, // 8: encore.runtime.v1.SQLDatabase.conn_pools:type_name -> encore.runtime.v1.SQLConnectionPool 11, // 9: encore.runtime.v1.RedisCluster.servers:type_name -> encore.runtime.v1.RedisServer 14, // 10: encore.runtime.v1.RedisCluster.databases:type_name -> encore.runtime.v1.RedisDatabase 0, // 11: encore.runtime.v1.RedisServer.kind:type_name -> encore.runtime.v1.ServerKind 4, // 12: encore.runtime.v1.RedisServer.tls_config:type_name -> encore.runtime.v1.TLSConfig 24, // 13: encore.runtime.v1.RedisRole.acl:type_name -> encore.runtime.v1.RedisRole.AuthACL 37, // 14: encore.runtime.v1.RedisRole.auth_string:type_name -> encore.runtime.v1.SecretData 12, // 15: encore.runtime.v1.RedisDatabase.conn_pools:type_name -> encore.runtime.v1.RedisConnectionPool 37, // 16: encore.runtime.v1.AppSecret.data:type_name -> encore.runtime.v1.SecretData 17, // 17: encore.runtime.v1.PubSubCluster.topics:type_name -> encore.runtime.v1.PubSubTopic 18, // 18: encore.runtime.v1.PubSubCluster.subscriptions:type_name -> encore.runtime.v1.PubSubSubscription 25, // 19: encore.runtime.v1.PubSubCluster.encore:type_name -> encore.runtime.v1.PubSubCluster.EncoreCloud 26, // 20: encore.runtime.v1.PubSubCluster.aws:type_name -> encore.runtime.v1.PubSubCluster.AWSSqsSns 27, // 21: encore.runtime.v1.PubSubCluster.gcp:type_name -> encore.runtime.v1.PubSubCluster.GCPPubSub 29, // 22: encore.runtime.v1.PubSubCluster.azure:type_name -> encore.runtime.v1.PubSubCluster.AzureServiceBus 28, // 23: encore.runtime.v1.PubSubCluster.nsq:type_name -> encore.runtime.v1.PubSubCluster.NSQ 1, // 24: encore.runtime.v1.PubSubTopic.delivery_guarantee:type_name -> encore.runtime.v1.PubSubTopic.DeliveryGuarantee 30, // 25: encore.runtime.v1.PubSubTopic.gcp_config:type_name -> encore.runtime.v1.PubSubTopic.GCPConfig 31, // 26: encore.runtime.v1.PubSubSubscription.gcp_config:type_name -> encore.runtime.v1.PubSubSubscription.GCPConfig 20, // 27: encore.runtime.v1.BucketCluster.buckets:type_name -> encore.runtime.v1.Bucket 32, // 28: encore.runtime.v1.BucketCluster.s3:type_name -> encore.runtime.v1.BucketCluster.S3 33, // 29: encore.runtime.v1.BucketCluster.gcs:type_name -> encore.runtime.v1.BucketCluster.GCS 35, // 30: encore.runtime.v1.Gateway.cors:type_name -> encore.runtime.v1.Gateway.CORS 6, // 31: encore.runtime.v1.Infrastructure.Credentials.client_certs:type_name -> encore.runtime.v1.ClientCert 7, // 32: encore.runtime.v1.Infrastructure.Credentials.sql_roles:type_name -> encore.runtime.v1.SQLRole 13, // 33: encore.runtime.v1.Infrastructure.Credentials.redis_roles:type_name -> encore.runtime.v1.RedisRole 21, // 34: encore.runtime.v1.Infrastructure.Resources.gateways:type_name -> encore.runtime.v1.Gateway 3, // 35: encore.runtime.v1.Infrastructure.Resources.sql_clusters:type_name -> encore.runtime.v1.SQLCluster 16, // 36: encore.runtime.v1.Infrastructure.Resources.pubsub_clusters:type_name -> encore.runtime.v1.PubSubCluster 10, // 37: encore.runtime.v1.Infrastructure.Resources.redis_clusters:type_name -> encore.runtime.v1.RedisCluster 15, // 38: encore.runtime.v1.Infrastructure.Resources.app_secrets:type_name -> encore.runtime.v1.AppSecret 19, // 39: encore.runtime.v1.Infrastructure.Resources.bucket_clusters:type_name -> encore.runtime.v1.BucketCluster 37, // 40: encore.runtime.v1.RedisRole.AuthACL.password:type_name -> encore.runtime.v1.SecretData 37, // 41: encore.runtime.v1.BucketCluster.S3.secret_access_key:type_name -> encore.runtime.v1.SecretData 34, // 42: encore.runtime.v1.BucketCluster.GCS.local_sign:type_name -> encore.runtime.v1.BucketCluster.GCS.LocalSignOptions 36, // 43: encore.runtime.v1.Gateway.CORS.allowed_origins:type_name -> encore.runtime.v1.Gateway.CORSAllowedOrigins 36, // 44: encore.runtime.v1.Gateway.CORS.allowed_origins_without_credentials:type_name -> encore.runtime.v1.Gateway.CORSAllowedOrigins 45, // [45:45] is the sub-list for method output_type 45, // [45:45] is the sub-list for method input_type 45, // [45:45] is the sub-list for extension type_name 45, // [45:45] is the sub-list for extension extendee 0, // [0:45] is the sub-list for field type_name } func init() { file_encore_runtime_v1_infra_proto_init() } func file_encore_runtime_v1_infra_proto_init() { if File_encore_runtime_v1_infra_proto != nil { return } file_encore_runtime_v1_secretdata_proto_init() file_encore_runtime_v1_infra_proto_msgTypes[2].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[3].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[5].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[9].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[11].OneofWrappers = []any{ (*RedisRole_Acl)(nil), (*RedisRole_AuthString)(nil), } file_encore_runtime_v1_infra_proto_msgTypes[12].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[14].OneofWrappers = []any{ (*PubSubCluster_Encore)(nil), (*PubSubCluster_Aws)(nil), (*PubSubCluster_Gcp)(nil), (*PubSubCluster_Azure)(nil), (*PubSubCluster_Nsq)(nil), } file_encore_runtime_v1_infra_proto_msgTypes[15].OneofWrappers = []any{ (*PubSubTopic_GcpConfig)(nil), } file_encore_runtime_v1_infra_proto_msgTypes[16].OneofWrappers = []any{ (*PubSubSubscription_GcpConfig)(nil), } file_encore_runtime_v1_infra_proto_msgTypes[17].OneofWrappers = []any{ (*BucketCluster_S3_)(nil), (*BucketCluster_Gcs)(nil), } file_encore_runtime_v1_infra_proto_msgTypes[18].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[29].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[30].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[31].OneofWrappers = []any{} file_encore_runtime_v1_infra_proto_msgTypes[33].OneofWrappers = []any{ (*Gateway_CORS_AllowedOrigins)(nil), (*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_infra_proto_rawDesc), len(file_encore_runtime_v1_infra_proto_rawDesc)), NumEnums: 2, NumMessages: 35, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_runtime_v1_infra_proto_goTypes, DependencyIndexes: file_encore_runtime_v1_infra_proto_depIdxs, EnumInfos: file_encore_runtime_v1_infra_proto_enumTypes, MessageInfos: file_encore_runtime_v1_infra_proto_msgTypes, }.Build() File_encore_runtime_v1_infra_proto = out.File file_encore_runtime_v1_infra_proto_goTypes = nil file_encore_runtime_v1_infra_proto_depIdxs = nil } ================================================ FILE: proto/encore/runtime/v1/infra.proto ================================================ syntax = "proto3"; package encore.runtime.v1; import "encore/runtime/v1/secretdata.proto"; option go_package = "encr.dev/proto/encore/runtime/v1;runtimev1"; message Infrastructure { Resources resources = 1; Credentials credentials = 2; message Credentials { repeated ClientCert client_certs = 1; repeated SQLRole sql_roles = 2; repeated RedisRole redis_roles = 3; } message Resources { repeated Gateway gateways = 1; repeated SQLCluster sql_clusters = 2; repeated PubSubCluster pubsub_clusters = 3; repeated RedisCluster redis_clusters = 4; repeated AppSecret app_secrets = 5; repeated BucketCluster bucket_clusters = 6; } } message SQLCluster { // The unique resource id for this cluster. string rid = 1; repeated SQLServer servers = 2; repeated SQLDatabase databases = 3; } enum ServerKind { SERVER_KIND_UNSPECIFIED = 0; SERVER_KIND_PRIMARY = 1; // A hot-standby (a read replica designed to take over write traffic // at a moment's notice). SERVER_KIND_HOT_STANDBY = 2; // A read-replica. SERVER_KIND_READ_REPLICA = 3; } message TLSConfig { // Server CA Cert PEM to use for verifying the server's certificate. optional string server_ca_cert = 1; // If true, skips hostname verification when connecting. // If invalid hostnames are trusted, *any* valid certificate for *any* site will be trusted for use. // This introduces significant vulnerabilities, and should only be used as a last resort. bool disable_tls_hostname_verification = 2; // If true, skips CA cert validation when connecting. // This introduces significant vulnerabilities, and should only be used as a last resort. bool disable_ca_validation = 3; } message SQLServer { // The unique resource id for this server. string rid = 1; // Host is the host to connect to. // Valid formats are "hostname", "hostname:port", and "/path/to/unix.socket". string host = 2; ServerKind kind = 3; // TLS configuration to use when connecting. optional TLSConfig tls_config = 4; } message ClientCert { // The unique resource id for this certificate. string rid = 1; string cert = 2; SecretData key = 3; } message SQLRole { // The unique resource id for this role. string rid = 1; string username = 2; SecretData password = 3; // The client cert to use to authenticate, if any. optional string client_cert_rid = 4; } message SQLDatabase { // The unique resource id for this database. string rid = 1; string encore_name = 2; // The physical name of the database in the cluster. string cloud_name = 3; // Connection pools to use for connecting to the database. repeated SQLConnectionPool conn_pools = 4; } message SQLConnectionPool { // Whether this connection pool is for read-only servers. bool is_readonly = 1; // The role to use to authenticate. string role_rid = 2; // The minimum and maximum number of connections to use. int32 min_connections = 3; int32 max_connections = 4; } message RedisCluster { // The unique resource id for this cluster. string rid = 1; repeated RedisServer servers = 2; repeated RedisDatabase databases = 3; // If true, the runtime will use an in-memory Redis implementation // instead of connecting to the configured servers. bool in_memory = 4; } message RedisServer { // The unique resource id for this server. string rid = 1; // Host is the host to connect to. // Valid formats are "hostname", "hostname:port", and "/path/to/unix.socket". string host = 2; ServerKind kind = 3; // TLS configuration to use when connecting. // If nil, TLS is not used. optional TLSConfig tls_config = 4; } message RedisConnectionPool { // Whether this connection pool is for read-only servers. bool is_readonly = 1; // The role to use to authenticate. string role_rid = 2; // The minimum and maximum number of connections to use. int32 min_connections = 3; int32 max_connections = 4; } message RedisRole { // The unique resource id for this role. string rid = 1; // The client cert to use to authenticate, if any. optional string client_cert_rid = 2; // How to authenticate with Redis. // If unset, no authentication is used. oneof auth { AuthACL acl = 10; // Redis ACL SecretData auth_string = 11; // Redis AUTH string } message AuthACL { string username = 1; SecretData password = 2; } } message RedisDatabase { // Unique resource id for this database. string rid = 1; // The encore name of the database. string encore_name = 2; // The database index to use, [0-15]. int32 database_idx = 3; // KeyPrefix specifies a prefix to add to all cache keys // for this database. It exists to enable multiple cache clusters // to use the same physical Redis database for local development // without having to coordinate and persist database index ids. optional string key_prefix = 4; // Connection pools to use for connecting to the database. repeated RedisConnectionPool conn_pools = 5; } message AppSecret { // The unique resource id for this secret. string rid = 1; // The encore name of the secret. string encore_name = 2; // The secret data. SecretData data = 3; } message PubSubCluster { // The unique resource id for this cluster. string rid = 1; repeated PubSubTopic topics = 2; repeated PubSubSubscription subscriptions = 3; oneof provider { EncoreCloud encore = 5; AWSSqsSns aws = 6; GCPPubSub gcp = 7; AzureServiceBus azure = 8; NSQ nsq = 9; } message EncoreCloud {} message AWSSqsSns {} message GCPPubSub {} message NSQ { // The hosts to connect to NSQ. Must be non-empty. repeated string hosts = 1; } message AzureServiceBus { string namespace = 1; } } message PubSubTopic { // The unique resource id for this topic. string rid = 1; // The encore name of the topic. string encore_name = 2; // The cloud name of the topic. string cloud_name = 3; // The delivery guarantee. DeliveryGuarantee delivery_guarantee = 4; // Optional ordering attribute. Specifies the attribute name // to use for message ordering. optional string ordering_attr = 5; // Provider-specific configuration. // Not all providers require this, but it must always be set // for the providers that are present. oneof provider_config { GCPConfig gcp_config = 10; // Null: no provider-specific configuration. } message GCPConfig { // The GCP project id where the topic exists. string project_id = 1; } enum DeliveryGuarantee { DELIVERY_GUARANTEE_UNSPECIFIED = 0; DELIVERY_GUARANTEE_AT_LEAST_ONCE = 1; // All messages will be delivered to each subscription at least once DELIVERY_GUARANTEE_EXACTLY_ONCE = 2; // All messages will be delivered to each subscription exactly once } } message PubSubSubscription { // The unique resource id for this subscription. string rid = 1; // The encore name of the topic this subscription is for. string topic_encore_name = 2; // The encore name of the subscription. string subscription_encore_name = 3; // The cloud name of the subscription. string topic_cloud_name = 4; // The cloud name of the subscription. string subscription_cloud_name = 5; // If true the application will not actively subscribe but wait // for incoming messages to be pushed to it. bool push_only = 6; // Subscription-specific provider configuration. // Not all providers require this, but it must always be set // for the providers that are present. oneof provider_config { GCPConfig gcp_config = 10; // Null: no provider-specific configuration. } message GCPConfig { // The GCP project id where the subscription exists. string project_id = 1; // The service account used to authenticate messages being delivered over push. // If unset, pushes are rejected. optional string push_service_account = 2; // The audience to use when validating JWTs delivered over push. // If set, the JWT audience claim must match. If unset, any JWT audience is allowed. optional string push_jwt_audience = 3; } } message BucketCluster { // The unique resource id for this cluster. string rid = 1; repeated Bucket buckets = 2; oneof provider { S3 s3 = 10; GCS gcs = 11; } message S3 { // Region to connect to. string region = 1; // Endpoint override, if any. Must be specified if using a non-standard AWS region. optional string endpoint = 2; // Set these to use explicit credentials for this bucket, // as opposed to resolving using AWS's default credential chain. optional string access_key_id = 3; optional SecretData secret_access_key = 4; } message GCS { // Endpoint override, if any. Defaults to https://storage.googleapis.com if unset. optional string endpoint = 1; // Whether to connect anonymously or if a service account should be resolved. bool anonymous = 2; // Additional options for signed URLs when running in local dev mode. // Only use with anonymous mode. optional LocalSignOptions local_sign = 3; message LocalSignOptions { // Base prefix to use for presigned URLs. string base_url = 1; // Use these credentials to sign local URLs. Only pass dummy credentials // here, no actual secrets. string access_id = 2; string private_key = 3; } } } message Bucket { // The unique resource id for this bucket. string rid = 1; // The encore name of the bucket. string encore_name = 2; // The cloud name of the bucket. string cloud_name = 3; // Optional key prefix to prepend to all bucket keys. // // Note: make sure it ends with a slash ("/") if you want // to group objects within a certain folder. optional string key_prefix = 4; // Public base URL for accessing objects in this bucket. // Must be set for public buckets. optional string public_base_url = 5; } message Gateway { // The unique id for this resource. string rid = 1; // The encore name of the gateway. string encore_name = 2; // The base url for reaching this gateway, for returning to the application // via e.g. the metadata APIs. string base_url = 3; // The hostnames this gateway accepts requests for. repeated string hostnames = 4; // CORS is the CORS configuration for this gateway. CORS cors = 5; // CORS describes the CORS configuration for a gateway. message CORS { bool debug = 1; // If true, causes Encore to respond to OPTIONS requests // without setting Access-Control-Allow-Credentials: true. bool disable_credentials = 2; // Specifies the allowed origins for requests that include credentials. // If a request is made from an Origin in this list // Encore responds with Access-Control-Allow-Origin: . // // If disable_credentials is true this field is not used. oneof allowed_origins_with_credentials { CORSAllowedOrigins allowed_origins = 3; bool unsafe_allow_all_origins_with_credentials = 4; } // Specifies the allowed origins for requests // that don't include credentials. // // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). CORSAllowedOrigins allowed_origins_without_credentials = 5; // Specifies extra headers to allow, beyond // the default set always recognized by Encore. // As a special case, if the list contains "*" all headers are allowed. repeated string extra_allowed_headers = 6; // Specifies extra headers to expose, beyond // the default set always recognized by Encore. // As a special case, if the list contains "*" all headers are allowed. repeated string extra_exposed_headers = 7; // If true, allows requests to Encore apps running // on private networks from websites. // See: https://wicg.github.io/private-network-access/ bool allow_private_network_access = 8; } message CORSAllowedOrigins { // The list of allowed origins. // The URLs in this list may include wildcards (e.g. "https://*.example.com" // or "https://*-myapp.example.com"). // // The string "*" allows all origins, except for requests with credentials; // use CORS.unsafe_allow_unsafe_all_origins_with_credentials for that. repeated string allowed_origins = 1; } } ================================================ FILE: proto/encore/runtime/v1/runtime.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/runtime/v1/runtime.proto package runtimev1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type Environment_Type int32 const ( Environment_TYPE_UNSPECIFIED Environment_Type = 0 Environment_TYPE_DEVELOPMENT Environment_Type = 1 Environment_TYPE_PRODUCTION Environment_Type = 2 Environment_TYPE_EPHEMERAL Environment_Type = 3 Environment_TYPE_TEST Environment_Type = 4 ) // Enum value maps for Environment_Type. var ( Environment_Type_name = map[int32]string{ 0: "TYPE_UNSPECIFIED", 1: "TYPE_DEVELOPMENT", 2: "TYPE_PRODUCTION", 3: "TYPE_EPHEMERAL", 4: "TYPE_TEST", } Environment_Type_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, "TYPE_DEVELOPMENT": 1, "TYPE_PRODUCTION": 2, "TYPE_EPHEMERAL": 3, "TYPE_TEST": 4, } ) func (x Environment_Type) Enum() *Environment_Type { p := new(Environment_Type) *p = x return p } func (x Environment_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Environment_Type) Descriptor() protoreflect.EnumDescriptor { return file_encore_runtime_v1_runtime_proto_enumTypes[0].Descriptor() } func (Environment_Type) Type() protoreflect.EnumType { return &file_encore_runtime_v1_runtime_proto_enumTypes[0] } func (x Environment_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Environment_Type.Descriptor instead. func (Environment_Type) EnumDescriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1, 0} } type Environment_Cloud int32 const ( Environment_CLOUD_UNSPECIFIED Environment_Cloud = 0 Environment_CLOUD_LOCAL Environment_Cloud = 1 Environment_CLOUD_ENCORE Environment_Cloud = 2 Environment_CLOUD_AWS Environment_Cloud = 3 Environment_CLOUD_GCP Environment_Cloud = 4 Environment_CLOUD_AZURE Environment_Cloud = 5 ) // Enum value maps for Environment_Cloud. var ( Environment_Cloud_name = map[int32]string{ 0: "CLOUD_UNSPECIFIED", 1: "CLOUD_LOCAL", 2: "CLOUD_ENCORE", 3: "CLOUD_AWS", 4: "CLOUD_GCP", 5: "CLOUD_AZURE", } Environment_Cloud_value = map[string]int32{ "CLOUD_UNSPECIFIED": 0, "CLOUD_LOCAL": 1, "CLOUD_ENCORE": 2, "CLOUD_AWS": 3, "CLOUD_GCP": 4, "CLOUD_AZURE": 5, } ) func (x Environment_Cloud) Enum() *Environment_Cloud { p := new(Environment_Cloud) *p = x return p } func (x Environment_Cloud) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Environment_Cloud) Descriptor() protoreflect.EnumDescriptor { return file_encore_runtime_v1_runtime_proto_enumTypes[1].Descriptor() } func (Environment_Cloud) Type() protoreflect.EnumType { return &file_encore_runtime_v1_runtime_proto_enumTypes[1] } func (x Environment_Cloud) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Environment_Cloud.Descriptor instead. func (Environment_Cloud) EnumDescriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1, 1} } type RuntimeConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Environment *Environment `protobuf:"bytes,1,opt,name=environment,proto3" json:"environment,omitempty"` Infra *Infrastructure `protobuf:"bytes,2,opt,name=infra,proto3" json:"infra,omitempty"` Deployment *Deployment `protobuf:"bytes,3,opt,name=deployment,proto3" json:"deployment,omitempty"` EncorePlatform *EncorePlatform `protobuf:"bytes,5,opt,name=encore_platform,json=encorePlatform,proto3,oneof" json:"encore_platform,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RuntimeConfig) Reset() { *x = RuntimeConfig{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RuntimeConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*RuntimeConfig) ProtoMessage() {} func (x *RuntimeConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RuntimeConfig.ProtoReflect.Descriptor instead. func (*RuntimeConfig) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{0} } func (x *RuntimeConfig) GetEnvironment() *Environment { if x != nil { return x.Environment } return nil } func (x *RuntimeConfig) GetInfra() *Infrastructure { if x != nil { return x.Infra } return nil } func (x *RuntimeConfig) GetDeployment() *Deployment { if x != nil { return x.Deployment } return nil } func (x *RuntimeConfig) GetEncorePlatform() *EncorePlatform { if x != nil { return x.EncorePlatform } return nil } type Environment struct { state protoimpl.MessageState `protogen:"open.v1"` AppId string `protobuf:"bytes,1,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` AppSlug string `protobuf:"bytes,2,opt,name=app_slug,json=appSlug,proto3" json:"app_slug,omitempty"` EnvId string `protobuf:"bytes,3,opt,name=env_id,json=envId,proto3" json:"env_id,omitempty"` EnvName string `protobuf:"bytes,4,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` EnvType Environment_Type `protobuf:"varint,5,opt,name=env_type,json=envType,proto3,enum=encore.runtime.v1.Environment_Type" json:"env_type,omitempty"` Cloud Environment_Cloud `protobuf:"varint,6,opt,name=cloud,proto3,enum=encore.runtime.v1.Environment_Cloud" json:"cloud,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Environment) Reset() { *x = Environment{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Environment) String() string { return protoimpl.X.MessageStringOf(x) } func (*Environment) ProtoMessage() {} func (x *Environment) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Environment.ProtoReflect.Descriptor instead. func (*Environment) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1} } func (x *Environment) GetAppId() string { if x != nil { return x.AppId } return "" } func (x *Environment) GetAppSlug() string { if x != nil { return x.AppSlug } return "" } func (x *Environment) GetEnvId() string { if x != nil { return x.EnvId } return "" } func (x *Environment) GetEnvName() string { if x != nil { return x.EnvName } return "" } func (x *Environment) GetEnvType() Environment_Type { if x != nil { return x.EnvType } return Environment_TYPE_UNSPECIFIED } func (x *Environment) GetCloud() Environment_Cloud { if x != nil { return x.Cloud } return Environment_CLOUD_UNSPECIFIED } // Describes the configuration related to a specific deployment, // meaning a group of services deployed together (think a single k8s Deployment). type Deployment struct { state protoimpl.MessageState `protogen:"open.v1"` DeployId string `protobuf:"bytes,1,opt,name=deploy_id,json=deployId,proto3" json:"deploy_id,omitempty"` DeployedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=deployed_at,json=deployedAt,proto3" json:"deployed_at,omitempty"` // A list of experiments to enable at runtime. DynamicExperiments []string `protobuf:"bytes,3,rep,name=dynamic_experiments,json=dynamicExperiments,proto3" json:"dynamic_experiments,omitempty"` // The gateways hosted by this deployment, by rid. HostedGateways []string `protobuf:"bytes,4,rep,name=hosted_gateways,json=hostedGateways,proto3" json:"hosted_gateways,omitempty"` // The services hosted by this deployment. HostedServices []*HostedService `protobuf:"bytes,5,rep,name=hosted_services,json=hostedServices,proto3" json:"hosted_services,omitempty"` // The authentication method(s) that can be used when talking // to this deployment for internal service-to-service calls. // // An empty list means no service-to-service calls can be made to this deployment. AuthMethods []*ServiceAuth `protobuf:"bytes,6,rep,name=auth_methods,json=authMethods,proto3" json:"auth_methods,omitempty"` // Observability-related configuration. Observability *Observability `protobuf:"bytes,7,opt,name=observability,proto3" json:"observability,omitempty"` // Service discovery configuration. ServiceDiscovery *ServiceDiscovery `protobuf:"bytes,8,opt,name=service_discovery,json=serviceDiscovery,proto3" json:"service_discovery,omitempty"` // Graceful shutdown behavior. GracefulShutdown *GracefulShutdown `protobuf:"bytes,9,opt,name=graceful_shutdown,json=gracefulShutdown,proto3" json:"graceful_shutdown,omitempty"` // The metrics used by this deployment. Metrics []*Metric `protobuf:"bytes,10,rep,name=metrics,proto3" json:"metrics,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Deployment) Reset() { *x = Deployment{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Deployment) String() string { return protoimpl.X.MessageStringOf(x) } func (*Deployment) ProtoMessage() {} func (x *Deployment) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Deployment.ProtoReflect.Descriptor instead. func (*Deployment) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{2} } func (x *Deployment) GetDeployId() string { if x != nil { return x.DeployId } return "" } func (x *Deployment) GetDeployedAt() *timestamppb.Timestamp { if x != nil { return x.DeployedAt } return nil } func (x *Deployment) GetDynamicExperiments() []string { if x != nil { return x.DynamicExperiments } return nil } func (x *Deployment) GetHostedGateways() []string { if x != nil { return x.HostedGateways } return nil } func (x *Deployment) GetHostedServices() []*HostedService { if x != nil { return x.HostedServices } return nil } func (x *Deployment) GetAuthMethods() []*ServiceAuth { if x != nil { return x.AuthMethods } return nil } func (x *Deployment) GetObservability() *Observability { if x != nil { return x.Observability } return nil } func (x *Deployment) GetServiceDiscovery() *ServiceDiscovery { if x != nil { return x.ServiceDiscovery } return nil } func (x *Deployment) GetGracefulShutdown() *GracefulShutdown { if x != nil { return x.GracefulShutdown } return nil } func (x *Deployment) GetMetrics() []*Metric { if x != nil { return x.Metrics } return nil } type Observability struct { state protoimpl.MessageState `protogen:"open.v1"` // The observability providers to use. Tracing []*TracingProvider `protobuf:"bytes,1,rep,name=tracing,proto3" json:"tracing,omitempty"` Metrics []*MetricsProvider `protobuf:"bytes,2,rep,name=metrics,proto3" json:"metrics,omitempty"` Logs []*LogsProvider `protobuf:"bytes,3,rep,name=logs,proto3" json:"logs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Observability) Reset() { *x = Observability{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Observability) String() string { return protoimpl.X.MessageStringOf(x) } func (*Observability) ProtoMessage() {} func (x *Observability) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Observability.ProtoReflect.Descriptor instead. func (*Observability) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{3} } func (x *Observability) GetTracing() []*TracingProvider { if x != nil { return x.Tracing } return nil } func (x *Observability) GetMetrics() []*MetricsProvider { if x != nil { return x.Metrics } return nil } func (x *Observability) GetLogs() []*LogsProvider { if x != nil { return x.Logs } return nil } type HostedService struct { state protoimpl.MessageState `protogen:"open.v1"` // The name of the service. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Number of worker threads to use. // If unset it defaults to 1. If set to 0 the runtime // automatically determines the number of threads to use // based on the number of CPUs available. WorkerThreads *int32 `protobuf:"varint,2,opt,name=worker_threads,json=workerThreads,proto3,oneof" json:"worker_threads,omitempty"` // The log configuration to use for this service. // If unset it defaults to "trace". LogConfig *string `protobuf:"bytes,3,opt,name=log_config,json=logConfig,proto3,oneof" json:"log_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *HostedService) Reset() { *x = HostedService{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *HostedService) String() string { return protoimpl.X.MessageStringOf(x) } func (*HostedService) ProtoMessage() {} func (x *HostedService) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HostedService.ProtoReflect.Descriptor instead. func (*HostedService) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{4} } func (x *HostedService) GetName() string { if x != nil { return x.Name } return "" } func (x *HostedService) GetWorkerThreads() int32 { if x != nil && x.WorkerThreads != nil { return *x.WorkerThreads } return 0 } func (x *HostedService) GetLogConfig() string { if x != nil && x.LogConfig != nil { return *x.LogConfig } return "" } type ServiceAuth struct { state protoimpl.MessageState `protogen:"open.v1"` // The auth method to use. // // Types that are valid to be assigned to AuthMethod: // // *ServiceAuth_Noop // *ServiceAuth_EncoreAuth_ AuthMethod isServiceAuth_AuthMethod `protobuf_oneof:"auth_method"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceAuth) Reset() { *x = ServiceAuth{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceAuth) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceAuth) ProtoMessage() {} func (x *ServiceAuth) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceAuth.ProtoReflect.Descriptor instead. func (*ServiceAuth) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5} } func (x *ServiceAuth) GetAuthMethod() isServiceAuth_AuthMethod { if x != nil { return x.AuthMethod } return nil } func (x *ServiceAuth) GetNoop() *ServiceAuth_NoopAuth { if x != nil { if x, ok := x.AuthMethod.(*ServiceAuth_Noop); ok { return x.Noop } } return nil } func (x *ServiceAuth) GetEncoreAuth() *ServiceAuth_EncoreAuth { if x != nil { if x, ok := x.AuthMethod.(*ServiceAuth_EncoreAuth_); ok { return x.EncoreAuth } } return nil } type isServiceAuth_AuthMethod interface { isServiceAuth_AuthMethod() } type ServiceAuth_Noop struct { // Messages start at 10 to allow for other fields on ServiceAuth. Noop *ServiceAuth_NoopAuth `protobuf:"bytes,10,opt,name=noop,proto3,oneof"` } type ServiceAuth_EncoreAuth_ struct { EncoreAuth *ServiceAuth_EncoreAuth `protobuf:"bytes,11,opt,name=encore_auth,json=encoreAuth,proto3,oneof"` } func (*ServiceAuth_Noop) isServiceAuth_AuthMethod() {} func (*ServiceAuth_EncoreAuth_) isServiceAuth_AuthMethod() {} type TracingProvider struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this provider. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // Types that are valid to be assigned to Provider: // // *TracingProvider_Encore Provider isTracingProvider_Provider `protobuf_oneof:"provider"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TracingProvider) Reset() { *x = TracingProvider{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TracingProvider) String() string { return protoimpl.X.MessageStringOf(x) } func (*TracingProvider) ProtoMessage() {} func (x *TracingProvider) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TracingProvider.ProtoReflect.Descriptor instead. func (*TracingProvider) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6} } func (x *TracingProvider) GetRid() string { if x != nil { return x.Rid } return "" } func (x *TracingProvider) GetProvider() isTracingProvider_Provider { if x != nil { return x.Provider } return nil } func (x *TracingProvider) GetEncore() *TracingProvider_EncoreTracingProvider { if x != nil { if x, ok := x.Provider.(*TracingProvider_Encore); ok { return x.Encore } } return nil } type isTracingProvider_Provider interface { isTracingProvider_Provider() } type TracingProvider_Encore struct { Encore *TracingProvider_EncoreTracingProvider `protobuf:"bytes,10,opt,name=encore,proto3,oneof"` } func (*TracingProvider_Encore) isTracingProvider_Provider() {} type MetricsProvider struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this provider. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` CollectionInterval *durationpb.Duration `protobuf:"bytes,2,opt,name=collection_interval,json=collectionInterval,proto3" json:"collection_interval,omitempty"` // Types that are valid to be assigned to Provider: // // *MetricsProvider_EncoreCloud // *MetricsProvider_Gcp // *MetricsProvider_Aws // *MetricsProvider_PromRemoteWrite // *MetricsProvider_Datadog_ Provider isMetricsProvider_Provider `protobuf_oneof:"provider"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MetricsProvider) Reset() { *x = MetricsProvider{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MetricsProvider) String() string { return protoimpl.X.MessageStringOf(x) } func (*MetricsProvider) ProtoMessage() {} func (x *MetricsProvider) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MetricsProvider.ProtoReflect.Descriptor instead. func (*MetricsProvider) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7} } func (x *MetricsProvider) GetRid() string { if x != nil { return x.Rid } return "" } func (x *MetricsProvider) GetCollectionInterval() *durationpb.Duration { if x != nil { return x.CollectionInterval } return nil } func (x *MetricsProvider) GetProvider() isMetricsProvider_Provider { if x != nil { return x.Provider } return nil } func (x *MetricsProvider) GetEncoreCloud() *MetricsProvider_GCPCloudMonitoring { if x != nil { if x, ok := x.Provider.(*MetricsProvider_EncoreCloud); ok { return x.EncoreCloud } } return nil } func (x *MetricsProvider) GetGcp() *MetricsProvider_GCPCloudMonitoring { if x != nil { if x, ok := x.Provider.(*MetricsProvider_Gcp); ok { return x.Gcp } } return nil } func (x *MetricsProvider) GetAws() *MetricsProvider_AWSCloudWatch { if x != nil { if x, ok := x.Provider.(*MetricsProvider_Aws); ok { return x.Aws } } return nil } func (x *MetricsProvider) GetPromRemoteWrite() *MetricsProvider_PrometheusRemoteWrite { if x != nil { if x, ok := x.Provider.(*MetricsProvider_PromRemoteWrite); ok { return x.PromRemoteWrite } } return nil } func (x *MetricsProvider) GetDatadog() *MetricsProvider_Datadog { if x != nil { if x, ok := x.Provider.(*MetricsProvider_Datadog_); ok { return x.Datadog } } return nil } type isMetricsProvider_Provider interface { isMetricsProvider_Provider() } type MetricsProvider_EncoreCloud struct { EncoreCloud *MetricsProvider_GCPCloudMonitoring `protobuf:"bytes,10,opt,name=encore_cloud,json=encoreCloud,proto3,oneof"` } type MetricsProvider_Gcp struct { Gcp *MetricsProvider_GCPCloudMonitoring `protobuf:"bytes,11,opt,name=gcp,proto3,oneof"` } type MetricsProvider_Aws struct { Aws *MetricsProvider_AWSCloudWatch `protobuf:"bytes,12,opt,name=aws,proto3,oneof"` } type MetricsProvider_PromRemoteWrite struct { PromRemoteWrite *MetricsProvider_PrometheusRemoteWrite `protobuf:"bytes,13,opt,name=prom_remote_write,json=promRemoteWrite,proto3,oneof"` } type MetricsProvider_Datadog_ struct { Datadog *MetricsProvider_Datadog `protobuf:"bytes,14,opt,name=datadog,proto3,oneof"` } func (*MetricsProvider_EncoreCloud) isMetricsProvider_Provider() {} func (*MetricsProvider_Gcp) isMetricsProvider_Provider() {} func (*MetricsProvider_Aws) isMetricsProvider_Provider() {} func (*MetricsProvider_PromRemoteWrite) isMetricsProvider_Provider() {} func (*MetricsProvider_Datadog_) isMetricsProvider_Provider() {} type LogsProvider struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this provider. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LogsProvider) Reset() { *x = LogsProvider{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *LogsProvider) String() string { return protoimpl.X.MessageStringOf(x) } func (*LogsProvider) ProtoMessage() {} func (x *LogsProvider) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LogsProvider.ProtoReflect.Descriptor instead. func (*LogsProvider) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{8} } func (x *LogsProvider) GetRid() string { if x != nil { return x.Rid } return "" } type EncoreAuthKey struct { state protoimpl.MessageState `protogen:"open.v1"` Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Data *SecretData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EncoreAuthKey) Reset() { *x = EncoreAuthKey{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EncoreAuthKey) String() string { return protoimpl.X.MessageStringOf(x) } func (*EncoreAuthKey) ProtoMessage() {} func (x *EncoreAuthKey) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EncoreAuthKey.ProtoReflect.Descriptor instead. func (*EncoreAuthKey) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{9} } func (x *EncoreAuthKey) GetId() uint32 { if x != nil { return x.Id } return 0 } func (x *EncoreAuthKey) GetData() *SecretData { if x != nil { return x.Data } return nil } // Describes service discovery configuration. type ServiceDiscovery struct { state protoimpl.MessageState `protogen:"open.v1"` // Where services are located, keyed by the service name. Services map[string]*ServiceDiscovery_Location `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceDiscovery) Reset() { *x = ServiceDiscovery{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceDiscovery) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceDiscovery) ProtoMessage() {} func (x *ServiceDiscovery) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceDiscovery.ProtoReflect.Descriptor instead. func (*ServiceDiscovery) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{10} } func (x *ServiceDiscovery) GetServices() map[string]*ServiceDiscovery_Location { if x != nil { return x.Services } return nil } // GracefulShutdown defines the graceful shutdown timings. type GracefulShutdown struct { state protoimpl.MessageState `protogen:"open.v1"` // Total is how long we allow the total shutdown to take // before the process forcibly exits. Total *durationpb.Duration `protobuf:"bytes,1,opt,name=total,proto3" json:"total,omitempty"` // ShutdownHooks is how long before [total] runs out that we cancel // the context that is passed to the shutdown hooks. // // It is expected that ShutdownHooks is a larger value than Handlers. ShutdownHooks *durationpb.Duration `protobuf:"bytes,2,opt,name=shutdown_hooks,json=shutdownHooks,proto3" json:"shutdown_hooks,omitempty"` // Handlers is how long before [total] runs out that we cancel // the context that is passed to API and PubSub Subscription handlers. // // For example, if [total] is 10 seconds and [handlers] is 2 seconds, // then we will cancel the context passed to handlers 8 seconds after // a graceful shutdown is initiated. Handlers *durationpb.Duration `protobuf:"bytes,3,opt,name=handlers,proto3" json:"handlers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GracefulShutdown) Reset() { *x = GracefulShutdown{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GracefulShutdown) String() string { return protoimpl.X.MessageStringOf(x) } func (*GracefulShutdown) ProtoMessage() {} func (x *GracefulShutdown) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GracefulShutdown.ProtoReflect.Descriptor instead. func (*GracefulShutdown) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{11} } func (x *GracefulShutdown) GetTotal() *durationpb.Duration { if x != nil { return x.Total } return nil } func (x *GracefulShutdown) GetShutdownHooks() *durationpb.Duration { if x != nil { return x.ShutdownHooks } return nil } func (x *GracefulShutdown) GetHandlers() *durationpb.Duration { if x != nil { return x.Handlers } return nil } type EncorePlatform struct { state protoimpl.MessageState `protogen:"open.v1"` // Auth keys for validating signed requests from the Encore Platform. PlatformSigningKeys []*EncoreAuthKey `protobuf:"bytes,1,rep,name=platform_signing_keys,json=platformSigningKeys,proto3" json:"platform_signing_keys,omitempty"` // The Encore Cloud configuration to use, if running in Encore Cloud. EncoreCloud *EncoreCloudProvider `protobuf:"bytes,2,opt,name=encore_cloud,json=encoreCloud,proto3,oneof" json:"encore_cloud,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EncorePlatform) Reset() { *x = EncorePlatform{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EncorePlatform) String() string { return protoimpl.X.MessageStringOf(x) } func (*EncorePlatform) ProtoMessage() {} func (x *EncorePlatform) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EncorePlatform.ProtoReflect.Descriptor instead. func (*EncorePlatform) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{12} } func (x *EncorePlatform) GetPlatformSigningKeys() []*EncoreAuthKey { if x != nil { return x.PlatformSigningKeys } return nil } func (x *EncorePlatform) GetEncoreCloud() *EncoreCloudProvider { if x != nil { return x.EncoreCloud } return nil } type RateLimiter struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Kind: // // *RateLimiter_TokenBucket_ Kind isRateLimiter_Kind `protobuf_oneof:"kind"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RateLimiter) Reset() { *x = RateLimiter{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RateLimiter) String() string { return protoimpl.X.MessageStringOf(x) } func (*RateLimiter) ProtoMessage() {} func (x *RateLimiter) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RateLimiter.ProtoReflect.Descriptor instead. func (*RateLimiter) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{13} } func (x *RateLimiter) GetKind() isRateLimiter_Kind { if x != nil { return x.Kind } return nil } func (x *RateLimiter) GetTokenBucket() *RateLimiter_TokenBucket { if x != nil { if x, ok := x.Kind.(*RateLimiter_TokenBucket_); ok { return x.TokenBucket } } return nil } type isRateLimiter_Kind interface { isRateLimiter_Kind() } type RateLimiter_TokenBucket_ struct { TokenBucket *RateLimiter_TokenBucket `protobuf:"bytes,1,opt,name=token_bucket,json=tokenBucket,proto3,oneof"` } func (*RateLimiter_TokenBucket_) isRateLimiter_Kind() {} type EncoreCloudProvider struct { state protoimpl.MessageState `protogen:"open.v1"` // The unique resource id for this provider. Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` // URL to use to authenticate with the server. ServerUrl string `protobuf:"bytes,2,opt,name=server_url,json=serverUrl,proto3" json:"server_url,omitempty"` AuthKeys []*EncoreAuthKey `protobuf:"bytes,3,rep,name=auth_keys,json=authKeys,proto3" json:"auth_keys,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *EncoreCloudProvider) Reset() { *x = EncoreCloudProvider{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *EncoreCloudProvider) String() string { return protoimpl.X.MessageStringOf(x) } func (*EncoreCloudProvider) ProtoMessage() {} func (x *EncoreCloudProvider) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EncoreCloudProvider.ProtoReflect.Descriptor instead. func (*EncoreCloudProvider) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{14} } func (x *EncoreCloudProvider) GetRid() string { if x != nil { return x.Rid } return "" } func (x *EncoreCloudProvider) GetServerUrl() string { if x != nil { return x.ServerUrl } return "" } func (x *EncoreCloudProvider) GetAuthKeys() []*EncoreAuthKey { if x != nil { return x.AuthKeys } return nil } type Metric struct { state protoimpl.MessageState `protogen:"open.v1"` // The encore name of the metric. EncoreName string `protobuf:"bytes,1,opt,name=encore_name,json=encoreName,proto3" json:"encore_name,omitempty"` // The services that have access to this metric. Services []string `protobuf:"bytes,2,rep,name=services,proto3" json:"services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Metric) Reset() { *x = Metric{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Metric) String() string { return protoimpl.X.MessageStringOf(x) } func (*Metric) ProtoMessage() {} func (x *Metric) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Metric.ProtoReflect.Descriptor instead. func (*Metric) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{15} } func (x *Metric) GetEncoreName() string { if x != nil { return x.EncoreName } return "" } func (x *Metric) GetServices() []string { if x != nil { return x.Services } return nil } type ServiceAuth_NoopAuth struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceAuth_NoopAuth) Reset() { *x = ServiceAuth_NoopAuth{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceAuth_NoopAuth) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceAuth_NoopAuth) ProtoMessage() {} func (x *ServiceAuth_NoopAuth) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceAuth_NoopAuth.ProtoReflect.Descriptor instead. func (*ServiceAuth_NoopAuth) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5, 0} } type ServiceAuth_EncoreAuth struct { state protoimpl.MessageState `protogen:"open.v1"` AuthKeys []*EncoreAuthKey `protobuf:"bytes,1,rep,name=auth_keys,json=authKeys,proto3" json:"auth_keys,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceAuth_EncoreAuth) Reset() { *x = ServiceAuth_EncoreAuth{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceAuth_EncoreAuth) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceAuth_EncoreAuth) ProtoMessage() {} func (x *ServiceAuth_EncoreAuth) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceAuth_EncoreAuth.ProtoReflect.Descriptor instead. func (*ServiceAuth_EncoreAuth) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5, 1} } func (x *ServiceAuth_EncoreAuth) GetAuthKeys() []*EncoreAuthKey { if x != nil { return x.AuthKeys } return nil } type TracingProvider_EncoreTracingProvider struct { state protoimpl.MessageState `protogen:"open.v1"` TraceEndpoint string `protobuf:"bytes,1,opt,name=trace_endpoint,json=traceEndpoint,proto3" json:"trace_endpoint,omitempty"` // Deprecated: Use sampling_config instead. // // Deprecated: Marked as deprecated in encore/runtime/v1/runtime.proto. SamplingRate *float64 `protobuf:"fixed64,2,opt,name=sampling_rate,json=samplingRate,proto3,oneof" json:"sampling_rate,omitempty"` // Sampling rates for different scopes. // // When deciding whether to sample a trace, the most specific matching // scope wins. // // If no scope matches, all traces are sampled. SamplingConfig []*TracingProvider_SamplingConfig `protobuf:"bytes,3,rep,name=sampling_config,json=samplingConfig,proto3" json:"sampling_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TracingProvider_EncoreTracingProvider) Reset() { *x = TracingProvider_EncoreTracingProvider{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TracingProvider_EncoreTracingProvider) String() string { return protoimpl.X.MessageStringOf(x) } func (*TracingProvider_EncoreTracingProvider) ProtoMessage() {} func (x *TracingProvider_EncoreTracingProvider) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TracingProvider_EncoreTracingProvider.ProtoReflect.Descriptor instead. func (*TracingProvider_EncoreTracingProvider) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 0} } func (x *TracingProvider_EncoreTracingProvider) GetTraceEndpoint() string { if x != nil { return x.TraceEndpoint } return "" } // Deprecated: Marked as deprecated in encore/runtime/v1/runtime.proto. func (x *TracingProvider_EncoreTracingProvider) GetSamplingRate() float64 { if x != nil && x.SamplingRate != nil { return *x.SamplingRate } return 0 } func (x *TracingProvider_EncoreTracingProvider) GetSamplingConfig() []*TracingProvider_SamplingConfig { if x != nil { return x.SamplingConfig } return nil } type TracingProvider_SamplingConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // The sampling rate, between [0, 1]. // 0 means never sample, 1 means always sample. Rate float64 `protobuf:"fixed64,1,opt,name=rate,proto3" json:"rate,omitempty"` // The scope this rate applies to. // // Types that are valid to be assigned to Scope: // // *TracingProvider_SamplingConfig_Default // *TracingProvider_SamplingConfig_Service // *TracingProvider_SamplingConfig_Endpoint_ // *TracingProvider_SamplingConfig_Topic // *TracingProvider_SamplingConfig_PubsubSubscription Scope isTracingProvider_SamplingConfig_Scope `protobuf_oneof:"scope"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TracingProvider_SamplingConfig) Reset() { *x = TracingProvider_SamplingConfig{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TracingProvider_SamplingConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*TracingProvider_SamplingConfig) ProtoMessage() {} func (x *TracingProvider_SamplingConfig) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TracingProvider_SamplingConfig.ProtoReflect.Descriptor instead. func (*TracingProvider_SamplingConfig) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1} } func (x *TracingProvider_SamplingConfig) GetRate() float64 { if x != nil { return x.Rate } return 0 } func (x *TracingProvider_SamplingConfig) GetScope() isTracingProvider_SamplingConfig_Scope { if x != nil { return x.Scope } return nil } func (x *TracingProvider_SamplingConfig) GetDefault() *emptypb.Empty { if x != nil { if x, ok := x.Scope.(*TracingProvider_SamplingConfig_Default); ok { return x.Default } } return nil } func (x *TracingProvider_SamplingConfig) GetService() string { if x != nil { if x, ok := x.Scope.(*TracingProvider_SamplingConfig_Service); ok { return x.Service } } return "" } func (x *TracingProvider_SamplingConfig) GetEndpoint() *TracingProvider_SamplingConfig_Endpoint { if x != nil { if x, ok := x.Scope.(*TracingProvider_SamplingConfig_Endpoint_); ok { return x.Endpoint } } return nil } func (x *TracingProvider_SamplingConfig) GetTopic() string { if x != nil { if x, ok := x.Scope.(*TracingProvider_SamplingConfig_Topic); ok { return x.Topic } } return "" } func (x *TracingProvider_SamplingConfig) GetPubsubSubscription() *TracingProvider_SamplingConfig_PubSubSubscription { if x != nil { if x, ok := x.Scope.(*TracingProvider_SamplingConfig_PubsubSubscription); ok { return x.PubsubSubscription } } return nil } type isTracingProvider_SamplingConfig_Scope interface { isTracingProvider_SamplingConfig_Scope() } type TracingProvider_SamplingConfig_Default struct { // Applies to all traces that don't match a more specific scope. Default *emptypb.Empty `protobuf:"bytes,2,opt,name=default,proto3,oneof"` } type TracingProvider_SamplingConfig_Service struct { // Applies to all endpoints within the named service. Service string `protobuf:"bytes,3,opt,name=service,proto3,oneof"` } type TracingProvider_SamplingConfig_Endpoint_ struct { // Applies to a specific API endpoint. Endpoint *TracingProvider_SamplingConfig_Endpoint `protobuf:"bytes,4,opt,name=endpoint,proto3,oneof"` } type TracingProvider_SamplingConfig_Topic struct { // Applies to all subscriptions within the named topic. Topic string `protobuf:"bytes,5,opt,name=topic,proto3,oneof"` } type TracingProvider_SamplingConfig_PubsubSubscription struct { // Applies to a specific pubsub subscription. PubsubSubscription *TracingProvider_SamplingConfig_PubSubSubscription `protobuf:"bytes,6,opt,name=pubsub_subscription,json=pubsubSubscription,proto3,oneof"` } func (*TracingProvider_SamplingConfig_Default) isTracingProvider_SamplingConfig_Scope() {} func (*TracingProvider_SamplingConfig_Service) isTracingProvider_SamplingConfig_Scope() {} func (*TracingProvider_SamplingConfig_Endpoint_) isTracingProvider_SamplingConfig_Scope() {} func (*TracingProvider_SamplingConfig_Topic) isTracingProvider_SamplingConfig_Scope() {} func (*TracingProvider_SamplingConfig_PubsubSubscription) isTracingProvider_SamplingConfig_Scope() {} type TracingProvider_SamplingConfig_Endpoint struct { state protoimpl.MessageState `protogen:"open.v1"` Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TracingProvider_SamplingConfig_Endpoint) Reset() { *x = TracingProvider_SamplingConfig_Endpoint{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TracingProvider_SamplingConfig_Endpoint) String() string { return protoimpl.X.MessageStringOf(x) } func (*TracingProvider_SamplingConfig_Endpoint) ProtoMessage() {} func (x *TracingProvider_SamplingConfig_Endpoint) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TracingProvider_SamplingConfig_Endpoint.ProtoReflect.Descriptor instead. func (*TracingProvider_SamplingConfig_Endpoint) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1, 0} } func (x *TracingProvider_SamplingConfig_Endpoint) GetService() string { if x != nil { return x.Service } return "" } func (x *TracingProvider_SamplingConfig_Endpoint) GetEndpoint() string { if x != nil { return x.Endpoint } return "" } type TracingProvider_SamplingConfig_PubSubSubscription struct { state protoimpl.MessageState `protogen:"open.v1"` Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` Subscription string `protobuf:"bytes,2,opt,name=subscription,proto3" json:"subscription,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TracingProvider_SamplingConfig_PubSubSubscription) Reset() { *x = TracingProvider_SamplingConfig_PubSubSubscription{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *TracingProvider_SamplingConfig_PubSubSubscription) String() string { return protoimpl.X.MessageStringOf(x) } func (*TracingProvider_SamplingConfig_PubSubSubscription) ProtoMessage() {} func (x *TracingProvider_SamplingConfig_PubSubSubscription) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TracingProvider_SamplingConfig_PubSubSubscription.ProtoReflect.Descriptor instead. func (*TracingProvider_SamplingConfig_PubSubSubscription) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1, 1} } func (x *TracingProvider_SamplingConfig_PubSubSubscription) GetTopic() string { if x != nil { return x.Topic } return "" } func (x *TracingProvider_SamplingConfig_PubSubSubscription) GetSubscription() string { if x != nil { return x.Subscription } return "" } type MetricsProvider_GCPCloudMonitoring struct { state protoimpl.MessageState `protogen:"open.v1"` // The GCP project id to send metrics to. ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` // The enum value for the monitored resource this application is monitoring. // See https://cloud.google.com/monitoring/api/resources for valid values. MonitoredResourceType string `protobuf:"bytes,2,opt,name=monitored_resource_type,json=monitoredResourceType,proto3" json:"monitored_resource_type,omitempty"` // The labels to specify for the monitored resource. // Each monitored resource type has a pre-defined set of labels that must be set. // See https://cloud.google.com/monitoring/api/resources for expected labels. MonitoredResourceLabels map[string]string `protobuf:"bytes,3,rep,name=monitored_resource_labels,json=monitoredResourceLabels,proto3" json:"monitored_resource_labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // The mapping between metric names in Encore and metric names in GCP. MetricNames map[string]string `protobuf:"bytes,4,rep,name=metric_names,json=metricNames,proto3" json:"metric_names,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MetricsProvider_GCPCloudMonitoring) Reset() { *x = MetricsProvider_GCPCloudMonitoring{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MetricsProvider_GCPCloudMonitoring) String() string { return protoimpl.X.MessageStringOf(x) } func (*MetricsProvider_GCPCloudMonitoring) ProtoMessage() {} func (x *MetricsProvider_GCPCloudMonitoring) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MetricsProvider_GCPCloudMonitoring.ProtoReflect.Descriptor instead. func (*MetricsProvider_GCPCloudMonitoring) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 0} } func (x *MetricsProvider_GCPCloudMonitoring) GetProjectId() string { if x != nil { return x.ProjectId } return "" } func (x *MetricsProvider_GCPCloudMonitoring) GetMonitoredResourceType() string { if x != nil { return x.MonitoredResourceType } return "" } func (x *MetricsProvider_GCPCloudMonitoring) GetMonitoredResourceLabels() map[string]string { if x != nil { return x.MonitoredResourceLabels } return nil } func (x *MetricsProvider_GCPCloudMonitoring) GetMetricNames() map[string]string { if x != nil { return x.MetricNames } return nil } type MetricsProvider_AWSCloudWatch struct { state protoimpl.MessageState `protogen:"open.v1"` // The namespace to use for metrics. Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MetricsProvider_AWSCloudWatch) Reset() { *x = MetricsProvider_AWSCloudWatch{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MetricsProvider_AWSCloudWatch) String() string { return protoimpl.X.MessageStringOf(x) } func (*MetricsProvider_AWSCloudWatch) ProtoMessage() {} func (x *MetricsProvider_AWSCloudWatch) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MetricsProvider_AWSCloudWatch.ProtoReflect.Descriptor instead. func (*MetricsProvider_AWSCloudWatch) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 1} } func (x *MetricsProvider_AWSCloudWatch) GetNamespace() string { if x != nil { return x.Namespace } return "" } type MetricsProvider_PrometheusRemoteWrite struct { state protoimpl.MessageState `protogen:"open.v1"` // The URL to send metrics to. RemoteWriteUrl *SecretData `protobuf:"bytes,1,opt,name=remote_write_url,json=remoteWriteUrl,proto3" json:"remote_write_url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MetricsProvider_PrometheusRemoteWrite) Reset() { *x = MetricsProvider_PrometheusRemoteWrite{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MetricsProvider_PrometheusRemoteWrite) String() string { return protoimpl.X.MessageStringOf(x) } func (*MetricsProvider_PrometheusRemoteWrite) ProtoMessage() {} func (x *MetricsProvider_PrometheusRemoteWrite) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MetricsProvider_PrometheusRemoteWrite.ProtoReflect.Descriptor instead. func (*MetricsProvider_PrometheusRemoteWrite) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 2} } func (x *MetricsProvider_PrometheusRemoteWrite) GetRemoteWriteUrl() *SecretData { if x != nil { return x.RemoteWriteUrl } return nil } type MetricsProvider_Datadog struct { state protoimpl.MessageState `protogen:"open.v1"` Site string `protobuf:"bytes,1,opt,name=site,proto3" json:"site,omitempty"` ApiKey *SecretData `protobuf:"bytes,2,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MetricsProvider_Datadog) Reset() { *x = MetricsProvider_Datadog{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *MetricsProvider_Datadog) String() string { return protoimpl.X.MessageStringOf(x) } func (*MetricsProvider_Datadog) ProtoMessage() {} func (x *MetricsProvider_Datadog) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MetricsProvider_Datadog.ProtoReflect.Descriptor instead. func (*MetricsProvider_Datadog) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 3} } func (x *MetricsProvider_Datadog) GetSite() string { if x != nil { return x.Site } return "" } func (x *MetricsProvider_Datadog) GetApiKey() *SecretData { if x != nil { return x.ApiKey } return nil } type ServiceDiscovery_Location struct { state protoimpl.MessageState `protogen:"open.v1"` // The base URL of the service (including scheme and port). BaseUrl string `protobuf:"bytes,1,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` // The auth methods to use when talking to this service. AuthMethods []*ServiceAuth `protobuf:"bytes,2,rep,name=auth_methods,json=authMethods,proto3" json:"auth_methods,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceDiscovery_Location) Reset() { *x = ServiceDiscovery_Location{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceDiscovery_Location) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceDiscovery_Location) ProtoMessage() {} func (x *ServiceDiscovery_Location) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServiceDiscovery_Location.ProtoReflect.Descriptor instead. func (*ServiceDiscovery_Location) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{10, 1} } func (x *ServiceDiscovery_Location) GetBaseUrl() string { if x != nil { return x.BaseUrl } return "" } func (x *ServiceDiscovery_Location) GetAuthMethods() []*ServiceAuth { if x != nil { return x.AuthMethods } return nil } type RateLimiter_TokenBucket struct { state protoimpl.MessageState `protogen:"open.v1"` // The rate (in events per per second) to allow. Rate float64 `protobuf:"fixed64,1,opt,name=rate,proto3" json:"rate,omitempty"` // The burst size to allow. Burst uint32 `protobuf:"varint,2,opt,name=burst,proto3" json:"burst,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RateLimiter_TokenBucket) Reset() { *x = RateLimiter_TokenBucket{} mi := &file_encore_runtime_v1_runtime_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *RateLimiter_TokenBucket) String() string { return protoimpl.X.MessageStringOf(x) } func (*RateLimiter_TokenBucket) ProtoMessage() {} func (x *RateLimiter_TokenBucket) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_runtime_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RateLimiter_TokenBucket.ProtoReflect.Descriptor instead. func (*RateLimiter_TokenBucket) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{13, 0} } func (x *RateLimiter_TokenBucket) GetRate() float64 { if x != nil { return x.Rate } return 0 } func (x *RateLimiter_TokenBucket) GetBurst() uint32 { if x != nil { return x.Burst } return 0 } var File_encore_runtime_v1_runtime_proto protoreflect.FileDescriptor const file_encore_runtime_v1_runtime_proto_rawDesc = "" + "\n" + "\x1fencore/runtime/v1/runtime.proto\x12\x11encore.runtime.v1\x1a\x1dencore/runtime/v1/infra.proto\x1a\"encore/runtime/v1/secretdata.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xae\x02\n" + "\rRuntimeConfig\x12@\n" + "\venvironment\x18\x01 \x01(\v2\x1e.encore.runtime.v1.EnvironmentR\venvironment\x127\n" + "\x05infra\x18\x02 \x01(\v2!.encore.runtime.v1.InfrastructureR\x05infra\x12=\n" + "\n" + "deployment\x18\x03 \x01(\v2\x1d.encore.runtime.v1.DeploymentR\n" + "deployment\x12O\n" + "\x0fencore_platform\x18\x05 \x01(\v2!.encore.runtime.v1.EncorePlatformH\x00R\x0eencorePlatform\x88\x01\x01B\x12\n" + "\x10_encore_platform\"\xcb\x03\n" + "\vEnvironment\x12\x15\n" + "\x06app_id\x18\x01 \x01(\tR\x05appId\x12\x19\n" + "\bapp_slug\x18\x02 \x01(\tR\aappSlug\x12\x15\n" + "\x06env_id\x18\x03 \x01(\tR\x05envId\x12\x19\n" + "\benv_name\x18\x04 \x01(\tR\aenvName\x12>\n" + "\benv_type\x18\x05 \x01(\x0e2#.encore.runtime.v1.Environment.TypeR\aenvType\x12:\n" + "\x05cloud\x18\x06 \x01(\x0e2$.encore.runtime.v1.Environment.CloudR\x05cloud\"j\n" + "\x04Type\x12\x14\n" + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x14\n" + "\x10TYPE_DEVELOPMENT\x10\x01\x12\x13\n" + "\x0fTYPE_PRODUCTION\x10\x02\x12\x12\n" + "\x0eTYPE_EPHEMERAL\x10\x03\x12\r\n" + "\tTYPE_TEST\x10\x04\"p\n" + "\x05Cloud\x12\x15\n" + "\x11CLOUD_UNSPECIFIED\x10\x00\x12\x0f\n" + "\vCLOUD_LOCAL\x10\x01\x12\x10\n" + "\fCLOUD_ENCORE\x10\x02\x12\r\n" + "\tCLOUD_AWS\x10\x03\x12\r\n" + "\tCLOUD_GCP\x10\x04\x12\x0f\n" + "\vCLOUD_AZURE\x10\x05\"\xef\x04\n" + "\n" + "Deployment\x12\x1b\n" + "\tdeploy_id\x18\x01 \x01(\tR\bdeployId\x12;\n" + "\vdeployed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + "deployedAt\x12/\n" + "\x13dynamic_experiments\x18\x03 \x03(\tR\x12dynamicExperiments\x12'\n" + "\x0fhosted_gateways\x18\x04 \x03(\tR\x0ehostedGateways\x12I\n" + "\x0fhosted_services\x18\x05 \x03(\v2 .encore.runtime.v1.HostedServiceR\x0ehostedServices\x12A\n" + "\fauth_methods\x18\x06 \x03(\v2\x1e.encore.runtime.v1.ServiceAuthR\vauthMethods\x12F\n" + "\robservability\x18\a \x01(\v2 .encore.runtime.v1.ObservabilityR\robservability\x12P\n" + "\x11service_discovery\x18\b \x01(\v2#.encore.runtime.v1.ServiceDiscoveryR\x10serviceDiscovery\x12P\n" + "\x11graceful_shutdown\x18\t \x01(\v2#.encore.runtime.v1.GracefulShutdownR\x10gracefulShutdown\x123\n" + "\ametrics\x18\n" + " \x03(\v2\x19.encore.runtime.v1.MetricR\ametrics\"\xc0\x01\n" + "\rObservability\x12<\n" + "\atracing\x18\x01 \x03(\v2\".encore.runtime.v1.TracingProviderR\atracing\x12<\n" + "\ametrics\x18\x02 \x03(\v2\".encore.runtime.v1.MetricsProviderR\ametrics\x123\n" + "\x04logs\x18\x03 \x03(\v2\x1f.encore.runtime.v1.LogsProviderR\x04logs\"\x95\x01\n" + "\rHostedService\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12*\n" + "\x0eworker_threads\x18\x02 \x01(\x05H\x00R\rworkerThreads\x88\x01\x01\x12\"\n" + "\n" + "log_config\x18\x03 \x01(\tH\x01R\tlogConfig\x88\x01\x01B\x11\n" + "\x0f_worker_threadsB\r\n" + "\v_log_config\"\x82\x02\n" + "\vServiceAuth\x12=\n" + "\x04noop\x18\n" + " \x01(\v2'.encore.runtime.v1.ServiceAuth.NoopAuthH\x00R\x04noop\x12L\n" + "\vencore_auth\x18\v \x01(\v2).encore.runtime.v1.ServiceAuth.EncoreAuthH\x00R\n" + "encoreAuth\x1a\n" + "\n" + "\bNoopAuth\x1aK\n" + "\n" + "EncoreAuth\x12=\n" + "\tauth_keys\x18\x01 \x03(\v2 .encore.runtime.v1.EncoreAuthKeyR\bauthKeysB\r\n" + "\vauth_method\"\xdd\x06\n" + "\x0fTracingProvider\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12R\n" + "\x06encore\x18\n" + " \x01(\v28.encore.runtime.v1.TracingProvider.EncoreTracingProviderH\x00R\x06encore\x1a\xda\x01\n" + "\x15EncoreTracingProvider\x12%\n" + "\x0etrace_endpoint\x18\x01 \x01(\tR\rtraceEndpoint\x12,\n" + "\rsampling_rate\x18\x02 \x01(\x01B\x02\x18\x01H\x00R\fsamplingRate\x88\x01\x01\x12Z\n" + "\x0fsampling_config\x18\x03 \x03(\v21.encore.runtime.v1.TracingProvider.SamplingConfigR\x0esamplingConfigB\x10\n" + "\x0e_sampling_rate\x1a\xfa\x03\n" + "\x0eSamplingConfig\x12\x12\n" + "\x04rate\x18\x01 \x01(\x01R\x04rate\x122\n" + "\adefault\x18\x02 \x01(\v2\x16.google.protobuf.EmptyH\x00R\adefault\x12\x1a\n" + "\aservice\x18\x03 \x01(\tH\x00R\aservice\x12X\n" + "\bendpoint\x18\x04 \x01(\v2:.encore.runtime.v1.TracingProvider.SamplingConfig.EndpointH\x00R\bendpoint\x12\x16\n" + "\x05topic\x18\x05 \x01(\tH\x00R\x05topic\x12w\n" + "\x13pubsub_subscription\x18\x06 \x01(\v2D.encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscriptionH\x00R\x12pubsubSubscription\x1a@\n" + "\bEndpoint\x12\x18\n" + "\aservice\x18\x01 \x01(\tR\aservice\x12\x1a\n" + "\bendpoint\x18\x02 \x01(\tR\bendpoint\x1aN\n" + "\x12PubSubSubscription\x12\x14\n" + "\x05topic\x18\x01 \x01(\tR\x05topic\x12\"\n" + "\fsubscription\x18\x02 \x01(\tR\fsubscriptionB\a\n" + "\x05scopeB\n" + "\n" + "\bprovider\"\xf6\t\n" + "\x0fMetricsProvider\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12J\n" + "\x13collection_interval\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\x12collectionInterval\x12Z\n" + "\fencore_cloud\x18\n" + " \x01(\v25.encore.runtime.v1.MetricsProvider.GCPCloudMonitoringH\x00R\vencoreCloud\x12I\n" + "\x03gcp\x18\v \x01(\v25.encore.runtime.v1.MetricsProvider.GCPCloudMonitoringH\x00R\x03gcp\x12D\n" + "\x03aws\x18\f \x01(\v20.encore.runtime.v1.MetricsProvider.AWSCloudWatchH\x00R\x03aws\x12f\n" + "\x11prom_remote_write\x18\r \x01(\v28.encore.runtime.v1.MetricsProvider.PrometheusRemoteWriteH\x00R\x0fpromRemoteWrite\x12F\n" + "\adatadog\x18\x0e \x01(\v2*.encore.runtime.v1.MetricsProvider.DatadogH\x00R\adatadog\x1a\xf3\x03\n" + "\x12GCPCloudMonitoring\x12\x1d\n" + "\n" + "project_id\x18\x01 \x01(\tR\tprojectId\x126\n" + "\x17monitored_resource_type\x18\x02 \x01(\tR\x15monitoredResourceType\x12\x8e\x01\n" + "\x19monitored_resource_labels\x18\x03 \x03(\v2R.encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntryR\x17monitoredResourceLabels\x12i\n" + "\fmetric_names\x18\x04 \x03(\v2F.encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntryR\vmetricNames\x1aJ\n" + "\x1cMonitoredResourceLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10MetricNamesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a-\n" + "\rAWSCloudWatch\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x1a`\n" + "\x15PrometheusRemoteWrite\x12G\n" + "\x10remote_write_url\x18\x01 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\x0eremoteWriteUrl\x1aU\n" + "\aDatadog\x12\x12\n" + "\x04site\x18\x01 \x01(\tR\x04site\x126\n" + "\aapi_key\x18\x02 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\x06apiKeyB\n" + "\n" + "\bprovider\" \n" + "\fLogsProvider\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\"R\n" + "\rEncoreAuthKey\x12\x0e\n" + "\x02id\x18\x01 \x01(\rR\x02id\x121\n" + "\x04data\x18\x02 \x01(\v2\x1d.encore.runtime.v1.SecretDataR\x04data\"\xb6\x02\n" + "\x10ServiceDiscovery\x12M\n" + "\bservices\x18\x01 \x03(\v21.encore.runtime.v1.ServiceDiscovery.ServicesEntryR\bservices\x1ai\n" + "\rServicesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12B\n" + "\x05value\x18\x02 \x01(\v2,.encore.runtime.v1.ServiceDiscovery.LocationR\x05value:\x028\x01\x1ah\n" + "\bLocation\x12\x19\n" + "\bbase_url\x18\x01 \x01(\tR\abaseUrl\x12A\n" + "\fauth_methods\x18\x02 \x03(\v2\x1e.encore.runtime.v1.ServiceAuthR\vauthMethods\"\xbc\x01\n" + "\x10GracefulShutdown\x12/\n" + "\x05total\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\x05total\x12@\n" + "\x0eshutdown_hooks\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\rshutdownHooks\x125\n" + "\bhandlers\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\bhandlers\"\xc7\x01\n" + "\x0eEncorePlatform\x12T\n" + "\x15platform_signing_keys\x18\x01 \x03(\v2 .encore.runtime.v1.EncoreAuthKeyR\x13platformSigningKeys\x12N\n" + "\fencore_cloud\x18\x02 \x01(\v2&.encore.runtime.v1.EncoreCloudProviderH\x00R\vencoreCloud\x88\x01\x01B\x0f\n" + "\r_encore_cloud\"\x9f\x01\n" + "\vRateLimiter\x12O\n" + "\ftoken_bucket\x18\x01 \x01(\v2*.encore.runtime.v1.RateLimiter.TokenBucketH\x00R\vtokenBucket\x1a7\n" + "\vTokenBucket\x12\x12\n" + "\x04rate\x18\x01 \x01(\x01R\x04rate\x12\x14\n" + "\x05burst\x18\x02 \x01(\rR\x05burstB\x06\n" + "\x04kind\"\x85\x01\n" + "\x13EncoreCloudProvider\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1d\n" + "\n" + "server_url\x18\x02 \x01(\tR\tserverUrl\x12=\n" + "\tauth_keys\x18\x03 \x03(\v2 .encore.runtime.v1.EncoreAuthKeyR\bauthKeys\"E\n" + "\x06Metric\x12\x1f\n" + "\vencore_name\x18\x01 \x01(\tR\n" + "encoreName\x12\x1a\n" + "\bservices\x18\x02 \x03(\tR\bservicesB,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\x06proto3" var ( file_encore_runtime_v1_runtime_proto_rawDescOnce sync.Once file_encore_runtime_v1_runtime_proto_rawDescData []byte ) func file_encore_runtime_v1_runtime_proto_rawDescGZIP() []byte { file_encore_runtime_v1_runtime_proto_rawDescOnce.Do(func() { file_encore_runtime_v1_runtime_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_runtime_proto_rawDesc), len(file_encore_runtime_v1_runtime_proto_rawDesc))) }) return file_encore_runtime_v1_runtime_proto_rawDescData } var file_encore_runtime_v1_runtime_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_encore_runtime_v1_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 31) var file_encore_runtime_v1_runtime_proto_goTypes = []any{ (Environment_Type)(0), // 0: encore.runtime.v1.Environment.Type (Environment_Cloud)(0), // 1: encore.runtime.v1.Environment.Cloud (*RuntimeConfig)(nil), // 2: encore.runtime.v1.RuntimeConfig (*Environment)(nil), // 3: encore.runtime.v1.Environment (*Deployment)(nil), // 4: encore.runtime.v1.Deployment (*Observability)(nil), // 5: encore.runtime.v1.Observability (*HostedService)(nil), // 6: encore.runtime.v1.HostedService (*ServiceAuth)(nil), // 7: encore.runtime.v1.ServiceAuth (*TracingProvider)(nil), // 8: encore.runtime.v1.TracingProvider (*MetricsProvider)(nil), // 9: encore.runtime.v1.MetricsProvider (*LogsProvider)(nil), // 10: encore.runtime.v1.LogsProvider (*EncoreAuthKey)(nil), // 11: encore.runtime.v1.EncoreAuthKey (*ServiceDiscovery)(nil), // 12: encore.runtime.v1.ServiceDiscovery (*GracefulShutdown)(nil), // 13: encore.runtime.v1.GracefulShutdown (*EncorePlatform)(nil), // 14: encore.runtime.v1.EncorePlatform (*RateLimiter)(nil), // 15: encore.runtime.v1.RateLimiter (*EncoreCloudProvider)(nil), // 16: encore.runtime.v1.EncoreCloudProvider (*Metric)(nil), // 17: encore.runtime.v1.Metric (*ServiceAuth_NoopAuth)(nil), // 18: encore.runtime.v1.ServiceAuth.NoopAuth (*ServiceAuth_EncoreAuth)(nil), // 19: encore.runtime.v1.ServiceAuth.EncoreAuth (*TracingProvider_EncoreTracingProvider)(nil), // 20: encore.runtime.v1.TracingProvider.EncoreTracingProvider (*TracingProvider_SamplingConfig)(nil), // 21: encore.runtime.v1.TracingProvider.SamplingConfig (*TracingProvider_SamplingConfig_Endpoint)(nil), // 22: encore.runtime.v1.TracingProvider.SamplingConfig.Endpoint (*TracingProvider_SamplingConfig_PubSubSubscription)(nil), // 23: encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscription (*MetricsProvider_GCPCloudMonitoring)(nil), // 24: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring (*MetricsProvider_AWSCloudWatch)(nil), // 25: encore.runtime.v1.MetricsProvider.AWSCloudWatch (*MetricsProvider_PrometheusRemoteWrite)(nil), // 26: encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite (*MetricsProvider_Datadog)(nil), // 27: encore.runtime.v1.MetricsProvider.Datadog nil, // 28: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntry nil, // 29: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntry nil, // 30: encore.runtime.v1.ServiceDiscovery.ServicesEntry (*ServiceDiscovery_Location)(nil), // 31: encore.runtime.v1.ServiceDiscovery.Location (*RateLimiter_TokenBucket)(nil), // 32: encore.runtime.v1.RateLimiter.TokenBucket (*Infrastructure)(nil), // 33: encore.runtime.v1.Infrastructure (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp (*durationpb.Duration)(nil), // 35: google.protobuf.Duration (*SecretData)(nil), // 36: encore.runtime.v1.SecretData (*emptypb.Empty)(nil), // 37: google.protobuf.Empty } var file_encore_runtime_v1_runtime_proto_depIdxs = []int32{ 3, // 0: encore.runtime.v1.RuntimeConfig.environment:type_name -> encore.runtime.v1.Environment 33, // 1: encore.runtime.v1.RuntimeConfig.infra:type_name -> encore.runtime.v1.Infrastructure 4, // 2: encore.runtime.v1.RuntimeConfig.deployment:type_name -> encore.runtime.v1.Deployment 14, // 3: encore.runtime.v1.RuntimeConfig.encore_platform:type_name -> encore.runtime.v1.EncorePlatform 0, // 4: encore.runtime.v1.Environment.env_type:type_name -> encore.runtime.v1.Environment.Type 1, // 5: encore.runtime.v1.Environment.cloud:type_name -> encore.runtime.v1.Environment.Cloud 34, // 6: encore.runtime.v1.Deployment.deployed_at:type_name -> google.protobuf.Timestamp 6, // 7: encore.runtime.v1.Deployment.hosted_services:type_name -> encore.runtime.v1.HostedService 7, // 8: encore.runtime.v1.Deployment.auth_methods:type_name -> encore.runtime.v1.ServiceAuth 5, // 9: encore.runtime.v1.Deployment.observability:type_name -> encore.runtime.v1.Observability 12, // 10: encore.runtime.v1.Deployment.service_discovery:type_name -> encore.runtime.v1.ServiceDiscovery 13, // 11: encore.runtime.v1.Deployment.graceful_shutdown:type_name -> encore.runtime.v1.GracefulShutdown 17, // 12: encore.runtime.v1.Deployment.metrics:type_name -> encore.runtime.v1.Metric 8, // 13: encore.runtime.v1.Observability.tracing:type_name -> encore.runtime.v1.TracingProvider 9, // 14: encore.runtime.v1.Observability.metrics:type_name -> encore.runtime.v1.MetricsProvider 10, // 15: encore.runtime.v1.Observability.logs:type_name -> encore.runtime.v1.LogsProvider 18, // 16: encore.runtime.v1.ServiceAuth.noop:type_name -> encore.runtime.v1.ServiceAuth.NoopAuth 19, // 17: encore.runtime.v1.ServiceAuth.encore_auth:type_name -> encore.runtime.v1.ServiceAuth.EncoreAuth 20, // 18: encore.runtime.v1.TracingProvider.encore:type_name -> encore.runtime.v1.TracingProvider.EncoreTracingProvider 35, // 19: encore.runtime.v1.MetricsProvider.collection_interval:type_name -> google.protobuf.Duration 24, // 20: encore.runtime.v1.MetricsProvider.encore_cloud:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring 24, // 21: encore.runtime.v1.MetricsProvider.gcp:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring 25, // 22: encore.runtime.v1.MetricsProvider.aws:type_name -> encore.runtime.v1.MetricsProvider.AWSCloudWatch 26, // 23: encore.runtime.v1.MetricsProvider.prom_remote_write:type_name -> encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite 27, // 24: encore.runtime.v1.MetricsProvider.datadog:type_name -> encore.runtime.v1.MetricsProvider.Datadog 36, // 25: encore.runtime.v1.EncoreAuthKey.data:type_name -> encore.runtime.v1.SecretData 30, // 26: encore.runtime.v1.ServiceDiscovery.services:type_name -> encore.runtime.v1.ServiceDiscovery.ServicesEntry 35, // 27: encore.runtime.v1.GracefulShutdown.total:type_name -> google.protobuf.Duration 35, // 28: encore.runtime.v1.GracefulShutdown.shutdown_hooks:type_name -> google.protobuf.Duration 35, // 29: encore.runtime.v1.GracefulShutdown.handlers:type_name -> google.protobuf.Duration 11, // 30: encore.runtime.v1.EncorePlatform.platform_signing_keys:type_name -> encore.runtime.v1.EncoreAuthKey 16, // 31: encore.runtime.v1.EncorePlatform.encore_cloud:type_name -> encore.runtime.v1.EncoreCloudProvider 32, // 32: encore.runtime.v1.RateLimiter.token_bucket:type_name -> encore.runtime.v1.RateLimiter.TokenBucket 11, // 33: encore.runtime.v1.EncoreCloudProvider.auth_keys:type_name -> encore.runtime.v1.EncoreAuthKey 11, // 34: encore.runtime.v1.ServiceAuth.EncoreAuth.auth_keys:type_name -> encore.runtime.v1.EncoreAuthKey 21, // 35: encore.runtime.v1.TracingProvider.EncoreTracingProvider.sampling_config:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig 37, // 36: encore.runtime.v1.TracingProvider.SamplingConfig.default:type_name -> google.protobuf.Empty 22, // 37: encore.runtime.v1.TracingProvider.SamplingConfig.endpoint:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig.Endpoint 23, // 38: encore.runtime.v1.TracingProvider.SamplingConfig.pubsub_subscription:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscription 28, // 39: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.monitored_resource_labels:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntry 29, // 40: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.metric_names:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntry 36, // 41: encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite.remote_write_url:type_name -> encore.runtime.v1.SecretData 36, // 42: encore.runtime.v1.MetricsProvider.Datadog.api_key:type_name -> encore.runtime.v1.SecretData 31, // 43: encore.runtime.v1.ServiceDiscovery.ServicesEntry.value:type_name -> encore.runtime.v1.ServiceDiscovery.Location 7, // 44: encore.runtime.v1.ServiceDiscovery.Location.auth_methods:type_name -> encore.runtime.v1.ServiceAuth 45, // [45:45] is the sub-list for method output_type 45, // [45:45] is the sub-list for method input_type 45, // [45:45] is the sub-list for extension type_name 45, // [45:45] is the sub-list for extension extendee 0, // [0:45] is the sub-list for field type_name } func init() { file_encore_runtime_v1_runtime_proto_init() } func file_encore_runtime_v1_runtime_proto_init() { if File_encore_runtime_v1_runtime_proto != nil { return } file_encore_runtime_v1_infra_proto_init() file_encore_runtime_v1_secretdata_proto_init() file_encore_runtime_v1_runtime_proto_msgTypes[0].OneofWrappers = []any{} file_encore_runtime_v1_runtime_proto_msgTypes[4].OneofWrappers = []any{} file_encore_runtime_v1_runtime_proto_msgTypes[5].OneofWrappers = []any{ (*ServiceAuth_Noop)(nil), (*ServiceAuth_EncoreAuth_)(nil), } file_encore_runtime_v1_runtime_proto_msgTypes[6].OneofWrappers = []any{ (*TracingProvider_Encore)(nil), } file_encore_runtime_v1_runtime_proto_msgTypes[7].OneofWrappers = []any{ (*MetricsProvider_EncoreCloud)(nil), (*MetricsProvider_Gcp)(nil), (*MetricsProvider_Aws)(nil), (*MetricsProvider_PromRemoteWrite)(nil), (*MetricsProvider_Datadog_)(nil), } file_encore_runtime_v1_runtime_proto_msgTypes[12].OneofWrappers = []any{} file_encore_runtime_v1_runtime_proto_msgTypes[13].OneofWrappers = []any{ (*RateLimiter_TokenBucket_)(nil), } file_encore_runtime_v1_runtime_proto_msgTypes[18].OneofWrappers = []any{} file_encore_runtime_v1_runtime_proto_msgTypes[19].OneofWrappers = []any{ (*TracingProvider_SamplingConfig_Default)(nil), (*TracingProvider_SamplingConfig_Service)(nil), (*TracingProvider_SamplingConfig_Endpoint_)(nil), (*TracingProvider_SamplingConfig_Topic)(nil), (*TracingProvider_SamplingConfig_PubsubSubscription)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_runtime_proto_rawDesc), len(file_encore_runtime_v1_runtime_proto_rawDesc)), NumEnums: 2, NumMessages: 31, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_runtime_v1_runtime_proto_goTypes, DependencyIndexes: file_encore_runtime_v1_runtime_proto_depIdxs, EnumInfos: file_encore_runtime_v1_runtime_proto_enumTypes, MessageInfos: file_encore_runtime_v1_runtime_proto_msgTypes, }.Build() File_encore_runtime_v1_runtime_proto = out.File file_encore_runtime_v1_runtime_proto_goTypes = nil file_encore_runtime_v1_runtime_proto_depIdxs = nil } ================================================ FILE: proto/encore/runtime/v1/runtime.proto ================================================ syntax = "proto3"; package encore.runtime.v1; import "encore/runtime/v1/infra.proto"; import "encore/runtime/v1/secretdata.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; option go_package = "encr.dev/proto/encore/runtime/v1;runtimev1"; message RuntimeConfig { Environment environment = 1; Infrastructure infra = 2; Deployment deployment = 3; optional EncorePlatform encore_platform = 5; } message Environment { string app_id = 1; string app_slug = 2; string env_id = 3; string env_name = 4; Type env_type = 5; Cloud cloud = 6; enum Type { TYPE_UNSPECIFIED = 0; TYPE_DEVELOPMENT = 1; TYPE_PRODUCTION = 2; TYPE_EPHEMERAL = 3; TYPE_TEST = 4; } enum Cloud { CLOUD_UNSPECIFIED = 0; CLOUD_LOCAL = 1; CLOUD_ENCORE = 2; CLOUD_AWS = 3; CLOUD_GCP = 4; CLOUD_AZURE = 5; } } // Describes the configuration related to a specific deployment, // meaning a group of services deployed together (think a single k8s Deployment). message Deployment { string deploy_id = 1; google.protobuf.Timestamp deployed_at = 2; // A list of experiments to enable at runtime. repeated string dynamic_experiments = 3; // The gateways hosted by this deployment, by rid. repeated string hosted_gateways = 4; // The services hosted by this deployment. repeated HostedService hosted_services = 5; // The authentication method(s) that can be used when talking // to this deployment for internal service-to-service calls. // // An empty list means no service-to-service calls can be made to this deployment. repeated ServiceAuth auth_methods = 6; // Observability-related configuration. Observability observability = 7; // Service discovery configuration. ServiceDiscovery service_discovery = 8; // Graceful shutdown behavior. GracefulShutdown graceful_shutdown = 9; // The metrics used by this deployment. repeated Metric metrics = 10; } message Observability { // The observability providers to use. repeated TracingProvider tracing = 1; repeated MetricsProvider metrics = 2; repeated LogsProvider logs = 3; } message HostedService { // The name of the service. string name = 1; // Number of worker threads to use. // If unset it defaults to 1. If set to 0 the runtime // automatically determines the number of threads to use // based on the number of CPUs available. optional int32 worker_threads = 2; // The log configuration to use for this service. // If unset it defaults to "trace". optional string log_config = 3; } message ServiceAuth { // The auth method to use. oneof auth_method { // Messages start at 10 to allow for other fields on ServiceAuth. NoopAuth noop = 10; EncoreAuth encore_auth = 11; } message NoopAuth {} message EncoreAuth { repeated EncoreAuthKey auth_keys = 1; } } message TracingProvider { // The unique resource id for this provider. string rid = 1; oneof provider { EncoreTracingProvider encore = 10; } message EncoreTracingProvider { string trace_endpoint = 1; // Deprecated: Use sampling_config instead. optional double sampling_rate = 2 [deprecated = true]; // Sampling rates for different scopes. // // When deciding whether to sample a trace, the most specific matching // scope wins. // // If no scope matches, all traces are sampled. repeated SamplingConfig sampling_config = 3; } message SamplingConfig { // The sampling rate, between [0, 1]. // 0 means never sample, 1 means always sample. double rate = 1; // The scope this rate applies to. oneof scope { // Applies to all traces that don't match a more specific scope. google.protobuf.Empty default = 2; // Applies to all endpoints within the named service. string service = 3; // Applies to a specific API endpoint. Endpoint endpoint = 4; // Applies to all subscriptions within the named topic. string topic = 5; // Applies to a specific pubsub subscription. PubSubSubscription pubsub_subscription = 6; } message Endpoint { string service = 1; string endpoint = 2; } message PubSubSubscription { string topic = 1; string subscription = 2; } } } message MetricsProvider { // The unique resource id for this provider. string rid = 1; google.protobuf.Duration collection_interval = 2; oneof provider { GCPCloudMonitoring encore_cloud = 10; GCPCloudMonitoring gcp = 11; AWSCloudWatch aws = 12; PrometheusRemoteWrite prom_remote_write = 13; Datadog datadog = 14; } message GCPCloudMonitoring { // The GCP project id to send metrics to. string project_id = 1; // The enum value for the monitored resource this application is monitoring. // See https://cloud.google.com/monitoring/api/resources for valid values. string monitored_resource_type = 2; // The labels to specify for the monitored resource. // Each monitored resource type has a pre-defined set of labels that must be set. // See https://cloud.google.com/monitoring/api/resources for expected labels. map monitored_resource_labels = 3; // The mapping between metric names in Encore and metric names in GCP. map metric_names = 4; } message AWSCloudWatch { // The namespace to use for metrics. string namespace = 1; } message PrometheusRemoteWrite { // The URL to send metrics to. SecretData remote_write_url = 1; } message Datadog { string site = 1; SecretData api_key = 2; } } message LogsProvider { // The unique resource id for this provider. string rid = 1; // Not yet implemented. } message EncoreAuthKey { uint32 id = 1; SecretData data = 2; } // Describes service discovery configuration. message ServiceDiscovery { // Where services are located, keyed by the service name. map services = 1; message Location { // The base URL of the service (including scheme and port). string base_url = 1; // The auth methods to use when talking to this service. repeated ServiceAuth auth_methods = 2; } } // GracefulShutdown defines the graceful shutdown timings. message GracefulShutdown { // Total is how long we allow the total shutdown to take // before the process forcibly exits. google.protobuf.Duration total = 1; // ShutdownHooks is how long before [total] runs out that we cancel // the context that is passed to the shutdown hooks. // // It is expected that ShutdownHooks is a larger value than Handlers. google.protobuf.Duration shutdown_hooks = 2; // Handlers is how long before [total] runs out that we cancel // the context that is passed to API and PubSub Subscription handlers. // // For example, if [total] is 10 seconds and [handlers] is 2 seconds, // then we will cancel the context passed to handlers 8 seconds after // a graceful shutdown is initiated. google.protobuf.Duration handlers = 3; } message EncorePlatform { // Auth keys for validating signed requests from the Encore Platform. repeated EncoreAuthKey platform_signing_keys = 1; // The Encore Cloud configuration to use, if running in Encore Cloud. optional EncoreCloudProvider encore_cloud = 2; } message RateLimiter { oneof kind { TokenBucket token_bucket = 1; } message TokenBucket { // The rate (in events per per second) to allow. double rate = 1; // The burst size to allow. uint32 burst = 2; } } message EncoreCloudProvider { // The unique resource id for this provider. string rid = 1; // URL to use to authenticate with the server. string server_url = 2; repeated EncoreAuthKey auth_keys = 3; } message Metric { // The encore name of the metric. string encore_name = 1; // The services that have access to this metric. repeated string services = 2; } ================================================ FILE: proto/encore/runtime/v1/secretdata.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 // protoc v6.32.1 // source: encore/runtime/v1/secretdata.proto package runtimev1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type SecretData_Encoding int32 const ( // Indicates the value is used as-is. SecretData_ENCODING_NONE SecretData_Encoding = 0 // Indicates the value is base64-encoded. SecretData_ENCODING_BASE64 SecretData_Encoding = 1 // Indicates the value is gzip-compressed and then base64-encoded. SecretData_ENCODING_GZIP SecretData_Encoding = 2 ) // Enum value maps for SecretData_Encoding. var ( SecretData_Encoding_name = map[int32]string{ 0: "ENCODING_NONE", 1: "ENCODING_BASE64", 2: "ENCODING_GZIP", } SecretData_Encoding_value = map[string]int32{ "ENCODING_NONE": 0, "ENCODING_BASE64": 1, "ENCODING_GZIP": 2, } ) func (x SecretData_Encoding) Enum() *SecretData_Encoding { p := new(SecretData_Encoding) *p = x return p } func (x SecretData_Encoding) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (SecretData_Encoding) Descriptor() protoreflect.EnumDescriptor { return file_encore_runtime_v1_secretdata_proto_enumTypes[0].Descriptor() } func (SecretData_Encoding) Type() protoreflect.EnumType { return &file_encore_runtime_v1_secretdata_proto_enumTypes[0] } func (x SecretData_Encoding) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use SecretData_Encoding.Descriptor instead. func (SecretData_Encoding) EnumDescriptor() ([]byte, []int) { return file_encore_runtime_v1_secretdata_proto_rawDescGZIP(), []int{0, 0} } // Defines how to resolve a secret value. type SecretData struct { state protoimpl.MessageState `protogen:"open.v1"` // How to resolve the initial secret value. // The output of this step is always a byte slice. // // Types that are valid to be assigned to Source: // // *SecretData_Embedded // *SecretData_Env Source isSecretData_Source `protobuf_oneof:"source"` // How the value is encoded. Encoding SecretData_Encoding `protobuf:"varint,20,opt,name=encoding,proto3,enum=encore.runtime.v1.SecretData_Encoding" json:"encoding,omitempty"` // sub_path is an optional path to a sub-value within the secret data. // // Types that are valid to be assigned to SubPath: // // *SecretData_JsonKey SubPath isSecretData_SubPath `protobuf_oneof:"sub_path"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SecretData) Reset() { *x = SecretData{} mi := &file_encore_runtime_v1_secretdata_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SecretData) String() string { return protoimpl.X.MessageStringOf(x) } func (*SecretData) ProtoMessage() {} func (x *SecretData) ProtoReflect() protoreflect.Message { mi := &file_encore_runtime_v1_secretdata_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SecretData.ProtoReflect.Descriptor instead. func (*SecretData) Descriptor() ([]byte, []int) { return file_encore_runtime_v1_secretdata_proto_rawDescGZIP(), []int{0} } func (x *SecretData) GetSource() isSecretData_Source { if x != nil { return x.Source } return nil } func (x *SecretData) GetEmbedded() []byte { if x != nil { if x, ok := x.Source.(*SecretData_Embedded); ok { return x.Embedded } } return nil } func (x *SecretData) GetEnv() string { if x != nil { if x, ok := x.Source.(*SecretData_Env); ok { return x.Env } } return "" } func (x *SecretData) GetEncoding() SecretData_Encoding { if x != nil { return x.Encoding } return SecretData_ENCODING_NONE } func (x *SecretData) GetSubPath() isSecretData_SubPath { if x != nil { return x.SubPath } return nil } func (x *SecretData) GetJsonKey() string { if x != nil { if x, ok := x.SubPath.(*SecretData_JsonKey); ok { return x.JsonKey } } return "" } type isSecretData_Source interface { isSecretData_Source() } type SecretData_Embedded struct { // The secret data is embedded directly in the configuration. // This is insecure unless `encrypted` is true, and should only // be used for local development. Embedded []byte `protobuf:"bytes,1,opt,name=embedded,proto3,oneof"` } type SecretData_Env struct { // Look up the secret data in an env variable with the given name. // Assumes the Env string `protobuf:"bytes,2,opt,name=env,proto3,oneof"` } func (*SecretData_Embedded) isSecretData_Source() {} func (*SecretData_Env) isSecretData_Source() {} type isSecretData_SubPath interface { isSecretData_SubPath() } type SecretData_JsonKey struct { // json_key indicates the secret data is a JSON map, // and the resolved secret value is a key in that map. // // The value is encoded differently based on its type. // Supported types are utf-8 strings and raw bytes: // - For strings, the value is the string itself, e.g. "foo". // - For raw bytes, the value is a JSON object with a single key "bytes" and the value is the base64-encoded bytes. // // For example: '{"foo": "string-value", "bar": {"bytes": "aGVsbG8="}}'. JsonKey string `protobuf:"bytes,10,opt,name=json_key,json=jsonKey,proto3,oneof"` } func (*SecretData_JsonKey) isSecretData_SubPath() {} var File_encore_runtime_v1_secretdata_proto protoreflect.FileDescriptor const file_encore_runtime_v1_secretdata_proto_rawDesc = "" + "\n" + "\"encore/runtime/v1/secretdata.proto\x12\x11encore.runtime.v1\"\x88\x02\n" + "\n" + "SecretData\x12\x1c\n" + "\bembedded\x18\x01 \x01(\fH\x00R\bembedded\x12\x12\n" + "\x03env\x18\x02 \x01(\tH\x00R\x03env\x12B\n" + "\bencoding\x18\x14 \x01(\x0e2&.encore.runtime.v1.SecretData.EncodingR\bencoding\x12\x1b\n" + "\bjson_key\x18\n" + " \x01(\tH\x01R\ajsonKey\"E\n" + "\bEncoding\x12\x11\n" + "\rENCODING_NONE\x10\x00\x12\x13\n" + "\x0fENCODING_BASE64\x10\x01\x12\x11\n" + "\rENCODING_GZIP\x10\x02B\b\n" + "\x06sourceB\n" + "\n" + "\bsub_pathJ\x04\b\x03\x10\n" + "J\x04\b\f\x10\x14B,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\x06proto3" var ( file_encore_runtime_v1_secretdata_proto_rawDescOnce sync.Once file_encore_runtime_v1_secretdata_proto_rawDescData []byte ) func file_encore_runtime_v1_secretdata_proto_rawDescGZIP() []byte { file_encore_runtime_v1_secretdata_proto_rawDescOnce.Do(func() { file_encore_runtime_v1_secretdata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_secretdata_proto_rawDesc), len(file_encore_runtime_v1_secretdata_proto_rawDesc))) }) return file_encore_runtime_v1_secretdata_proto_rawDescData } var file_encore_runtime_v1_secretdata_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_encore_runtime_v1_secretdata_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_encore_runtime_v1_secretdata_proto_goTypes = []any{ (SecretData_Encoding)(0), // 0: encore.runtime.v1.SecretData.Encoding (*SecretData)(nil), // 1: encore.runtime.v1.SecretData } var file_encore_runtime_v1_secretdata_proto_depIdxs = []int32{ 0, // 0: encore.runtime.v1.SecretData.encoding:type_name -> encore.runtime.v1.SecretData.Encoding 1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name } func init() { file_encore_runtime_v1_secretdata_proto_init() } func file_encore_runtime_v1_secretdata_proto_init() { if File_encore_runtime_v1_secretdata_proto != nil { return } file_encore_runtime_v1_secretdata_proto_msgTypes[0].OneofWrappers = []any{ (*SecretData_Embedded)(nil), (*SecretData_Env)(nil), (*SecretData_JsonKey)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_secretdata_proto_rawDesc), len(file_encore_runtime_v1_secretdata_proto_rawDesc)), NumEnums: 1, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_encore_runtime_v1_secretdata_proto_goTypes, DependencyIndexes: file_encore_runtime_v1_secretdata_proto_depIdxs, EnumInfos: file_encore_runtime_v1_secretdata_proto_enumTypes, MessageInfos: file_encore_runtime_v1_secretdata_proto_msgTypes, }.Build() File_encore_runtime_v1_secretdata_proto = out.File file_encore_runtime_v1_secretdata_proto_goTypes = nil file_encore_runtime_v1_secretdata_proto_depIdxs = nil } ================================================ FILE: proto/encore/runtime/v1/secretdata.proto ================================================ syntax = "proto3"; package encore.runtime.v1; option go_package = "encr.dev/proto/encore/runtime/v1;runtimev1"; // Defines how to resolve a secret value. message SecretData { // How to resolve the initial secret value. // The output of this step is always a byte slice. oneof source { // The secret data is embedded directly in the configuration. // This is insecure unless `encrypted` is true, and should only // be used for local development. bytes embedded = 1; // Look up the secret data in an env variable with the given name. // Assumes the string env = 2; } reserved 3 to 9; // for future sources // How the value is encoded. Encoding encoding = 20; // sub_path is an optional path to a sub-value within the secret data. oneof sub_path { // json_key indicates the secret data is a JSON map, // and the resolved secret value is a key in that map. // // The value is encoded differently based on its type. // Supported types are utf-8 strings and raw bytes: // - For strings, the value is the string itself, e.g. "foo". // - For raw bytes, the value is a JSON object with a single key "bytes" and the value is the base64-encoded bytes. // // For example: '{"foo": "string-value", "bar": {"bytes": "aGVsbG8="}}'. string json_key = 10; // null: the raw secret data is the resolved value. } reserved 12 to 19; // for future sub_paths enum Encoding { // Indicates the value is used as-is. ENCODING_NONE = 0; // Indicates the value is base64-encoded. ENCODING_BASE64 = 1; // Indicates the value is gzip-compressed and then base64-encoded. ENCODING_GZIP = 2; } } ================================================ FILE: proto/gen.go ================================================ package pb //go:generate ./gen.sh ================================================ FILE: proto/gen.sh ================================================ #!/usr/bin/env bash set -e -x GO_OPT=paths=source_relative GRPC_OPT=paths=source_relative protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/parser/meta/v1/meta.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/parser/schema/v1/schema.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/engine/trace/trace.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/engine/trace2/trace2.proto protoc -I . --go_out=. --go_opt=$GO_OPT --go-grpc_out=. --go-grpc_opt=$GRPC_OPT \ ./encore/daemon/daemon.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/runtime/v1/infra.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/runtime/v1/runtime.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/runtime/v1/secretdata.proto protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/runtime/v1/secretdata.proto # Prometheus protos for metrics exporter protoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \ ./prompb/types.proto protoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \ ./prompb/remote.proto ================================================ FILE: proto/prompb/remote.proto ================================================ // Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package prometheus; option go_package = "encore.dev/appruntime/metrics/prometheus/prompb"; import "prompb/types.proto"; message WriteRequest { repeated prometheus.TimeSeries timeseries = 1; // Cortex uses this field to determine the source of the write request. // We reserve it to avoid any compatibility issues. reserved 2; repeated prometheus.MetricMetadata metadata = 3; } // ReadRequest represents a remote read request. message ReadRequest { repeated Query queries = 1; enum ResponseType { // Server will return a single ReadResponse message with matched series that // includes list of raw samples. It's recommended to use streamed response // types instead. // // Response headers: // Content-Type: "application/x-protobuf" // Content-Encoding: "snappy" SAMPLES = 0; // Server will stream a delimited ChunkedReadResponse message that // contains XOR or HISTOGRAM(!) encoded chunks for a single series. // Each message is following varint size and fixed size bigendian // uint32 for CRC32 Castagnoli checksum. // // Response headers: // Content-Type: "application/x-streamed-protobuf; // proto=prometheus.ChunkedReadResponse" Content-Encoding: "" STREAMED_XOR_CHUNKS = 1; } // accepted_response_types allows negotiating the content type of the // response. // // Response types are taken from the list in the FIFO order. If no response // type in `accepted_response_types` is implemented by server, error is // returned. For request that do not contain `accepted_response_types` field // the SAMPLES response type will be used. repeated ResponseType accepted_response_types = 2; } // ReadResponse is a response when response_type equals SAMPLES. message ReadResponse { // In same order as the request's queries. repeated QueryResult results = 1; } message Query { int64 start_timestamp_ms = 1; int64 end_timestamp_ms = 2; repeated prometheus.LabelMatcher matchers = 3; prometheus.ReadHints hints = 4; } message QueryResult { // Samples within a time series must be ordered by time. repeated prometheus.TimeSeries timeseries = 1; } // ChunkedReadResponse is a response when response_type equals // STREAMED_XOR_CHUNKS. We strictly stream full series after series, optionally // split by time. This means that a single frame can contain partition of the // single series, but once a new series is started to be streamed it means that // no more chunks will be sent for previous one. Series are returned sorted in // the same way TSDB block are internally. message ChunkedReadResponse { repeated prometheus.ChunkedSeries chunked_series = 1; // query_index represents an index of the query from ReadRequest.queries these // chunks relates to. int64 query_index = 2; } ================================================ FILE: proto/prompb/types.proto ================================================ // Copyright 2017 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package prometheus; option go_package = "encore.dev/appruntime/metrics/prometheus/prompb"; message MetricMetadata { enum MetricType { UNKNOWN = 0; COUNTER = 1; GAUGE = 2; HISTOGRAM = 3; GAUGEHISTOGRAM = 4; SUMMARY = 5; INFO = 6; STATESET = 7; } // Represents the metric type, these match the set from Prometheus. // Refer to model/textparse/interface.go for details. MetricType type = 1; string metric_family_name = 2; string help = 4; string unit = 5; } message Sample { double value = 1; // timestamp is in ms format, see model/timestamp/timestamp.go for // conversion from time.Time to Prometheus timestamp. int64 timestamp = 2; } message Exemplar { // Optional, can be empty. repeated Label labels = 1; double value = 2; // timestamp is in ms format, see model/timestamp/timestamp.go for // conversion from time.Time to Prometheus timestamp. int64 timestamp = 3; } // A native histogram, also known as a sparse histogram. // Original design doc: // https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit // The appendix of this design doc also explains the concept of float // histograms. This Histogram message can represent both, the usual // integer histogram as well as a float histogram. message Histogram { enum ResetHint { UNKNOWN = 0; // Need to test for a counter reset explicitly. YES = 1; // This is the 1st histogram after a counter reset. NO = 2; // There was no counter reset between this and the previous Histogram. GAUGE = 3; // This is a gauge histogram where counter resets don't happen. } oneof count { // Count of observations in the histogram. uint64 count_int = 1; double count_float = 2; } double sum = 3; // Sum of observations in the histogram. // The schema defines the bucket schema. Currently, valid numbers // are -4 <= n <= 8. They are all for base-2 bucket schemas, where 1 // is a bucket boundary in each case, and then each power of two is // divided into 2^n logarithmic buckets. Or in other words, each // bucket boundary is the previous boundary times 2^(2^-n). In the // future, more bucket schemas may be added using numbers < -4 or > // 8. sint32 schema = 4; double zero_threshold = 5; // Breadth of the zero bucket. oneof zero_count { // Count in zero bucket. uint64 zero_count_int = 6; double zero_count_float = 7; } // Negative Buckets. repeated BucketSpan negative_spans = 8; // Use either "negative_deltas" or "negative_counts", the former for // regular histograms with integer counts, the latter for float // histograms. repeated sint64 negative_deltas = 9; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). repeated double negative_counts = 10; // Absolute count of each bucket. // Positive Buckets. repeated BucketSpan positive_spans = 11; // Use either "positive_deltas" or "positive_counts", the former for // regular histograms with integer counts, the latter for float // histograms. repeated sint64 positive_deltas = 12; // Count delta of each bucket compared to previous one (or to zero for 1st bucket). repeated double positive_counts = 13; // Absolute count of each bucket. ResetHint reset_hint = 14; // timestamp is in ms format, see model/timestamp/timestamp.go for // conversion from time.Time to Prometheus timestamp. int64 timestamp = 15; } // A BucketSpan defines a number of consecutive buckets with their // offset. Logically, it would be more straightforward to include the // bucket counts in the Span. However, the protobuf representation is // more compact in the way the data is structured here (with all the // buckets in a single array separate from the Spans). message BucketSpan { sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative). uint32 length = 2; // Length of consecutive buckets. } // TimeSeries represents samples and labels for a single time series. message TimeSeries { // For a timeseries to be valid, and for the samples and exemplars // to be ingested by the remote system properly, the labels field is required. repeated Label labels = 1; repeated Sample samples = 2; repeated Exemplar exemplars = 3; repeated Histogram histograms = 4; } message Label { string name = 1; string value = 2; } message Labels { repeated Label labels = 1; } // Matcher specifies a rule, which can match or set of labels or not. message LabelMatcher { enum Type { EQ = 0; NEQ = 1; RE = 2; NRE = 3; } Type type = 1; string name = 2; string value = 3; } message ReadHints { int64 step_ms = 1; // Query step size in milliseconds. string func = 2; // String representation of surrounding function or aggregation. int64 start_ms = 3; // Start time in milliseconds. int64 end_ms = 4; // End time in milliseconds. repeated string grouping = 5; // List of label names used in aggregation. bool by = 6; // Indicate whether it is without or by. int64 range_ms = 7; // Range vector selector range in milliseconds. } // Chunk represents a TSDB chunk. // Time range [min, max] is inclusive. message Chunk { int64 min_time_ms = 1; int64 max_time_ms = 2; // We require this to match chunkenc.Encoding. enum Encoding { UNKNOWN = 0; XOR = 1; HISTOGRAM = 2; } Encoding type = 3; bytes data = 4; } // ChunkedSeries represents single, encoded time series. message ChunkedSeries { // Labels should be sorted. repeated Label labels = 1; // Chunks will be in start time order and may overlap. repeated Chunk chunks = 2; } ================================================ FILE: runtimes/core/Cargo.toml ================================================ [package] name = "encore-runtime-core" version = "0.1.0" edition = "2021" [features] # Tracing of the Encore runtime itself. rttrace = [] [dependencies] pingora = { version = "0.8", features = ["lb", "openssl"] } anyhow = "1.0.76" async-trait = "0.1" base64 = "0.21.5" gjson = "0.8.1" prost = "0.12.3" prost-types = "0.12.3" serde = "1.0.193" serde_json = { version = "1.0.108", features = ["raw_value"] } tokio = { version = "1.35.1", features = ["sync"] } tokio-stream = "0.1.17" tokio-nsq = "0.14.0" xid = "1.0.3" log = { version = "0.4.20", features = ["kv_unstable", "kv_unstable_serde"] } bytes = { version = "1.5.0", features = [] } postgres-protocol = "0.6.8" pgvector = { version = "0.4", features = ["postgres", "serde"] } tokio-postgres = { version = "0.7.13", features = [ "array-impls", "with-serde_json-1", "with-geo-types-0_7", "with-uuid-1", "with-chrono-0_4", "with-cidr-0_3", ] } cidr = "0.3.1" tokio-util = "0.7.10" tokio-tungstenite = { version = "0.21.0", features = [ "rustls-tls-native-roots", ] } futures-util = "0.3.31" rand = "0.8.5" env_logger = "0.10.1" google-cloud-pubsub = "0.22.1" google-cloud-googleapis = "0.12.0" hyper = { version = "1.1.0", features = ["server", "http1", "http2", "client"] } http-body-util = "0.1.0" http = "1.0.0" matchit = "0.7.3" axum = { version = "0.7.5", features = ["ws"] } chrono = { version = "0.4.31", features = ["serde"] } once_cell = "1.19.0" colored = "2.1.0" backtrace = "0.3.69" serde_with = "3.4.0" mime = "0.3.17" futures = "0.3.30" native-tls = "0.2.11" postgres-native-tls = "0.5.0" reqwest = { version = "0.12.4", features = ["stream", "json"] } url = "2.5.0" futures-core = { version = "0.3.30", features = [] } serde_urlencoded = "0.7.1" form_urlencoded = "1.2.1" httpdate = "1.0.3" hmac = "0.12.1" sha2 = "0.10.8" sha3 = "0.10.8" hex = "0.4.3" subtle = "2.5.0" radix_fmt = "1.0.0" indexmap = { version = "2.2.1", features = ["serde"] } tower-service = "0.3.2" duct = "0.13.7" base32 = "0.4.0" # We need to vendor openssl to allow cross-compilation on our build systems openssl = { version = "0.10.57", features = ["vendored"] } bb8 = "0.9" bb8-postgres = "0.9" bb8-redis = "0.26" redis = { version = "1.0", features = [ "tokio-rustls-comp", "tls-rustls-insecure", "connection-manager", ] } uuid = "1.7.0" openssl-probe = "0.1.5" jsonwebtoken = "9.2.0" google-cloud-gax = "0.17.0" aws-sdk-sns = "1.20.0" aws-config = "1.1.10" aws-sdk-sqs = "1.19.0" tokio-retry = "0.3.0" rsa = { version = "0.9.6", features = ["pem"] } flate2 = "1.0.30" urlencoding = "2.1.3" tower-http = { version = "0.5.2", features = ["fs"] } google-cloud-storage = "0.22.1" serde_path_to_error = "0.1.16" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = [ "alloc", "ansi", "env-filter", "fmt", "matchers", "nu-ansi-term", "once_cell", "regex", "registry", "sharded-slab", "smallvec", "std", "thread_local", "tracing", ], default-features = false } thiserror = "1.0.64" async-stream = "0.3.6" md5 = "0.7.0" aws-sdk-s3 = "1.58.0" aws-smithy-types = { version = "1.2.8", features = [ "byte-stream-poll-next", "rt-tokio", ] } percent-encoding = "2.3.1" aws-credential-types = "1.2.1" regex = "1.11.1" email_address = "0.2.9" cookie = "0.18.1" malachite = "0.6.1" byteorder = "1.5.0" metrics = "0.24.2" dashmap = "6.1.0" google-cloud-monitoring-v3 = "1.0.0" google-cloud-api = "1.0.0" google-cloud-wkt = "1.0.0" sysinfo = "0.37.2" aws-sdk-cloudwatch = { version = "1.94.0", default-features = false, features = [ "behavior-version-latest", "rt-tokio", "rustls", ] } datadog-api-client = "0.20.0" snap = "1.1.1" miniredis-rs = { path = "../../miniredis" } [build-dependencies] prost-build = "0.12.3" [dev-dependencies] assert_matches = "1.5.0" insta = { version = "1.38.0", features = ["yaml"] } quickcheck = "1.0.3" proptest = "1.7.0" ================================================ FILE: runtimes/core/build.rs ================================================ use std::path::{Path, PathBuf}; fn main() -> std::io::Result<()> { prost_build::compile_protos( &[ "../../proto/encore/runtime/v1/runtime.proto", "../../proto/encore/parser/meta/v1/meta.proto", "../../proto/prompb/remote.proto", ], &["../../proto/"], )?; // We add an extra compile time environment variable which allows our error module // to know where the root of the workspace that we are compiling is - thus in stack traces // we can show the relative path to the file that caused the error. println!( "cargo:rustc-env=ENCORE_BINARY_SRC_PATH={}", workspace_dir().to_string_lossy() ); println!("cargo:rustc-env=ENCORE_BINARY_GIT_HASH={}", get_git_hash()); Ok(()) } fn workspace_dir() -> PathBuf { let output = std::process::Command::new(env!("CARGO")) .arg("locate-project") .arg("--workspace") .arg("--message-format=plain") .output() .unwrap() .stdout; let cargo_toml_file = Path::new(std::str::from_utf8(&output).unwrap().trim()); cargo_toml_file.parent().unwrap().to_path_buf() } use std::env; fn get_git_hash() -> String { use std::process::Command; let commit = Command::new("git") .arg("rev-parse") .arg("--verify") .arg("HEAD") .output(); if let Ok(commit_output) = commit { let commit_string = String::from_utf8_lossy(&commit_output.stdout); commit_string .lines() .next() .unwrap_or("unknown") .to_string() } else { "unknown".to_string() } } ================================================ FILE: runtimes/core/resources/test/infra.config.json ================================================ { "$schema": "https://encore.dev/schemas/infra.schema.json", "metadata": { "app_id": "my-app", "env_name": "my-env", "env_type": "production", "cloud": "gcp", "base_url": "https://my-app.com" }, "sql_servers": [ { "host": "my-db-host:5432", "tls_config": { "ca": "test", "client_cert": { "cert": "test", "key": "test" }, "disable_tls_hostname_verification": false, "disabled": false }, "databases": { "mydb": { "client_cert": { "cert": "test", "key": "test" }, "max_connections": 10, "min_connections": 10, "username": "my-db-owner", "password": {"$env": "DB_PASSWORD"} } } } ], "service_discovery": { "myservice": { "base_url": "https://my-service:8044" }, "myservice2": { "base_url": "https://my-service2:8044", "auth": [{ "type": "key", "key": { "$env": "SVC_TO_SVC_KEY" }, "id": 1 }] } }, "redis": { "encoreredis": { "database_index": 5, "max_connections": 10, "min_connections": 10, "key_prefix": "my-app:my-env:", "tls_config": { "disable_tls_hostname_verification": false, "disabled": false, "ca": "test", "client_cert": { "cert": "test", "key": "test" } }, "auth": { "type": "acl", "username": "encoreredis", "password": {"$env": "REDIS_PASSWORD"} }, "host": "my-redis-host" } }, "metrics": { "type": "prometheus", "remote_write_url": "https://my-remote-write-url" }, "graceful_shutdown": { "total": 30, "handlers": 20, "shutdown_hooks": 10 }, "auth": [ { "type": "key", "id": 1, "key": {"$env": "SVC_TO_SVC_KEY"} } ], "secrets": { "AppSecret": {"$env": "APP_SECRET"} }, "pubsub": [ { "type": "gcp_pubsub", "project_id": "my-project", "topics": { "encore-topic": { "name": "gcp-topic-name", "subscriptions": { "encore-subscription": { "name": "gcp-subscription-name", "project_id": "test", "push_config": { "id": "test", "jwt_audience": "test", "service_account": "test" } } } } } } ], "cors": { "debug": true, "allow_headers": ["Authorization", "Content-Type"], "expose_headers": ["*"], "allow_origins_with_credentials": ["https://test.com"], "allow_origins_without_credentials": ["https://test.com"] }, "hosted_gateways": ["api-gateway"], "hosted_services": ["my-service", "my-service2"] } ================================================ FILE: runtimes/core/src/api/auth/local.rs ================================================ use crate::metrics::counter; use crate::api::auth::{AuthHandler, AuthPayload, AuthRequest, AuthResponse}; use crate::api::schema::encoding::Schema; use crate::api::{APIResult, HandlerResponse, HandlerResponseInner, PValues}; use crate::log::LogFromRust; use crate::model::{AuthRequestData, RequestData}; use crate::trace::Tracer; use crate::{api, model, EndpointName}; use std::future::Future; use std::pin::Pin; use std::sync::{Arc, RwLock}; pub struct LocalAuthHandler { pub name: EndpointName, pub schema: Schema, pub handler: RwLock>>, pub tracer: Tracer, pub requests_total: counter::Schema, } impl LocalAuthHandler { pub fn set_handler(&self, handler: Option>) { let mut guard = self.handler.write().unwrap(); *guard = handler; } } impl AuthHandler for LocalAuthHandler { fn name(&self) -> &EndpointName { &self.name } fn handle_auth( self: Arc, req: AuthRequest, ) -> Pin> + Send + 'static>> { let this = self.clone(); Box::pin(async move { let handler = { let guard = this.handler.read().unwrap(); // If we don't have a handler set, return an error. let Some(handler) = guard.as_ref() else { return Err(api::Error::internal(anyhow::anyhow!( "auth handler implementation not registered for {}", this.name ))); }; handler.clone() }; let query = match &self.schema.query { None => None, Some(qry) => qry.parse(req.query.as_deref())?, }; let header = match &self.schema.header { None => None, Some(hdr) => hdr.parse(&req.headers)?, }; let cookie = match &self.schema.cookie { None => None, Some(c) => c.parse_req(&req.headers)?, }; let meta = req.call_meta; let span_id = meta.this_span_id.unwrap_or_else(model::SpanId::generate); let span = model::SpanKey(meta.trace_id, span_id); let parent_span = meta.parent_span_id.map(|sp| meta.trace_id.with_span(sp)); let traced = meta .trace_sampled .unwrap_or_else(|| self.tracer.should_sample(&self.name)); let req = Arc::new(model::Request { span, parent_trace: None, parent_span, caller_event_id: meta.parent_event_id, ext_correlation_id: meta.ext_correlation_id, is_platform_request: false, // TODO internal_caller: None, // TODO start: tokio::time::Instant::now(), start_time: std::time::SystemTime::now(), data: RequestData::Auth(AuthRequestData { auth_handler: this.name().clone(), parsed_payload: AuthPayload { query, header, cookie, }, }), traced, }); let logger = crate::log::root(); logger.info(Some(&req), "running auth handler", None); self.tracer.request_span_start(&req, false); let auth_response: HandlerResponse = handler.call(req.clone()).await; let duration = tokio::time::Instant::now().duration_since(req.start); if let Err(e) = &auth_response { logger.error(Some(&req), "auth handler failed", Some(e), None); } logger.info(Some(&req), "auth handler completed", { let mut fields = crate::log::Fields::new(); let dur_ms = (duration.as_secs() as f64 * 1000f64) + (duration.subsec_nanos() as f64 / 1_000_000f64); fields.insert( "duration".into(), serde_json::Value::Number(serde_json::Number::from_f64(dur_ms).unwrap_or_else( || { // Fall back to integer if the f64 conversion fails serde_json::Number::from(duration.as_millis() as u64) }, )), ); Some(fields) }); let result: APIResult<(PValues, String)> = match auth_response { Ok(HandlerResponseInner { payload: Some(payload), .. }) => { let auth_uid = payload .get("userID") .and_then(|v| v.as_str()) .map(String::from); match auth_uid { Some(uid) => Ok((payload, uid.to_string())), None => Err(api::Error { code: api::ErrCode::Unauthenticated, message: "unauthenticated".to_string(), internal_message: Some( "auth handler did not return a userID field".to_string(), ), stack: None, details: None, }), } } Ok(HandlerResponseInner { payload: None, .. }) => Err(api::Error { code: api::ErrCode::Unauthenticated, message: "unauthenticated".to_string(), internal_message: Some("auth handler returned null".to_string()), stack: None, details: None, }), Err(e) => Err(e), }; match result { Ok((auth_data, auth_uid)) => { let model_resp = model::Response { request: req.clone(), duration, data: model::ResponseData::Auth(Ok(model::AuthSuccessResponse { user_data: auth_data.clone(), user_id: auth_uid.clone(), })), }; self.tracer.request_span_end(&model_resp, false); self.requests_total.with([("code", "ok")]).increment(); Ok(AuthResponse::Authenticated { auth_uid, auth_data, }) } Err(e) => { let model_resp = model::Response { request: req.clone(), duration, data: model::ResponseData::Auth(Err(e.clone())), }; self.tracer.request_span_end(&model_resp, false); self.requests_total .with([("code", e.code.to_string())]) .increment(); Err(e) } } }) } } ================================================ FILE: runtimes/core/src/api/auth/mod.rs ================================================ use std::future::Future; use std::pin::Pin; use std::sync::Arc; use http::header::COOKIE; use serde::Serialize; use crate::api::reqauth::CallMeta; use crate::api::APIResult; use crate::{api, EndpointName}; use crate::api::schema::encoding::Schema; pub use local::LocalAuthHandler; pub use remote::RemoteAuthHandler; use super::jsonschema::JSONSchema; use super::PValues; mod local; mod remote; pub type AxumRequest = axum::http::Request; #[derive(Debug)] pub struct AuthRequest { pub headers: axum::http::HeaderMap, pub query: Option, pub call_meta: CallMeta, } #[derive(Debug)] pub enum AuthResponse { Authenticated { auth_uid: String, auth_data: PValues, }, Unauthenticated { error: api::Error, }, } /// A trait for handlers that accept auth parameters and return an auth result. pub trait AuthHandler: Sync + Send + 'static { fn name(&self) -> &EndpointName; fn handle_auth( self: Arc, req: AuthRequest, ) -> Pin> + Send + 'static>>; } pub struct Authenticator { schema: Schema, auth_data: JSONSchema, auth_handler: AuthHandlerType, } #[derive(Clone)] pub enum AuthHandlerType { Local(Arc), Remote(Arc), } impl AuthHandlerType { fn set_local_handler(&self, handler: Option>) { if let Self::Local(local) = self { local.set_handler(handler); } } } impl Authenticator { pub fn new( schema: Schema, auth_data: JSONSchema, auth_handler: AuthHandlerType, ) -> anyhow::Result { Ok(Self { schema, auth_data, auth_handler, }) } pub fn local( schema: Schema, auth_data: JSONSchema, local: LocalAuthHandler, ) -> anyhow::Result { Self::new(schema, auth_data, AuthHandlerType::Local(Arc::new(local))) } pub fn remote( schema: Schema, auth_data: JSONSchema, remote: RemoteAuthHandler, ) -> anyhow::Result { Self::new(schema, auth_data, AuthHandlerType::Remote(Arc::new(remote))) } pub fn schema(&self) -> &Schema { &self.schema } pub fn auth_data(&self) -> &JSONSchema { &self.auth_data } pub async fn authenticate( &self, req: &R, meta: CallMeta, ) -> APIResult { if !self.contains_auth_params(req) { return Ok(AuthResponse::Unauthenticated { error: api::Error::unauthenticated(), }); } let auth_req = self.build_auth_request(req, meta); let resp = match &self.auth_handler { AuthHandlerType::Local(local) => local.clone().handle_auth(auth_req).await, AuthHandlerType::Remote(remote) => remote.clone().handle_auth(auth_req).await, }; match resp { Ok(resp) => Ok(resp), Err(error) if error.code == api::ErrCode::Unauthenticated => { Ok(AuthResponse::Unauthenticated { error }) } Err(err) => Err(err), } } pub fn set_local_handler_impl(&self, handler: Option>) { self.auth_handler.set_local_handler(handler); } fn build_auth_request( &self, inbound: &R, mut call_meta: CallMeta, ) -> AuthRequest { // Ignore the parent span id as gateways don't currently record a span. call_meta.parent_span_id = None; // Headers. let mut headers = match &self.schema.header { None => axum::http::header::HeaderMap::new(), Some(schema) => { let mut dest = axum::http::header::HeaderMap::with_capacity(schema.len()); let inbound_headers = inbound.headers(); for (json_key, field) in schema.fields() { let header_name = field.name_override.as_deref().unwrap_or(json_key.as_ref()); let Ok(header_name) = axum::http::HeaderName::from_bytes(header_name.as_bytes()) else { continue; }; for value in inbound_headers.get_all(&header_name) { dest.append(header_name.clone(), value.to_owned()); } } dest } }; // Cookies. if let Some(schema) = &self.schema.cookie { let mut inbound_cookies = cookie::CookieJar::new(); inbound .headers() .get_all(COOKIE) .iter() .filter_map(|raw| raw.to_str().ok()) .flat_map(cookie::Cookie::split_parse) .flatten() .for_each(|c| inbound_cookies.add_original(c.into_owned())); for (key, field) in schema.fields() { let cookie_name = field.name_override.as_deref().unwrap_or(key.as_ref()); let Some(c) = inbound_cookies.get(cookie_name) else { continue; }; let Ok(val) = axum::http::HeaderValue::from_bytes(c.to_string().as_bytes()) else { continue; }; headers.append(http::header::COOKIE, val); } }; // Move query params. let query = match &self.schema.query { None => None, Some(schema) => { let query_data = inbound.query().unwrap_or_default().as_bytes(); let parsed = form_urlencoded::parse(query_data); let mut dest = form_urlencoded::Serializer::new(String::new()); for (key, value) in parsed { if schema.contains_name(key.as_ref()) { dest.append_pair(key.as_ref(), value.as_ref()); } } Some(dest.finish()) } }; AuthRequest { headers, query, call_meta, } } fn contains_auth_params(&self, req: &R) -> bool { if let Some(query) = &self.schema.query { if query.contains_any(req.query().unwrap_or_default().as_bytes()) { return true; } } if let Some(header) = &self.schema.header { if header.contains_any(&req.headers()) { return true; } } if let Some(cookie) = &self.schema.cookie { if cookie.contains_any(&req.headers()) { return true; } } false } } #[derive(Debug, Serialize, Clone)] pub struct AuthPayload { #[serde(flatten)] pub query: Option, #[serde(flatten)] pub header: Option, #[serde(flatten)] pub cookie: Option, } pub trait InboundRequest { fn headers(&self) -> &axum::http::HeaderMap; fn query(&self) -> Option<&str>; } impl InboundRequest for axum::http::Request { fn headers(&self) -> &axum::http::HeaderMap { self.headers() } fn query(&self) -> Option<&str> { self.uri().query() } } ================================================ FILE: runtimes/core/src/api/auth/remote.rs ================================================ use crate::api::auth::{AuthHandler, AuthRequest, AuthResponse}; use crate::api::call::{CallDesc, ServiceRegistry}; use crate::api::httputil::{convert_headers, join_url_path, merge_query}; use crate::api::jsonschema::{DecodeConfig, JSONSchema}; use crate::api::reqauth::caller::Caller; use crate::api::reqauth::meta::{MetaKey, MetaMap}; use crate::api::reqauth::svcauth; use crate::api::{APIResult, PValues}; use crate::{api, trace, EndpointName}; use anyhow::Context; use std::borrow::Cow; use std::future::Future; use std::pin::Pin; use std::sync::Arc; pub struct RemoteAuthHandler { name: EndpointName, svc_auth_method: Arc, auth_handler_url: reqwest::Url, http_client: reqwest::Client, auth_data_schema: JSONSchema, tracer: trace::Tracer, } impl RemoteAuthHandler { pub fn new( name: EndpointName, reg: &ServiceRegistry, http_client: reqwest::Client, auth_data_schema: JSONSchema, tracer: trace::Tracer, ) -> anyhow::Result { let svc_auth_method = reg .service_auth_method(name.service()) .context("no service auth method found for auth handler")?; let auth_handler_url = { let mut base_url: reqwest::Url = reg .service_base_url(name.service()) .context("no base url found for auth handler")? .parse() .context("invalid service base url")?; let auth_path = format!("/__encore/authhandler/{}", name.endpoint()); let combined_path = join_url_path(base_url.path(), &auth_path).context("invalid auth handler path")?; base_url.set_path(&combined_path); base_url }; Ok(Self { name, svc_auth_method, auth_handler_url, http_client, auth_data_schema, tracer, }) } fn build_req(&self, auth_req: &AuthRequest) -> APIResult { let dest = self.auth_handler_url.clone(); let mut req = self .http_client .post(dest) .headers(convert_headers(&auth_req.headers)) .build() .map_err(api::Error::internal)?; if let Some(query) = merge_query(req.url().query(), auth_req.query.as_deref()) { let query = query.as_ref(); req.url_mut().set_query(Some(query)); } Ok(req) } async fn handle_auth(self: Arc, req: AuthRequest) -> APIResult { // TODO this is copied from the Go version but should be better designed. // We should have a way of identifying the gateway as the caller. // There is Caller::Gateway but it means something else. let caller = Caller::APIEndpoint(EndpointName::new("gateway", "__encore/authhandler")); let meta = &req.call_meta; let desc: CallDesc<()> = CallDesc { caller: &caller, parent_span: meta.parent_span_id.map(|sp| meta.trace_id.with_span(sp)), parent_event_id: None, ext_correlation_id: meta .ext_correlation_id .as_ref() .map(|s| Cow::Borrowed(s.as_str())), traced: meta .trace_sampled .unwrap_or_else(|| self.tracer.should_sample(&self.name)), auth_user_id: None, auth_data: None, svc_auth_method: self.svc_auth_method.as_ref(), }; let mut req = self.build_req(&req)?; desc.add_meta(req.headers_mut()) .map_err(api::Error::internal)?; let resp = self .http_client .execute(req) .await .map_err(api::Error::internal)?; // Resolve the user id, if present, since parse_api_response consumes resp. let user_id = resp .headers() .get_meta(MetaKey::UserId) .map(|s| s.to_string()); match parse_auth_response(resp, &self.auth_data_schema).await { Ok(data) => { if let Some(user_id) = user_id { Ok(AuthResponse::Authenticated { auth_uid: user_id, auth_data: data, }) } else { Ok(AuthResponse::Unauthenticated { error: api::Error::unauthenticated(), }) } } // Map the unauthenticated error code to the unauthenticated result. Err(error) if error.code == api::ErrCode::Unauthenticated => { Ok(AuthResponse::Unauthenticated { error }) } Err(err) => Err(err), } } } impl AuthHandler for RemoteAuthHandler { fn name(&self) -> &EndpointName { &self.name } fn handle_auth( self: Arc, req: AuthRequest, ) -> Pin> + Send + 'static>> { Box::pin(self.handle_auth(req)) } } async fn parse_auth_response(resp: reqwest::Response, schema: &JSONSchema) -> APIResult { let status = resp.status(); if status.is_success() { // Do we have a JSON response? match resp.headers().get(reqwest::header::CONTENT_TYPE) { Some(content_type) if content_type == "application/json" => { let bytes = resp.bytes().await.map_err(api::Error::internal)?; let mut jsonde = serde_json::Deserializer::from_slice(&bytes); let cfg = DecodeConfig { coerce_strings: false, arrays_as_repeated_fields: false, }; let value = schema.deserialize(&mut jsonde, cfg).map_err(|e| { api::Error::invalid_argument("unable to decode response body", e) })?; Ok(value) } _ => Err(api::Error::internal(anyhow::anyhow!( "missing auth data from auth handler" ))), } } else { match resp.headers().get(reqwest::header::CONTENT_TYPE) { Some(content_type) if content_type == "application/json" => { match resp.json::().await { Ok(data) => Err(data), Err(e) => Err(api::Error::internal(e)), } } _ => { // We have some non-JSON error response. let body = resp.text().await.unwrap_or_else(|_| "".into()); Err(api::Error { code: api::ErrCode::Internal, message: body, internal_message: None, stack: None, details: None, }) } } } } ================================================ FILE: runtimes/core/src/api/call.rs ================================================ use std::borrow::{Borrow, Cow}; use std::collections::HashMap; use std::future::Future; use std::sync::Arc; use std::time::SystemTime; use anyhow::Context; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use url::Url; use encore::runtime::v1 as pb; use crate::api::reqauth::caller::Caller; use crate::api::reqauth::meta::MetaKey; use crate::api::reqauth::{service_auth_method, svcauth}; use crate::api::schema::{JSONPayload, ToOutgoingRequest}; use crate::api::{schema, APIResult, Endpoint, EndpointMap}; use crate::model::{SpanKey, TraceEventId}; use crate::names::EndpointName; use crate::trace::Tracer; use crate::{api, encore, model, secrets, EncoreName, Hosted}; use super::reqauth::meta::MetaMapMut; use super::websocket_client::WebSocketClient; use super::HandshakeSchema; use super::ResponsePayload; /// Tracks where services are located and how to call them. pub struct ServiceRegistry { endpoints: Arc, base_urls: HashMap, http_client: reqwest::Client, tracer: Tracer, service_auth: HashMap>, deploy_id: String, } impl ServiceRegistry { #[allow(clippy::too_many_arguments)] pub fn new( secrets: &secrets::Manager, endpoints: Arc, env: &pb::Environment, sd: pb::ServiceDiscovery, own_address: Option<&str>, own_auth_methods: &[Arc], hosted_services: &Hosted, deploy_id: String, http_client: reqwest::Client, tracer: Tracer, ) -> anyhow::Result { let mut base_urls = HashMap::with_capacity(sd.services.len()); let mut service_auth = HashMap::with_capacity(sd.services.len()); for (svc, mut loc) in sd.services { let svc = EncoreName::from(svc); base_urls.insert(svc.clone(), loc.base_url); let auth_method = if loc.auth_methods.is_empty() { Arc::new(svcauth::Noop) } else { service_auth_method(secrets, env, loc.auth_methods.swap_remove(0)) .context("compute service auth methods")? }; service_auth.insert(svc, auth_method); } if let Some(own_address) = own_address { let own_address = format!("http://{own_address}"); for svc_name in hosted_services.iter() { if !base_urls.contains_key(svc_name) { let svc = EncoreName::from(svc_name); base_urls.insert(svc.clone(), own_address.clone()); let auth_method = if own_auth_methods.is_empty() { Arc::new(svcauth::Noop) } else { own_auth_methods[0].clone() }; service_auth.insert(svc, auth_method); } } } else if !hosted_services.is_empty() { // This shouldn't happen if things are configured correctly. ::log::error!( "internal encore error: cannot host services without provided own address" ); } Ok(Self { endpoints, base_urls, http_client, tracer, service_auth, deploy_id, }) } pub fn endpoints(&self) -> &EndpointMap { self.endpoints.as_ref() } pub fn service_base_url(&self, service_name: &Q) -> Option<&String> where EncoreName: Borrow, Q: Eq + std::hash::Hash + ?Sized, { self.base_urls.get(service_name) } pub fn service_auth_method( &self, service_name: &Q, ) -> Option> where EncoreName: Borrow, Q: Eq + std::hash::Hash + ?Sized, { self.service_auth.get(service_name).cloned() } pub fn api_call( &self, target: EndpointName, data: JSONPayload, source: Option>, opts: Option, ) -> impl Future> + 'static { let tracer = self.tracer.clone(); let call = model::APICall { source, target }; let start_event_id = tracer.rpc_call_start(&call); let fut = self.do_api_call( &call.target, data, call.source.as_deref(), start_event_id, opts.as_ref(), ); async move { let result = fut.await; tracer.rpc_call_end(crate::trace::protocol::RPCCallEndData { start_id: start_event_id, call: &call, err: result.as_ref().err(), }); result } } pub fn connect_stream( &self, target: EndpointName, data: JSONPayload, source: Option>, opts: Option, ) -> impl Future> + 'static { let tracer = self.tracer.clone(); let call = model::APICall { source, target }; let start_event_id = tracer.rpc_call_start(&call); let fut = self.do_connect_stream( &call.target, data, call.source.as_deref(), start_event_id, opts.as_ref(), ); async move { let result = fut.await; tracer.rpc_call_end(crate::trace::protocol::RPCCallEndData { call: &call, start_id: start_event_id, err: result.as_ref().err(), }); result } } fn do_api_call( &self, target: &EndpointName, data: JSONPayload, source: Option<&model::Request>, start_event_id: Option, opts: Option<&api::CallOpts>, ) -> impl Future> + 'static { let http_client = self.http_client.clone(); let req = self.prepare_api_call_request(target, data, source, start_event_id, opts); async move { match req { Ok((req, resp_schema)) => { let fut = http_client.execute(req); match fut.await { Ok(resp) => { if !resp.status().is_success() { return Err(extract_error(resp).await); } resp_schema.extract(resp).await } Err(e) => Err(api::Error::internal(e)), } } Err(e) => Err(e), } } } fn prepare_api_call_request( &self, target: &EndpointName, mut data: JSONPayload, source: Option<&model::Request>, start_event_id: Option, opts: Option<&api::CallOpts>, ) -> APIResult<(reqwest::Request, Arc)> { let base_url = self .base_urls .get(target.service()) .ok_or_else(|| api::Error { code: api::ErrCode::NotFound, message: "service not found".into(), internal_message: Some(format!( "no service discovery configuration found for service {}", target.service() )), stack: None, details: None, })?; let Some(endpoint) = self.endpoints.get(target).cloned() else { return Err(api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".into(), internal_message: Some(format!( "endpoint {target} not found in application metadata" )), stack: None, details: None, }); }; let req_schema = &endpoint.request[0]; let method = req_schema.methods[0]; let req_path = req_schema.path.to_request_path(&mut data)?; let req_url = format!("{base_url}{req_path}"); let req_url = Url::parse(&req_url).map_err(|_| api::Error { code: api::ErrCode::Internal, message: "failed to build endpoint url".into(), internal_message: Some(format!( "failed to build endpoint url for endpoint {target}" )), stack: None, details: None, })?; let mut req = self .http_client .request(method.into(), req_url) .build() .map_err(api::Error::internal)?; if let Some(qry) = &req_schema.query { qry.to_outgoing_request(&mut data, &mut req)?; } if let Some(hdr) = &req_schema.header { hdr.to_outgoing_request(&mut data, &mut req)?; } if let Some(c) = &req_schema.cookie { c.to_outgoing_request(&mut data, &mut req)?; } match &req_schema.body { schema::RequestBody::Typed(Some(body)) => { body.to_outgoing_request(&mut data, &mut req)? } schema::RequestBody::Typed(None) => {} schema::RequestBody::Raw => { return Err(api::Error { code: api::ErrCode::Internal, message: "internal error".into(), internal_message: Some("cannot make api calls to raw endpoints".to_string()), stack: None, details: None, }); } } // Add call metadata. let headers = req.headers_mut(); self.propagate_call_meta(headers, &endpoint, source, start_event_id, opts) .map_err(api::Error::internal)?; let resp_schema = endpoint.response.clone(); Ok((req, resp_schema)) } fn do_connect_stream( &self, target: &EndpointName, data: JSONPayload, source: Option<&model::Request>, start_event_id: Option, opts: Option<&api::CallOpts>, ) -> impl Future> + 'static { let req = self.prepare_stream_request(target, data, source, start_event_id, opts); async move { match req { Ok((req, outgoing, incoming)) => { let schema = schema::Stream::new(incoming, outgoing); WebSocketClient::connect(req, schema).await } Err(e) => Err(e), } } } fn prepare_stream_request( &self, target: &EndpointName, mut data: JSONPayload, source: Option<&model::Request>, start_event_id: Option, opts: Option<&api::CallOpts>, ) -> APIResult<( http::Request<()>, Arc, Arc, )> { let base_url = self .base_urls .get(target.service()) .ok_or_else(|| api::Error { code: api::ErrCode::NotFound, message: "service not found".into(), internal_message: Some(format!( "no service discovery configuration found for service {}", target.service() )), stack: None, details: None, })?; let Some(endpoint) = self.endpoints.get(target) else { return Err(api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".into(), internal_message: Some(format!( "endpoint {target} not found in application metadata" )), stack: None, details: None, }); }; let Some(handshake) = &endpoint.handshake else { return Err(api::Error { code: api::ErrCode::NotFound, message: "no handshake schema found".into(), internal_message: Some(format!( "endpoint {target} doesn't have a handshake schema specified" )), stack: None, details: None, }); }; let req_path = handshake.path().to_request_path(&mut data)?; let base_url = base_url .replace("http://", "ws://") .replace("https://", "wss://"); let req_url = Url::parse(&format!("{base_url}{req_path}")).map_err(|_| api::Error { code: api::ErrCode::Internal, message: "failed to build endpoint url".into(), internal_message: Some(format!( "failed to build endpoint url for endpoint {target}" )), stack: None, details: None, })?; let mut req = req_url .into_client_request() .map_err(|e| api::Error::invalid_argument("unable to create request", e))?; if let HandshakeSchema::Request(req_schema) = handshake.as_ref() { if let Some(qry) = &req_schema.query { qry.to_outgoing_request(&mut data, &mut req)?; } if let Some(hdr) = &req_schema.header { hdr.to_outgoing_request(&mut data, &mut req)?; } if let Some(c) = &req_schema.cookie { c.to_outgoing_request(&mut data, &mut req)?; } } self.propagate_call_meta(req.headers_mut(), endpoint, source, start_event_id, opts) .map_err(api::Error::internal)?; let outgoing = endpoint.request[0].clone(); let incoming = endpoint.response.clone(); Ok((req, outgoing, incoming)) } fn propagate_call_meta( &self, headers: &mut reqwest::header::HeaderMap, endpoint: &Endpoint, source: Option<&model::Request>, parent_event_id: Option, opts: Option<&api::CallOpts>, ) -> anyhow::Result<()> { let svc_auth_method = self .service_auth_method(endpoint.name.service()) .ok_or_else(|| api::Error { code: api::ErrCode::NotFound, message: "not found".into(), internal_message: Some(format!( "no service auth method found for service {}", endpoint.name.service() )), stack: None, details: None, })?; let caller = match source { Some(source) => match source.data { model::RequestData::RPC(ref data) => { Caller::APIEndpoint(data.endpoint.name.clone()) } model::RequestData::Auth(ref data) => { Caller::APIEndpoint(data.auth_handler.clone()) } model::RequestData::PubSub(ref data) => Caller::PubSubMessage { topic: data.topic.clone(), subscription: data.subscription.clone(), message_id: data.message_id.clone(), }, model::RequestData::Stream(ref data) => { Caller::APIEndpoint(data.endpoint.name.clone()) } }, None => Caller::App { deploy_id: self.deploy_id.clone(), }, }; let auth_opts = opts.as_ref().and_then(|o| o.auth.as_ref()); let auth_data = auth_opts.map(|o| &o.data).or_else(|| { source.and_then(|r| match &r.data { model::RequestData::RPC(data) => data.auth_data.as_ref(), model::RequestData::Stream(data) => data.auth_data.as_ref(), model::RequestData::Auth(_) => None, model::RequestData::PubSub(_) => None, }) }); let auth_user_id = auth_opts .map(|o| &o.user_id) .or_else(|| { source.and_then(|r| match &r.data { model::RequestData::RPC(data) => data.auth_user_id.as_ref(), model::RequestData::Stream(data) => data.auth_user_id.as_ref(), model::RequestData::Auth(_) => None, model::RequestData::PubSub(_) => None, }) }) .map(|id| Cow::Borrowed(id.as_str())); let desc = CallDesc { caller: &caller, svc_auth_method: svc_auth_method.as_ref(), parent_span: source.map(|r| r.span), parent_event_id, ext_correlation_id: source.and_then(|r| { r.ext_correlation_id .as_ref() .map(|id| Cow::Borrowed(id.as_str())) }), traced: source.map(|r| r.traced).unwrap_or(false), auth_user_id, auth_data, }; desc.add_meta(headers)?; Ok(()) } } pub struct CallDesc<'a, AuthData> { pub caller: &'a Caller, pub parent_span: Option, pub parent_event_id: Option, pub ext_correlation_id: Option>, /// Whether the source request is being traced. pub traced: bool, pub auth_user_id: Option>, pub auth_data: Option, pub svc_auth_method: &'a dyn svcauth::ServiceAuthMethod, } impl<'a, AuthData> CallDesc<'a, AuthData> where AuthData: serde::ser::Serialize + 'a, { pub fn add_meta(self, headers: &mut R) -> anyhow::Result<()> { headers.set(MetaKey::Version, "1".to_string())?; if let Some(span) = self.parent_span { headers.set( MetaKey::TraceParent, format!( "00-{}-{}-{}", span.0.serialize_std(), span.1.serialize_std(), if self.traced { "01" } else { "00" }, ), )?; let mut trace_state = format!("encore/span-id={}", span.1.serialize_std()); if let Some(event_id) = self.parent_event_id.map(|id| id.serialize()) { trace_state.push_str(",encore/event-id="); trace_state.push_str(event_id.to_string().as_str()); } headers.set(MetaKey::TraceState, trace_state)?; } // TODO handle GCP span propagation with tracestate key. // headers.set(MetaKey::TraceState, "")?; if let Some(corr_id) = self.ext_correlation_id { headers.set(MetaKey::XCorrelationId, corr_id.into_owned())?; } // Add auth data. if let Some(auth_uid) = self.auth_user_id { headers.set(MetaKey::UserId, auth_uid.into_owned())?; if let Some(auth_data) = self.auth_data { if let Ok(auth_data) = serde_json::to_string(&auth_data) { headers.set(MetaKey::UserData, auth_data)?; } } } // Caller. headers.set(MetaKey::Caller, self.caller.serialize())?; let now = SystemTime::now(); self.svc_auth_method .sign(headers, now) .map_err(api::Error::internal)?; headers.set( MetaKey::SvcAuthMethod, self.svc_auth_method.name().to_string(), )?; Ok(()) } } async fn extract_error(resp: reqwest::Response) -> api::Error { match resp.bytes().await { Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_else(|err| { api::Error::invalid_argument("unable to parse error response", err) }), Err(err) => api::Error::invalid_argument("unable to read response body", err), } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/LICENSE ================================================ Copyright (c) 2019-2021 Tower Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/allow_credentials.rs ================================================ use std::{fmt, sync::Arc}; use http::{ header::{self, HeaderName, HeaderValue}, request::Parts as RequestParts, }; /// Holds configuration for how to set the [`Access-Control-Allow-Credentials`][mdn] header. /// /// See [`CorsLayer::allow_credentials`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials /// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials #[derive(Clone, Default)] #[must_use] pub struct AllowCredentials(AllowCredentialsInner); impl AllowCredentials { /// Allow credentials for all requests /// /// See [`CorsLayer::allow_credentials`] for more details. /// /// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials pub fn yes() -> Self { Self(AllowCredentialsInner::Yes) } /// Allow credentials for some requests, based on a given predicate /// /// The first argument to the predicate is the request origin. /// /// See [`CorsLayer::allow_credentials`] for more details. /// /// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials pub fn predicate(f: F) -> Self where F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static, { Self(AllowCredentialsInner::Predicate(Arc::new(f))) } pub(super) fn is_true(&self) -> bool { matches!(&self.0, AllowCredentialsInner::Yes) } pub(super) fn to_header( &self, origin: Option<&HeaderValue>, parts: &RequestParts, ) -> Option<(HeaderName, HeaderValue)> { #[allow(clippy::declare_interior_mutable_const)] const TRUE: HeaderValue = HeaderValue::from_static("true"); let allow_creds = match &self.0 { AllowCredentialsInner::Yes => true, AllowCredentialsInner::No => false, AllowCredentialsInner::Predicate(c) => c(origin?, parts), }; allow_creds.then_some((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, TRUE)) } } impl From for AllowCredentials { fn from(v: bool) -> Self { match v { true => Self(AllowCredentialsInner::Yes), false => Self(AllowCredentialsInner::No), } } } impl fmt::Debug for AllowCredentials { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { AllowCredentialsInner::Yes => f.debug_tuple("Yes").finish(), AllowCredentialsInner::No => f.debug_tuple("No").finish(), AllowCredentialsInner::Predicate(_) => f.debug_tuple("Predicate").finish(), } } } type PredicateFn = Arc Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>; #[derive(Clone, Default)] enum AllowCredentialsInner { Yes, #[default] No, Predicate(PredicateFn), } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/allow_headers.rs ================================================ use std::fmt; use http::{ header::{self, HeaderName, HeaderValue}, request::Parts as RequestParts, }; use super::{separated_by_commas, Any, WILDCARD}; /// Holds configuration for how to set the [`Access-Control-Allow-Headers`][mdn] header. /// /// See [`CorsLayer::allow_headers`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers #[derive(Clone, Default)] #[must_use] pub struct AllowHeaders(AllowHeadersInner); impl AllowHeaders { /// Allow any headers by sending a wildcard (`*`) /// /// See [`CorsLayer::allow_headers`] for more details. /// /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers pub fn any() -> Self { Self(AllowHeadersInner::Const(Some(WILDCARD))) } /// Set multiple allowed headers /// /// See [`CorsLayer::allow_headers`] for more details. /// /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers pub fn list(headers: I) -> Self where I: IntoIterator, { Self(AllowHeadersInner::Const(separated_by_commas( headers.into_iter().map(Into::into), ))) } /// Allow any headers, by mirroring the preflight [`Access-Control-Request-Headers`][mdn] /// header. /// /// See [`CorsLayer::allow_headers`] for more details. /// /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers pub fn mirror_request() -> Self { Self(AllowHeadersInner::MirrorRequest) } #[allow(clippy::borrow_interior_mutable_const)] pub(super) fn is_wildcard(&self) -> bool { matches!(&self.0, AllowHeadersInner::Const(Some(v)) if v == WILDCARD) } pub(super) fn to_header(&self, parts: &RequestParts) -> Option<(HeaderName, HeaderValue)> { let allow_headers = match &self.0 { AllowHeadersInner::Const(v) => v.clone()?, AllowHeadersInner::MirrorRequest => parts .headers .get(header::ACCESS_CONTROL_REQUEST_HEADERS)? .clone(), }; Some((header::ACCESS_CONTROL_ALLOW_HEADERS, allow_headers)) } } impl fmt::Debug for AllowHeaders { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { AllowHeadersInner::Const(inner) => f.debug_tuple("Const").field(inner).finish(), AllowHeadersInner::MirrorRequest => f.debug_tuple("MirrorRequest").finish(), } } } impl From for AllowHeaders { fn from(_: Any) -> Self { Self::any() } } impl From<[HeaderName; N]> for AllowHeaders { fn from(arr: [HeaderName; N]) -> Self { Self::list(arr) } } impl From> for AllowHeaders { fn from(vec: Vec) -> Self { Self::list(vec) } } #[derive(Clone)] enum AllowHeadersInner { Const(Option), MirrorRequest, } impl Default for AllowHeadersInner { fn default() -> Self { Self::Const(None) } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/allow_methods.rs ================================================ use std::fmt; use http::{ header::{self, HeaderName, HeaderValue}, request::Parts as RequestParts, Method, }; use super::{separated_by_commas, Any, WILDCARD}; /// Holds configuration for how to set the [`Access-Control-Allow-Methods`][mdn] header. /// /// See [`CorsLayer::allow_methods`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods #[derive(Clone, Default)] #[must_use] pub struct AllowMethods(AllowMethodsInner); impl AllowMethods { /// Allow any method by sending a wildcard (`*`) /// /// See [`CorsLayer::allow_methods`] for more details. /// /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods pub fn any() -> Self { Self(AllowMethodsInner::Const(Some(WILDCARD))) } /// Set a single allowed method /// /// See [`CorsLayer::allow_methods`] for more details. /// /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods pub fn exact(method: Method) -> Self { Self(AllowMethodsInner::Const(Some( HeaderValue::from_str(method.as_str()).unwrap(), ))) } /// Set multiple allowed methods /// /// See [`CorsLayer::allow_methods`] for more details. /// /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods pub fn list(methods: I) -> Self where I: IntoIterator, { Self(AllowMethodsInner::Const(separated_by_commas( methods .into_iter() .map(|m| HeaderValue::from_str(m.as_str()).unwrap()), ))) } /// Allow any method, by mirroring the preflight [`Access-Control-Request-Method`][mdn] /// header. /// /// See [`CorsLayer::allow_methods`] for more details. /// /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method pub fn mirror_request() -> Self { Self(AllowMethodsInner::MirrorRequest) } #[allow(clippy::borrow_interior_mutable_const)] pub(super) fn is_wildcard(&self) -> bool { matches!(&self.0, AllowMethodsInner::Const(Some(v)) if v == WILDCARD) } pub(super) fn to_header(&self, parts: &RequestParts) -> Option<(HeaderName, HeaderValue)> { let allow_methods = match &self.0 { AllowMethodsInner::Const(v) => v.clone()?, AllowMethodsInner::MirrorRequest => parts .headers .get(header::ACCESS_CONTROL_REQUEST_METHOD)? .clone(), }; Some((header::ACCESS_CONTROL_ALLOW_METHODS, allow_methods)) } } impl fmt::Debug for AllowMethods { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { AllowMethodsInner::Const(inner) => f.debug_tuple("Const").field(inner).finish(), AllowMethodsInner::MirrorRequest => f.debug_tuple("MirrorRequest").finish(), } } } impl From for AllowMethods { fn from(_: Any) -> Self { Self::any() } } impl From for AllowMethods { fn from(method: Method) -> Self { Self::exact(method) } } impl From<[Method; N]> for AllowMethods { fn from(arr: [Method; N]) -> Self { Self::list(arr) } } impl From> for AllowMethods { fn from(vec: Vec) -> Self { Self::list(vec) } } #[derive(Clone)] enum AllowMethodsInner { Const(Option), MirrorRequest, } impl Default for AllowMethodsInner { fn default() -> Self { Self::Const(None) } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/allow_origin.rs ================================================ use http::{ header::{self, HeaderValue}, request::Parts as RequestParts, HeaderName, }; use std::{fmt, sync::Arc}; use super::{Any, WILDCARD}; /// Holds configuration for how to set the [`Access-Control-Allow-Origin`][mdn] header. /// /// See [`CorsLayer::allow_origin`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin #[derive(Clone, Default)] #[must_use] pub struct AllowOrigin(OriginInner); impl AllowOrigin { /// Allow any origin by sending a wildcard (`*`) /// /// See [`CorsLayer::allow_origin`] for more details. /// /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin pub fn any() -> Self { Self(OriginInner::Const(WILDCARD)) } /// Set a single allowed origin /// /// See [`CorsLayer::allow_origin`] for more details. /// /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin pub fn exact(origin: HeaderValue) -> Self { Self(OriginInner::Const(origin)) } /// Set multiple allowed origins /// /// See [`CorsLayer::allow_origin`] for more details. /// /// # Panics /// /// If the iterator contains a wildcard (`*`). /// /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin #[allow(clippy::borrow_interior_mutable_const)] pub fn list(origins: I) -> Self where I: IntoIterator, { let origins = origins.into_iter().collect::>(); if origins.contains(&WILDCARD) { panic!( "Wildcard origin (`*`) cannot be passed to `AllowOrigin::list`. \ Use `AllowOrigin::any()` instead" ); } Self(OriginInner::List(origins)) } /// Set the allowed origins from a predicate /// /// See [`CorsLayer::allow_origin`] for more details. /// /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin pub fn predicate(f: F) -> Self where F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static, { Self(OriginInner::Predicate(Arc::new(f))) } /// Allow any origin, by mirroring the request origin /// /// This is equivalent to /// [`AllowOrigin::predicate(|_, _| true)`][Self::predicate]. /// /// See [`CorsLayer::allow_origin`] for more details. /// /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin pub fn mirror_request() -> Self { Self::predicate(|_, _| true) } #[allow(clippy::borrow_interior_mutable_const)] pub(super) fn is_wildcard(&self) -> bool { matches!(&self.0, OriginInner::Const(v) if v == WILDCARD) } pub(super) fn to_header( &self, origin: Option<&HeaderValue>, parts: &RequestParts, ) -> Option<(HeaderName, HeaderValue)> { let name = header::ACCESS_CONTROL_ALLOW_ORIGIN; match &self.0 { OriginInner::Const(v) => Some((name, v.clone())), OriginInner::List(l) => origin.filter(|o| l.contains(o)).map(|o| (name, o.clone())), OriginInner::Predicate(c) => origin.filter(|o| c(o, parts)).map(|o| (name, o.clone())), } } } impl fmt::Debug for AllowOrigin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { OriginInner::Const(inner) => f.debug_tuple("Const").field(inner).finish(), OriginInner::List(inner) => f.debug_tuple("List").field(inner).finish(), OriginInner::Predicate(_) => f.debug_tuple("Predicate").finish(), } } } impl From for AllowOrigin { fn from(_: Any) -> Self { Self::any() } } impl From for AllowOrigin { fn from(val: HeaderValue) -> Self { Self::exact(val) } } impl From<[HeaderValue; N]> for AllowOrigin { fn from(arr: [HeaderValue; N]) -> Self { Self::list(arr) } } impl From> for AllowOrigin { fn from(vec: Vec) -> Self { Self::list(vec) } } type PredicateFn = Arc Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>; #[derive(Clone)] enum OriginInner { Const(HeaderValue), List(Vec), Predicate(PredicateFn), } impl Default for OriginInner { fn default() -> Self { Self::List(Vec::new()) } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/allow_private_network.rs ================================================ use std::{fmt, sync::Arc}; use http::{ header::{HeaderName, HeaderValue}, request::Parts as RequestParts, }; /// Holds configuration for how to set the [`Access-Control-Allow-Private-Network`][wicg] header. /// /// See [`CorsLayer::allow_private_network`] for more details. /// /// [wicg]: https://wicg.github.io/private-network-access/ /// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network #[derive(Clone, Default)] #[must_use] pub struct AllowPrivateNetwork(AllowPrivateNetworkInner); impl AllowPrivateNetwork { /// Allow requests via a more private network than the one used to access the origin /// /// See [`CorsLayer::allow_private_network`] for more details. /// /// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network pub fn yes() -> Self { Self(AllowPrivateNetworkInner::Yes) } /// Allow requests via private network for some requests, based on a given predicate /// /// The first argument to the predicate is the request origin. /// /// See [`CorsLayer::allow_private_network`] for more details. /// /// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network pub fn predicate(f: F) -> Self where F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static, { Self(AllowPrivateNetworkInner::Predicate(Arc::new(f))) } #[allow( clippy::declare_interior_mutable_const, clippy::borrow_interior_mutable_const )] pub(super) fn to_header( &self, origin: Option<&HeaderValue>, parts: &RequestParts, ) -> Option<(HeaderName, HeaderValue)> { #[allow(clippy::declare_interior_mutable_const)] const REQUEST_PRIVATE_NETWORK: HeaderName = HeaderName::from_static("access-control-request-private-network"); #[allow(clippy::declare_interior_mutable_const)] const ALLOW_PRIVATE_NETWORK: HeaderName = HeaderName::from_static("access-control-allow-private-network"); const TRUE: HeaderValue = HeaderValue::from_static("true"); // Cheapest fallback: allow_private_network hasn't been set if let AllowPrivateNetworkInner::No = &self.0 { return None; } // Access-Control-Allow-Private-Network is only relevant if the request // has the Access-Control-Request-Private-Network header set, else skip if parts.headers.get(REQUEST_PRIVATE_NETWORK) != Some(&TRUE) { return None; } let allow_private_network = match &self.0 { AllowPrivateNetworkInner::Yes => true, AllowPrivateNetworkInner::No => false, // unreachable, but not harmful AllowPrivateNetworkInner::Predicate(c) => c(origin?, parts), }; allow_private_network.then_some((ALLOW_PRIVATE_NETWORK, TRUE)) } } impl From for AllowPrivateNetwork { fn from(v: bool) -> Self { match v { true => Self(AllowPrivateNetworkInner::Yes), false => Self(AllowPrivateNetworkInner::No), } } } impl fmt::Debug for AllowPrivateNetwork { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { AllowPrivateNetworkInner::Yes => f.debug_tuple("Yes").finish(), AllowPrivateNetworkInner::No => f.debug_tuple("No").finish(), AllowPrivateNetworkInner::Predicate(_) => f.debug_tuple("Predicate").finish(), } } } type PredicateFn = Arc Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>; #[derive(Clone, Default)] enum AllowPrivateNetworkInner { Yes, #[default] No, Predicate(PredicateFn), } #[cfg(test)] mod tests { #![allow( clippy::declare_interior_mutable_const, clippy::borrow_interior_mutable_const )] use super::AllowPrivateNetwork; use crate::api::cors::cors_headers_config::CorsHeadersConfig; use http::{header::ORIGIN, request::Parts, HeaderName, HeaderValue}; use pingora::http::{RequestHeader, ResponseHeader}; const REQUEST_PRIVATE_NETWORK: HeaderName = HeaderName::from_static("access-control-request-private-network"); const ALLOW_PRIVATE_NETWORK: HeaderName = HeaderName::from_static("access-control-allow-private-network"); const TRUE: HeaderValue = HeaderValue::from_static("true"); #[tokio::test] async fn cors_private_network_header_is_added_correctly() { let conf = CorsHeadersConfig::new().allow_private_network(true); let mut req = RequestHeader::build(http::Method::POST, b"/some/path", None).unwrap(); req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap(); let mut resp = ResponseHeader::build(200, None).unwrap(); conf.apply(&req, &mut resp).unwrap(); assert_eq!(resp.headers.get(ALLOW_PRIVATE_NETWORK).unwrap(), TRUE); let req = RequestHeader::build(http::Method::POST, b"/some/path", None).unwrap(); let mut resp = ResponseHeader::build(200, None).unwrap(); conf.apply(&req, &mut resp).unwrap(); assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none()); } #[tokio::test] async fn cors_private_network_header_is_added_correctly_with_predicate() { let allow_private_network = AllowPrivateNetwork::predicate(|origin: &HeaderValue, parts: &Parts| { parts.uri.path() == "/allow-private" && origin == "localhost" }); let conf = CorsHeadersConfig::new().allow_private_network(allow_private_network); let mut req = RequestHeader::build(http::Method::POST, b"/allow-private", None).unwrap(); req.insert_header(ORIGIN, "localhost").unwrap(); req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap(); let mut resp = ResponseHeader::build(200, None).unwrap(); conf.apply(&req, &mut resp).unwrap(); assert_eq!(resp.headers.get(ALLOW_PRIVATE_NETWORK).unwrap(), TRUE); let mut req = RequestHeader::build(http::Method::POST, b"/other", None).unwrap(); req.insert_header(ORIGIN, "localhost").unwrap(); req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap(); let mut resp = ResponseHeader::build(200, None).unwrap(); conf.apply(&req, &mut resp).unwrap(); assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none()); let mut req = RequestHeader::build(http::Method::POST, b"/allow-private", None).unwrap(); req.insert_header(ORIGIN, "not-localhost").unwrap(); req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap(); let mut resp = ResponseHeader::build(200, None).unwrap(); conf.apply(&req, &mut resp).unwrap(); assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none()); } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/expose_headers.rs ================================================ use std::fmt; use http::header::{self, HeaderName, HeaderValue}; use super::{separated_by_commas, Any, WILDCARD}; /// Holds configuration for how to set the [`Access-Control-Expose-Headers`][mdn] header. /// /// See [`CorsLayer::expose_headers`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers /// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers #[derive(Clone, Default)] #[must_use] pub struct ExposeHeaders(ExposeHeadersInner); impl ExposeHeaders { /// Expose any / all headers by sending a wildcard (`*`) /// /// See [`CorsLayer::expose_headers`] for more details. /// /// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers pub fn any() -> Self { Self(ExposeHeadersInner::Const(Some(WILDCARD))) } /// Set multiple exposed header names /// /// See [`CorsLayer::expose_headers`] for more details. /// /// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers pub fn list(headers: I) -> Self where I: IntoIterator, { Self(ExposeHeadersInner::Const(separated_by_commas( headers.into_iter().map(Into::into), ))) } #[allow(clippy::borrow_interior_mutable_const)] pub(super) fn is_wildcard(&self) -> bool { matches!(&self.0, ExposeHeadersInner::Const(Some(v)) if v == WILDCARD) } pub(super) fn to_header(&self) -> Option<(HeaderName, HeaderValue)> { let expose_headers = match &self.0 { ExposeHeadersInner::Const(v) => v.clone()?, }; Some((header::ACCESS_CONTROL_EXPOSE_HEADERS, expose_headers)) } } impl fmt::Debug for ExposeHeaders { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { ExposeHeadersInner::Const(inner) => f.debug_tuple("Const").field(inner).finish(), } } } impl From for ExposeHeaders { fn from(_: Any) -> Self { Self::any() } } impl From<[HeaderName; N]> for ExposeHeaders { fn from(arr: [HeaderName; N]) -> Self { Self::list(arr) } } impl From> for ExposeHeaders { fn from(vec: Vec) -> Self { Self::list(vec) } } #[derive(Clone)] enum ExposeHeadersInner { Const(Option), } impl Default for ExposeHeadersInner { fn default() -> Self { ExposeHeadersInner::Const(None) } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/max_age.rs ================================================ use std::{fmt, sync::Arc, time::Duration}; use http::{ header::{self, HeaderName, HeaderValue}, request::Parts as RequestParts, }; /// Holds configuration for how to set the [`Access-Control-Max-Age`][mdn] header. /// /// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age #[derive(Clone, Default)] #[must_use] pub struct MaxAge(MaxAgeInner); impl MaxAge { /// Set a static max-age value /// /// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details. pub fn exact(max_age: Duration) -> Self { Self(MaxAgeInner::Exact(Some(max_age.as_secs().into()))) } /// Set the max-age based on the preflight request parts /// /// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details. pub fn dynamic(f: F) -> Self where F: Fn(&HeaderValue, &RequestParts) -> Duration + Send + Sync + 'static, { Self(MaxAgeInner::Fn(Arc::new(f))) } pub(super) fn to_header( &self, origin: Option<&HeaderValue>, parts: &RequestParts, ) -> Option<(HeaderName, HeaderValue)> { let max_age = match &self.0 { MaxAgeInner::Exact(v) => v.clone()?, MaxAgeInner::Fn(c) => c(origin?, parts).as_secs().into(), }; Some((header::ACCESS_CONTROL_MAX_AGE, max_age)) } } impl fmt::Debug for MaxAge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { MaxAgeInner::Exact(inner) => f.debug_tuple("Exact").field(inner).finish(), MaxAgeInner::Fn(_) => f.debug_tuple("Fn").finish(), } } } impl From for MaxAge { fn from(max_age: Duration) -> Self { Self::exact(max_age) } } type MaxAgeFn = Arc Fn(&'a HeaderValue, &'a RequestParts) -> Duration + Send + Sync + 'static>; #[derive(Clone)] enum MaxAgeInner { Exact(Option), Fn(MaxAgeFn), } impl Default for MaxAgeInner { fn default() -> Self { Self::Exact(None) } } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/mod.rs ================================================ #![allow(clippy::enum_variant_names)] use bytes::{BufMut, BytesMut}; use http::{ header::{self, HeaderName}, HeaderValue, Method, }; use pingora::http::{RequestHeader, ResponseHeader}; mod allow_credentials; mod allow_headers; mod allow_methods; mod allow_origin; mod allow_private_network; mod expose_headers; mod max_age; mod vary; pub use self::{ allow_credentials::AllowCredentials, allow_headers::AllowHeaders, allow_methods::AllowMethods, allow_origin::AllowOrigin, allow_private_network::AllowPrivateNetwork, expose_headers::ExposeHeaders, max_age::MaxAge, vary::Vary, }; /// Configuration for how cors headers should be appied /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS #[derive(Debug, Clone)] #[must_use] pub struct CorsHeadersConfig { allow_credentials: AllowCredentials, allow_headers: AllowHeaders, allow_methods: AllowMethods, allow_origin: AllowOrigin, allow_private_network: AllowPrivateNetwork, expose_headers: ExposeHeaders, max_age: MaxAge, vary: Vary, } #[allow(clippy::declare_interior_mutable_const)] const WILDCARD: HeaderValue = HeaderValue::from_static("*"); impl CorsHeadersConfig { /// Create a new `CorsHeadersConfig`. /// /// No headers are sent by default. Use the builder methods to customize /// the behavior. /// /// You need to set at least an allowed origin for browsers to make /// successful cross-origin requests to your service. pub fn new() -> Self { Self { allow_credentials: Default::default(), allow_headers: Default::default(), allow_methods: Default::default(), allow_origin: Default::default(), allow_private_network: Default::default(), expose_headers: Default::default(), max_age: Default::default(), vary: Default::default(), } } /// A permissive configuration: /// /// - All request headers allowed. /// - All methods allowed. /// - All origins allowed. /// - All headers exposed. pub fn permissive() -> Self { Self::new() .allow_headers(Any) .allow_methods(Any) .allow_origin(Any) .expose_headers(Any) } /// A very permissive configuration: /// /// - **Credentials allowed.** /// - The method received in `Access-Control-Request-Method` is sent back /// as an allowed method. /// - The origin of the preflight request is sent back as an allowed origin. /// - The header names received in `Access-Control-Request-Headers` are sent /// back as allowed headers. /// - No headers are currently exposed, but this may change in the future. pub fn very_permissive() -> Self { Self::new() .allow_credentials(true) .allow_headers(AllowHeaders::mirror_request()) .allow_methods(AllowMethods::mirror_request()) .allow_origin(AllowOrigin::mirror_request()) } /// Set the [`Access-Control-Allow-Credentials`][mdn] header. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials pub fn allow_credentials(mut self, allow_credentials: T) -> Self where T: Into, { self.allow_credentials = allow_credentials.into(); self } /// Set the value of the [`Access-Control-Allow-Headers`][mdn] header. /// /// Note that multiple calls to this method will override any previous /// calls. /// /// Also note that `Access-Control-Allow-Headers` is required for requests that have /// `Access-Control-Request-Headers`. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers pub fn allow_headers(mut self, headers: T) -> Self where T: Into, { self.allow_headers = headers.into(); self } /// Set the value of the [`Access-Control-Max-Age`][mdn] header. /// /// By default the header will not be set which disables caching and will /// require a preflight call for all requests. /// /// Note that each browser has a maximum internal value that takes /// precedence when the Access-Control-Max-Age is greater. For more details /// see [mdn]. /// /// If you need more flexibility, you can use supply a function which can /// dynamically decide the max-age based on the origin and other parts of /// each preflight request, using `MaxAge::dynamic`. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age pub fn max_age(mut self, max_age: T) -> Self where T: Into, { self.max_age = max_age.into(); self } /// Set the value of the [`Access-Control-Allow-Methods`][mdn] header. /// /// Note that multiple calls to this method will override any previous /// calls. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods pub fn allow_methods(mut self, methods: T) -> Self where T: Into, { self.allow_methods = methods.into(); self } /// Set the value of the [`Access-Control-Allow-Origin`][mdn] header. /// /// You can also use a closure with `AllowOrigin::predicate` /// /// Note that multiple calls to this method will override any previous /// calls. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin pub fn allow_origin(mut self, origin: T) -> Self where T: Into, { self.allow_origin = origin.into(); self } /// Set the value of the [`Access-Control-Expose-Headers`][mdn] header. /// /// Note that multiple calls to this method will override any previous /// calls. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers pub fn expose_headers(mut self, headers: T) -> Self where T: Into, { self.expose_headers = headers.into(); self } /// Set the value of the [`Access-Control-Allow-Private-Network`][wicg] header. /// /// [wicg]: https://wicg.github.io/private-network-access/ pub fn allow_private_network(mut self, allow_private_network: T) -> Self where T: Into, { self.allow_private_network = allow_private_network.into(); self } /// Set the value(s) of the [`Vary`][mdn] header. /// /// In contrast to the other headers, this one has a non-empty default of /// [`preflight_request_headers()`]. /// /// You only need to set this is you want to remove some of these defaults, /// or if you use a closure for one of the other headers and want to add a /// vary header accordingly. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary pub fn vary(mut self, headers: T) -> Self where T: Into, { self.vary = headers.into(); self } } /// Represents a wildcard value (`*`) used with some CORS headers such as /// [`CorsHeadersConfig::allow_methods`]. #[derive(Debug, Clone, Copy)] #[must_use] pub struct Any; fn separated_by_commas(mut iter: I) -> Option where I: Iterator, { match iter.next() { Some(fst) => { let mut result = BytesMut::from(fst.as_bytes()); for val in iter { result.reserve(val.len() + 1); result.put_u8(b','); result.extend_from_slice(val.as_bytes()); } Some(HeaderValue::from_maybe_shared(result.freeze()).unwrap()) } None => None, } } impl Default for CorsHeadersConfig { fn default() -> Self { Self::new() } } impl CorsHeadersConfig { pub fn apply(&self, req: &RequestHeader, resp: &mut ResponseHeader) -> pingora::Result<()> { let origin = req.headers.get(&header::ORIGIN); // These headers are applied to both preflight and subsequent regular CORS requests: // https://fetch.spec.whatwg.org/#http-responses append_response_header(resp, self.allow_credentials.to_header(origin, req))?; append_response_header(resp, self.allow_private_network.to_header(origin, req))?; append_response_header(resp, self.vary.to_header())?; append_response_header(resp, self.allow_origin.to_header(origin, req))?; // Return results immediately upon preflight request if req.method == Method::OPTIONS { // These headers are applied only to preflight requests append_response_header(resp, self.allow_methods.to_header(req))?; append_response_header(resp, self.allow_headers.to_header(req))?; append_response_header(resp, self.max_age.to_header(origin, req))?; } else { // This header is applied only to non-preflight requests append_response_header(resp, self.expose_headers.to_header())?; } Ok(()) } } fn append_response_header( resp: &mut ResponseHeader, header: Option<(HeaderName, HeaderValue)>, ) -> pingora::Result<()> { if let Some((key, value)) = header { resp.append_header(key, value)?; } Ok(()) } pub fn ensure_usable_cors_rules(config: &CorsHeadersConfig) { if config.allow_credentials.is_true() { assert!( !config.allow_headers.is_wildcard(), "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \ with `Access-Control-Allow-Headers: *`" ); assert!( !config.allow_methods.is_wildcard(), "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \ with `Access-Control-Allow-Methods: *`" ); assert!( !config.allow_origin.is_wildcard(), "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \ with `Access-Control-Allow-Origin: *`" ); assert!( !config.expose_headers.is_wildcard(), "Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \ with `Access-Control-Expose-Headers: *`" ); } } /// Returns an iterator over the three request headers that may be involved in a CORS preflight request. /// /// This is the default set of header names returned in the `vary` header pub fn preflight_request_headers() -> impl Iterator { [ header::ORIGIN, header::ACCESS_CONTROL_REQUEST_METHOD, header::ACCESS_CONTROL_REQUEST_HEADERS, ] .into_iter() } ================================================ FILE: runtimes/core/src/api/cors/cors_headers_config/vary.rs ================================================ use http::header::{self, HeaderName, HeaderValue}; use super::preflight_request_headers; /// Holds configuration for how to set the [`Vary`][mdn] header. /// /// See [`CorsLayer::vary`] for more details. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary /// [`CorsLayer::vary`]: super::CorsLayer::vary #[derive(Clone, Debug)] pub struct Vary(Vec); impl Vary { /// Set the list of header names to return as vary header values /// /// See [`CorsLayer::vary`] for more details. /// /// [`CorsLayer::vary`]: super::CorsLayer::vary pub fn list(headers: I) -> Self where I: IntoIterator, { Self(headers.into_iter().map(Into::into).collect()) } pub(super) fn to_header(&self) -> Option<(HeaderName, HeaderValue)> { let values = &self.0; let mut res = values.first()?.as_bytes().to_owned(); for val in &values[1..] { res.extend_from_slice(b", "); res.extend_from_slice(val.as_bytes()); } let header_val = HeaderValue::from_bytes(&res) .expect("comma-separated list of HeaderValues is always a valid HeaderValue"); Some((header::VARY, header_val)) } } impl Default for Vary { fn default() -> Self { Self::list(preflight_request_headers()) } } impl From<[HeaderName; N]> for Vary { fn from(arr: [HeaderName; N]) -> Self { Self::list(arr) } } impl From> for Vary { fn from(vec: Vec) -> Self { Self::list(vec) } } ================================================ FILE: runtimes/core/src/api/cors/mod.rs ================================================ use crate::api::{auth, EndpointMap}; use crate::encore::runtime::v1 as pb; use crate::encore::runtime::v1::gateway::CorsAllowedOrigins; use anyhow::Context; use axum::http::{HeaderName, HeaderValue}; use http::header::{ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION, COOKIE}; use std::collections::HashSet; use std::str::FromStr; use self::cors_headers_config::{ensure_usable_cors_rules, CorsHeadersConfig}; pub mod cors_headers_config; #[cfg(test)] mod tests; /// The default set of allowed headers. #[allow(clippy::declare_interior_mutable_const)] const ALWAYS_ALLOWED_HEADERS: [HeaderName; 8] = [ HeaderName::from_static("accept"), HeaderName::from_static("authorization"), HeaderName::from_static("content-type"), HeaderName::from_static("origin"), HeaderName::from_static("user-agent"), HeaderName::from_static("x-correlation-id"), HeaderName::from_static("x-request-id"), HeaderName::from_static("x-requested-with"), ]; #[allow(clippy::declare_interior_mutable_const)] pub const ALWAYS_EXPOSED_HEADERS: [HeaderName; 3] = [ HeaderName::from_static("x-request-id"), HeaderName::from_static("x-correlation-id"), HeaderName::from_static("x-encore-trace-id"), ]; pub fn config(cfg: &pb::gateway::Cors, meta: MetaHeaders) -> anyhow::Result { let allow_any_headers = cfg.extra_allowed_headers.iter().any(|val| val == "*"); let allow_headers = if allow_any_headers { cors_headers_config::AllowHeaders::mirror_request() } else { let mut allowed_headers = cfg .extra_allowed_headers .iter() .map(|s| HeaderName::from_str(s)) .collect::, _>>() .context("failed to parse extra allowed headers")?; #[allow(clippy::borrow_interior_mutable_const)] allowed_headers.extend_from_slice(&ALWAYS_ALLOWED_HEADERS); allowed_headers.extend(meta.allow_headers); cors_headers_config::AllowHeaders::list(allowed_headers) }; let mut exposed_headers = cfg .extra_exposed_headers .iter() .map(|s| HeaderName::from_str(s)) .collect::, _>>() .context("failed to parse extra exposed headers")?; #[allow(clippy::borrow_interior_mutable_const)] exposed_headers.extend_from_slice(&ALWAYS_EXPOSED_HEADERS); exposed_headers.extend(meta.expose_headers); // Compute the allowed origins. let allow_origin = { use pb::gateway::cors::AllowedOriginsWithCredentials; let with_creds = match &cfg.allowed_origins_with_credentials { Some(AllowedOriginsWithCredentials::UnsafeAllowAllOriginsWithCredentials(true)) => { OriginSet::All } Some(AllowedOriginsWithCredentials::AllowedOrigins(list)) => { OriginSet::new(list.allowed_origins.clone()) } _ => OriginSet::Some(vec![]), }; let without_creds = { if let Some(CorsAllowedOrigins { allowed_origins }) = &cfg.allowed_origins_without_credentials { OriginSet::new(allowed_origins.to_vec()) } else { OriginSet::All } }; let request_has_creds = |req: &axum::http::request::Parts| -> bool { if req.headers.contains_key(AUTHORIZATION) || req.headers.contains_key(COOKIE) { return true; } if req.method == http::method::Method::OPTIONS { return req .headers .get_all(ACCESS_CONTROL_REQUEST_HEADERS) .iter() .any(|val| { val.to_str() .map(|val| { val.split(",") .map(|val| val.trim()) .any(|val| val == "authorization" || val == "cookie") }) .unwrap_or(false) }); } false }; let pred = move |origin: &HeaderValue, req: &axum::http::request::Parts| { let Ok(origin) = origin.to_str() else { return false; }; if request_has_creds(req) { with_creds.allows(origin) } else { without_creds.allows(origin) } }; pred }; let config = CorsHeadersConfig::new() .allow_private_network(cfg.allow_private_network_access) .allow_headers(allow_headers) .expose_headers(cors_headers_config::ExposeHeaders::list(exposed_headers)) .allow_credentials(!cfg.disable_credentials) .allow_methods(cors_headers_config::AllowMethods::mirror_request()) .allow_origin(cors_headers_config::AllowOrigin::predicate(allow_origin)); ensure_usable_cors_rules(&config); Ok(config) } enum OriginSet { All, Some(Vec), } impl OriginSet { fn new(origins: Vec) -> Self { let mut set = Vec::with_capacity(origins.len()); for o in origins { if o == "*" { return Self::All; } set.push(crate::api::cors::Origin::new(o)); } Self::Some(set) } fn allows(&self, origin: &str) -> bool { let origin = origin.to_lowercase(); match self { Self::All => true, Self::Some(origins) => origins.iter().any(|o| o.matches(&origin)), } } } enum Origin { Exact(String), Wildcard { prefix: String, suffix: String }, } impl Origin { fn new(origin: String) -> Self { match origin.split_once('*') { Some((prefix, suffix)) => Self::Wildcard { prefix: prefix.to_string(), suffix: suffix.to_string(), }, None => Self::Exact(origin), } } fn matches(&self, origin: &str) -> bool { match self { Self::Exact(exact) => origin == exact, Self::Wildcard { prefix, suffix } => { // Length must be greater than the prefix and suffix combined, // to ensure the wildcard matches at least one character. origin.len() > (prefix.len() + suffix.len()) && origin.starts_with(prefix) && origin.ends_with(suffix) } } } } /// Additional CORS configuration based on the app metadata. pub struct MetaHeaders { pub allow_headers: HashSet, pub expose_headers: HashSet, } impl MetaHeaders { pub fn from_schema(endpoints: &EndpointMap, auth: Option<&auth::Authenticator>) -> Self { let mut allow_headers = HashSet::new(); let mut expose_headers = HashSet::new(); for ep in endpoints.values() { if !ep.exposed { continue; } for h in ep.request.iter().flat_map(|req| req.header.iter()) { allow_headers.extend(h.header_names()); } expose_headers.extend(ep.response.header.iter().flat_map(|h| h.header_names())); } // If we have an auth handler, add the auth headers to the allow list. if let Some(auth) = auth { allow_headers.extend(auth.schema().header.iter().flat_map(|h| h.header_names())); } Self { allow_headers, expose_headers, } } } ================================================ FILE: runtimes/core/src/api/cors/tests.rs ================================================ use http::header::{ ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AUTHORIZATION, CONTENT_TYPE, ORIGIN, }; use pingora::http::{RequestHeader, ResponseHeader}; use super::*; fn check_origins(cors: &CorsHeadersConfig, creds: bool, good: bool, origins: &[HeaderValue]) { for origin in origins { let mut req = RequestHeader::build("OPTIONS", b"/", None).expect("construct request"); req.insert_header(ORIGIN, origin) .expect("insert origin header"); if creds { req.insert_header(ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION) .expect("insert access-control-request-headers"); } let mut resp = ResponseHeader::build(200, None).expect("construct response"); cors.apply(&req, &mut resp).expect("apply cors config"); let allow_origin = resp.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN); let allowed = allow_origin.map(|val| val == origin).unwrap_or(false); if allowed != good { panic!("origin={origin:?} creds={creds}: got allowed={allowed}, want {good}"); } else { println!("origin={origin:?} creds={creds}: ok allowed={allowed}"); } } } fn check_headers(cors: &CorsHeadersConfig, allowed_headers: &[HeaderName], want_ok: bool) { let mut req = RequestHeader::build("OPTIONS", b"/", None).expect("construct request"); req.insert_header(ORIGIN, HeaderValue::from_static("https://example.org")) .expect("insert origin header"); req.insert_header( ACCESS_CONTROL_REQUEST_METHOD, HeaderValue::from_static("GET"), ) .expect("insert access-control-request-method"); let allowed_headers_val = HeaderValue::from_bytes(allowed_headers.join(", ").as_bytes()) .expect("create access-control-request-headers value"); req.insert_header(ACCESS_CONTROL_REQUEST_HEADERS, allowed_headers_val) .expect("insert access-control-request-headers"); let mut resp = ResponseHeader::build(200, None).expect("construct response"); cors.apply(&req, &mut resp).expect("apply cors headers"); if !allowed_headers.is_empty() && want_ok { // check that the CORS response is valid (e.g if its a request with credentials) assert!( resp.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).is_some(), "expected cors request to be valid, but access-control-allow-origin is not set" ) } // check allowed headers. let allow_headers_val = resp .headers .get(ACCESS_CONTROL_ALLOW_HEADERS) .expect("access-control-allow-headers to be present") .to_str() .expect("convert to str"); let allow_headers: HashSet = allow_headers_val .split(",") .map(|val| HeaderName::from_bytes(val.trim().as_bytes()).expect("construct header name")) .collect(); if want_ok { for val in allowed_headers { assert!( allow_headers.contains(val), "want header {val:?} to be allowed, got false; resp header={allow_headers_val}" ) } } else { assert_ne!( allow_headers_val, "", "want headers not to be allowed, got {allow_headers_val}" ); } } struct TestCase<'a> { cors_cfg: pb::gateway::Cors, creds_good_origins: &'a [HeaderValue], creds_bad_origins: &'a [HeaderValue], nocreds_good_origins: &'a [HeaderValue], nocreds_bad_origins: &'a [HeaderValue], good_headers: &'a [HeaderName], bad_headers: &'a [HeaderName], } fn run_test_case(test_case: TestCase) { let meta = MetaHeaders { allow_headers: [HeaderName::from_static("x-static-test")].into(), expose_headers: [HeaderName::from_static("x-exposed-test")].into(), }; let cors = config(&test_case.cors_cfg, meta).expect("run cors config"); check_origins(&cors, true, true, test_case.creds_good_origins); check_origins(&cors, true, false, test_case.creds_bad_origins); check_origins(&cors, false, true, test_case.nocreds_good_origins); check_origins(&cors, false, false, test_case.nocreds_bad_origins); check_headers(&cors, test_case.good_headers, true); for header in test_case.bad_headers { let mut test_headers = test_case.good_headers.to_vec(); test_headers.push(header.clone()); check_headers(&cors, &test_headers, false); } } #[test] fn test_empty() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: None, extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static("evil.com"), HeaderValue::from_static("localhost"), ], nocreds_good_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static("localhost"), HeaderValue::from_static(""), HeaderValue::from_static("icanhazcheezburger.com"), ], nocreds_bad_origins: &[], good_headers: &[CONTENT_TYPE, ORIGIN], bad_headers: &[ HeaderName::from_static("x-requested-with"), HeaderName::from_static("x-forwarded-for"), ], }); } #[test] fn test_allowed_creds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: Some( pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins( pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("localhost"), String::from("ok.org")], }, ), ), allowed_origins_without_credentials: None, extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[ HeaderValue::from_static("localhost"), HeaderValue::from_static("ok.org"), ], creds_bad_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static("evil.com"), ], nocreds_good_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static("localhost"), HeaderValue::from_static(""), HeaderValue::from_static("icanhazcheezburger.com"), HeaderValue::from_static("ok.org"), ], nocreds_bad_origins: &[], good_headers: &[], bad_headers: &[], }); } #[test] fn test_allowed_glob_creds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: Some( pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins( pb::gateway::CorsAllowedOrigins { allowed_origins: vec![ String::from("https://*.example.com"), String::from("wss://ok1-*.example.com"), String::from("https://*-ok2.example.com"), ], }, ), ), allowed_origins_without_credentials: None, extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[ HeaderValue::from_static("https://foo.example.com"), HeaderValue::from_static("wss://ok1-foo.example.com"), HeaderValue::from_static("https://foo-ok2.example.com"), ], creds_bad_origins: &[ HeaderValue::from_static("http://foo.example.com"), // Wrong scheme HeaderValue::from_static("htts://.example.com"), // No subdomain HeaderValue::from_static("ws://ok1-foo.example.com"), // Wrong scheme HeaderValue::from_static(".example.com"), // No scheme HeaderValue::from_static("https://evil.com"), // bad domain ], nocreds_good_origins: &[], nocreds_bad_origins: &[], good_headers: &[], bad_headers: &[], }); } #[test] fn test_allowed_nocreds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("localhost"), String::from("ok.org")], }), extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[ HeaderValue::from_static("localhost"), HeaderValue::from_static("ok.org"), HeaderValue::from_static("foo.com"), HeaderValue::from_static("evil.com"), ], nocreds_good_origins: &[ HeaderValue::from_static("localhost"), HeaderValue::from_static("ok.org"), ], nocreds_bad_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static(""), HeaderValue::from_static("icanhazcheezburger.com"), ], good_headers: &[], bad_headers: &[], }); } #[test] fn test_allowed_disjoint_sets() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: Some( pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins( pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("foo.com")], }, ), ), allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("bar.org")], }), extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[HeaderValue::from_static("foo.com")], creds_bad_origins: &[ HeaderValue::from_static("bar.org"), HeaderValue::from_static(""), HeaderValue::from_static("localhost"), ], nocreds_good_origins: &[HeaderValue::from_static("bar.org")], nocreds_bad_origins: &[ HeaderValue::from_static("foo.com"), HeaderValue::from_static(""), HeaderValue::from_static("localhost"), ], good_headers: &[], bad_headers: &[], }); } #[test] fn test_allowed_wildcard_without_creds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("*")], }), extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[ HeaderValue::from_static("bar.org"), HeaderValue::from_static(""), HeaderValue::from_static("localhost"), ], nocreds_good_origins: &[ HeaderValue::from_static("bar.org"), HeaderValue::from_static("bar.com"), HeaderValue::from_static(""), HeaderValue::from_static("localhost"), ], nocreds_bad_origins: &[], good_headers: &[], bad_headers: &[], }); } #[test] fn test_allowed_unsafe_wildcard_with_creds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: Some( pb::gateway::cors::AllowedOriginsWithCredentials::UnsafeAllowAllOriginsWithCredentials(true), ), allowed_origins_without_credentials: None, extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[ HeaderValue::from_static("bar.org"), HeaderValue::from_static("bar.com"), HeaderValue::from_static(""), HeaderValue::from_static("localhost"), HeaderValue::from_static("unsafe.evil.com"), ], creds_bad_origins: &[], nocreds_good_origins: &[], nocreds_bad_origins: &[], good_headers: &[], bad_headers: &[], }); } #[test] fn test_extra_headers() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: None, extra_allowed_headers: vec![ "Not-Authorization".to_string(), "X-Forwarded-For".to_string(), "X-Real-Ip".to_string(), ], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[], nocreds_good_origins: &[], nocreds_bad_origins: &[], good_headers: &[ CONTENT_TYPE, ORIGIN, HeaderName::from_static("x-requested-with"), HeaderName::from_static("x-real-ip"), HeaderName::from_static("not-authorization"), ], bad_headers: &[ HeaderName::from_static("x-forwarded-for"), HeaderName::from_static("x-evil-header"), HeaderName::from_static("authorization"), ], }); } #[test] fn test_extra_headers_wildcard() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: None, extra_allowed_headers: vec![ "X-Forwarded-For".to_string(), "*".to_string(), "X-Real-Ip".to_string(), ], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[], nocreds_good_origins: &[], nocreds_bad_origins: &[], good_headers: &[ CONTENT_TYPE, ORIGIN, HeaderName::from_static("x-requested-with"), HeaderName::from_static("x-real-ip"), HeaderName::from_static("x-forwarded-for"), HeaderName::from_static("x-evil-header"), HeaderName::from_static("not-authorization"), ], bad_headers: &[HeaderName::from_static("authorization")], }); } #[test] fn test_static_headers() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: None, allowed_origins_without_credentials: None, extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[], nocreds_good_origins: &[], nocreds_bad_origins: &[], good_headers: &[ CONTENT_TYPE, ORIGIN, HeaderName::from_static("x-static-test"), ], bad_headers: &[], }); } #[test] fn test_wildcard_without_creds() { run_test_case(TestCase { cors_cfg: pb::gateway::Cors { debug: false, disable_credentials: false, allowed_origins_with_credentials: Some( pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins( pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("https://vercel.app")], }, ), ), allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins { allowed_origins: vec![String::from("https://*-foo.vercel.app")], }), extra_allowed_headers: vec![], extra_exposed_headers: vec![], allow_private_network_access: false, }, creds_good_origins: &[], creds_bad_origins: &[HeaderValue::from_static("https://blah-foo.vercel.app")], nocreds_good_origins: &[HeaderValue::from_static("https://blah-foo.vercel.app")], nocreds_bad_origins: &[], good_headers: &[], bad_headers: &[], }); } ================================================ FILE: runtimes/core/src/api/encore_routes/healthz.rs ================================================ use axum::extract::Request; use axum::response::{IntoResponse, Json}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct Handler { pub app_revision: String, pub deploy_id: String, } impl Handler { pub fn health_check(self) -> Response { log::trace!(code = "ok"; "handling incoming health check request"); Response { code: "ok".into(), message: "Your Encore app is up and running!".into(), details: Details { app_revision: self.app_revision, encore_compiler: "".into(), deploy_id: self.deploy_id, checks: vec![], enabled_experiments: vec![], }, } } } impl axum::handler::Handler<(), ()> for Handler { type Future = std::pin::Pin< Box> + Send>, >; fn call(self, _req: Request, _state: ()) -> Self::Future { let resp = self.health_check(); Box::pin(async move { Json(resp).into_response() }) } } #[derive(Serialize, Deserialize)] pub struct Response { pub code: String, pub message: String, pub details: Details, } #[derive(Serialize, Deserialize)] pub struct Details { pub app_revision: String, pub encore_compiler: String, pub deploy_id: String, pub checks: Vec, pub enabled_experiments: Vec, } #[derive(Serialize, Deserialize)] pub struct CheckResult { pub name: String, pub passed: bool, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } ================================================ FILE: runtimes/core/src/api/encore_routes/mod.rs ================================================ use axum::routing; use crate::pubsub; pub mod healthz; pub struct Desc { pub healthz: healthz::Handler, pub push_registry: pubsub::PushHandlerRegistry, } impl Desc { pub fn router(self) -> axum::Router<()> { axum::Router::new() .route("/__encore/healthz", routing::any(self.healthz)) .route( "/__encore/pubsub/push/:subscription_id", routing::any(self.push_registry), ) } } ================================================ FILE: runtimes/core/src/api/endpoint.rs ================================================ use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; use anyhow::Context; use axum::extract::{FromRequestParts, WebSocketUpgrade}; use axum::http::HeaderValue; use axum::response::IntoResponse; use bytes::{BufMut, BytesMut}; use http::HeaderMap; use indexmap::IndexMap; use percent_encoding::percent_decode_str; use serde::Serialize; use crate::api::reqauth::{platform, svcauth, CallMeta}; use crate::api::schema::encoding::{ handshake_encoding, request_encoding, response_encoding, HandshakeSchemaUnderConstruction, ReqSchemaUnderConstruction, SchemaUnderConstruction, }; use crate::api::schema::{JSONPayload, Method}; use crate::api::{jsonschema, schema, ErrCode, Error}; use crate::encore::parser::meta::v1::rpc; use crate::encore::parser::meta::v1::{self as meta, selector}; use crate::log::LogFromRust; use crate::metrics::counter; use crate::model::StreamDirection; use crate::names::EndpointName; use crate::trace; use crate::{model, Hosted}; use super::pvalue::{PValue, PValues}; use super::reqauth::caller::Caller; /// Cached environment variable for whether to include error stacks in error response logs. static ENCORE_LOG_INCLUDE_ERROR_STACK: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { std::env::var("ENCORE_LOG_INCLUDE_ERROR_STACK") .map(|v| !v.is_empty() && v != "0") .unwrap_or(false) }); #[derive(Debug)] pub struct SuccessResponse { pub status: axum::http::StatusCode, pub headers: axum::http::HeaderMap, pub body: Option, } /// Represents the result of calling an API endpoint. pub type Response = APIResult; pub type APIResult = Result; impl IntoResponse for SuccessResponse { fn into_response(self) -> axum::http::Response { // Serialize the response body. // Use a small initial capacity of 128 bytes like serde_json::to_vec // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189 let bld = { let mut bld = axum::http::Response::builder(); *(bld.headers_mut().unwrap()) = self.headers; bld } .status(self.status); match self.body { Some(body) => { let bld = bld.header( axum::http::header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), ); let mut buf = BytesMut::with_capacity(128).writer(); match serde_json::to_writer(&mut buf, &body) { Ok(()) => bld .body(axum::body::Body::from(buf.into_inner().freeze())) .unwrap(), Err(err) => Error::internal(err).to_response(None), } } None => bld.body(axum::body::Body::empty()).unwrap(), } } } pub trait ToResponse { fn to_response(&self, caller: Option) -> axum::response::Response; } impl ToResponse for Error { fn to_response(&self, caller: Option) -> axum::http::Response { // considure response to be external if caller is gateway, or if the caller is // unknown let internal_call = caller.map(|caller| !caller.is_gateway()).unwrap_or(false); let mut buf = BytesMut::with_capacity(128).writer(); if internal_call { serde_json::to_writer(&mut buf, &self).unwrap(); } else { serde_json::to_writer(&mut buf, &self.as_external()).unwrap(); } axum::http::Response::builder() .status::(self.code.into()) .header( axum::http::header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), ) .body(axum::body::Body::from(buf.into_inner().freeze())) .unwrap() } } pub type HandlerRequest = Arc; pub type HandlerResponse = APIResult; pub struct HandlerResponseInner { pub payload: JSONPayload, pub extra_headers: Option, pub status: Option, } /// A trait for handlers that accept a request and return a response. pub trait TypedHandler: Send + Sync + 'static { fn call( self: Arc, req: HandlerRequest, ) -> Pin + Send + 'static>>; } /// A trait for handlers that accept a request and return a response. pub trait BoxedHandler: Send + Sync + 'static { fn call( self: Arc, req: HandlerRequest, ) -> Pin + Send + 'static>>; } pub enum ResponseData { Typed(HandlerResponse), Raw(axum::http::Response), } /// Schema variations for stream handshake #[derive(Debug)] pub enum HandshakeSchema { // Handshake with only a path, no parseable data Path(schema::Path), // Handshake with a request schema Request(schema::Request), } impl HandshakeSchema { pub fn path(&self) -> &schema::Path { match self { HandshakeSchema::Path(path) => path, HandshakeSchema::Request(schema::Request { path, .. }) => path, } } } /// Represents a single API Endpoint. #[derive(Debug)] pub struct Endpoint { pub name: EndpointName, pub path: meta::Path, pub handshake: Option>, pub request: Vec>, pub response: Arc, /// Whether this is a raw endpoint. pub raw: bool, /// Whether the service is exposed publicly. pub exposed: bool, /// Whether the service requires authentication data. pub requires_auth: bool, /// The maximum size of the request body. /// If None, no limits are applied. pub body_limit: Option, /// The static assets to serve from this endpoint. /// Set only for static asset endpoints. pub static_assets: Option, /// The tags for this endpoint. pub tags: Vec, /// Treat endpoint as sensitive and redact details from traces. pub sensitive: bool, } impl Endpoint { pub fn methods(&self) -> impl Iterator + '_ { self.request .iter() .flat_map(|schema| schema.methods.iter().copied()) } } #[derive(Debug, Serialize, Clone)] pub struct RequestPayload { #[serde(flatten)] pub path: Option>, #[serde(flatten)] pub query: Option, #[serde(flatten)] pub header: Option, #[serde(flatten)] pub cookie: Option, #[serde(flatten, skip_serializing_if = "Body::is_raw")] pub body: Body, } #[derive(Debug, Serialize, Clone)] #[serde(untagged)] pub enum Body { Typed(Option), #[serde(skip)] Raw(Arc>>), } impl Body { pub fn is_raw(&self) -> bool { matches!(self, Body::Raw(_)) } } #[derive(Debug, Serialize, Clone)] pub struct ResponsePayload { #[serde(flatten)] pub header: Option, #[serde(flatten)] pub cookie: Option, #[serde(flatten, skip_serializing_if = "Body::is_raw")] pub body: Body, } pub type EndpointMap = HashMap>; /// Compute a set of endpoint descriptions based on metadata /// and a list of which endpoints are hosted by this runtime. pub fn endpoints_from_meta( md: &meta::Data, hosted_services: &Hosted, ) -> anyhow::Result<(Arc, Vec)> { let mut registry_builder = jsonschema::Builder::new(md); struct EndpointUnderConstruction<'a> { svc: &'a meta::Service, ep: &'a meta::Rpc, handshake_schema: Option, request_schemas: Vec, response_schema: SchemaUnderConstruction, } // Compute the schemas for each endpoint. let mut endpoints = Vec::new(); let mut hosted_endpoints = Vec::new(); for svc in &md.svcs { let is_hosted = hosted_services.contains(&svc.name); for ep in &svc.rpcs { // If this endpoint is hosted, mark it as such. if is_hosted { hosted_endpoints.push(EndpointName::new(&svc.name, &ep.name)); } let handshake_schema = handshake_encoding(&mut registry_builder, md, ep)?; let request_schemas = request_encoding(&mut registry_builder, md, ep)?; let response_schema = response_encoding(&mut registry_builder, md, ep)?; endpoints.push(EndpointUnderConstruction { svc, ep, handshake_schema, request_schemas, response_schema, }); } } let registry = registry_builder.build(); let mut endpoint_map = EndpointMap::with_capacity(endpoints.len()); for ep in endpoints { let mut request_schemas = Vec::with_capacity(ep.request_schemas.len()); let raw = rpc::Protocol::try_from(ep.ep.proto).is_ok_and(|p| p == rpc::Protocol::Raw); let handshake_schema = ep .handshake_schema .map(|schema| schema.build(®istry)) .transpose()?; let handshake = handshake_schema .map(|handshake_schema| -> anyhow::Result> { let path = handshake_schema .schema .path .context("endpoint must have a path defined")?; let handshake_schema = if handshake_schema.parse_data { let handshake_schema = schema::Request { methods: vec![], path, header: handshake_schema.schema.header, query: handshake_schema.schema.query, body: schema::RequestBody::Typed(None), cookie: handshake_schema.schema.cookie, stream: false, }; HandshakeSchema::Request(handshake_schema) } else { HandshakeSchema::Path(path) }; Ok(Arc::new(handshake_schema)) }) .transpose()?; for req_schema in ep.request_schemas { let req_schema = req_schema.build(®istry)?; let path = req_schema .schema .path .or_else(|| handshake.as_ref().map(|hs| hs.path().clone())) .context("endpoint must have path defined")?; request_schemas.push(Arc::new(schema::Request { methods: req_schema.methods, path, header: req_schema.schema.header, query: req_schema.schema.query, cookie: req_schema.schema.cookie, body: if raw { schema::RequestBody::Raw } else { schema::RequestBody::Typed(req_schema.schema.body) }, stream: ep.ep.streaming_request, })); } let resp_schema = ep.response_schema.build(®istry)?; // We only support a single gateway right now. let exposed = ep.ep.expose.contains_key("api-gateway"); let raw = rpc::Protocol::try_from(ep.ep.proto).is_ok_and(|proto| proto == rpc::Protocol::Raw); let tags = ep .ep .tags .iter() .filter(|item| item.r#type() == selector::Type::Tag) .map(|item| item.value.clone()) .collect(); let endpoint = Endpoint { name: EndpointName::new(ep.svc.name.clone(), ep.ep.name.clone()), path: ep.ep.path.clone().unwrap_or_else(|| meta::Path { r#type: meta::path::Type::Url as i32, segments: vec![meta::PathSegment { r#type: meta::path_segment::SegmentType::Literal as i32, value_type: meta::path_segment::ParamType::String as i32, value: format!("/{}.{}", ep.ep.service_name, ep.ep.name), validation: None, }], }), handshake, request: request_schemas, response: Arc::new(schema::Response { header: resp_schema.header, cookie: resp_schema.cookie, body: resp_schema.body, http_status: resp_schema.http_status, stream: ep.ep.streaming_response, }), raw, exposed, requires_auth: !ep.ep.allow_unauthenticated, body_limit: ep.ep.body_limit, static_assets: ep.ep.static_assets.clone(), tags, sensitive: ep.ep.sensitive, }; endpoint_map.insert( EndpointName::new(&ep.svc.name, &ep.ep.name), Arc::new(endpoint), ); } Ok((Arc::new(endpoint_map), hosted_endpoints)) } pub(super) struct EndpointHandler { pub endpoint: Arc, pub handler: Arc, pub shared: Arc, pub requests_total: counter::Schema, } #[derive(Debug)] pub(super) struct SharedEndpointData { pub tracer: trace::Tracer, pub platform_auth: Arc, pub inbound_svc_auth: Vec>, /// The schema to use when parsing auth data, if any. /// NOTE: This assumes there's at most a single API Gateway. /// When we support multiple this needs to be made into a map, and the /// correct schema looked up based on the gateway being used. pub auth_data_schemas: HashMap>, } impl Clone for EndpointHandler { fn clone(&self) -> Self { Self { endpoint: self.endpoint.clone(), handler: self.handler.clone(), shared: self.shared.clone(), requests_total: self.requests_total.clone(), } } } impl EndpointHandler { async fn parse_request( &self, axum_req: axum::extract::Request, ) -> APIResult> { let method = axum_req.method(); // Method conversion should never fail since we only register valid methods. let api_method = Method::try_from(method.clone()).expect("invalid method"); let req_schema = self .endpoint .request .iter() .find(|schema| schema.methods.contains(&api_method)) .expect("request schema must exist for all endpoints"); let streaming_request = req_schema.stream; let streaming_response = self.endpoint.response.stream; let stream_direction = match (streaming_request, streaming_response) { (true, true) => Some(StreamDirection::InOut), (true, false) => Some(StreamDirection::In), (false, true) => Some(StreamDirection::Out), (false, false) => None, }; let (mut parts, body) = axum_req .map(|b| match self.endpoint.body_limit { None => b, Some(limit) => { axum::body::Body::new(http_body_util::Limited::new(b, limit as usize)) } }) .into_parts(); // Authenticate the request from the platform, if applicable. #[allow(clippy::manual_unwrap_or_default)] let platform_seal_of_approval = match self.authenticate_platform(&parts) { Ok(seal) => seal, Err(_err) => None, }; let meta = CallMeta::parse_with_caller( &self.shared.inbound_svc_auth, &parts.headers, &self.shared.auth_data_schemas, )?; let parsed_payload = if let Some(handshake_schema) = &self.endpoint.handshake { match handshake_schema.as_ref() { HandshakeSchema::Request(req_schema) => { req_schema.extract(&mut parts, body).await? } HandshakeSchema::Path(_) => None, } } else { req_schema.extract(&mut parts, body).await? }; // Extract caller information. let (internal_caller, auth_user_id, auth_data) = match meta.internal { Some(internal) => (Some(internal.caller), internal.auth_uid, internal.auth_data), None => (None, None, None), }; let trace_id = meta.trace_id; let span_id = meta.this_span_id.unwrap_or_else(model::SpanId::generate); let span = trace_id.with_span(span_id); let parent_span = meta.parent_span_id.map(|sp| trace_id.with_span(sp)); let traced = if platform_seal_of_approval.is_some() { true } else { meta.trace_sampled .unwrap_or_else(|| self.shared.tracer.should_sample(&self.endpoint.name)) }; let data = if let Some(direction) = stream_direction { let websocket_upgrade = Mutex::new(Some( WebSocketUpgrade::from_request_parts(&mut parts, &()).await?, )); model::RequestData::Stream(model::StreamRequestData { endpoint: self.endpoint.clone(), path: parts.uri.path().to_string(), path_and_query: parts .uri .path_and_query() .map(|q| q.to_string()) .unwrap_or_default(), path_params: parsed_payload.as_ref().and_then(|p| p.path.clone()), req_headers: parts.headers, auth_user_id, auth_data, parsed_payload, websocket_upgrade, direction, }) } else { model::RequestData::RPC(model::RPCRequestData { endpoint: self.endpoint.clone(), method: api_method, path: parts.uri.path().to_string(), path_and_query: parts .uri .path_and_query() .map(|q| q.to_string()) .unwrap_or_default(), path_params: parsed_payload.as_ref().and_then(|p| p.path.clone()), req_headers: parts.headers, auth_user_id, auth_data, parsed_payload, }) }; let request = Arc::new(model::Request { span, parent_trace: None, parent_span, caller_event_id: meta.parent_event_id, ext_correlation_id: meta.ext_correlation_id, start: tokio::time::Instant::now(), start_time: std::time::SystemTime::now(), is_platform_request: platform_seal_of_approval.is_some(), internal_caller, data, traced, }); Ok(request) } fn handle( self, axum_req: axum::extract::Request, ) -> Pin> + Send + 'static>> { Box::pin(async move { let request = match self.parse_request(axum_req).await { Ok(req) => req, Err(err) => return err.to_response(None), }; let internal_caller = request.internal_caller.clone(); let sensitive = self.endpoint.sensitive; // If the endpoint isn't exposed, return a 404. if !self.endpoint.exposed && !request.allows_private_endpoint_call() { return Error { code: ErrCode::NotFound, message: "endpoint not found".into(), internal_message: Some("the endpoint was found, but is not exposed".into()), stack: None, details: None, } .to_response(internal_caller); } else if self.endpoint.requires_auth && !request.has_authenticated_user() { return Error { code: ErrCode::Unauthenticated, message: "endpoint requires auth but none provided".into(), internal_message: None, stack: None, details: None, } .to_response(internal_caller); } let logger = crate::log::root(); logger.info(Some(&request), "starting request", None); self.shared.tracer.request_span_start(&request, sensitive); let resp: ResponseData = self.handler.call(request.clone()).await; let duration = tokio::time::Instant::now().duration_since(request.start); // If we had a request failure, log that separately. if let ResponseData::Typed(Err(err)) = &resp { logger.error(Some(&request), "request failed", Some(err), { let mut fields = crate::log::Fields::new(); fields.insert( "code".into(), serde_json::Value::String(err.code.to_string()), ); if let Some(internal_message) = &err.internal_message { fields.insert( "internal_message".into(), serde_json::Value::String(internal_message.clone()), ); } if *ENCORE_LOG_INCLUDE_ERROR_STACK { if let Some(stack) = &err.stack { if let Ok(value) = serde_json::to_value(stack) { fields.insert("stack".into(), value); } } } Some(fields) }); } let code = match &resp { ResponseData::Typed(Ok(_)) => "ok".to_string(), ResponseData::Typed(Err(err)) => err.code.to_string(), ResponseData::Raw(resp) => ErrCode::from(resp.status()).to_string(), }; logger.info(Some(&request), "request completed", { let mut fields = crate::log::Fields::new(); let dur_ms = (duration.as_secs() as f64 * 1000f64) + (duration.subsec_nanos() as f64 / 1_000_000f64); fields.insert( "duration".into(), serde_json::Value::Number(serde_json::Number::from_f64(dur_ms).unwrap_or_else( || { // Fall back to integer if the f64 conversion fails serde_json::Number::from(duration.as_millis() as u64) }, )), ); fields.insert("code".into(), serde_json::Value::String(code.clone())); Some(fields) }); let (mut encoded_resp, resp_payload, extra_headers, error) = match resp { ResponseData::Raw(resp) => (resp, None, None, None), ResponseData::Typed(Ok(response)) => ( self.endpoint .response .encode(&response.payload, response.status.unwrap_or(200)) .unwrap_or_else(|err| err.to_response(internal_caller)), Some(response.payload), response.extra_headers, None, ), ResponseData::Typed(Err(err)) => ( err.as_ref().to_response(internal_caller), None, None, Some(err), ), }; { let model_resp = model::Response { request: request.clone(), duration, data: model::ResponseData::RPC(model::RPCResponseData { status_code: encoded_resp.status().as_u16(), resp_payload, error, resp_headers: encoded_resp.headers().clone(), }), }; self.shared.tracer.request_span_end(&model_resp, sensitive); self.requests_total.with([("code", code)]).increment(); } if let Ok(val) = HeaderValue::from_str(request.span.0.serialize_encore().as_str()) { encoded_resp.headers_mut().insert("x-encore-trace-id", val); } if let Some(extra_headers) = extra_headers { encoded_resp.headers_mut().extend(extra_headers) } encoded_resp }) } fn authenticate_platform( &self, req: &axum::http::request::Parts, ) -> Result, platform::ValidationError> { let Some(x_encore_auth_header) = req.headers.get("x-encore-auth") else { return Ok(None); }; let x_encore_auth_header = x_encore_auth_header .to_str() .map_err(|_| platform::ValidationError::InvalidMac)?; let Some(date_header) = req.headers.get("Date") else { return Err(platform::ValidationError::InvalidDateHeader); }; let date_header = date_header .to_str() .map_err(|_| platform::ValidationError::InvalidDateHeader)?; let request_path = percent_decode_str(req.uri.path()).decode_utf8_lossy(); let req = platform::ValidationData { request_path: &request_path, date_header, x_encore_auth_header, }; self.shared .platform_auth .validate_platform_request(&req) .map(Some) } } impl axum::handler::Handler<(), ()> for EndpointHandler { type Future = Pin> + Send + 'static>>; fn call(self, axum_req: axum::extract::Request, _state: ()) -> Self::Future { self.handle(axum_req) } } pub fn path_supports_tsr(path: &str) -> bool { path != "/" && !path.ends_with('/') && !path.contains("/*") } ================================================ FILE: runtimes/core/src/api/error.rs ================================================ use std::fmt::{Debug, Display}; use std::str::FromStr; use crate::api::jsonschema; use crate::error::{AppError, StackTrace}; use axum::extract::ws::rejection::WebSocketUpgradeRejection; use serde::ser::SerializeStruct; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use super::jsonschema::JSONSchema; use super::PValues; /// Represents an API Error. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Error { pub code: ErrCode, pub message: String, pub internal_message: Option, #[serde(deserialize_with = "deserialize_details_as_any")] pub details: ErrDetails, #[serde(skip_serializing)] pub stack: Option, } pub type ErrDetails = Option>; // We don't have a schema for error details, so we do best effort to deserialize them. // Special types like Date will be lost, as we can't know if its a Date or a string. fn deserialize_details_as_any<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(JSONSchema::any() .deserialize(deserializer, jsonschema::DecodeConfig::default()) .map(|pvalues| Some(Box::new(pvalues))) .unwrap_or(None)) } /// ErrorExternal hides internal information on `Error` when it serializes #[derive(Debug)] pub struct ExternalError<'a>(&'a Error); /// Cached environment variable for whether to include internal message in all errors. static ENCORE_API_INCLUDE_INTERNAL_MESSAGE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { std::env::var("ENCORE_API_INCLUDE_INTERNAL_MESSAGE") .map(|v| !v.is_empty() && v != "0") .unwrap_or(false) }); /// Cached environment variable for whether to include the stack in error responses. static ENCORE_API_INCLUDE_ERROR_STACK: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { std::env::var("ENCORE_API_INCLUDE_ERROR_STACK") .map(|v| !v.is_empty() && v != "0") .unwrap_or(false) }); impl Serialize for ExternalError<'_> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut error = serializer.serialize_struct("Error", 3)?; error.serialize_field("code", &self.0.code)?; error.serialize_field("message", &self.0.message)?; error.serialize_field("details", &self.0.details)?; // Include internal message even in external errors iff the environment variable is set. if *ENCORE_API_INCLUDE_INTERNAL_MESSAGE { error.serialize_field("internal_message", &self.0.internal_message)?; } // Do the same for stack traces. if *ENCORE_API_INCLUDE_ERROR_STACK { error.serialize_field("stack", &self.0.stack)?; } error.end() } } impl Error { pub fn as_external(&self) -> ExternalError<'_> { ExternalError(self) } pub fn internal(cause: E) -> Self where E: Into, { Self { code: ErrCode::Internal, message: ErrCode::Internal.default_public_message().into(), internal_message: Some(format!("{:#?}", cause.into())), stack: None, details: None, } } pub fn invalid_argument(public_msg: S, cause: E) -> Self where S: Into, E: Into, { Self { code: ErrCode::InvalidArgument, message: format!("{}: {:?}", public_msg.into(), cause.into()), internal_message: None, stack: None, details: None, } } pub fn not_found(public_msg: S) -> Self where S: Into, { Self { code: ErrCode::NotFound, message: public_msg.into(), internal_message: None, stack: None, details: None, } } pub fn unauthenticated() -> Self { Self { code: ErrCode::Unauthenticated, message: ErrCode::Unauthenticated.default_public_message().into(), internal_message: None, stack: None, details: None, } } } impl From for Error { fn from(value: WebSocketUpgradeRejection) -> Self { Error { code: value.status().into(), message: value.body_text(), internal_message: Some(value.body_text()), stack: None, details: None, } } } impl From for AppError { fn from(val: Error) -> Self { let message = match val.internal_message { Some(ref internal_msg) => format!("{}: {}", val.message, internal_msg), None => val.message, }; AppError::new(message) } } impl From<&Error> for AppError { fn from(val: &Error) -> Self { let message = match val.internal_message { Some(ref internal_msg) => format!("{}: {}", val.message, internal_msg), None => val.message.clone(), }; // TODO: capture the JS stack trace for this error AppError { message, stack: vec![], cause: None, } } } impl From for Box { fn from(err: Error) -> Self { pingora::Error::because( pingora::ErrorType::HTTPStatus(err.code.status_code().into()), err.code.to_string(), err, ) } } impl std::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.internal_message { Some(msg) => write!(f, "{msg}"), None => write!(f, "{}", self.message), } } } impl AsRef for Error { fn as_ref(&self) -> &Self { self } } /// Represents an API Error. #[derive(SerializeDisplay, DeserializeFromStr, Debug, Copy, Clone, PartialEq, Eq)] pub enum ErrCode { /// Canceled indicates the operation was canceled (typically by the caller). /// /// Encore will generate this error code when cancellation is requested. Canceled, /// Unknown error. An example of where this error may be returned is /// if a Status value received from another address space belongs to /// an error-space that is not known in this address space. Also /// errors raised by APIs that do not return enough error information /// may be converted to this error. /// /// Encore will generate this error code in the above two mentioned cases. Unknown, /// InvalidArgument indicates client specified an invalid argument. /// Note that this differs from FailedPrecondition. It indicates arguments /// that are problematic regardless of the state of the system /// (e.g., a malformed file name). /// /// Encore will generate this error code if the request data cannot be parsed. InvalidArgument, /// DeadlineExceeded means operation expired before completion. /// For operations that change the state of the system, this error may be /// returned even if the operation has completed successfully. For /// example, a successful response from a server could have been delayed /// long enough for the deadline to expire. /// /// Encore will generate this error code when the deadline is exceeded. DeadlineExceeded, /// NotFound means some requested entity (e.g., file or directory) was /// not found. /// /// Encore will not generate this error code. NotFound, /// AlreadyExists means an attempt to create an entity failed because one /// already exists. /// /// Encore will not generate this error code. AlreadyExists, /// PermissionDenied indicates the caller does not have permission to /// execute the specified operation. It must not be used for rejections /// caused by exhausting some resource (use ResourceExhausted /// instead for those errors). It must not be /// used if the caller cannot be identified (use Unauthenticated /// instead for those errors). /// /// Encore will not generate this error code. PermissionDenied, /// ResourceExhausted indicates some resource has been exhausted, perhaps /// a per-user quota, or perhaps the entire file system is out of space. /// /// Encore will generate this error code in out-of-memory and server overload /// situations, or when a message is larger than the configured maximum size. ResourceExhausted, /// FailedPrecondition indicates operation was rejected because the /// system is not in a state required for the operation's execution. /// For example, directory to be deleted may be non-empty, an rmdir /// operation is applied to a non-directory, etc. /// /// A litmus test that may help a service implementor in deciding /// between FailedPrecondition, Aborted, and Unavailable: /// (a) Use Unavailable if the client can retry just the failing call. /// (b) Use Aborted if the client should retry at a higher-level /// (e.g., restarting a read-modify-write sequence). /// (c) Use FailedPrecondition if the client should not retry until /// the system state has been explicitly fixed. E.g., if an "rmdir" /// fails because the directory is non-empty, FailedPrecondition /// should be returned since the client should not retry unless /// they have first fixed up the directory by deleting files from it. /// (d) Use FailedPrecondition if the client performs conditional /// Get/Update/Delete on a resource and the resource on the /// server does not match the condition. E.g., conflicting /// read-modify-write on the same resource. /// /// Encore will not generate this error code. FailedPrecondition, /// Aborted indicates the operation was aborted, typically due to a /// concurrency issue like sequencer check failures, transaction aborts, /// etc. /// /// See litmus test above for deciding between FailedPrecondition, /// Aborted, and Unavailable. Aborted, /// OutOfRange means operation was attempted past the valid range. /// E.g., seeking or reading past end of file. /// /// Unlike InvalidArgument, this error indicates a problem that may /// be fixed if the system state changes. For example, a 32-bit file /// system will generate InvalidArgument if asked to read at an /// offset that is not in the range [0,2^32-1], but it will generate /// OutOfRange if asked to read from an offset past the current /// file size. /// /// There is a fair bit of overlap between FailedPrecondition and /// OutOfRange. We recommend using OutOfRange (the more specific /// error) when it applies so that callers who are iterating through /// a space can easily look for an OutOfRange error to detect when /// they are done. /// /// Encore will not generate this error code. OutOfRange, /// Unimplemented indicates operation is not implemented or not /// supported/enabled in this service. /// /// Encore will generate this error code when an endpoint does not exist. Unimplemented, /// Internal errors. Means some invariants expected by underlying /// system has been broken. If you see one of these errors, /// something is very broken. /// /// Encore will generate this error code in several internal error conditions. Internal, /// Unavailable indicates the service is currently unavailable. /// This is a most likely a transient condition and may be corrected /// by retrying with a backoff. Note that it is not always safe to retry /// non-idempotent operations. /// /// See litmus test above for deciding between FailedPrecondition, /// Aborted, and Unavailable. /// /// Encore will generate this error code in aubrupt shutdown of a server process /// or network connection. Unavailable, /// DataLoss indicates unrecoverable data loss or corruption. /// /// Encore will not generate this error code. DataLoss, /// Unauthenticated indicates the request does not have valid /// authentication credentials for the operation. /// /// Encore will generate this error code when the authentication metadata /// is invalid or missing, and expects auth handlers to return errors with /// this code when the auth token is not valid. Unauthenticated, } impl ErrCode { pub fn default_public_message(&self) -> &'static str { match self { ErrCode::Canceled => "the operation was canceled", ErrCode::Unknown => "an unknown error occurred", ErrCode::InvalidArgument => "the request is invalid", ErrCode::DeadlineExceeded => "the operation timed out", ErrCode::NotFound => "the requested resource was not found", ErrCode::AlreadyExists => "the resource already exists", ErrCode::PermissionDenied => "the caller does not have permission to execute the specified operation", ErrCode::ResourceExhausted => "the resource has been exhausted", ErrCode::FailedPrecondition => "the operation was rejected because the system is not in a state required for the operation's execution", ErrCode::Aborted => "the operation was aborted", ErrCode::OutOfRange => "the operation was attempted past the valid range", ErrCode::Unimplemented => "the operation is not implemented or not supported/enabled in this service", ErrCode::Internal => "an internal error occurred", ErrCode::Unavailable => "the service is currently unavailable", ErrCode::DataLoss => "unrecoverable data loss or corruption occurred", ErrCode::Unauthenticated => "the request does not have valid authentication credentials for the operation", } } /// Converts the error code to the trace protocol byte value. pub fn to_trace_code(&self) -> u8 { match self { ErrCode::Canceled => 1, ErrCode::Unknown => 2, ErrCode::InvalidArgument => 3, ErrCode::DeadlineExceeded => 4, ErrCode::NotFound => 5, ErrCode::AlreadyExists => 6, ErrCode::PermissionDenied => 7, ErrCode::ResourceExhausted => 8, ErrCode::FailedPrecondition => 9, ErrCode::Aborted => 10, ErrCode::OutOfRange => 11, ErrCode::Unimplemented => 12, ErrCode::Internal => 13, ErrCode::Unavailable => 14, ErrCode::DataLoss => 15, ErrCode::Unauthenticated => 16, } } pub fn status_code(&self) -> axum::http::StatusCode { match self { ErrCode::Canceled => axum::http::StatusCode::from_u16(499).unwrap(), ErrCode::Unknown => axum::http::StatusCode::INTERNAL_SERVER_ERROR, ErrCode::InvalidArgument => axum::http::StatusCode::BAD_REQUEST, ErrCode::DeadlineExceeded => axum::http::StatusCode::GATEWAY_TIMEOUT, ErrCode::NotFound => axum::http::StatusCode::NOT_FOUND, ErrCode::AlreadyExists => axum::http::StatusCode::CONFLICT, ErrCode::PermissionDenied => axum::http::StatusCode::FORBIDDEN, ErrCode::ResourceExhausted => axum::http::StatusCode::TOO_MANY_REQUESTS, ErrCode::FailedPrecondition => axum::http::StatusCode::BAD_REQUEST, ErrCode::Aborted => axum::http::StatusCode::CONFLICT, ErrCode::OutOfRange => axum::http::StatusCode::BAD_REQUEST, ErrCode::Unimplemented => axum::http::StatusCode::NOT_IMPLEMENTED, ErrCode::Internal => axum::http::StatusCode::INTERNAL_SERVER_ERROR, ErrCode::Unavailable => axum::http::StatusCode::SERVICE_UNAVAILABLE, ErrCode::DataLoss => axum::http::StatusCode::INTERNAL_SERVER_ERROR, ErrCode::Unauthenticated => axum::http::StatusCode::UNAUTHORIZED, } } } impl Display for ErrCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ErrCode::Canceled => write!(f, "canceled"), ErrCode::Unknown => write!(f, "unknown"), ErrCode::InvalidArgument => write!(f, "invalid_argument"), ErrCode::DeadlineExceeded => write!(f, "deadline_exceeded"), ErrCode::NotFound => write!(f, "not_found"), ErrCode::AlreadyExists => write!(f, "already_exists"), ErrCode::PermissionDenied => write!(f, "permission_denied"), ErrCode::ResourceExhausted => write!(f, "resource_exhausted"), ErrCode::FailedPrecondition => write!(f, "failed_precondition"), ErrCode::Aborted => write!(f, "aborted"), ErrCode::OutOfRange => write!(f, "out_of_range"), ErrCode::Unimplemented => write!(f, "unimplemented"), ErrCode::Internal => write!(f, "internal"), ErrCode::Unavailable => write!(f, "unavailable"), ErrCode::DataLoss => write!(f, "data_loss"), ErrCode::Unauthenticated => write!(f, "unauthenticated"), } } } #[derive(Debug)] pub struct UnknownErrCode { pub code: String, } impl Display for UnknownErrCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} (unknown)", self.code) } } impl std::error::Error for UnknownErrCode {} impl FromStr for ErrCode { type Err = UnknownErrCode; fn from_str(s: &str) -> Result { match s { "canceled" => Ok(ErrCode::Canceled), "unknown" => Ok(ErrCode::Unknown), "invalid_argument" => Ok(ErrCode::InvalidArgument), "deadline_exceeded" => Ok(ErrCode::DeadlineExceeded), "not_found" => Ok(ErrCode::NotFound), "already_exists" => Ok(ErrCode::AlreadyExists), "permission_denied" => Ok(ErrCode::PermissionDenied), "resource_exhausted" => Ok(ErrCode::ResourceExhausted), "failed_precondition" => Ok(ErrCode::FailedPrecondition), "aborted" => Ok(ErrCode::Aborted), "out_of_range" => Ok(ErrCode::OutOfRange), "unimplemented" => Ok(ErrCode::Unimplemented), "internal" => Ok(ErrCode::Internal), "unavailable" => Ok(ErrCode::Unavailable), "data_loss" => Ok(ErrCode::DataLoss), "unauthenticated" => Ok(ErrCode::Unauthenticated), other => Err(UnknownErrCode { code: other.to_owned(), }), } } } impl From for axum::http::status::StatusCode { fn from(val: ErrCode) -> Self { val.status_code() } } impl From for ErrCode { fn from(status: axum::http::status::StatusCode) -> Self { match status.as_u16() { 400 => ErrCode::InvalidArgument, 401 => ErrCode::Unauthenticated, 403 => ErrCode::PermissionDenied, 404 => ErrCode::NotFound, 409 => ErrCode::AlreadyExists, 429 => ErrCode::ResourceExhausted, 499 => ErrCode::Canceled, 500 => ErrCode::Internal, 501 => ErrCode::Unimplemented, 503 => ErrCode::Unavailable, 504 => ErrCode::DeadlineExceeded, _ => ErrCode::Unknown, } } } ================================================ FILE: runtimes/core/src/api/gateway/mod.rs ================================================ mod router; mod websocket; use std::borrow::Cow; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use anyhow::Context; use axum::async_trait; use bytes::{BufMut, Bytes, BytesMut}; use http::uri::Scheme; use http::HeaderValue; use hyper::header; use pingora::http::{RequestHeader, ResponseHeader}; use pingora::protocols::http::error_resp; use pingora::proxy::{http_proxy_service, FailToProxy, ProxyHttp, Session}; use pingora::server::configuration::{Opt, ServerConf}; use pingora::services::Service; use pingora::upstreams::peer::HttpPeer; use pingora::{Error, ErrorSource, ErrorType, OkOrErr, OrErr}; use router::Target; use tokio::sync::watch; use url::Url; use crate::api::auth; use crate::api::call::{CallDesc, ServiceRegistry}; use crate::api::paths::PathSet; use crate::api::reqauth::caller::Caller; use crate::api::reqauth::meta::{MetaKey, MetaMap}; use crate::api::reqauth::{svcauth, CallMeta}; use crate::{api, model, trace, EncoreName}; use super::cors::cors_headers_config::CorsHeadersConfig; use super::encore_routes::healthz; #[derive(Clone)] pub struct Gateway { inner: Arc, } struct Inner { shared: Arc, service_registry: Arc, router: router::Router, cors_config: CorsHeadersConfig, healthz: healthz::Handler, own_api_address: Option, proxied_push_subs: HashMap, } /// A push subscription that the gateway proxies to another service. #[derive(Clone, Debug)] pub struct ProxiedPushSub { pub service_name: EncoreName, pub topic: EncoreName, pub subscription: EncoreName, } pub struct GatewayCtx { upstream_service_name: EncoreName, upstream_sampling_target: router::SamplingTarget, upstream_base_path: String, upstream_host: Option, upstream_require_auth: bool, trace_id: Option, } impl GatewayCtx { fn prepend_base_path(&self, uri: &http::Uri) -> anyhow::Result { let mut builder = http::Uri::builder(); if let Some(scheme) = uri.scheme() { builder = builder.scheme(scheme.clone()); } if let Some(authority) = uri.authority() { builder = builder.authority(authority.clone()); } let base_path = self.upstream_base_path.trim_end_matches('/'); builder = builder.path_and_query(format!( "{}{}", base_path, uri.path_and_query().map_or("", |pq| pq.as_str()) )); builder.build().context("failed to build uri") } } impl Gateway { #[allow(clippy::too_many_arguments)] pub fn new( name: EncoreName, service_registry: Arc, service_routes: PathSet>, auth_handler: Option, cors_config: CorsHeadersConfig, healthz: healthz::Handler, own_api_address: Option, proxied_push_subs: HashMap, tracer: trace::Tracer, inbound_svc_auth: Vec>, ) -> anyhow::Result { // Filter out noop auth methods since they provide no actual // authentication guarantees for verifying internal callers. let authenticated_inbound_svc_auth = inbound_svc_auth .into_iter() .filter(|a| a.name() != "noop") .collect(); let shared = Arc::new(SharedGatewayData { name, auth: auth_handler, tracer, authenticated_inbound_svc_auth, }); let mut router = router::Router::new(); router.add_routes(&service_routes)?; Ok(Gateway { inner: Arc::new(Inner { shared, service_registry, router, cors_config, healthz, own_api_address, proxied_push_subs, }), }) } pub fn auth_handler(&self) -> Option<&auth::Authenticator> { self.inner.shared.auth.as_ref() } pub async fn serve(self, listen_addr: &str) -> anyhow::Result<()> { let conf = Arc::new( ServerConf::new_with_opt_override(&Opt { upgrade: false, daemon: false, nocapture: false, test: false, conf: None, }) .unwrap(), ); let mut proxy = http_proxy_service(&conf, self); proxy.add_tcp(listen_addr); let (_tx, rx) = watch::channel(false); proxy .start_service( #[cfg(unix)] None, rx, 1, // listeners_per_fd ) .await; Ok(()) } } #[async_trait] impl ProxyHttp for Gateway { type CTX = Option; fn new_ctx(&self) -> Self::CTX { None } // see https://github.com/cloudflare/pingora/blob/main/docs/user_guide/internals.md for // details on when different filters are called. async fn request_filter( &self, session: &mut Session, _ctx: &mut Self::CTX, ) -> pingora::Result where Self::CTX: Send + Sync, { if session.req_header().uri.path() == "/__encore/healthz" { let healthz_resp = self.inner.healthz.clone().health_check(); let healthz_bytes: Vec = serde_json::to_vec(&healthz_resp) .or_err(ErrorType::HTTPStatus(500), "could not encode response")?; let mut header = ResponseHeader::build(200, None)?; header.insert_header(header::CONTENT_LENGTH, healthz_bytes.len())?; header.insert_header(header::CONTENT_TYPE, "application/json")?; session .write_response_header(Box::new(header), false) .await?; session .write_response_body(Some(Bytes::from(healthz_bytes)), true) .await?; return Ok(true); } // preflight request, return early with cors headers if axum::http::Method::OPTIONS == session.req_header().method { let mut resp = ResponseHeader::build(200, None)?; self.inner .cors_config .apply(session.req_header(), &mut resp)?; resp.insert_header(header::CONTENT_LENGTH, 0)?; session.write_response_header(Box::new(resp), true).await?; return Ok(true); } Ok(false) } async fn upstream_peer( &self, session: &mut Session, ctx: &mut Self::CTX, ) -> pingora::Result> { let path = session.req_header().uri.path(); // Check if this is a pubsub push request and if we need to proxy it to another service let push_proxy_svc = path .strip_prefix("/__encore/pubsub/push/") .and_then(|sub_id| self.inner.proxied_push_subs.get(sub_id)) .map(|sub| Target { service_name: sub.service_name.clone(), sampling_target: router::SamplingTarget::PubSub, requires_auth: false, }); if let Some(own_api_addr) = &self.inner.own_api_address { if push_proxy_svc.is_none() && path.starts_with("/__encore/") { return Ok(Box::new(HttpPeer::new(own_api_addr, false, "".to_string()))); } } let target = push_proxy_svc.map_or_else( || { // Find which service handles the path route session .req_header() .method .as_ref() .try_into() .map_err(|e: anyhow::Error| api::Error { code: api::ErrCode::InvalidArgument, message: "invalid method".to_string(), internal_message: Some(e.to_string()), stack: None, details: None, }) .and_then(|method| self.inner.router.route_to_service(method, path)) .cloned() }, Ok, )?; let upstream = self .inner .service_registry .service_base_url(&target.service_name) .or_err(ErrorType::InternalError, "couldn't find upstream")?; let upstream_url: Url = upstream .parse() .or_err(ErrorType::InternalError, "upstream not a valid url")?; let upstream_addrs = upstream_url .socket_addrs(|| match upstream_url.scheme() { "https" => Some(443), "http" => Some(80), _ => None, }) .or_err( ErrorType::InternalError, "couldn't lookup upstream ip address", )?; let upstream_addr = upstream_addrs.first().or_err( ErrorType::InternalError, "didn't find any upstream ip addresses", )?; let tls = upstream_url.scheme() == "https"; let host = upstream_url.host().map(|h| h.to_string()); let peer = HttpPeer::new(upstream_addr, tls, host.clone().unwrap_or_default()); ctx.replace(GatewayCtx { upstream_base_path: upstream_url.path().to_string(), upstream_host: host, upstream_service_name: target.service_name.clone(), upstream_sampling_target: target.sampling_target.clone(), upstream_require_auth: target.requires_auth, trace_id: None, }); Ok(Box::new(peer)) } async fn response_filter( &self, session: &mut Session, upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX, ) -> pingora::Result<()> where Self::CTX: Send + Sync, { if let Some(gateway_ctx) = ctx.as_ref() { self.inner .cors_config .apply(session.req_header(), upstream_response)?; if let Some(trace_id) = gateway_ctx.trace_id { maybe_add_trace_id_header(upstream_response, trace_id); } } Ok(()) } async fn upstream_request_filter( &self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX, ) -> pingora::Result<()> where Self::CTX: Send + Sync, { if let Some(gateway_ctx) = ctx.as_mut() { let new_uri = gateway_ctx .prepend_base_path(&upstream_request.uri) .or_err( ErrorType::InternalError, "failed to prepend upstream base path", )?; upstream_request.set_uri(new_uri); if let Some(ref host) = gateway_ctx.upstream_host { upstream_request.insert_header(header::HOST, host)?; } if session.is_upgrade_req() { websocket::update_headers_from_websocket_protocol(upstream_request).or_err( ErrorType::HTTPStatus(400), "invalid auth data passed in websocket protocol header", )?; } // Set X-Forwarded-* headers, based on https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/net/http/httputil/reverseproxy.go;l=78 if let Some(client_addr) = session.client_addr().and_then(|addr| addr.as_inet()) { let client_ip = client_addr.ip().to_string(); let prior_headers = upstream_request .headers .get_all("x-forwarded-for") .iter() .filter_map(|v| std::str::from_utf8(v.as_bytes()).ok()) .fold(String::new(), |mut acc, header| { if !acc.is_empty() { acc.push_str(", "); } acc.push_str(header); acc }); if !prior_headers.is_empty() { let combined = format!("{prior_headers}, {client_ip}"); upstream_request.insert_header("x-forwarded-for", combined)?; } else { upstream_request.insert_header("x-forwarded-for", client_ip)?; } } else { upstream_request.remove_header("x-forwarded-for"); } if let Some(host) = session.req_header().headers.get(header::HOST) { upstream_request.insert_header("x-forwarded-host", host)?; } upstream_request.insert_header( "x-forwarded-proto", match session.req_header().uri.scheme() == Some(&Scheme::HTTPS) { true => "https", false => "http", }, )?; let svc_auth_method = self .inner .service_registry .service_auth_method(&gateway_ctx.upstream_service_name) .unwrap_or_else(|| Arc::new(svcauth::Noop)); let headers = &upstream_request.headers; // If the request has a caller header, try to authenticate it. // If authenticated, use the internal caller directly so that // private endpoint access is granted through the gateway. // Note: we only use non-noop auth methods here, since noop auth // provides no actual authentication guarantees. let has_internal_caller = headers.get_meta(MetaKey::Caller).is_some() && !self.inner.shared.authenticated_inbound_svc_auth.is_empty(); let (mut call_meta, authenticated_internal_caller) = if has_internal_caller { match CallMeta::parse_with_caller( &self.inner.shared.authenticated_inbound_svc_auth, headers, &HashMap::new(), ) { Ok(meta) => { let caller = meta.internal.as_ref().map(|i| i.caller.clone()); (meta, caller) } Err(_) => { // Caller verification failed (e.g. invalid signature). // Treat as an external request. let meta = CallMeta::parse_without_caller(headers).or_err( ErrorType::InternalError, "couldn't parse CallMeta from request", )?; (meta, None) } } } else { let meta = CallMeta::parse_without_caller(headers).or_err( ErrorType::InternalError, "couldn't parse CallMeta from request", )?; (meta, None) }; gateway_ctx.trace_id = Some(call_meta.trace_id); if call_meta.parent_span_id.is_none() { call_meta.parent_span_id = Some(model::SpanId::generate()); } // If the request comes from an authenticated internal service, // use that as the caller directly so that private endpoint access // is granted. Otherwise, use the gateway as the caller. let caller = authenticated_internal_caller.unwrap_or(Caller::Gateway { gateway: self.inner.shared.name.clone(), }); let mut desc = CallDesc { caller: &caller, parent_span: call_meta .parent_span_id .map(|sp| call_meta.trace_id.with_span(sp)), parent_event_id: None, ext_correlation_id: call_meta .ext_correlation_id .as_ref() .map(|s| Cow::Borrowed(s.as_str())), traced: call_meta.trace_sampled.unwrap_or_else(|| { match &gateway_ctx.upstream_sampling_target { router::SamplingTarget::Api(name) => { self.inner.shared.tracer.should_sample(name) } router::SamplingTarget::PubSub => { self.inner.shared.tracer.should_sample_default() } } }), auth_user_id: None, auth_data: None, svc_auth_method: svc_auth_method.as_ref(), }; if let Some(auth_handler) = &self.inner.shared.auth { // Use the same trace sampling decision for the auth handler // as for the rest of the request. let auth_response = auth_handler .authenticate( upstream_request, CallMeta { trace_sampled: Some(desc.traced), ..call_meta.clone() }, ) .await .or_err(ErrorType::InternalError, "couldn't authenticate request")?; match auth_response { auth::AuthResponse::Authenticated { auth_uid, auth_data, } => { desc.auth_user_id = Some(Cow::Owned(auth_uid)); desc.auth_data = Some(auth_data); } auth::AuthResponse::Unauthenticated { error } => { if gateway_ctx.upstream_require_auth { return Err(error.into()); } } }; } desc.add_meta(upstream_request) .or_err(ErrorType::InternalError, "couldn't set request meta")?; } Ok(()) } async fn fail_to_proxy( &self, session: &mut Session, e: &Error, ctx: &mut Self::CTX, ) -> FailToProxy where Self::CTX: Send + Sync, { // modified version of `Session::respond_error` that adds cors headers, // and handles specific errors let code = match e.etype() { ErrorType::HTTPStatus(code) => *code, _ => { match e.esource() { ErrorSource::Upstream => 502, ErrorSource::Downstream => { match e.etype() { ErrorType::WriteError | ErrorType::ReadError | ErrorType::ConnectionClosed => { /* conn already dead */ return FailToProxy { error_code: 0, can_reuse_downstream: false, }; } _ => 400, } } ErrorSource::Internal | ErrorSource::Unset => 500, } } }; let (mut resp, body) = if let Some(api_error) = as_api_error(e) { let (resp, body) = api_error_response(api_error); (resp, Some(body)) } else { ( match code { /* common error responses are pre-generated */ 502 => error_resp::HTTP_502_RESPONSE.clone(), 400 => error_resp::HTTP_400_RESPONSE.clone(), _ => error_resp::gen_error_response(code), }, None, ) }; if let Err(e) = self .inner .cors_config .apply(session.req_header(), &mut resp) { log::error!("failed setting cors header in error response: {e}"); } if let Some(gateway_ctx) = ctx.as_ref() { if let Some(trace_id) = gateway_ctx.trace_id { maybe_add_trace_id_header(&mut resp, trace_id); } } session.set_keepalive(None); session .write_response_header(Box::new(resp), false) .await .unwrap_or_else(|e| { log::error!("failed to send error response to downstream: {e}"); }); session .write_response_body(body, true) .await .unwrap_or_else(|e| log::error!("failed to write body: {e}")); FailToProxy { error_code: code, can_reuse_downstream: false, } } } fn as_api_error(err: &pingora::Error) -> Option<&api::Error> { err.root_cause().downcast_ref::() } fn maybe_add_trace_id_header(resp: &mut ResponseHeader, trace_id: model::TraceId) { if resp.headers.contains_key("x-encore-trace-id") { return; } let value = trace_id.serialize_encore(); if let Ok(val) = HeaderValue::from_str(value.as_str()) { let _ = resp.insert_header("x-encore-trace-id", val); } } fn api_error_response(err: &api::Error) -> (ResponseHeader, bytes::Bytes) { let mut buf = BytesMut::with_capacity(128).writer(); serde_json::to_writer(&mut buf, &err.as_external()).unwrap(); let mut resp = ResponseHeader::build(err.code.status_code(), Some(5)).unwrap(); resp.insert_header(header::SERVER, &pingora::protocols::http::SERVER_NAME[..]) .unwrap(); resp.insert_header(header::DATE, "Sun, 06 Nov 1994 08:49:37 GMT") .unwrap(); // placeholder resp.insert_header(header::CONTENT_LENGTH, buf.get_ref().len()) .unwrap(); resp.insert_header(header::CACHE_CONTROL, "private, no-store") .unwrap(); resp.insert_header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) .unwrap(); (resp, buf.into_inner().into()) } impl crate::api::auth::InboundRequest for RequestHeader { fn headers(&self) -> &axum::http::HeaderMap { &self.headers } fn query(&self) -> Option<&str> { self.uri.query() } } struct SharedGatewayData { name: EncoreName, auth: Option, tracer: trace::Tracer, /// Non-noop service auth methods for verifying incoming internal callers. /// Noop auth is excluded since it provides no authentication guarantees. authenticated_inbound_svc_auth: Vec>, } #[cfg(test)] mod tests { use super::*; #[test] fn maybe_add_trace_id_header_adds_when_missing() { let mut resp = ResponseHeader::build(200, None).unwrap(); let trace_id = model::TraceId([1; 16]); maybe_add_trace_id_header(&mut resp, trace_id); let val = resp .headers .get("x-encore-trace-id") .expect("header missing"); assert_eq!(val.to_str().unwrap(), trace_id.serialize_encore()); } #[test] fn maybe_add_trace_id_header_keeps_existing() { let mut resp = ResponseHeader::build(200, None).unwrap(); resp.insert_header("x-encore-trace-id", HeaderValue::from_static("existing")) .unwrap(); let trace_id = model::TraceId([2; 16]); maybe_add_trace_id_header(&mut resp, trace_id); let val = resp .headers .get("x-encore-trace-id") .expect("header missing"); assert_eq!(val.to_str().unwrap(), "existing"); } } ================================================ FILE: runtimes/core/src/api/gateway/router.rs ================================================ use std::sync::Arc; use crate::{ api::{self, paths::PathSet, schema::Method}, EncoreName, EndpointName, }; #[derive(Clone)] pub struct Router { main: matchit::Router, fallback: matchit::Router, } impl Router { pub fn new() -> Self { let main = matchit::Router::new(); let fallback = matchit::Router::new(); Router { main, fallback } } pub fn add_routes( &mut self, routes: &PathSet>, ) -> anyhow::Result<()> { for (router, routes) in [ (&mut self.main, &routes.main), (&mut self.fallback, &routes.fallback), ] { fn register_methods( mr: &mut MethodRoute, path: &str, service: &EncoreName, endpoint: &api::Endpoint, ) { for method in endpoint.methods() { let dst = match method { Method::GET => &mut mr.get, Method::HEAD => &mut mr.head, Method::POST => &mut mr.post, Method::PUT => &mut mr.put, Method::DELETE => &mut mr.delete, Method::OPTIONS => &mut mr.option, Method::TRACE => &mut mr.trace, Method::PATCH => &mut mr.patch, }; log::trace!(path = path, method = method.as_str(); "registering route"); if dst.is_some() { ::log::error!(method = method.as_str(), path = path; "tried to register same route twice, skipping"); continue; } dst.replace(Target { service_name: service.clone(), sampling_target: SamplingTarget::Api(endpoint.name.clone()), requires_auth: endpoint.requires_auth, }); } } for (service, routes) in routes { for (endpoint, paths) in routes { for path in paths { // Create a method route where we register each method the endpoint supports. let mut mr = MethodRoute::default(); register_methods(&mut mr, path, service, endpoint); match router.insert(path, mr) { Ok(()) => {} Err(matchit::InsertError::Conflict { .. }) => { // If we have a conflict, we need to merge the method routes. let mr = router.at_mut(path).unwrap().value; register_methods(mr, path, service, endpoint); } Err(e) => return Err(e.into()), } } } } } Ok(()) } pub fn route_to_service( &self, method: api::schema::Method, path: &str, ) -> Result<&Target, api::Error> { let mut found_path_match = false; for router in [&self.main, &self.fallback] { if let Ok(service) = router.at(path) { found_path_match = true; let service = service.value.for_method(method); if let Some(service) = service { return Ok(service); } } } // We couldn't find a matching route. Err(if found_path_match { api::Error { code: api::ErrCode::NotFound, message: "no route for method".to_string(), internal_message: Some(format!("no route for method {method:?}: {path}")), stack: None, details: None, } } else { api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {path}")), stack: None, details: None, } }) } } #[derive(Clone, Debug)] pub enum SamplingTarget { Api(EndpointName), PubSub, } #[derive(Clone, Debug)] pub struct Target { pub service_name: EncoreName, pub sampling_target: SamplingTarget, pub requires_auth: bool, } #[derive(Clone, Default)] pub struct MethodRoute { get: Option, head: Option, post: Option, put: Option, delete: Option, option: Option, trace: Option, patch: Option, } impl MethodRoute { fn for_method(&self, method: api::schema::Method) -> Option<&Target> { match method { Method::GET => self.get.as_ref(), Method::HEAD => self.head.as_ref(), Method::POST => self.post.as_ref(), Method::PUT => self.put.as_ref(), Method::DELETE => self.delete.as_ref(), Method::OPTIONS => self.option.as_ref(), Method::TRACE => self.trace.as_ref(), Method::PATCH => self.patch.as_ref(), } } } ================================================ FILE: runtimes/core/src/api/gateway/websocket.rs ================================================ use std::{collections::HashMap, str::FromStr}; use anyhow::anyhow; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use http::{header::SEC_WEBSOCKET_PROTOCOL, HeaderName, HeaderValue}; use pingora::http::RequestHeader; use serde::{ de::{self, Visitor}, Deserialize, }; const ENCORE_DEV_HEADERS: &str = "encore.dev.headers."; // hack to be able to have browsers send request headers when setting up a websocket // inspired by https://github.com/kubernetes/kubernetes/commit/714f97d7baf4975ad3aa47735a868a81a984d1f0 pub fn update_headers_from_websocket_protocol( upstream_request: &mut RequestHeader, ) -> anyhow::Result<()> { let headers = upstream_request .headers .get_all(SEC_WEBSOCKET_PROTOCOL) .into_iter() .cloned() .collect::>(); if upstream_request .remove_header(&SEC_WEBSOCKET_PROTOCOL) .is_none() { return Ok(()); } for header_value in headers { let mut filterd_protocols = Vec::new(); for protocol in header_value.to_str()?.split(',') { let protocol = protocol.trim(); if protocol.starts_with(ENCORE_DEV_HEADERS) { let data = protocol.strip_prefix(ENCORE_DEV_HEADERS).unwrap(); let decoded = URL_SAFE_NO_PAD.decode(data)?; let auth_data: AuthHeaderMap = serde_json::from_slice(&decoded)?; for (name, value) in auth_data.0 { if is_forbidden_request_header(&name) { return Err(anyhow!("header {name} not allowed to be set")); } upstream_request.append_header(name, value)?; } } else { filterd_protocols.push(protocol); } } if !filterd_protocols.is_empty() { upstream_request.append_header(SEC_WEBSOCKET_PROTOCOL, filterd_protocols.join(", "))?; } } Ok(()) } struct AuthHeaderMap(HashMap); impl<'de> Deserialize<'de> for AuthHeaderMap { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_map(AuthHeaderMapVisitor) } } struct AuthHeaderMapVisitor; impl<'de> Visitor<'de> for AuthHeaderMapVisitor { type Value = AuthHeaderMap; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("AuthHeaderMap") } fn visit_map(self, mut access: A) -> Result where A: serde::de::MapAccess<'de>, { let mut map = HashMap::with_capacity(access.size_hint().unwrap_or_default()); while let Some((key, value)) = access.next_entry::<&str, &str>()? { let name = HeaderName::from_str(key).map_err(de::Error::custom)?; let value = HeaderValue::from_str(value).map_err(de::Error::custom)?; map.insert(name, value); } Ok(AuthHeaderMap(map)) } } // see https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name fn is_forbidden_request_header(name: &HeaderName) -> bool { use http::header::*; match *name { ACCEPT_CHARSET | ACCEPT_ENCODING | ACCESS_CONTROL_REQUEST_HEADERS | ACCESS_CONTROL_REQUEST_METHOD | CONNECTION | CONTENT_LENGTH | COOKIE | DATE | DNT | EXPECT | HOST | ORIGIN | REFERER | TE | TRAILER | TRANSFER_ENCODING | UPGRADE | VIA => true, ref n if n == HeaderName::from_static("keep-alive") => true, ref n if n == HeaderName::from_static("permissions-policy") => true, ref n if n.as_str().starts_with("sec-") => true, ref n if n.as_str().starts_with("proxy-") => true, _ => false, } } #[cfg(test)] mod tests { use http::{header::AUTHORIZATION, HeaderMap, HeaderName, HeaderValue}; use serde_json::json; use super::*; #[test] fn test_headers_from_websocket_protocol_preserve_order() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("protocol-a"), ) .unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("protocol-b1, protocol-b2"), ) .unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("protocol-c"), ) .unwrap(); let expected = req.headers.clone(); update_headers_from_websocket_protocol(&mut req) .expect("update headers from websocket protocol"); assert_eq!(expected, req.headers); } #[test] fn test_filter_encore_headers() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("main-protocol"), ) .unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("protocol-1, encore.dev.headers.e30K, protocol-2"), ) .unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("encore.dev.headers.e30K"), ) .unwrap(); update_headers_from_websocket_protocol(&mut req) .expect("update headers from websocket protocol"); let expected = HeaderMap::from_iter(vec![ ( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("main-protocol"), ), ( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("protocol-1, protocol-2"), ), ]); assert_eq!(expected, req.headers); } #[test] fn test_adds_auth_data_headers() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); let data = json!({ "authorization": "token", "x-other-header": "value" }); let bytes = serde_json::to_vec(&data).unwrap(); let encoded = URL_SAFE_NO_PAD.encode(bytes); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_str(&format!("main-protocol, encore.dev.headers.{encoded}")).unwrap(), ) .unwrap(); update_headers_from_websocket_protocol(&mut req) .expect("update headers from websocket protocol"); let expected = HeaderMap::from_iter(vec![ ( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("main-protocol"), ), (AUTHORIZATION, HeaderValue::from_static("token")), ( HeaderName::from_static("x-other-header"), HeaderValue::from_static("value"), ), ]); assert_eq!(expected, req.headers); } #[test] fn test_appends_auth_data_headers() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); let data = json!({ "x-other-header": "new-value" }); let bytes = serde_json::to_vec(&data).unwrap(); let encoded = URL_SAFE_NO_PAD.encode(bytes); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_str(&format!("main-protocol, encore.dev.headers.{encoded}")).unwrap(), ) .unwrap(); req.append_header( HeaderName::from_static("x-other-header"), HeaderValue::from_static("prev-value"), ) .unwrap(); update_headers_from_websocket_protocol(&mut req) .expect("update headers from websocket protocol"); let expected = HeaderMap::from_iter(vec![ ( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("main-protocol"), ), ( HeaderName::from_static("x-other-header"), HeaderValue::from_static("prev-value"), ), ( HeaderName::from_static("x-other-header"), HeaderValue::from_static("new-value"), ), ]); assert_eq!(expected, req.headers); } #[test] fn test_invalid_auth_data_header() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("main-protocol, encore.dev.headers.invalid"), ) .unwrap(); assert!(update_headers_from_websocket_protocol(&mut req).is_err()); } #[test] fn test_forbidden_request_headers() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); let data = json!({ "cOOkie": "xyz", }); let bytes = serde_json::to_vec(&data).unwrap(); let encoded = URL_SAFE_NO_PAD.encode(bytes); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_str(&format!("main-protocol, encore.dev.headers.{encoded}")).unwrap(), ) .unwrap(); assert!(update_headers_from_websocket_protocol(&mut req).is_err()); } #[test] fn test_forbidden_prefix_request_headers() { let mut req = RequestHeader::build(http::Method::GET, b"/some/path", None).unwrap(); let data = json!({ "SEC-anything": "xyz", }); let bytes = serde_json::to_vec(&data).unwrap(); let encoded = URL_SAFE_NO_PAD.encode(bytes); req.append_header( SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_str(&format!("main-protocol, encore.dev.headers.{encoded}")).unwrap(), ) .unwrap(); assert!(update_headers_from_websocket_protocol(&mut req).is_err()); } } ================================================ FILE: runtimes/core/src/api/http.rs ================================================ #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum Method { GET, HEAD, POST, PUT, DELETE, // CONNECT, OPTIONS, TRACE, PATCH, } impl TryFrom<&str> for Method { type Error = anyhow::Error; fn try_from(s: &str) -> Result { match s { "GET" => Ok(Method::GET), "HEAD" => Ok(Method::HEAD), "POST" => Ok(Method::POST), "PUT" => Ok(Method::PUT), "DELETE" => Ok(Method::DELETE), // "CONNECT" => Ok(Method::CONNECT), "OPTIONS" => Ok(Method::OPTIONS), "TRACE" => Ok(Method::TRACE), "PATCH" => Ok(Method::PATCH), _ => Err(anyhow::anyhow!("invalid method: {}", s)), } } } impl Into for Method { fn into(self) -> axum::http::Method { match self { Method::GET => axum::http::Method::GET, Method::HEAD => axum::http::Method::HEAD, Method::POST => axum::http::Method::POST, Method::PUT => axum::http::Method::PUT, Method::DELETE => axum::http::Method::DELETE, // Method::CONNECT => axum::http::Method::CONNECT, Method::OPTIONS => axum::http::Method::OPTIONS, Method::TRACE => axum::http::Method::TRACE, Method::PATCH => axum::http::Method::PATCH, } } } pub(super) fn method_filter(methods: M) -> Option where M: Iterator, { use axum::routing::MethodFilter; let mut filter = None; for method in methods { let method_filter = match method { Method::GET => MethodFilter::GET, Method::HEAD => MethodFilter::HEAD, Method::POST => MethodFilter::POST, Method::PUT => MethodFilter::PUT, Method::DELETE => MethodFilter::DELETE, // Method::CONNECT => MethodFilter::CONNECT, Method::OPTIONS => MethodFilter::OPTIONS, Method::TRACE => MethodFilter::TRACE, Method::PATCH => MethodFilter::PATCH, }; filter = Some(filter.unwrap_or(method_filter).or(method_filter)); } filter } ================================================ FILE: runtimes/core/src/api/http_server.rs ================================================ use std::convert::Infallible; use std::task::{Context, Poll}; use axum::body::HttpBody; use axum::http::Request; use axum::response::Response; use axum::routing::future::RouteFuture; use axum::serve::IncomingStream; use axum::Router; use tower_service::Service; #[derive(Clone)] pub struct HttpServer { encore_routes: Router, api: Option, fallback: Router, } impl HttpServer { pub fn new(encore_routes: Router, api: Option, fallback: Router) -> Self { Self { encore_routes, api, fallback, } } } impl Service> for HttpServer { type Response = Self; type Error = Infallible; type Future = std::future::Ready>; #[inline] fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } #[inline] fn call(&mut self, _req: IncomingStream<'_>) -> Self::Future { std::future::ready(Ok(self.clone())) } } impl Service> for HttpServer where B: HttpBody + Send + 'static, B::Error: Into, { type Response = Response; type Error = Infallible; type Future = RouteFuture; #[inline] fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } #[inline] fn call(&mut self, req: Request) -> Self::Future { if req.uri().path().starts_with("/__encore/") { return self.encore_routes.call(req); } let router = match self.api.as_mut() { Some(api) => api, None => &mut self.fallback, }; router.call(req) } } ================================================ FILE: runtimes/core/src/api/httputil.rs ================================================ use std::borrow::Cow; use anyhow::Context; pub fn convert_headers(headers: &axum::http::HeaderMap) -> reqwest::header::HeaderMap { let mut out = reqwest::header::HeaderMap::with_capacity(headers.len()); for (k, v) in headers { let Ok((k, v)) = convert_header(k, v) else { continue; }; out.insert(k, v); } out } pub fn convert_header( key: &axum::http::HeaderName, value: &axum::http::HeaderValue, ) -> anyhow::Result<(reqwest::header::HeaderName, reqwest::header::HeaderValue)> { let k = reqwest::header::HeaderName::from_bytes(key.as_str().as_bytes()) .context("invalid header name")?; let v = reqwest::header::HeaderValue::from_bytes(value.as_bytes()) .context("invalid header value")?; Ok((k, v)) } pub fn merge_query<'b>(target: Option<&str>, inbound: Option<&'b str>) -> Option> { match (target, inbound) { (Some(a), Some(b)) => { let mut s = String::with_capacity(a.len() + b.len() + 1); s.push_str(a); s.push('&'); s.push_str(b); Some(Cow::Owned(s)) } (None, Some(b)) => Some(Cow::Borrowed(b)), (_, None) => None, } } pub fn join_url_path<'b>(target: &str, inbound: &'b str) -> Option> { if inbound.is_empty() { return None; } else if target.is_empty() { return Some(Cow::Borrowed(inbound)); } let a_slash = target.ends_with('/'); let b_slash = inbound.starts_with('/'); Some(match (a_slash, b_slash) { (true, true) => { let mut s = String::with_capacity(target.len() + inbound.len() - 1); s.push_str(target); s.push_str(&inbound[1..]); Cow::Owned(s) } (false, false) => { let mut s = String::with_capacity(target.len() + inbound.len() + 1); s.push_str(target); s.push('/'); s.push_str(inbound); Cow::Owned(s) } _ => { let mut s = String::with_capacity(target.len() + inbound.len()); s.push_str(target); s.push_str(inbound); Cow::Owned(s) } }) } ================================================ FILE: runtimes/core/src/api/jsonschema/de.rs ================================================ use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::fmt::Display; use std::marker::PhantomData; use std::str::FromStr; use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Unexpected, Visitor}; use serde::Deserializer; use crate::api::jsonschema::{validation::Validation, Registry}; use crate::api::{self, PValue, PValues}; use serde_json::Number as JSONNumber; use serde_json::Value as JVal; #[derive(Debug, Clone)] pub enum Value { // A JSON primitive (e.g. string, number, boolean, null). Basic(Basic), // A literal value. Literal(Literal), /// Consume a value if present. Option(BasicOrValue), /// Consume a map of key-value pairs (the keys are always strings in JSON). Map(BasicOrValue), /// A struct with a set of known fields. Struct(Struct), /// Consume an array of values. Array(BasicOrValue), /// Consume a single value, one of a union of possible types. Union(Vec), /// Reference to another value. Ref(usize), /// A value with additional value-based validation. Validation(Validation), } #[derive(Debug, Clone, Default)] pub struct Struct { pub fields: HashMap, } impl Struct { pub fn is_empty(&self) -> bool { self.fields.is_empty() } pub fn contains_name(&self, name: &str) -> bool { self.fields .iter() .any(|(field_name, field)| field.name_override.as_deref().unwrap_or(field_name) == name) } } #[derive(Debug, Clone)] pub struct Field { pub value: BasicOrValue, pub optional: bool, pub name_override: Option, } impl Value { pub fn is_basic(&self) -> bool { matches!(self, Value::Basic(_)) } pub fn is_option(&self) -> bool { matches!(self, Value::Option(_)) } pub fn is_map(&self) -> bool { matches!(self, Value::Map(_)) } pub fn is_struct(&self) -> bool { matches!(self, Value::Struct { .. }) } pub fn is_array(&self) -> bool { matches!(self, Value::Array(_)) } pub fn is_ref(&self) -> bool { matches!(self, Value::Ref(_)) } pub fn expecting<'a>(&'a self, reg: &'a Registry) -> Cow<'a, str> { match self { Value::Array(_) => Cow::Borrowed("a JSON array"), Value::Basic(basic) => Cow::Borrowed(basic.expecting()), Value::Map(_) => Cow::Borrowed("a JSON map"), Value::Literal(lit) => Cow::Owned(lit.expecting()), Value::Option(bov) => bov.expecting(reg), Value::Ref(idx) => reg.get(*idx).expecting(reg), Value::Struct { .. } => Cow::Borrowed("a JSON object"), Value::Validation(v) => v.bov.expecting(reg), Value::Union(types) => { let mut s = String::new(); let num = types.len(); for (i, typ) in types.iter().enumerate() { if i > 0 { if i == (num - 1) && num > 2 { s.push_str(", or "); } else if i == (num - 1) { s.push_str(" or "); } else { s.push_str(", "); } } s.push_str(&typ.expecting(reg)); } Cow::Owned(s) } } } } #[derive(Debug, Copy, Clone)] pub enum Basic { Any, // Any valid JSON value. Null, Bool, Number, String, DateTime, Decimal, } impl Basic { pub fn expecting(&self) -> &'static str { match self { Basic::Any => "any valid JSON value", Basic::Null => "null", Basic::Bool => "a boolean", Basic::Number => "a number", Basic::String => "a string", Basic::DateTime => "a datetime string", Basic::Decimal => "a decimal", } } } #[derive(Debug, Clone)] pub enum Literal { Str(String), // A literal string Bool(bool), Int(i64), Float(f64), } impl Literal { pub fn expecting(&self) -> String { match self { Literal::Str(lit) => format!("{lit:#?}"), Literal::Bool(lit) => format!("{lit:#?}"), Literal::Int(lit) => format!("{lit:#?}"), Literal::Float(lit) => format!("{lit:#?}"), } } pub fn expecting_type(&self) -> &'static str { match self { Literal::Str(_) => "string", Literal::Bool(_) => "boolean", Literal::Int(_) => "integer", Literal::Float(_) => "number", } } } #[derive(Debug, Copy, Clone)] pub enum BasicOrValue { Basic(Basic), Value(usize), } impl BasicOrValue { pub fn expecting<'a>(&'a self, reg: &'a Registry) -> Cow<'a, str> { match self { BasicOrValue::Basic(basic) => Cow::Borrowed(basic.expecting()), BasicOrValue::Value(idx) => reg.get(*idx).expecting(reg), } } } #[derive(Debug, Default, Clone)] pub struct DecodeConfig { // If true, attempts to parse strings as other primitive types // when there's a type mismatch. pub coerce_strings: bool, // Set to true if arrays are serialized into repeated fields // (e.g query string parameters) pub arrays_as_repeated_fields: bool, } #[derive(Copy, Clone, Debug)] pub(super) struct DecodeValue<'a> { pub(super) cfg: &'a DecodeConfig, pub(super) reg: &'a Registry, pub(super) value: &'a Value, } impl<'a> DecodeValue<'a> { fn resolve(&'a self, idx: usize) -> DecodeValue<'a> { DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[idx], } } } impl<'de: 'a, 'a> DeserializeSeed<'de> for DecodeValue<'a> { type Value = PValue; fn deserialize(self, deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_any(self) } } macro_rules! recurse_ref { ($self:ident, $idx:expr, $method:ident, $value:expr) => {{ let visitor = DecodeValue { cfg: $self.cfg, reg: $self.reg, value: &$self.reg.values[*$idx], }; visitor.$method($value) }}; } macro_rules! recurse_ref0 { ($self:ident, $idx:expr, $method:ident) => {{ let visitor = DecodeValue { cfg: $self.cfg, reg: $self.reg, value: &$self.reg.values[*$idx], }; visitor.$method() }}; } macro_rules! recurse { ($self:ident, $bov:expr, $method:ident, $value:expr) => {{ match $bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: $self.cfg, reg: $self.reg, value: &basic_val, }; visitor.$method($value) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: $self.cfg, reg: $self.reg, value: &$self.reg.values[*idx], }; visitor.$method($value) } } }}; } macro_rules! validate_pval { ($self:ident, $v:ident, $method:ident, $value:expr) => {{ let inner = recurse!($self, &$v.bov, $method, $value)?; match $v.validate_pval(&inner) { Ok(()) => Ok(inner), Err(err) => Err(serde::de::Error::custom(err)), } }}; } macro_rules! validate_jval { ($self:ident, $v:ident, $method:ident, $value:expr) => {{ recurse!($self, &$v.bov, $method, $value)?; match $v.validate_jval($value) { Ok(()) => Ok(()), Err(err) => Err(serde::de::Error::custom(err)), } }}; } macro_rules! recurse0 { ($self:ident, $bov:expr, $method:ident) => {{ match $bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: $self.cfg, reg: $self.reg, value: &basic_val, }; visitor.$method() } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: $self.cfg, reg: &$self.reg, value: &$self.reg.values[*idx], }; visitor.$method() } } }}; } impl<'de> Visitor<'de> for DecodeValue<'_> { type Value = PValue; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { match self.value { Value::Literal(lit) => { let s = lit.expecting(); formatter.write_str(&s) } Value::Basic(b) => formatter.write_str(match b { Basic::Any => "any valid JSON value", Basic::Null => "null", Basic::Bool => "a boolean", Basic::Number => "a number", Basic::String => "a string", Basic::DateTime => "a datetime string", Basic::Decimal => "a decimal", }), Value::Map(_) => formatter.write_str("a JSON object"), Value::Array(_) => formatter.write_str("a JSON array"), Value::Union(union) => { let num = union.len(); let mut s = String::new(); for (i, typ) in union.iter().enumerate() { if i > 0 { if num > 2 && i == (num - 1) { s.push_str(", or "); } else if i == (num - 1) { s.push_str(" or "); } else { s.push_str(", "); } } let expecting = typ.expecting(self.reg); s.push_str(&expecting); } formatter.write_str(&s) } Value::Option(_) => formatter.write_str("any valid JSON value or null"), Value::Struct { .. } => formatter.write_str("a JSON object"), Value::Ref(_) => formatter.write_str("a JSON value"), Value::Validation(v) => formatter.write_str(v.bov.expecting(self.reg).as_ref()), } } #[inline] fn visit_bool(self, value: bool) -> Result where E: serde::de::Error, { match &self.value { Value::Basic(Basic::Any | Basic::Bool) => Ok(PValue::Bool(value)), Value::Ref(idx) => recurse_ref!(self, idx, visit_bool, value), Value::Option(val) => { recurse!(self, val, visit_bool, value) } Value::Literal(Literal::Bool(bool)) if *bool == value => Ok(PValue::Bool(value)), Value::Validation(v) => validate_pval!(self, v, visit_bool, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, visit_bool, value); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type( Unexpected::Bool(value), &self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Bool(value), &self, )), } } #[inline] fn visit_i64(self, value: i64) -> Result where E: serde::de::Error, { match self.value { Value::Basic(Basic::Any | Basic::Number) => Ok(PValue::Number(value.into())), Value::Ref(idx) => recurse_ref!(self, idx, visit_i64, value), Value::Option(val) => { recurse!(self, val, visit_i64, value) } Value::Literal(Literal::Int(val)) if *val == value => Ok(PValue::Number(value.into())), Value::Validation(v) => validate_pval!(self, v, visit_i64, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, visit_i64, value); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type( Unexpected::Signed(value), &self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Signed(value), &self, )), } } #[inline] fn visit_u64(self, value: u64) -> Result where E: serde::de::Error, { match self.value { Value::Basic(Basic::Any | Basic::Number) => Ok(PValue::Number(value.into())), Value::Ref(idx) => recurse_ref!(self, idx, visit_u64, value), Value::Option(val) => { recurse!(self, val, visit_u64, value) } Value::Literal(Literal::Int(val)) if *val == value as i64 => { Ok(PValue::Number(value.into())) } Value::Validation(v) => validate_pval!(self, v, visit_u64, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, visit_u64, value); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type( Unexpected::Unsigned(value), &self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Unsigned(value), &self, )), } } #[inline] fn visit_f64(self, value: f64) -> Result where E: serde::de::Error, { match self.value { Value::Basic(Basic::Any | Basic::Number) => { Ok(JSONNumber::from_f64(value).map_or(PValue::Null, PValue::Number)) } Value::Ref(idx) => recurse_ref!(self, idx, visit_f64, value), Value::Option(bov) => { recurse!(self, bov, visit_f64, value) } Value::Literal(Literal::Float(val)) if *val == value => { if let Some(num) = JSONNumber::from_f64(value) { Ok(PValue::Number(num)) } else { Err(serde::de::Error::custom(format_args!( "expected {val}, got {value}" ))) } } Value::Validation(v) => validate_pval!(self, v, visit_f64, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, visit_f64, value); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type( Unexpected::Float(value), &self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Float(value), &self, )), } } #[inline] fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { self.visit_string(String::from(value)) } #[inline] #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self), ret, level = "trace") )] fn visit_string(self, value: String) -> Result where E: serde::de::Error, { match self.value { Value::Array(bov) if self.cfg.arrays_as_repeated_fields => match bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; Ok(PValue::Array(vec![visitor.visit_string(value)?])) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; Ok(PValue::Array(vec![visitor.visit_string(value)?])) } }, Value::Basic(b) => match b { Basic::Any | Basic::String => Ok(PValue::String(value)), Basic::DateTime => api::DateTime::parse_from_rfc3339(&value) .map(PValue::DateTime) .map_err(|e| serde::de::Error::custom(format_args!("invalid datetime: {e}",))), Basic::Decimal => api::Decimal::from_str(&value) .map(PValue::Decimal) .map_err(|e| serde::de::Error::custom(format_args!("invalid decimal: {e}",))), Basic::Bool if self.cfg.coerce_strings => { if value == "true" { Ok(PValue::Bool(true)) } else if value == "false" { Ok(PValue::Bool(false)) } else { Err(serde::de::Error::custom(format_args!( "expected a boolean, got {value}" ))) } } Basic::Number if self.cfg.coerce_strings => { if let Ok(num) = value.parse::() { Ok(PValue::Number(num.into())) } else if let Ok(num) = value.parse::() { Ok(JSONNumber::from_f64(num).map_or(PValue::Null, PValue::Number)) } else { Err(serde::de::Error::custom(format_args!( "expected a number, got {value}" ))) } } Basic::Null if self.cfg.coerce_strings => { if value == "null" { Ok(PValue::Null) } else { Err(serde::de::Error::custom(format_args!( "expected null, got {value}" ))) } } _ => Err(serde::de::Error::invalid_type( Unexpected::Str(&value), &self, )), }, Value::Ref(idx) => recurse_ref!(self, idx, visit_string, value), Value::Option(bov) => { recurse!(self, bov, visit_string, value) } Value::Literal(Literal::Str(val)) if val.as_str() == value => Ok(PValue::String(value)), Value::Literal(lit) => match lit { Literal::Str(val) if val.as_str() == value => Ok(PValue::String(value)), Literal::Bool(_) if self.cfg.coerce_strings => { if let Ok(got) = value.parse::() { self.visit_bool(got) } else { Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), value, ))) } } Literal::Int(_) if self.cfg.coerce_strings => { if let Ok(got) = value.parse::() { self.visit_i64(got) } else { Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), value, ))) } } Literal::Float(_) if self.cfg.coerce_strings => { if let Ok(got) = value.parse::() { self.visit_f64(got) } else { Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), value, ))) } } _ => Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), value, ))), }, Value::Validation(v) => validate_pval!(self, v, visit_string, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, visit_string, value.clone()); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type( Unexpected::Str(&value), &self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Str(&value), &self, )), } } #[inline] fn visit_none(self) -> Result where E: serde::de::Error, { match self.value { Value::Basic(Basic::Any | Basic::Null) | Value::Option(_) => Ok(PValue::Null), Value::Ref(idx) => recurse_ref0!(self, idx, visit_none), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse0!(self, typ, visit_none); if let Ok(val) = res { return Ok(val); } } Err(serde::de::Error::invalid_type(Unexpected::Option, &self)) } _ => Err(serde::de::Error::invalid_type(Unexpected::Option, &self)), } } #[inline] fn visit_some(self, deserializer: D) -> Result where D: Deserializer<'de>, { DeserializeSeed::deserialize(self, deserializer) } #[inline] fn visit_unit(self) -> Result where E: serde::de::Error, { self.visit_none() } #[inline] #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self, seq), ret, level = "trace") )] fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { match &self.value { Value::Basic(Basic::Any) => visit_seq(self, seq), Value::Array(bov) => match bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; visit_seq(visitor, seq) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; visit_seq(visitor, seq) } }, Value::Ref(idx) => recurse_ref!(self, idx, visit_seq, seq), Value::Option(bov) => recurse!(self, bov, visit_seq, seq), Value::Validation(v) => validate_pval!(self, v, visit_seq, seq), Value::Union(candidates) => { let mut vec: Vec = Vec::new(); while let Some(val) = seq.next_element()? { vec.push(val); } let arr = JVal::Array(vec); for c in candidates { match c { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; if visitor.validate::(&arr).is_ok() { return visitor.transform(arr); } } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; if visitor.validate::(&arr).is_ok() { return visitor.transform(arr); } } } } Err(serde::de::Error::invalid_type(Unexpected::Seq, &self)) } _ => Err(serde::de::Error::invalid_type(Unexpected::Seq, &self)), } } #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self, map), ret, level = "trace") )] fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, { match &self.value { Value::Basic(Basic::Any) => visit_map(self, map), Value::Map(bov) => match bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; visit_map(visitor, map) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; visit_map(visitor, map) } }, Value::Struct(Struct { fields }) => { let mut values = PValues::new(); let mut seen = HashSet::new(); while let Some(key) = map.next_key::()? { // Get the corresponding value from the schema. match fields.get(&key) { Some(entry) => { // Resolve the field value. let value = match &entry.value { BasicOrValue::Value(field_idx) => { let field = self.resolve(*field_idx); map.next_value_seed(field)? } BasicOrValue::Basic(basic) => { let field = DecodeValue { cfg: self.cfg, reg: self.reg, value: &Value::Basic(*basic), }; map.next_value_seed(field)? } }; let allow_duplicate_fields = self.cfg.arrays_as_repeated_fields && value.is_array(); let duplicate = !seen.insert(key.clone()); // Check for duplicate keys. if duplicate && !allow_duplicate_fields { return Err(serde::de::Error::custom(format_args!( "duplicate field {key}" ))); } // Insert it into our map. if self.cfg.arrays_as_repeated_fields && value.is_array() { if let PValue::Array(vec) = value { values .entry(key) .and_modify(|prev| { if let PValue::Array(prev) = prev { prev.extend(vec.clone()); } }) .or_insert_with(|| PValue::Array(vec)); } } else { values.insert(key, value); } } None => { // Unknown field; ignore it. map.next_value::()?; } } } // Report any missing fields. if seen.len() != fields.len() { let missing = fields .iter() .filter_map(|(key, field)| { if seen.contains(key) { return None; } // If the field is optional, don't consider it missing. if field.optional { return None; } else if let BasicOrValue::Value(idx) = &field.value { if matches!(self.resolve(*idx).value, Value::Option(_)) { return None; } } Some(key.as_str()) }) .collect::>(); match missing.len() { 0 => {} // do nothing 1 => { return Err(serde::de::Error::custom(format_args!( "missing field {}", missing[0] ))) } _ => { return Err(serde::de::Error::custom(format_args!( "missing fields {}", FieldList { names: &missing } ))) } } } Ok(PValue::Object(values)) } Value::Ref(idx) => recurse_ref!(self, idx, visit_map, map), Value::Option(bov) => recurse!(self, bov, visit_map, map), Value::Validation(v) => validate_pval!(self, v, visit_map, map), Value::Union(candidates) => { let mut values = serde_json::Map::new(); while let Some((key, value)) = map.next_entry()? { values.insert(key, value); } let map = JVal::Object(values); for c in candidates { match c { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; if visitor.validate::(&map).is_ok() { return visitor.transform(map); } } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; if visitor.validate::(&map).is_ok() { return visitor.transform(map); } } } } Err(serde::de::Error::invalid_type(Unexpected::Map, &self)) } _ => Err(serde::de::Error::invalid_type(Unexpected::Map, &self)), } } } fn visit_seq<'de, A>(elem: DecodeValue, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut vec = Vec::new(); // TODO optimize to stop using JSONValueVisitor and use serde_json's visitor directly? while let Some(elem) = seq.next_element_seed(elem)? { vec.push(elem); } Ok(PValue::Array(vec)) } fn visit_map<'de, A>(elem: DecodeValue, mut map: A) -> Result where A: MapAccess<'de>, { let mut values = PValues::new(); while let Some((key, value)) = map.next_entry_seed(PhantomData, elem)? { values.insert(key, value); } Ok(PValue::Object(values)) } struct FieldList<'a> { names: &'a [&'a str], } impl DecodeValue<'_> { #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self), ret, level = "trace") )] pub fn validate(&self, value: &JVal) -> Result<(), E> where E: serde::de::Error, { match value { JVal::Null => match self.value { Value::Basic(Basic::Any | Basic::Null) => Ok(()), Value::Option(_) => Ok(()), Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Validation(v) => validate_jval!(self, v, validate, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type(Unexpected::Option, self)) } _ => Err(serde::de::Error::invalid_type(Unexpected::Option, self)), }, JVal::Bool(bool) => match self.value { Value::Basic(Basic::Any | Basic::Bool) => Ok(()), Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Option(val) => { recurse!(self, val, validate, value) } Value::Literal(lit) => match lit { Literal::Bool(val) if *bool == *val => Ok(()), _ => Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), bool, ))), }, Value::Validation(v) => validate_jval!(self, v, validate, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type( Unexpected::Bool(*bool), self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Bool(*bool), self, )), }, JVal::Number(num) => match self.value { Value::Basic(Basic::Any | Basic::Number) => Ok(()), Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Option(val) => { recurse!(self, val, validate, value) } Value::Literal(lit) => match lit { Literal::Int(val) if num.as_i64() == Some(*val) => Ok(()), Literal::Float(val) if num.as_f64() == Some(*val) => Ok(()), _ => Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), num, ))), }, Value::Validation(v) => validate_jval!(self, v, validate, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type( Unexpected::Other("number"), self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Other("number"), self, )), }, JVal::String(string) => match self.value { Value::Basic(Basic::Any | Basic::String) => Ok(()), Value::Basic(Basic::DateTime) => api::DateTime::parse_from_rfc3339(string) .map(|_| ()) .map_err(|e| serde::de::Error::custom(format_args!("invalid datetime: {e}",))), Value::Basic(Basic::Decimal) => api::Decimal::from_str(string) .map(|_| ()) .map_err(|e| serde::de::Error::custom(format_args!("invalid decimal: {e}",))), Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Option(val) => { recurse!(self, val, validate, value) } Value::Literal(lit) => match lit { Literal::Str(val) if string.as_str() == *val => Ok(()), _ => Err(serde::de::Error::custom(format_args!( "expected {}, got {}", lit.expecting(), string, ))), }, Value::Validation(v) => validate_jval!(self, v, validate, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type( Unexpected::Str(string), self, )) } _ => Err(serde::de::Error::invalid_type( Unexpected::Str(string), self, )), }, JVal::Array(array) => match self.value { Value::Basic(Basic::Any) => Ok(()), Value::Array(bov) => match bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; for elem in array { visitor.validate(elem)?; } Ok(()) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; for elem in array { visitor.validate(elem)?; } Ok(()) } }, Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Option(bov) => { for elem in array { recurse!(self, bov, validate, elem)?; } Ok(()) } Value::Validation(v) => validate_jval!(self, v, validate, value), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type(Unexpected::Seq, self)) } _ => Err(serde::de::Error::invalid_type(Unexpected::Seq, self)), }, JVal::Object(map) => match self.value { Value::Ref(idx) => recurse_ref!(self, idx, validate, value), Value::Option(bov) => recurse!(self, bov, validate, value), Value::Basic(Basic::Any) => Ok(()), Value::Union(types) => { for typ in types { let res: Result<_, E> = recurse!(self, typ, validate, value); if res.is_ok() { return res; } } Err(serde::de::Error::invalid_type(Unexpected::Map, self)) } Value::Map(bov) => match bov { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; for (_key, value) in map { visitor.validate(value)?; } Ok(()) } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; for (_key, value) in map { visitor.validate(value)?; } Ok(()) } }, Value::Struct(Struct { fields }) => { let mut seen = HashSet::new(); for (key, value) in map { match fields.get(key.as_str()) { Some(entry) => { seen.insert(key.clone()); match &entry.value { BasicOrValue::Value(field_idx) => { let field = self.resolve(*field_idx); field.validate(value)? } BasicOrValue::Basic(basic) => { let field = DecodeValue { cfg: self.cfg, reg: self.reg, value: &Value::Basic(*basic), }; field.validate(value)? } } } None => { // Unknown field; ignore it. } } } // Report any missing fields. if seen.len() != fields.len() { let missing = fields .iter() .filter_map(|(key, field)| { if seen.contains(key) { return None; } if field.optional { return None; } else if let BasicOrValue::Value(idx) = &field.value { if matches!(self.resolve(*idx).value, Value::Option(_)) { return None; } } Some(key.as_str()) }) .collect::>(); match missing.len() { 0 => {} // do nothing 1 => { return Err(serde::de::Error::custom(format_args!( "missing field {}", missing[0] ))) } _ => { return Err(serde::de::Error::custom(format_args!( "missing fields {}", FieldList { names: &missing } ))) } } } Ok(()) } Value::Validation(v) => validate_jval!(self, v, validate, value), _ => Err(serde::de::Error::invalid_type(Unexpected::Map, self)), }, } } #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self), ret, level = "trace") )] fn transform(&self, value: JVal) -> Result where E: serde::de::Error, { Ok(match value { JVal::Null => PValue::Null, JVal::Bool(val) => PValue::Bool(val), JVal::Number(num) => PValue::Number(num), JVal::Array(vals) => match self.value { Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::Array(vals)), Value::Option(bov) => return recurse!(self, bov, transform, JVal::Array(vals)), Value::Validation(v) => { return recurse!(self, &v.bov, transform, JVal::Array(vals)) } Value::Basic(Basic::Any) => { let mut new_vals = Vec::with_capacity(vals.len()); for val in vals { new_vals.push(self.transform(val)?); } PValue::Array(new_vals) } Value::Array(bov) => { let mut new_vals = Vec::with_capacity(vals.len()); for val in vals { let val = recurse!(self, bov, transform, val)?; new_vals.push(val); } PValue::Array(new_vals) } Value::Union(candidates) => { let val = JVal::Array(vals); for c in candidates { match c { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; if visitor.validate::(&val).is_ok() { return visitor.transform(val); } } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; if visitor.validate::(&val).is_ok() { return visitor.transform(val); } } } } return Err(serde::de::Error::invalid_type(Unexpected::Seq, self)); } Value::Basic(basic) => { return Err(serde::de::Error::invalid_type( Unexpected::Other(basic.expecting()), self, )) } Value::Literal(lit) => { return Err(serde::de::Error::invalid_type( Unexpected::Other(lit.expecting_type()), self, )) } Value::Map(_) | Value::Struct(_) => { return Err(serde::de::Error::invalid_type(Unexpected::Map, self)) } }, JVal::Object(obj) => match self.value { Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::Object(obj)), Value::Option(bov) => return recurse!(self, bov, transform, JVal::Object(obj)), Value::Validation(v) => { return recurse!(self, &v.bov, transform, JVal::Object(obj)) } Value::Basic(Basic::Any) => { let mut new_obj = BTreeMap::new(); for (key, val) in obj { new_obj.insert(key, self.transform(val)?); } PValue::Object(new_obj) } Value::Map(bov) => { let mut new_obj = BTreeMap::new(); for (key, val) in obj { let val = recurse!(self, bov, transform, val)?; new_obj.insert(key, val); } PValue::Object(new_obj) } Value::Struct(Struct { fields }) => { let mut new_obj = BTreeMap::new(); for (key, value) in obj { match fields.get(key.as_str()) { Some(entry) => { let val = recurse!(self, &entry.value, transform, value)?; new_obj.insert(key, val); } None => { // Unknown field; ignore it. } } } PValue::Object(new_obj) } Value::Union(candidates) => { let val = JVal::Object(obj); for c in candidates { match c { BasicOrValue::Basic(basic) => { let basic_val = Value::Basic(*basic); let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &basic_val, }; if visitor.validate::(&val).is_ok() { return visitor.transform(val); } } BasicOrValue::Value(idx) => { let visitor = DecodeValue { cfg: self.cfg, reg: self.reg, value: &self.reg.values[*idx], }; if visitor.validate::(&val).is_ok() { return visitor.transform(val); } } } } return Err(serde::de::Error::invalid_type(Unexpected::Map, self)); } Value::Basic(basic) => { return Err(serde::de::Error::invalid_type( Unexpected::Other(basic.expecting()), self, )) } Value::Literal(lit) => { return Err(serde::de::Error::invalid_type( Unexpected::Other(lit.expecting_type()), self, )) } Value::Array(_) => { return Err(serde::de::Error::invalid_type(Unexpected::Seq, self)) } }, JVal::String(str) => match self.value { Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::String(str)), Value::Option(bov) => return recurse!(self, bov, transform, JVal::String(str)), Value::Validation(v) => { return recurse!(self, &v.bov, transform, JVal::String(str)) } Value::Basic(Basic::DateTime) => api::DateTime::parse_from_rfc3339(&str) .map(PValue::DateTime) .map_err(|e| { serde::de::Error::custom(format_args!("invalid datetime: {e}",)) })?, Value::Basic(Basic::Decimal) => api::Decimal::from_str(&str) .map(PValue::Decimal) .map_err(|e| { serde::de::Error::custom(format_args!("invalid decimal: {e}",)) })?, // Any non-datetime, non-decimal basic value gets transformed into a string. Value::Basic(_) => PValue::String(str), Value::Literal(_) => PValue::String(str), Value::Union(types) => { let val = JVal::String(str); for typ in types { let res: Result<_, E> = recurse!(self, typ, transform, val.clone()); if res.is_ok() { return res; } } return Err(serde::de::Error::invalid_type( Unexpected::Other("string"), self, )); } Value::Map(_) | Value::Struct(_) | Value::Array(_) => { return Err(serde::de::Error::invalid_type(Unexpected::Str(&str), self)) } }, }) } } impl Display for FieldList<'_> { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self.names.len() { 0 => panic!(), // special case elsewhere 1 => write!(formatter, "`{}`", self.names[0]), 2 => write!(formatter, "`{}` and `{}`", self.names[0], self.names[1]), _ => { for (i, alt) in self.names.iter().enumerate() { if i > 0 { write!(formatter, ", ")?; } if i == self.names.len() - 1 { write!(formatter, "and ")?; } write!(formatter, "`{alt}`")?; } Ok(()) } } } } ================================================ FILE: runtimes/core/src/api/jsonschema/meta.rs ================================================ use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use anyhow::{Context, Result}; use crate::api::jsonschema::de::{Basic, BasicOrValue, Field, Literal, Struct}; use crate::api::jsonschema::{JSONSchema, Registry, Value}; use crate::encore::parser::meta::v1 as meta; use crate::encore::parser::schema::v1 as schema; use crate::encore::parser::schema::v1::r#type::Typ; use super::validation; impl Registry { pub fn schema(self: &Arc, id: usize) -> JSONSchema { JSONSchema { registry: self.clone(), root: id, } } } /// Builder builds a JSONSchema registry. pub struct Builder<'a> { md: &'a meta::Data, /// Values that have been computed so far. /// None indicates a declaration that's being computed; /// it's stored as None until it's computed to be able to handle /// recursive references. values: Vec>, /// Map of declaration ids to value indices. decls: HashMap, } struct BuilderCtx<'a, 'b: 'a> { builder: &'a mut Builder<'b>, /// The ids of computed type arguments, for the current declaration being processed. type_args: &'a [usize], } impl<'a> Builder<'a> { pub fn new(md: &'a meta::Data) -> Self { Self { md, values: Vec::new(), decls: HashMap::new(), } } pub fn build(self) -> Arc { // Ensure all values have been computed. let mut values = Vec::with_capacity(self.values.len()); for v in self.values { values.push(v.expect("missing value")); } Arc::new(Registry { values }) } #[inline] pub fn get(&self, idx: usize) -> Option<&Value> { self.values.get(idx).and_then(|v| v.as_ref()) } #[inline] pub fn register_value(&mut self, val: Value) -> usize { match val { // If it's already a ref, return it unmodified. Value::Ref(idx) => idx, val => { let mut ctx = BuilderCtx { builder: self, type_args: &[], }; ctx.reg(val) } } } #[inline] pub fn register_type(&mut self, typ: &schema::Type) -> Result { let mut ctx = BuilderCtx { builder: self, type_args: &[], }; let val = ctx.typ(typ)?; Ok(match val { // If it's a ref, return its index directly. Value::Ref(idx) => idx, val => ctx.reg(val), }) } pub fn struct_field<'b>(&mut self, f: &'b schema::Field) -> Result<(&'b String, Field)> { // This should be safe to do because it's only called for schema types, // and schema types don't include any type arguments, so we shouldn't need to worry // about missing type arguments. let ctx = &mut BuilderCtx { builder: self, type_args: &[], }; ctx.struct_field(f) } } impl BuilderCtx<'_, '_> { /// Computes the JSONSchema value for the given type. #[inline] fn typ(&mut self, typ: T) -> Result { let tt = typ.tt()?; let val = match tt { Typ::Named(named) => self.named(named), Typ::Builtin(builtin) => { let builtin = schema::Builtin::try_from(*builtin).context("invalid builtin")?; Ok(self.builtin(builtin)) } Typ::Pointer(ptr) => self.ptr(ptr), Typ::Option(opt) => self.option(opt), Typ::Struct(st) => Ok(Value::Struct(self.struct_val(st)?)), Typ::Map(map) => self.map(map), Typ::List(list) => self.list(list), Typ::Union(union) => self.union(union), Typ::Literal(lit) => self.literal(lit), Typ::Config(_) => anyhow::bail!("config not yet supported"), Typ::TypeParameter(param) => { let idx = self .type_args .get(param.param_idx as usize) .ok_or_else(|| anyhow::anyhow!("missing type argument"))?; Ok(Value::Ref(*idx)) } }?; if let Some(expr) = typ.validation() { let bov = self.bov(val); Ok(Value::Validation(validation::Validation { expr: expr.try_into()?, bov, })) } else { Ok(val) } } #[inline] fn named(&mut self, named: &schema::Named) -> Result { let decl = self .builder .md .decls .get(named.id as usize) .context("missing decl")?; // Compute indices for the type arguments. let type_args: Result> = named .type_arguments .iter() .map(|t| self.typ(t).map(|v| self.reg(v))) .collect(); let type_args = type_args?; // Create a nested context that includes the type arguments. let mut nested = BuilderCtx { builder: self.builder, type_args: &type_args, }; let idx = nested.decl(decl)?; Ok(Value::Ref(idx)) } #[inline] fn decl(&mut self, decl: &schema::Decl) -> Result { // Do we have a value for this decl already? if let Some(idx) = self.builder.decls.get(&decl.id) { return Ok(*idx); } // Allocate an index first to handle recursive references. let idx = self.builder.values.len(); self.builder.values.push(None); self.builder.decls.insert(decl.id, idx); // Then compute the type and update the stored value. let typ = self.typ(&decl.r#type)?; self.builder.values[idx] = Some(typ); Ok(idx) } #[inline] fn ptr(&mut self, ptr: &schema::Pointer) -> Result { self.typ(&ptr.base) } #[inline] fn option(&mut self, opt: &schema::Option) -> Result { let value = self.typ(&opt.value)?; Ok(Value::Union(vec![ self.bov(value), BasicOrValue::Basic(Basic::Null), ])) } #[inline] fn builtin(&mut self, b: schema::Builtin) -> Value { use schema::Builtin; Value::Basic(match b { Builtin::Any | Builtin::Json => Basic::Any, Builtin::Bool => Basic::Bool, Builtin::String | Builtin::Bytes | Builtin::Uuid | Builtin::UserId => Basic::String, Builtin::Time => Basic::DateTime, Builtin::Decimal => Basic::Decimal, Builtin::Int | Builtin::Uint | Builtin::Int8 | Builtin::Int16 | Builtin::Int32 | Builtin::Int64 | Builtin::Uint8 | Builtin::Uint16 | Builtin::Uint32 | Builtin::Uint64 | Builtin::Float32 | Builtin::Float64 => Basic::Number, }) } #[inline] pub fn struct_val(&mut self, st: &schema::Struct) -> Result { Ok(Struct { fields: { let mut map = HashMap::with_capacity(st.fields.len()); for f in &st.fields { let (k, v) = self.struct_field(f)?; map.insert(k.to_owned(), v); } map }, }) } #[inline] fn struct_field<'c>(&mut self, f: &'c schema::Field) -> Result<(&'c String, Field)> { let typ = self.typ(&f.typ)?; let value = match typ { Value::Basic(basic) => BasicOrValue::Basic(basic), val => self.bov(val), }; Ok(( &f.name, Field { value, optional: f.optional, name_override: None, }, )) } #[inline] fn map(&mut self, map: &schema::Map) -> Result { // Note: JSON doesn't support anything but string keys, // so we don't actually track the key type for the purpose // of JSON schemas. Ignore it here. let value = self.typ(&map.value)?; Ok(Value::Map(self.bov(value))) } #[inline] fn list(&mut self, list: &schema::List) -> Result { let value = self.typ(&list.elem)?; Ok(Value::Array(self.bov(value))) } #[inline] fn union(&mut self, union: &schema::Union) -> Result { let values: Result> = union .types .iter() .map(|t| self.typ(t).map(|v| self.bov(v))) .collect(); Ok(Value::Union(values?)) } #[inline] fn literal(&mut self, literal: &schema::Literal) -> Result { Ok(match literal.value.clone() { Some(schema::literal::Value::Str(val)) => Value::Literal(Literal::Str(val)), Some(schema::literal::Value::Boolean(val)) => Value::Literal(Literal::Bool(val)), Some(schema::literal::Value::Int(val)) => Value::Literal(Literal::Int(val)), Some(schema::literal::Value::Float(val)) => Value::Literal(Literal::Float(val)), Some(schema::literal::Value::Null(_)) => Value::Basic(Basic::Null), None => anyhow::bail!("missing literal value"), }) } #[inline] fn bov(&mut self, value: Value) -> BasicOrValue { match value { Value::Basic(basic) => BasicOrValue::Basic(basic), val => BasicOrValue::Value(self.reg(val)), } } #[inline] fn reg(&mut self, value: Value) -> usize { let idx = self.builder.values.len(); self.builder.values.push(Some(value)); idx } } trait ToType: std::fmt::Debug { fn tt(&self) -> Result<&Typ>; fn validation(&self) -> Option<&schema::ValidationExpr>; } impl ToType for Option where T: ToType, { fn tt(&self) -> Result<&Typ> { self.as_ref().context("missing type")?.tt() } fn validation(&self) -> Option<&schema::ValidationExpr> { self.as_ref().and_then(|t| t.validation()) } } impl ToType for Box where T: ToType, { fn tt(&self) -> Result<&Typ> { self.deref().tt() } fn validation(&self) -> Option<&schema::ValidationExpr> { self.deref().validation() } } impl ToType for schema::Type { fn tt(&self) -> Result<&Typ> { self.typ.as_ref().context("missing type") } fn validation(&self) -> Option<&schema::ValidationExpr> { self.validation.as_ref() } } impl ToType for &T { fn tt(&self) -> Result<&Typ> { (*self).tt() } fn validation(&self) -> Option<&schema::ValidationExpr> { (*self).validation() } } ================================================ FILE: runtimes/core/src/api/jsonschema/mod.rs ================================================ use std::fmt; use std::sync::Arc; use serde::de::{DeserializeSeed, Deserializer}; pub use de::{Basic, BasicOrValue, Field, Struct, Value}; pub use crate::api::jsonschema::de::DecodeConfig; use crate::api::jsonschema::de::DecodeValue; mod de; mod meta; mod parse; mod ser; pub mod validation; use crate::api::jsonschema::parse::ParseWithSchema; use crate::api::APIResult; pub use meta::Builder; use super::{PValue, PValues}; #[derive(Clone)] pub struct JSONSchema { registry: Arc, root: usize, } pub struct Registry { /// Vector of allocated values. values: Vec, } impl Registry { pub fn get(&self, mut idx: usize) -> &Value { loop { match &self.values[idx] { Value::Ref(i) => idx = *i, other => return other, } } } } impl fmt::Debug for Registry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Don't render the list of values since it's too large. f.debug_struct("Registry").finish() } } impl JSONSchema { pub fn root_value(&self) -> &Value { &self.registry.values[self.root] } #[inline] pub fn root(&self) -> &Struct { let Value::Struct(str) = &self.registry.values[self.root] else { panic!("root is not a struct"); }; str } pub fn parse(&self, payload: P) -> APIResult where P: ParseWithSchema, O: Sized, { payload.parse_with_schema(self) } pub fn seed_deserializer<'de: 'a, 'a>( &'a self, cfg: DecodeConfig, ) -> impl DeserializeSeed<'de> + 'a { SchemaDeserializer { cfg, schema: self } } pub fn deserialize<'de, T>( &self, de: T, cfg: DecodeConfig, ) -> Result> where T: Deserializer<'de>, { let seed = SchemaDeserializer { cfg, schema: self }; let mut track = serde_path_to_error::Track::new(); let de = serde_path_to_error::Deserializer::new(de, &mut track); match seed.deserialize(de) { Ok(t) => Ok(t), Err(err) => Err(serde_path_to_error::Error::new(track.path(), err)), } } pub fn null() -> Self { JSONSchema { registry: Arc::new(Registry { values: vec![Value::Basic(Basic::Null)], }), root: 0, } } pub fn any() -> Self { JSONSchema { registry: Arc::new(Registry { values: vec![Value::Basic(Basic::Any)], }), root: 0, } } } impl fmt::Debug for JSONSchema { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.registry.values.get(self.root) { Some(v) => v.write_debug(&self.registry, f), None => write!(f, "Ref({})", self.root), } } } impl Value { fn write_debug(&self, reg: &Registry, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Value::Basic(b) => write!(f, "{b:?}"), Value::Struct(Struct { fields }) => { f.debug_struct("Struct").field("fields", &fields).finish() } Value::Option(v) => f.debug_struct("Option").field("value", &v).finish(), Value::Array(v) => f.debug_struct("Array").field("value", &v).finish(), Value::Map(v) => f.debug_struct("Map").field("value", &v).finish(), Value::Union(v) => f.debug_struct("Union").field("types", &v).finish(), Value::Literal(v) => f.debug_struct("Literal").field("value", &v).finish(), Value::Ref(idx) => match reg.values.get(*idx) { Some(v) => v.write_debug(reg, f), None => write!(f, "Ref({idx})"), }, Value::Validation(v) => f .debug_struct("Validation") .field("bov", &v.bov) .field("expr", &v.expr) .finish(), } } } impl<'de: 'a, 'a> DeserializeSeed<'de> for SchemaDeserializer<'a> { type Value = PValues; fn deserialize(self, deserializer: D) -> Result where D: Deserializer<'de>, { let visitor = DecodeValue { cfg: &self.cfg, reg: &self.schema.registry, value: &self.schema.registry.values[self.schema.root], }; let value = deserializer.deserialize_any(visitor)?; match value { PValue::Object(map) => Ok(map), _ => Err(serde::de::Error::custom("expected object")), } } } pub struct SchemaDeserializer<'a> { cfg: DecodeConfig, schema: &'a JSONSchema, } #[cfg(test)] mod tests { use super::*; use crate::api::jsonschema::de::*; use std::collections::HashMap; #[test] fn test() { let reg = Arc::new(Registry { values: vec![ Value::Struct(Struct { fields: { let mut fields = HashMap::new(); fields.insert( "bar".to_string(), Field { value: BasicOrValue::Value(1), optional: false, name_override: None, }, ); fields }, }), Value::Option(BasicOrValue::Value(2)), Value::Basic(Basic::Any), ], }); let schema = JSONSchema { registry: reg.clone(), root: 0, }; let str = r#"{"foo": "bar", "blah": "baz"}"#; let mut jsonde = serde_json::Deserializer::from_str(str); let res = schema.deserialize(&mut jsonde, DecodeConfig::default()); println!("{res:?}"); } } ================================================ FILE: runtimes/core/src/api/jsonschema/parse.rs ================================================ use crate::api::jsonschema::{Basic, BasicOrValue, JSONSchema, Registry, Struct, Value}; use crate::api::{self, Cookie, PValue, PValues, SameSite}; use crate::api::{schema, APIResult}; use schema::ToHeaderStr; use std::str::FromStr; use crate::api::jsonschema::de::Literal; pub trait ParseWithSchema { fn parse_with_schema(self, schema: &JSONSchema) -> APIResult; } macro_rules! header_to_str { ($header_value:expr) => { $header_value.to_str().map_err(|err| api::Error { code: api::ErrCode::InvalidArgument, message: "invalid header value".to_string(), internal_message: Some(format!("invalid header value: {}", err)), stack: None, details: None, }) }; } impl ParseWithSchema for H where H: schema::HTTPHeaders, { fn parse_with_schema(self, schema: &JSONSchema) -> APIResult { let mut result = PValues::new(); let reg = schema.registry.as_ref(); for (field_key, field) in schema.root().fields.iter() { let header_name = field.name_override.as_deref().unwrap_or(field_key.as_str()); let mut values = self.get_all(header_name); let Some(header_value) = values.next() else { if field.optional { continue; } else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("missing required header: {header_name}"), internal_message: None, stack: None, details: None, }); } }; result.insert( field_key.clone(), match &field.value { BasicOrValue::Basic(basic) => { let basic = Value::Basic(*basic); parse_header_value(header_to_str!(header_value)?, reg, &basic)? } BasicOrValue::Value(idx) => { // Determine the type of the value(s). let basic_val: Value; // for borrowing below let (value_type, is_array) = match reg.get(*idx) { Value::Array(bov) => ( match bov { BasicOrValue::Value(idx) => reg.get(*idx), BasicOrValue::Basic(basic) => { basic_val = Value::Basic(*basic); &basic_val } }, true, ), val => (val, false), }; if is_array { let values = std::iter::once(header_value).chain(values); let mut arr = Vec::new(); for header_value in values { let value = parse_header_value( header_to_str!(header_value)?, reg, value_type, )?; arr.push(value); } PValue::Array(arr) } else { parse_header_value(header_to_str!(header_value)?, reg, value_type)? } } }, ); } Ok(result) } } #[derive(Clone, Copy)] enum ValueType { Header, Cookie, } impl ValueType { fn error_message(&self) -> &'static str { match self { ValueType::Header => "invalid header value", ValueType::Cookie => "invalid cookie value", } } } fn parse_str_value( value_str: &str, reg: &Registry, schema: &Value, value_type: ValueType, ) -> APIResult { match schema { // Recurse Value::Ref(idx) => parse_str_value(value_str, reg, ®.values[*idx], value_type), Value::Validation(v) => { let inner = match &v.bov { BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str), BasicOrValue::Value(idx) => { parse_str_value(value_str, reg, ®.values[*idx], value_type) } }?; match v.validate_pval(&inner) { Ok(()) => Ok(inner), Err(err) => Err(api::Error { code: api::ErrCode::InvalidArgument, message: value_type.error_message().to_string(), internal_message: Some(format!("{}: {}", value_type.error_message(), err)), stack: None, details: None, }), } } // If we have an empty value for an option, that's fine. Value::Option(_) if value_str.is_empty() => Ok(PValue::Null), // Otherwise recurse. Value::Option(opt) => match opt { BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str), BasicOrValue::Value(idx) => { parse_str_value(value_str, reg, ®.values[*idx], value_type) } }, Value::Basic(basic) => parse_basic_str(basic, value_str), Value::Struct { .. } | Value::Map(_) | Value::Array(_) => unsupported(reg, schema), Value::Literal(lit) => match lit { Literal::Str(want) if value_str == want => Ok(PValue::String(want.to_string())), Literal::Bool(true) if value_str == "true" => Ok(PValue::Bool(true)), Literal::Bool(false) if value_str == "false" => Ok(PValue::Bool(false)), Literal::Int(want) if value_str.parse() == Ok(*want) => { Ok(PValue::Number(serde_json::Number::from(*want))) } Literal::Float(want) if value_str.parse() == Ok(*want) => { if let Some(num) = serde_json::Number::from_f64(*want) { Ok(PValue::Number(num)) } else { Err(api::Error { code: api::ErrCode::InvalidArgument, message: value_type.error_message().to_string(), internal_message: Some(format!("invalid float value: {value_str}")), stack: None, details: None, }) } } want => Err(api::Error { code: api::ErrCode::InvalidArgument, message: value_type.error_message().to_string(), internal_message: Some(format!("expected {}, got {}", want.expecting(), value_str)), stack: None, details: None, }), }, Value::Union(union) => { // Find the first value that matches. for value in union { let result = match value { BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str), BasicOrValue::Value(idx) => { let value = reg.get(*idx); parse_str_value(value_str, reg, value, value_type) } }; match result { Ok(value) => return Ok(value), Err(_) => continue, } } Err(api::Error { code: api::ErrCode::InvalidArgument, message: value_type.error_message().to_string(), internal_message: Some(format!("no union value matched: {value_str}")), stack: None, details: None, }) } } } fn parse_header_value(header: &str, reg: &Registry, schema: &Value) -> APIResult { parse_str_value(header, reg, schema, ValueType::Header) } fn parse_cookie_value(cookie_value: &str, reg: &Registry, schema: &Value) -> APIResult { parse_str_value(cookie_value, reg, schema, ValueType::Cookie) } impl ParseWithSchema for PValue { fn parse_with_schema(self, schema: &JSONSchema) -> APIResult { let reg = schema.registry.as_ref(); let fields = &schema.root().fields; match self { PValue::Object(obj) => { let mut result = PValues::new(); for (key, value) in obj { let value = match fields.get(&key) { // Not known to schema; pass it unmodified. None => value, Some(field) => match &field.value { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?, BasicOrValue::Value(idx) => { parse_json_value(value, reg, ®.values[*idx])? } }, }; result.insert(key, value); } Ok(PValue::Object(result)) } _ => unexpected_json(reg, schema.root_value(), &self), } } } impl ParseWithSchema for cookie::CookieJar { fn parse_with_schema(self, schema: &JSONSchema) -> APIResult { let mut result = PValues::new(); let reg = schema.registry.as_ref(); for (field_key, field) in schema.root().fields.iter() { let name = field.name_override.as_deref().unwrap_or(field_key.as_str()); let Some(cookie) = self.get(name) else { if field.optional { continue; } else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("missing required cookie: {name}"), internal_message: None, stack: None, details: None, }); } }; let cookie_value = match field.value { BasicOrValue::Basic(basic) => parse_basic_str(&basic, cookie.value())?, BasicOrValue::Value(idx) => parse_cookie_value(cookie.value(), reg, reg.get(idx))?, }; let cookie = Cookie { name: name.to_string(), value: Box::new(cookie_value), path: cookie.path().map(|s| s.to_string()), domain: cookie.domain().map(|s| s.to_string()), secure: cookie.secure(), http_only: cookie.http_only(), expires: cookie.expires_datetime().map(|dt| { let system_time: std::time::SystemTime = dt.into(); let utc: chrono::DateTime = chrono::DateTime::from(system_time); utc.into() }), max_age: cookie .max_age() .map(|duration| duration.whole_seconds() as u64), same_site: cookie.same_site().map(|ss| match ss { cookie::SameSite::Strict => SameSite::Strict, cookie::SameSite::Lax => SameSite::Lax, cookie::SameSite::None => SameSite::None, }), partitioned: cookie.partitioned(), }; result.insert(field_key.clone(), PValue::Cookie(cookie)); } Ok(result) } } #[cfg_attr( feature = "rttrace", tracing::instrument(skip(reg), ret, level = "trace") )] fn parse_json_value(this: PValue, reg: &Registry, schema: &Value) -> APIResult { match schema { // Recurse Value::Ref(idx) => parse_json_value(this, reg, ®.values[*idx]), Value::Validation(v) => { let inner = match &v.bov { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this), BasicOrValue::Value(idx) => parse_json_value(this, reg, ®.values[*idx]), }?; match v.validate_pval(&inner) { Ok(()) => Ok(inner), Err(err) => Err(api::Error { code: api::ErrCode::InvalidArgument, message: err.to_string(), internal_message: None, stack: None, details: None, }), } } // If we have a null value for an option, that's fine. Value::Option(_) if this.is_null() => Ok(PValue::Null), // Otherwise recurse. Value::Option(opt) => match opt { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this), BasicOrValue::Value(idx) => parse_json_value(this, reg, ®.values[*idx]), }, Value::Basic(basic) => parse_basic_json(reg, basic, this), Value::Literal(lit) => { let invalid = |got| { Err(api::Error { code: api::ErrCode::InvalidArgument, message: "invalid value".to_string(), internal_message: Some(format!("expected {}, got {:#?}", lit.expecting(), got)), stack: None, details: None, }) }; match (this, lit) { (PValue::String(got), Literal::Str(want)) if &got == want => { Ok(PValue::String(got)) } (PValue::Bool(got), Literal::Bool(want)) if &got == want => Ok(PValue::Bool(got)), (PValue::Number(got), Literal::Int(want)) => { if got.as_i64() == Some(*want) { Ok(PValue::Number(got)) } else { invalid(PValue::Number(got)) } } (PValue::Number(got), Literal::Float(want)) => { if got.as_f64() == Some(*want) { Ok(PValue::Number(got)) } else { invalid(PValue::Number(got)) } } (got, _) => invalid(got), } } Value::Struct(Struct { fields }) => match this { PValue::Object(obj) => { let mut result = PValues::new(); for (key, value) in obj { let value = match fields.get(&key) { // Not known to schema; pass it unmodified. None => value, Some(field) => match &field.value { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?, BasicOrValue::Value(idx) => { parse_json_value(value, reg, ®.values[*idx])? } }, }; result.insert(key, value); } Ok(PValue::Object(result)) } _ => unexpected_json(reg, schema, &this), }, Value::Map(value_type) => match this { PValue::Object(obj) => { let mut result = PValues::new(); for (key, value) in obj { let value = match value_type { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?, BasicOrValue::Value(idx) => { parse_json_value(value, reg, ®.values[*idx])? } }; result.insert(key, value); } Ok(PValue::Object(result)) } _ => unexpected_json(reg, schema, &this), }, Value::Array(value_type) => match this { PValue::Array(arr) => { let mut result = Vec::with_capacity(arr.len()); for val in arr { let value = match value_type { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, val)?, BasicOrValue::Value(idx) => parse_json_value(val, reg, ®.values[*idx])?, }; result.push(value); } Ok(PValue::Array(result)) } _ => unexpected_json(reg, schema, &this), }, Value::Union(types) => { // Find the first type that matches. for candidate in types { let result = match candidate { BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this.clone()), BasicOrValue::Value(idx) => { parse_json_value(this.clone(), reg, ®.values[*idx]) } }; if let Ok(value) = result { return Ok(value); } } // Couldn't find a match. Err(api::Error { code: api::ErrCode::InvalidArgument, message: "invalid value".to_string(), internal_message: Some(format!("no union type matched: {}", describe_json(&this),)), stack: None, details: None, }) } } } fn unexpected_json(reg: &Registry, schema: &Value, value: &PValue) -> APIResult { Err(api::Error { code: api::ErrCode::InvalidArgument, message: "invalid value".to_string(), internal_message: Some(format!( "expected {}, got {}", schema.expecting(reg), describe_json(value), )), stack: None, details: None, }) } fn unsupported(reg: &Registry, schema: &Value) -> APIResult { Err(api::Error { code: api::ErrCode::InvalidArgument, message: "unsupported schema type".to_string(), internal_message: Some(format!( "got an unsupported schema type: {}", schema.expecting(reg), )), stack: None, details: None, }) } fn describe_json(value: &PValue) -> &'static str { match value { PValue::Null => "null", PValue::Bool(_) => "a boolean", PValue::Number(_) => "a number", PValue::Decimal(_) => "a decimal", PValue::String(_) => "a string", PValue::DateTime(_) => "a datetime", PValue::Array(_) => "an array", PValue::Object(_) => "an object", PValue::Cookie(_) => "a cookie", } } fn parse_basic_json(reg: &Registry, basic: &Basic, value: PValue) -> APIResult { match (basic, &value) { (Basic::Any, _) => Ok(value), (Basic::Null, PValue::Null) => Ok(value), (Basic::Bool, PValue::Bool(_)) => Ok(value), (Basic::Number, PValue::Number(_)) => Ok(value), (Basic::String, PValue::String(_)) => Ok(value), (Basic::String, PValue::Number(num)) => Ok(PValue::String(num.to_string())), (Basic::String, PValue::Bool(bool)) => Ok(PValue::String(bool.to_string())), (_, PValue::String(str)) => match basic { Basic::Bool => match str.as_str() { "true" => Ok(PValue::Bool(true)), "false" => Ok(PValue::Bool(false)), _ => Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid boolean value: {str}"), internal_message: None, stack: None, details: None, }), }, Basic::Number => serde_json::Number::from_str(str) .map(PValue::Number) .map_err(|_err| api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid number value: {str}"), internal_message: None, stack: None, details: None, }), Basic::Null if str == "null" => Ok(PValue::Null), _ => unexpected_json(reg, &Value::Basic(*basic), &value), }, _ => unexpected_json(reg, &Value::Basic(*basic), &value), } } fn parse_basic_str(basic: &Basic, str: &str) -> APIResult { match basic { Basic::Any | Basic::String => Ok(PValue::String(str.to_string())), Basic::Null if str.is_empty() || str == "null" => Ok(PValue::Null), Basic::Null => Err(api::Error { code: api::ErrCode::InvalidArgument, message: "invalid value".to_string(), internal_message: Some(format!("expected {}, got {:#?}", basic.expecting(), str)), stack: None, details: None, }), Basic::Bool => match str { "true" => Ok(PValue::Bool(true)), "false" => Ok(PValue::Bool(false)), _ => Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid boolean value: {str}"), internal_message: None, stack: None, details: None, }), }, Basic::Number => serde_json::Number::from_str(str) .map(PValue::Number) .map_err(|_err| api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid number value: {str}"), internal_message: None, stack: None, details: None, }), Basic::DateTime => api::DateTime::parse_from_rfc3339(str) .map(PValue::DateTime) .map_err(|_err| api::Error { code: api::ErrCode::InvalidArgument, message: "invalid datetime".to_string(), internal_message: Some(format!("invalid datetime string {str:?}")), stack: None, details: None, }), Basic::Decimal => api::Decimal::from_str(str) .map(PValue::Decimal) .map_err(|_err| api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid decimal value: {str}"), internal_message: None, stack: None, details: None, }), } } ================================================ FILE: runtimes/core/src/api/jsonschema/ser.rs ================================================ use crate::api::jsonschema::{JSONSchema, Struct}; use crate::api::schema::JSONPayload; use serde::{ser::SerializeMap, Serialize, Serializer}; struct SchemaSerializeWrapper<'a> { schema: &'a Struct, payload: &'a JSONPayload, } impl Serialize for SchemaSerializeWrapper<'_> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut map = serializer.serialize_map(None)?; if let Some(payload) = self.payload { for (key, _value) in self.schema.fields.iter() { if let Some(value) = payload.get(key) { map.serialize_entry(key, value)?; } } } map.end() } } impl JSONSchema { pub fn to_json(&self, payload: &JSONPayload) -> serde_json::Result { serde_json::to_string(&self.serialize(payload)) } pub fn to_json_pretty(&self, payload: &JSONPayload) -> serde_json::Result { serde_json::to_string_pretty(&self.serialize(payload)) } pub fn to_vec(&self, payload: &JSONPayload) -> serde_json::Result> { serde_json::to_vec(&self.serialize(payload)) } pub fn to_vec_pretty(&self, payload: &JSONPayload) -> serde_json::Result> { serde_json::to_vec_pretty(&self.serialize(payload)) } pub fn serialize<'a>(&'a self, payload: &'a JSONPayload) -> impl Serialize + 'a { SchemaSerializeWrapper { schema: self.root(), payload, } } } ================================================ FILE: runtimes/core/src/api/jsonschema/validation.rs ================================================ use std::fmt::Display; use crate::{ api::{Decimal, PValue}, encore::parser::schema::v1 as schema, }; use thiserror::Error; use super::BasicOrValue; #[derive(Debug, Clone)] pub struct Validation { pub bov: BasicOrValue, pub expr: Expr, } impl Validation { pub fn validate_pval<'a>(&'a self, val: &'a PValue) -> Result<(), Error<'a>> { self.expr.validate_pval(val) } pub fn validate_jval<'a>(&'a self, val: &'a serde_json::Value) -> Result<(), Error<'a>> { self.expr.validate_jval(val) } } #[derive(Debug, Clone)] pub enum Expr { Rule(Rule), And(Vec), Or(Vec), } macro_rules! impl_validate { ($method:ident, $typ:ty) => { pub fn $method<'a>(&'a self, val: &'a $typ) -> Result<(), Error<'a>> { match self { Expr::Rule(rule) => rule.$method(val), Expr::And(exprs) => { for expr in exprs { expr.$method(val)?; } Ok(()) } Expr::Or(exprs) => { let mut first_err = None; for expr in exprs { match expr.$method(val) { Ok(()) => return Ok(()), Err(err) => { if first_err.is_none() { first_err = Some(err); } } } } match first_err { Some(err) => Err(err), None => Ok(()), } } } } }; } impl Expr { impl_validate!(validate_pval, PValue); impl_validate!(validate_jval, serde_json::Value); } #[derive(Debug, Clone)] pub enum Rule { MinLen(u64), MaxLen(u64), MinVal(f64), MaxVal(f64), StartsWith(String), EndsWith(String), MatchesRegexp(regex::Regex), Is(Is), } #[derive(Debug, Clone)] pub enum Is { Email, Url, } #[derive(Debug)] pub enum Num<'a> { Number(&'a serde_json::Number), Decimal(&'a Decimal), } impl<'a> Display for Num<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Num::Number(number) => write!(f, "{number}"), Num::Decimal(decimal) => write!(f, "{decimal}"), } } } #[derive(Error, Debug)] pub enum Error<'a> { #[error("length too short (got {got}, expected at least {min})")] MinLen { got: usize, min: usize }, #[error("length too long (got {got}, expected at most {max})")] MaxLen { got: usize, max: usize }, #[error("value must be at least {min} (got {got})")] MinVal { got: Num<'a>, min: f64 }, #[error("value must be at most {max} (got {got})")] MaxVal { got: Num<'a>, max: f64 }, #[error("value does not match the regexp {regexp:#?}")] MatchesRegexp { regexp: &'a str }, #[error("value does not start with {prefix:#?}")] StartsWith { prefix: &'a str }, #[error("value does not end with {suffix:#?}")] EndsWith { suffix: &'a str }, #[error("value is not {expected}")] Is { expected: &'a str }, #[error("unexpected type (expected {want})")] UnexpectedType { want: &'a str }, } impl Rule { #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self), ret, level = "trace") )] pub fn validate_pval<'a>(&'a self, val: &'a PValue) -> Result<(), Error<'a>> { match self { Rule::MinLen(min_len) => match val { PValue::Array(arr) => { if arr.len() < *min_len as usize { Err(Error::MinLen { got: arr.len(), min: *min_len as usize, }) } else { Ok(()) } } PValue::String(str) => { if str.len() < *min_len as usize { Err(Error::MinLen { got: str.len(), min: *min_len as usize, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "string or array", }), }, Rule::MaxLen(max_len) => match val { PValue::Array(arr) => { if arr.len() > *max_len as usize { Err(Error::MaxLen { got: arr.len(), max: *max_len as usize, }) } else { Ok(()) } } PValue::String(str) => { if str.len() > *max_len as usize { Err(Error::MaxLen { got: str.len(), max: *max_len as usize, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "string or array", }), }, Rule::MinVal(min_val) => match val { PValue::Number(num) => { let bad = if num.is_i64() { num.as_i64().unwrap() < *min_val as i64 } else if num.is_u64() { num.as_u64().unwrap() < *min_val as u64 } else if num.is_f64() { num.as_f64().unwrap() < *min_val } else { return Err(Error::UnexpectedType { want: "number" }); }; if bad { Err(Error::MinVal { got: Num::Number(num), min: *min_val, }) } else { Ok(()) } } PValue::Decimal(d) => { let bad = d < *min_val; if bad { Err(Error::MinVal { got: Num::Decimal(d), min: *min_val, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "number" }), }, Rule::MaxVal(max_val) => match val { PValue::Number(num) => { let bad = if num.is_i64() { num.as_i64().unwrap() > *max_val as i64 } else if num.is_u64() { num.as_u64().unwrap() > *max_val as u64 } else if num.is_f64() { num.as_f64().unwrap() > *max_val } else { return Err(Error::UnexpectedType { want: "number" }); }; if bad { Err(Error::MaxVal { got: Num::Number(num), max: *max_val, }) } else { Ok(()) } } PValue::Decimal(d) => { let bad = d > *max_val; if bad { Err(Error::MaxVal { got: Num::Decimal(d), max: *max_val, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "number" }), }, Rule::StartsWith(prefix) => match val { PValue::String(str) => { if str.starts_with(prefix) { Ok(()) } else { Err(Error::StartsWith { prefix }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::EndsWith(suffix) => match val { PValue::String(str) => { if str.ends_with(suffix) { Ok(()) } else { Err(Error::EndsWith { suffix }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::MatchesRegexp(re) => match val { PValue::String(str) => { if re.is_match(str) { Ok(()) } else { Err(Error::MatchesRegexp { regexp: re.as_str(), }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::Is(Is::Email) => match val { PValue::String(str) => { let email = email_address::EmailAddress::parse_with_options( str, email_address::Options::default().without_display_text(), ); match email { Ok(_) => Ok(()), Err(_) => Err(Error::Is { expected: "an email", }), } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::Is(Is::Url) => match val { PValue::String(str) => { let u = url::Url::parse(str); match u { Ok(_) => Ok(()), Err(_) => Err(Error::Is { expected: "a url" }), } } _ => Err(Error::UnexpectedType { want: "string" }), }, } } #[cfg_attr( feature = "rttrace", tracing::instrument(skip(self), ret, level = "trace") )] pub fn validate_jval<'a>(&'a self, val: &'a serde_json::Value) -> Result<(), Error<'a>> { use serde_json::Value as JVal; match self { Rule::MinLen(min_len) => match val { JVal::Array(arr) => { if arr.len() < *min_len as usize { Err(Error::MinLen { got: arr.len(), min: *min_len as usize, }) } else { Ok(()) } } JVal::String(str) => { if str.len() < *min_len as usize { Err(Error::MinLen { got: str.len(), min: *min_len as usize, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "string or array", }), }, Rule::MaxLen(max_len) => match val { JVal::Array(arr) => { if arr.len() > *max_len as usize { Err(Error::MaxLen { got: arr.len(), max: *max_len as usize, }) } else { Ok(()) } } JVal::String(str) => { if str.len() > *max_len as usize { Err(Error::MaxLen { got: str.len(), max: *max_len as usize, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "string or array", }), }, Rule::MinVal(min_val) => match val { JVal::Number(num) => { let bad = if num.is_i64() { num.as_i64().unwrap() < *min_val as i64 } else if num.is_u64() { num.as_u64().unwrap() < *min_val as u64 } else if num.is_f64() { num.as_f64().unwrap() < *min_val } else { return Err(Error::UnexpectedType { want: "number" }); }; if bad { Err(Error::MinVal { got: Num::Number(num), min: *min_val, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "number" }), }, Rule::MaxVal(max_val) => match val { JVal::Number(num) => { let bad = if num.is_i64() { num.as_i64().unwrap() > *max_val as i64 } else if num.is_u64() { num.as_u64().unwrap() > *max_val as u64 } else if num.is_f64() { num.as_f64().unwrap() > *max_val } else { return Err(Error::UnexpectedType { want: "number" }); }; if bad { Err(Error::MaxVal { got: Num::Number(num), max: *max_val, }) } else { Ok(()) } } _ => Err(Error::UnexpectedType { want: "number" }), }, Rule::StartsWith(want) => match val { JVal::String(got) => { if got.starts_with(got) { Ok(()) } else { Err(Error::StartsWith { prefix: want }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::EndsWith(want) => match val { JVal::String(got) => { if got.ends_with(got) { Ok(()) } else { Err(Error::EndsWith { suffix: want }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::MatchesRegexp(re) => match val { JVal::String(str) => { if re.is_match(str) { Ok(()) } else { Err(Error::MatchesRegexp { regexp: re.as_str(), }) } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::Is(Is::Email) => match val { JVal::String(str) => { let email = email_address::EmailAddress::parse_with_options( str, email_address::Options::default().without_display_text(), ); match email { Ok(_) => Ok(()), Err(_) => Err(Error::Is { expected: "email" }), } } _ => Err(Error::UnexpectedType { want: "string" }), }, Rule::Is(Is::Url) => match val { JVal::String(str) => { let u = url::Url::parse(str); match u { Ok(_) => Ok(()), Err(_) => Err(Error::Is { expected: "url" }), } } _ => Err(Error::UnexpectedType { want: "string" }), }, } } } impl TryFrom<&schema::ValidationExpr> for Expr { type Error = anyhow::Error; fn try_from(expr: &schema::ValidationExpr) -> Result { let Some(expr) = &expr.expr else { return Err(anyhow::anyhow!("missing expr")); }; use schema::validation_expr::Expr as PbExpr; match expr { PbExpr::Rule(rule) => Ok(Expr::Rule(rule.try_into()?)), PbExpr::And(expr) => { let mut and = Vec::new(); for expr in &expr.exprs { and.push(expr.try_into()?); } Ok(Expr::And(and)) } PbExpr::Or(expr) => { let mut or = Vec::new(); for expr in &expr.exprs { or.push(expr.try_into()?); } Ok(Expr::Or(or)) } } } } impl TryFrom<&schema::ValidationRule> for Rule { type Error = anyhow::Error; fn try_from(rule: &schema::ValidationRule) -> Result { let Some(rule) = &rule.rule else { return Err(anyhow::anyhow!("missing validation rule")); }; use schema::validation_rule::Is as PbIs; use schema::validation_rule::Rule as PbRule; match rule { PbRule::MinLen(val) => Ok(Rule::MinLen(*val)), PbRule::MaxLen(val) => Ok(Rule::MaxLen(*val)), PbRule::MinVal(val) => Ok(Rule::MinVal(*val)), PbRule::MaxVal(val) => Ok(Rule::MaxVal(*val)), PbRule::StartsWith(val) => Ok(Rule::StartsWith(val.clone())), PbRule::EndsWith(val) => Ok(Rule::EndsWith(val.clone())), PbRule::MatchesRegexp(val) => { let re = regex::Regex::new(val)?; Ok(Rule::MatchesRegexp(re)) } PbRule::Is(is) => Ok(Rule::Is(match PbIs::try_from(*is)? { PbIs::Unknown => anyhow::bail!("unknown 'is' rule"), PbIs::Email => Is::Email, PbIs::Url => Is::Url, })), } } } ================================================ FILE: runtimes/core/src/api/manager.rs ================================================ use std::collections::HashMap; use std::future::{Future, IntoFuture}; use std::sync::{Arc, Mutex}; use anyhow::Context; use crate::api::auth::{LocalAuthHandler, RemoteAuthHandler}; use crate::api::call::ServiceRegistry; use crate::api::gateway::Gateway; use crate::api::http_server::HttpServer; use crate::api::paths::Pather; use crate::api::reqauth::platform; use crate::api::schema::encoding::EncodingConfig; use crate::api::schema::JSONPayload; use crate::api::{ auth, cors, encore_routes, endpoints_from_meta, jsonschema, paths, reqauth, server, APIResult, Endpoint, ToResponse, }; use crate::encore::parser::meta::v1 as meta; use crate::encore::runtime::v1 as runtime; use crate::trace::Tracer; use crate::{api, metrics, model, pubsub, secrets, EncoreName, EndpointName, Hosted}; use super::encore_routes::healthz; use super::websocket_client::WebSocketClient; use super::{PValues, ResponsePayload}; pub struct ManagerConfig<'a> { pub meta: &'a meta::Data, pub environment: &'a runtime::Environment, pub gateways: Vec, pub hosted_services: Vec, pub hosted_gateway_rids: Vec, pub svc_auth_methods: Vec, pub deploy_id: String, pub platform: &'a runtime::EncorePlatform, pub secrets: &'a secrets::Manager, pub service_discovery: runtime::ServiceDiscovery, pub http_client: reqwest::Client, pub tracer: Tracer, pub platform_validator: Arc, pub pubsub_push_registry: pubsub::PushHandlerRegistry, pub runtime: tokio::runtime::Handle, pub testing: bool, pub proxied_push_subs: HashMap, pub metrics: &'a metrics::Manager, } pub struct Manager { gateway_listen_addr: Option, api_listener: Mutex>, service_registry: Arc, healthz: healthz::Handler, pubsub_push_registry: pubsub::PushHandlerRegistry, api_server: Option, runtime: tokio::runtime::Handle, gateways: HashMap, testing: bool, metrics: metrics::Manager, } impl ManagerConfig<'_> { pub fn build(mut self) -> anyhow::Result { let gateway_listen_addr = if !self.hosted_gateway_rids.is_empty() { // We have a gateway. Have the gateway listen on the provided listen_addr. Some(listen_addr()) } else { None }; let api_listener = if !self.hosted_services.is_empty() { // If we already have a gateway, it's listening on the externally provided listen addr. // Use a random local port in that case. let addr = if gateway_listen_addr.is_some() { "127.0.0.1:0".to_string() } else { listen_addr() }; let ln = std::net::TcpListener::bind(addr).context("unable to bind to port")?; Some(ln) } else { None }; // Get the local address for use by the service registry // for calling services hosted by this instance. let own_api_address = match api_listener { None => None, Some(ref ln) => { let addr = ln .local_addr() .context("unable to determine listen address")?; Some(addr) } }; let healthz_handler = encore_routes::healthz::Handler { app_revision: self.meta.app_revision.clone(), // Remove the "roll_" prefix from the deploy_id. deploy_id: self .deploy_id .strip_prefix("roll_") .unwrap_or(&self.deploy_id) .to_string(), }; let hosted_services = Hosted::from_iter(self.hosted_services.into_iter().map(|s| s.name)); let (endpoints, hosted_endpoints) = endpoints_from_meta(self.meta, &hosted_services) .context("unable to compute endpoints descriptions")?; let inbound_svc_auth = { let mut entries = Vec::with_capacity(self.svc_auth_methods.len()); for auth in self.svc_auth_methods.drain(..) { let auth_method = reqauth::service_auth_method(self.secrets, self.environment, auth) .context("unable to initialize service auth method")?; entries.push(auth_method); } entries }; let service_registry = ServiceRegistry::new( self.secrets, endpoints.clone(), self.environment, self.service_discovery, own_api_address .as_ref() .map(|addr| addr.to_string()) .as_deref(), &inbound_svc_auth, &hosted_services, self.deploy_id.clone(), self.http_client.clone(), self.tracer.clone(), ) .context("unable to create service registry")?; let service_registry = Arc::new(service_registry); let gateways_by_rid: HashMap = HashMap::from_iter(self.gateways.drain(..).map(|gw| (gw.rid.clone(), gw))); let hosted_gateways: HashMap<&str, &runtime::Gateway> = HashMap::from_iter(self.hosted_gateway_rids.iter().filter_map(|rid| { gateways_by_rid .get(rid) .map(|gw| (gw.encore_name.as_str(), gw)) })); let mut gateways = HashMap::new(); let routes = paths::compute(endpoints.values().map(|ep| RoutePerService(ep.to_owned()))); let mut auth_data_schemas = HashMap::new(); for gw in &self.meta.gateways { let Some(gw_cfg) = hosted_gateways.get(gw.encore_name.as_str()) else { continue; }; let Some(cors_cfg) = &gw_cfg.cors else { anyhow::bail!("missing CORS configuration for gateway {}", gw.encore_name); }; let auth_handler = build_auth_handler( self.meta, gw, &service_registry, self.http_client.clone(), self.tracer.clone(), self.metrics.registry(), ) .context("unable to build authenticator")?; let meta_headers = cors::MetaHeaders::from_schema(&endpoints, auth_handler.as_ref()); let cors_config = cors::config(cors_cfg, meta_headers) .context("failed to parse CORS configuration")?; auth_data_schemas.insert( gw.encore_name.clone(), auth_handler.as_ref().map(|ah| ah.auth_data().clone()), ); gateways.insert( gw.encore_name.clone().into(), Gateway::new( gw.encore_name.clone().into(), service_registry.clone(), routes.clone(), auth_handler, cors_config, healthz_handler.clone(), own_api_address, self.proxied_push_subs.clone(), self.tracer.clone(), inbound_svc_auth.clone(), ) .context("couldn't create gateway")?, ); } let api_server = if !hosted_services.is_empty() { let server = server::Server::new( endpoints.clone(), hosted_endpoints, self.platform_validator, inbound_svc_auth, self.tracer.clone(), auth_data_schemas, Arc::clone(self.metrics.registry()), ) .context("unable to create API server")?; Some(server) } else { None }; Ok(Manager { gateway_listen_addr, api_listener: Mutex::new(api_listener), service_registry, api_server, gateways, pubsub_push_registry: self.pubsub_push_registry, runtime: self.runtime, healthz: healthz_handler, testing: self.testing, metrics: self.metrics.clone(), }) } } #[derive(Debug)] struct RoutePerService(Arc); impl Pather for RoutePerService { type Key = EncoreName; type Value = Arc; fn key(&self) -> Self::Key { self.0.name.service().into() } fn value(&self) -> Self::Value { self.0.clone() } fn path(&self) -> &meta::Path { &self.0.path } } fn build_auth_handler( meta: &meta::Data, gw: &meta::Gateway, service_registry: &ServiceRegistry, http_client: reqwest::Client, tracer: Tracer, metrics_registry: &Arc, ) -> anyhow::Result> { let Some(explicit) = &gw.explicit else { return Ok(None); }; let Some(auth) = &explicit.auth_handler else { return Ok(None); }; let auth_params = auth.params.as_ref().context("missing auth params")?; let mut builder = jsonschema::Builder::new(meta); let schema = { let mut cfg = EncodingConfig { meta, registry_builder: &mut builder, default_loc: None, rpc_path: None, supports_body: false, supports_query: true, supports_header: true, supports_path: false, supports_http_status: false, }; cfg.compute(auth_params) .context("unable to compute auth handler schema")? }; let auth_data_schema_idx = builder.register_type(auth.auth_data.as_ref().context("missing auth data")?)?; let registry = builder.build(); let schema = schema .build(®istry) .context("unable to build auth handler schema")?; // let is_local = hosted_services.contains(&explicit.service_name); let is_local = true; let name = EndpointName::new(explicit.service_name.clone(), auth.name.clone()); let requests_total = metrics::requests_total_counter(metrics_registry, &explicit.service_name, &auth.name); let auth_data = registry.schema(auth_data_schema_idx); let auth_handler = if is_local { auth::Authenticator::local( schema.clone(), auth_data, LocalAuthHandler { name, schema, handler: Default::default(), tracer, requests_total, }, )? } else { auth::Authenticator::remote( schema, auth_data.clone(), RemoteAuthHandler::new(name, service_registry, http_client, auth_data, tracer)?, )? }; Ok(Some(auth_handler)) } impl Manager { pub fn gateway(&self, name: &EncoreName) -> Option<&Gateway> { self.gateways.get(name) } pub fn server(&self) -> Option<&server::Server> { self.api_server.as_ref() } pub fn call( &self, target: EndpointName, data: JSONPayload, source: Option>, opts: Option, ) -> impl Future> + 'static { self.service_registry.api_call(target, data, source, opts) } pub fn endpoints(&self) -> &api::EndpointMap { self.service_registry.endpoints() } pub fn metrics_registry(&self) -> &Arc { self.metrics.registry() } pub fn stream( &self, endpoint_name: EndpointName, data: JSONPayload, source: Option>, opts: Option, ) -> impl Future> + 'static { self.service_registry .connect_stream(endpoint_name, data, source, opts) } /// Starts serving the API. pub fn start_serving(&self) -> tokio::task::JoinHandle> { let api = self.api_server.as_ref().map(|srv| srv.router()); async fn fallback( req: axum::http::Request, ) -> axum::response::Response { api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {}", req.uri().path())), stack: None, details: None, } .to_response(None) } let encore_routes = encore_routes::Desc { healthz: self.healthz.clone(), push_registry: self.pubsub_push_registry.clone(), } .router(); let fallback = axum::Router::new().fallback(fallback); let server = HttpServer::new(encore_routes, api, fallback); let api_listener = self.api_listener.lock().unwrap().take(); let gateway_listener = self.gateway_listen_addr.clone(); // TODO handle multiple gateways let gateway = self.gateways.values().next().cloned(); let testing = self.testing; self.runtime.spawn(async move { let gateway_parts = (gateway, gateway_listener); let gateway_fut = match gateway_parts { (Some(gw), Some(ref ln)) => { if !testing { log::debug!(addr=ln; "gateway listening for incoming requests"); Some(gw.serve(ln)) } else { // No need running the gateway in tests None } }, (Some(_), None) => { ::log::error!("internal encore error: misconfigured api gateway (missing listener), skipping"); None } (None, Some(_)) => { ::log::error!("internal encore error: misconfigured api gateway (missing gateway config), skipping"); None } (None, None) => None, }; let api_fut = match api_listener { Some(ln) => { let addr = ln.local_addr().map(|addr| addr.to_string()).unwrap_or_default(); log::debug!(addr = addr; "api server listening for incoming requests"); ln .set_nonblocking(true) .context("unable to set nonblocking")?; let axum_listener = tokio::net::TcpListener::from_std(ln) .context("unable to convert listener to tokio")?; let fut = axum::serve(axum_listener, server).into_future(); Some(fut) } None => None, }; tokio::select! { res = async { gateway_fut.unwrap().await }, if gateway_fut.is_some() => { res.context("serve gateway").inspect_err(|err| log::error!("api gateway failed: {:?}", err))?; }, res = async { api_fut.unwrap().await }, if api_fut.is_some() => { res.context("serve api").inspect_err(|err| log::error!("api server failed: {:?}", err))?; }, else => { // Nothing to serve. ::log::debug!("no api server or gateway to serve"); } }; Ok(()) }) } } fn listen_addr() -> String { if let Ok(addr) = std::env::var("ENCORE_LISTEN_ADDR") { return addr; } if let Ok(port) = std::env::var("PORT") { return format!("0.0.0.0:{port}"); } "0.0.0.0:8080".to_string() } #[derive(Debug)] pub struct CallOpts { pub auth: Option, } #[derive(Debug)] pub struct AuthOpts { pub data: PValues, pub user_id: String, } ================================================ FILE: runtimes/core/src/api/mod.rs ================================================ pub mod auth; pub mod call; mod cors; mod encore_routes; mod endpoint; mod error; pub mod gateway; mod http_server; mod httputil; pub mod jsonschema; mod manager; mod paths; mod pvalue; pub mod reqauth; pub mod schema; mod server; mod static_assets; pub mod websocket; pub mod websocket_client; pub use endpoint::*; pub use error::*; pub use manager::*; pub use pvalue::*; ================================================ FILE: runtimes/core/src/api/paths.rs ================================================ use crate::encore::parser::meta::v1 as meta; use serde::Serialize; use std::collections::{HashMap, HashSet}; pub trait Pather { type Key; type Value; fn key(&self) -> Self::Key; fn value(&self) -> Self::Value; fn path(&self) -> &meta::Path; } #[derive(Debug, Serialize, Clone)] pub struct PathSet { pub main: HashMap)>>, pub fallback: HashMap)>>, } /// Computes paths to register, grouped by the given key for easier correlation. pub fn compute(endpoints: impl Iterator) -> PathSet where P: Pather, K: Eq + Clone + std::hash::Hash, { use crate::encore::parser::meta::v1::path_segment::SegmentType; let mut main: HashMap)>> = HashMap::new(); let mut fallback: HashMap)>> = HashMap::new(); for ep in endpoints { let path = ep.path(); let mut entries = Vec::with_capacity(2); // Compute the axum path. let mut result = String::new(); for seg in &path.segments { let typ = SegmentType::try_from(seg.r#type).unwrap_or(SegmentType::Literal); match typ { SegmentType::Literal => { result.push('/'); result.push_str(&seg.value) } SegmentType::Param => result.push_str("/:_"), SegmentType::Wildcard => { // The wildcard is the last segment. // Axum doesn't match e.g. "/" for "/*wildcard", so we need to register both. result.push('/'); entries.push(result.clone()); result.push_str("*_"); } SegmentType::Fallback => { // Axum doesn't match e.g. "/" for "/*wildcard", so we need to register both. result.push('/'); entries.push(result.clone()); result.push_str("*_"); } } } entries.push(result); let is_fallback = path .segments .last() .is_some_and(|seg| seg.r#type == SegmentType::Fallback as i32); let key = ep.key(); let routes = (ep.value(), entries); if is_fallback { fallback.entry(key).or_default().push(routes); } else { main.entry(key).or_default().push(routes); } } // Add paths for TSR redirects. { for paths in [&mut main, &mut fallback] { let path_set: HashSet = HashSet::from_iter( paths .values() .flatten() .flat_map(|(_, routes)| routes.iter()) .cloned(), ); for entries in paths.values_mut() { for (_, endpoint_routes) in entries { let mut tsr_routes = Vec::new(); for route in endpoint_routes.iter() { // Is this entry incompatible with TSR? if route == "/" || route.contains("/*") || route.ends_with('/') { continue; } let tsr = format!("{route}/"); if !path_set.contains(&tsr) { tsr_routes.push(tsr); } } endpoint_routes.extend(tsr_routes); } } } } PathSet { main, fallback } } #[cfg(test)] mod tests { use super::*; use crate::encore::parser::meta::v1::path_segment::SegmentType; use serde::ser::SerializeStruct; #[test] fn test_basic() { let endpoints = vec![ ep("one", "a", &[lit("foo")]), ep("two", "a", &[lit("foo"), lit("bar")]), ]; let paths = compute(endpoints.into_iter()); insta::with_settings!({sort_maps => true}, { insta::assert_yaml_snapshot!(paths); }); } #[test] fn test_tsr_conflict() { let endpoints = vec![ ep("one", "a", &[lit("foo"), lit("")]), ep("two", "b", &[lit("foo")]), ]; let paths = compute(endpoints.into_iter()); insta::with_settings!({sort_maps => true}, { insta::assert_yaml_snapshot!(paths); }); } #[test] fn test_wildcard() { let endpoints = vec![ep("one", "a", &[wildcard("foo")])]; let paths = compute(endpoints.into_iter()); insta::with_settings!({sort_maps => true}, { insta::assert_yaml_snapshot!(paths); }); } #[test] fn test_fallback() { let endpoints = vec![ep("one", "a", &[fallback("foo")])]; let paths = compute(endpoints.into_iter()); insta::with_settings!({sort_maps => true}, { insta::assert_yaml_snapshot!(paths); }); } fn path(segs: &[meta::PathSegment]) -> meta::Path { meta::Path { segments: segs.to_vec(), r#type: meta::path::Type::Url as i32, } } fn seg(typ: SegmentType, value: &str) -> meta::PathSegment { meta::PathSegment { r#type: typ as i32, value: value.to_string(), value_type: meta::path_segment::ParamType::String as i32, validation: None, } } fn lit(str: &str) -> meta::PathSegment { seg(SegmentType::Literal, str) } fn wildcard(str: &str) -> meta::PathSegment { seg(SegmentType::Wildcard, str) } fn fallback(str: &str) -> meta::PathSegment { seg(SegmentType::Fallback, str) } #[derive(Clone, Debug)] struct TestEndpoint { name: &'static str, key: &'static str, path: meta::Path, } impl Serialize for TestEndpoint { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_struct("TestEndpoint", 2)?; let path = path_to_str(&self.path); state.serialize_field("key", &self.key)?; state.serialize_field("path", &path)?; state.end() } } fn ep(name: &'static str, key: &'static str, segs: &[meta::PathSegment]) -> TestEndpoint { TestEndpoint { name, key, path: path(segs), } } impl Pather for TestEndpoint { type Key = String; type Value = String; fn key(&self) -> Self::Key { self.key.to_string() } fn value(&self) -> Self::Value { self.name.to_string() } fn path(&self) -> &meta::Path { &self.path } } fn path_to_str(path: &meta::Path) -> String { let mut result = String::new(); for seg in &path.segments { result.push('/'); use meta::path_segment::SegmentType; match SegmentType::try_from(seg.r#type).unwrap() { SegmentType::Literal => result.push_str(&seg.value), SegmentType::Param => { result.push(':'); result.push_str(&seg.value) } SegmentType::Wildcard | SegmentType::Fallback => { result.push('*'); result.push_str(&seg.value) } } } result } } ================================================ FILE: runtimes/core/src/api/pvalue.rs ================================================ use std::{ collections::BTreeMap, fmt::{Debug, Display}, ops::{Add, Div, Mul, Sub}, str::FromStr, }; use bytes::BytesMut; use malachite::rational::{conversion::primitive_int_from_rational, Rational}; use malachite::{ base::num::conversion::{ string::options::ToSciOptions, traits::{FromSciString, ToSci}, }, rational::conversion::primitive_float_from_rational, }; use serde::{Serialize, Serializer}; use crate::sqldb; /// Represents any valid value in a request/response payload. /// /// It is a more type-safe version of JSON, where we support additional /// semantic types like timestamps. #[derive(Clone, Eq, PartialEq, Debug)] pub enum PValue { /// Represents a JSON null value. Null, /// Represents a JSON boolean. Bool(bool), /// Represents a JSON number, whether integer or floating point. Number(serde_json::Number), /// Represents a Decimal type with arbitrary precision. Decimal(Decimal), /// Represents a JSON string. String(String), /// Represents a JSON array. Array(Vec), /// Represents a JSON object. Object(PValues), // Represents a datetime value. DateTime(DateTime), // Represents a cookie. Cookie(Cookie), } impl PValue { pub fn is_null(&self) -> bool { matches!(self, PValue::Null) } pub fn is_array(&self) -> bool { matches!(self, PValue::Array(..)) } /// If the `PValue` is a String, returns the associated str. /// Returns None otherwise. pub fn as_str(&self) -> Option<&str> { match self { PValue::String(s) => Some(s), _ => None, } } pub fn type_name(&self) -> &'static str { match self { PValue::Null => "null", PValue::Bool(_) => "boolean", PValue::Number(_) => "number", PValue::String(_) => "string", PValue::Array(_) => "array", PValue::Object(_) => "object", PValue::DateTime(_) => "datetime", PValue::Cookie(_) => "cookie", PValue::Decimal(_) => "decimal", } } } pub type PValues = BTreeMap; pub type DateTime = chrono::DateTime; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cookie { pub name: String, pub value: Box, pub path: Option, pub domain: Option, pub secure: Option, pub http_only: Option, pub expires: Option, pub max_age: Option, pub same_site: Option, pub partitioned: Option, } impl<'a> From<&'a Cookie> for cookie::Cookie<'a> { fn from(value: &'a Cookie) -> Self { let mut builder = cookie::CookieBuilder::new(&value.name, value.value.to_string()); if let Some(path) = &value.path { builder = builder.path(path); } if let Some(domain) = &value.domain { builder = builder.domain(domain); } if let Some(secure) = &value.secure { builder = builder.secure(*secure); } if let Some(http_only) = &value.http_only { builder = builder.http_only(*http_only); } if let Some(expires) = &value.expires { let system_time: std::time::SystemTime = (*expires).into(); let expire = cookie::time::OffsetDateTime::from(system_time); builder = builder.expires(expire); } if let Some(max_age) = &value.max_age { builder = builder.max_age(cookie::time::Duration::seconds(*max_age as i64)); } if let Some(same_site) = &value.same_site { let same_site = match same_site { SameSite::Strict => cookie::SameSite::Strict, SameSite::Lax => cookie::SameSite::Lax, SameSite::None => cookie::SameSite::None, }; builder = builder.same_site(same_site); } if let Some(partitioned) = &value.partitioned { builder = builder.partitioned(*partitioned); } builder.build() } } impl Display for Cookie { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let c: cookie::Cookie<'_> = self.into(); write!(f, "{c}") } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SameSite { Strict, Lax, None, } impl Display for SameSite { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SameSite::Strict => write!(f, "Strict"), SameSite::Lax => write!(f, "Lax"), SameSite::None => write!(f, "None"), } } } #[derive(Clone, Hash, Eq, PartialEq, Debug)] pub struct Decimal(Rational); impl Add for &Decimal { type Output = Decimal; fn add(self, rhs: Self) -> Self::Output { Decimal((&self.0).add(&rhs.0)) } } impl Sub for &Decimal { type Output = Decimal; fn sub(self, rhs: Self) -> Self::Output { Decimal((&self.0).sub(&rhs.0)) } } impl Mul for &Decimal { type Output = Decimal; fn mul(self, rhs: Self) -> Self::Output { Decimal((&self.0).mul(&rhs.0)) } } impl Div for &Decimal { type Output = Decimal; fn div(self, rhs: Self) -> Self::Output { Decimal((&self.0).div(&rhs.0)) } } impl TryFrom<&Decimal> for i64 { type Error = primitive_int_from_rational::SignedFromRationalError; fn try_from(value: &Decimal) -> Result { i64::try_from(&value.0) } } impl TryFrom<&Decimal> for i32 { type Error = primitive_int_from_rational::SignedFromRationalError; fn try_from(value: &Decimal) -> Result { i32::try_from(&value.0) } } impl TryFrom<&Decimal> for i16 { type Error = primitive_int_from_rational::SignedFromRationalError; fn try_from(value: &Decimal) -> Result { i16::try_from(&value.0) } } impl TryFrom<&Decimal> for f64 { type Error = primitive_float_from_rational::FloatConversionError; fn try_from(value: &Decimal) -> Result { f64::try_from(&value.0) } } impl TryFrom<&Decimal> for f32 { type Error = primitive_float_from_rational::FloatConversionError; fn try_from(value: &Decimal) -> Result { f32::try_from(&value.0) } } impl PartialEq for &Decimal { fn eq(&self, other: &f64) -> bool { self.0 == *other } } impl PartialOrd for &Decimal { fn partial_cmp(&self, other: &f64) -> Option { self.0.partial_cmp(other) } } impl FromStr for Decimal { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let r = Rational::from_sci_string(s) .ok_or_else(|| anyhow::anyhow!("Failed to parse decimal from string: {s}"))?; Ok(Decimal(r)) } } impl Display for Decimal { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut opts = ToSciOptions::default(); opts.set_size_complete(); opts.set_include_trailing_zeros(true); if !self.0.fmt_sci_valid(opts) { // e.g the number is 1/3 opts.set_scale(10); } self.0.fmt_sci(f, opts) } } impl From for Decimal { fn from(r: Rational) -> Self { Decimal(r) } } impl tokio_postgres::types::ToSql for Decimal { fn to_sql( &self, _ty: &tokio_postgres::types::Type, out: &mut BytesMut, ) -> Result> { let n = sqldb::numeric::Numeric::from_str(&self.to_string())?; sqldb::numeric::numeric_to_sql(n, out); Ok(tokio_postgres::types::IsNull::No) } tokio_postgres::types::accepts!(NUMERIC); tokio_postgres::types::to_sql_checked!(); } impl<'a> tokio_postgres::types::FromSql<'a> for Decimal { fn from_sql( _ty: &tokio_postgres::types::Type, raw: &[u8], ) -> Result> { let n = sqldb::numeric::numeric_from_sql(raw)?; let d = Decimal::from_str(&n.to_string())?; Ok(d) } tokio_postgres::types::accepts!(NUMERIC); } impl Serialize for PValue { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { PValue::Null => serializer.serialize_unit(), PValue::Bool(b) => serializer.serialize_bool(*b), PValue::Number(n) => n.serialize(serializer), PValue::String(s) => serializer.serialize_str(s), PValue::Array(a) => a.serialize(serializer), PValue::Object(o) => o.serialize(serializer), PValue::DateTime(dt) => dt.serialize(serializer), PValue::Cookie(c) => serializer.serialize_str(&c.to_string()), PValue::Decimal(d) => serializer.serialize_str(&d.to_string()), } } } impl Display for PValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PValue::Null => write!(f, "null"), PValue::Bool(b) => write!(f, "{b}"), PValue::Number(n) => write!(f, "{n}"), PValue::String(s) => write!(f, "{s}"), PValue::DateTime(dt) => write!(f, "{}", dt.to_rfc3339()), PValue::Array(a) => { write!(f, "[")?; for (i, v) in a.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{v}")?; } write!(f, "]") } PValue::Object(o) => { write!(f, "{{")?; for (i, (k, v)) in o.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{k}: {v}")?; } write!(f, "}}") } PValue::Cookie(c) => write!(f, "{c}"), PValue::Decimal(d) => write!(f, "{d}",), } } } impl From for PValue { fn from(value: serde_json::Value) -> Self { match value { serde_json::Value::Null => PValue::Null, serde_json::Value::Bool(b) => PValue::Bool(b), serde_json::Value::Number(n) => PValue::Number(n), serde_json::Value::String(s) => PValue::String(s), serde_json::Value::Array(a) => PValue::Array(a.into_iter().map(PValue::from).collect()), serde_json::Value::Object(o) => { PValue::Object(o.into_iter().map(|(k, v)| (k, PValue::from(v))).collect()) } } } } ================================================ FILE: runtimes/core/src/api/reqauth/caller.rs ================================================ use crate::{EncoreName, EndpointName}; use std::str::FromStr; #[derive(Debug, Clone)] pub enum Caller { APIEndpoint(EndpointName), PubSubMessage { topic: EncoreName, subscription: EncoreName, message_id: String, }, App { deploy_id: String, }, Gateway { /// The name of the gateway. gateway: EncoreName, }, EncorePrincipal(String), } impl Caller { pub fn serialize(&self) -> String { match self { Caller::APIEndpoint(name) => format!("api:{}.{}", name.service(), name.endpoint()), Caller::PubSubMessage { topic, subscription, message_id, } => format!("pubsub:{topic}:{subscription}:{message_id}"), Caller::Gateway { gateway } => { format!("gateway:{gateway}") } Caller::App { deploy_id } => format!("app:{deploy_id}"), Caller::EncorePrincipal(name) => format!("encore:{name}"), } } /// Whether private APIs can be called pub fn private_api_access(&self) -> bool { use Caller::*; match self { APIEndpoint(_) | PubSubMessage { .. } | App { .. } | EncorePrincipal(_) => true, Gateway { .. } => false, } } pub fn is_gateway(&self) -> bool { matches!(&self, Caller::Gateway { .. }) } } impl FromStr for Caller { type Err = anyhow::Error; fn from_str(s: &str) -> Result { fn parse(s: &str) -> Option { let (kind, rest) = s.split_once(':')?; Some(match kind { "api" => { let (service, endpoint) = rest.split_once('.')?; Caller::APIEndpoint(EndpointName::new(service, endpoint)) } "pubsub" => { let mut parts = rest.splitn(3, ':'); let topic = parts.next()?; let subscription = parts.next()?; let message_id = parts.next()?; Caller::PubSubMessage { topic: EncoreName::from(topic), subscription: EncoreName::from(subscription), message_id: message_id.to_string(), } } "app" => Caller::App { deploy_id: rest.to_string(), }, "gateway" => { let mut parts = rest.splitn(2, '.'); let gateway = parts.next()?; Caller::Gateway { gateway: EncoreName::from(gateway), } } "encore" => Caller::EncorePrincipal(rest.to_string()), _ => return None, }) } parse(s).ok_or_else(|| anyhow::anyhow!("invalid caller string")) } } ================================================ FILE: runtimes/core/src/api/reqauth/encoreauth/mod.rs ================================================ mod ophash; mod sign; pub use ophash::OperationHash; pub use sign::{sign, sign_for_verification, InvalidSignature, SignatureComponents}; ================================================ FILE: runtimes/core/src/api/reqauth/encoreauth/ophash.rs ================================================ use anyhow::Context; use sha3::{Digest, Sha3_256}; use std::str::FromStr; pub struct OperationHash { output: sha3::digest::Output, hex: String, } impl OperationHash { pub fn new<'a>( obj: &[u8], action: &[u8], payload: Option<&[u8]>, additional_context: impl Iterator, ) -> Self { let mut hasher = ::new(); hasher.update(obj); hasher.update(action); if let Some(payload) = payload { hasher.update(b"\0"); hasher.update((payload.len() as u32).to_le_bytes()); hasher.update(payload); } for c in additional_context { hasher.update(b"\0"); hasher.update((c.len() as u32).to_le_bytes()); hasher.update(c); } let output = hasher.finalize(); let hex = hex::encode(output.as_slice()); Self { output, hex } } pub fn as_hex(&self) -> &str { &self.hex } pub fn ct_eq(&self, other: &Self) -> bool { use subtle::ConstantTimeEq; self.output.ct_eq(&other.output).into() } } impl FromStr for OperationHash { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let raw = hex::decode(s).context("invalid hex")?; let output = >::from_exact_iter(raw.into_iter()) .context("invalid hash length")?; Ok(Self { output, hex: s.to_string(), }) } } ================================================ FILE: runtimes/core/src/api/reqauth/encoreauth/sign.rs ================================================ use std::fmt::{Display, Formatter}; use std::str::FromStr; use std::time::SystemTime; use bytes::{BufMut, BytesMut}; use chrono::{DateTime, SecondsFormat, Utc}; use hmac::{Hmac, Mac}; use crate::api::reqauth::encoreauth::ophash::OperationHash; const SIGNATURE_VERSION: &str = "ENCORE1"; const _HASH_IMPL: &str = "HMAC-SHA3-256"; // This must match the values of the constants above. const AUTH_SCHEME: &str = "ENCORE1-HMAC-SHA3-256"; /// Sign creates the authorization headers for a new request. /// /// The signature algorithm is based on the AWS Signature Version 4 signing process and is valid for 2 minutes /// from the time the request is signed. pub fn sign( key: (u32, &[u8]), app_slug: &str, env_name: &str, timestamp: SystemTime, operation: &OperationHash, ) -> String { sign_for_verification(key, app_slug, env_name, timestamp, operation) } pub fn sign_for_verification( key: (u32, &[u8]), app_slug: &str, env_name: &str, timestamp: SystemTime, operation: &OperationHash, ) -> String { let credentials = create_credential_string(timestamp, app_slug, env_name, key.0); let request_digest = build_request_digest(timestamp, &credentials, operation); let signing_key = derive_signing_key(key.1, timestamp, app_slug, env_name).into_bytes(); let signature = hash_hmac(&signing_key, request_digest.as_bytes()).into_bytes(); let signature = hex::encode(signature); format!( "{} cred=\"{}\", op={}, sig={}", AUTH_SCHEME, credentials, operation.as_hex(), signature ) } pub struct SignatureComponents { pub key_id: u32, pub app_slug: String, pub env_name: String, pub timestamp: SystemTime, pub operation_hash: OperationHash, } #[derive(Debug)] pub enum InvalidSignature { InvalidAuthorizationHeader, InvalidDateHeader, InvalidAuthScheme, InvalidCredentialString, InvalidOperationHash, UnknownParameter(String), } impl Display for InvalidSignature { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { use InvalidSignature::*; match self { InvalidAuthorizationHeader => write!(f, "invalid authorization header"), InvalidDateHeader => write!(f, "invalid date header"), InvalidAuthScheme => write!(f, "invalid auth scheme"), InvalidCredentialString => write!(f, "invalid credential string"), InvalidOperationHash => write!(f, "invalid operation hash"), UnknownParameter(name) => write!(f, "unknown parameter: {name}"), } } } impl std::error::Error for InvalidSignature {} impl SignatureComponents { pub fn parse(authorization: &str, date: &str) -> Result { let http_date = httpdate::parse_http_date(date).map_err(|_| InvalidSignature::InvalidDateHeader)?; let date_str = >::from(http_date) .format("%Y%m%d") .to_string(); let mut auth_components = authorization.splitn(2, ' '); let scheme = auth_components .next() .ok_or(InvalidSignature::InvalidAuthorizationHeader)?; if scheme != AUTH_SCHEME { return Err(InvalidSignature::InvalidAuthScheme); } let parameters = auth_components .next() .ok_or(InvalidSignature::InvalidAuthorizationHeader)?; let mut op_hash = None; let mut creds = None; for param in parameters.split(", ") { let (name, value) = param .split_once('=') .ok_or(InvalidSignature::InvalidAuthorizationHeader)?; match name { "cred" => { if creds.is_some() { return Err(InvalidSignature::InvalidAuthorizationHeader); } // Unquote the value. let value = value .strip_prefix('"') .and_then(|v| v.strip_suffix('"')) .ok_or(InvalidSignature::InvalidCredentialString)?; let parsed = parse_credential_string(value)?; if parsed.date != date_str { return Err(InvalidSignature::InvalidDateHeader); } creds = Some(parsed); } "op" => { if op_hash.is_some() { return Err(InvalidSignature::InvalidAuthorizationHeader); } op_hash = Some( OperationHash::from_str(value) .map_err(|_| InvalidSignature::InvalidOperationHash)?, ); } "sig" => { // No need to do anything with the signature } _ => { return Err(InvalidSignature::UnknownParameter(name.to_string())); } } } let Some(creds) = creds else { return Err(InvalidSignature::InvalidAuthorizationHeader); }; Ok(Self { key_id: creds.key_id, app_slug: creds.app_slug, env_name: creds.env_name, timestamp: http_date, operation_hash: op_hash.ok_or(InvalidSignature::InvalidAuthorizationHeader)?, }) } } fn create_credential_string( timestamp: SystemTime, app_slug: &str, env_name: &str, key_id: u32, ) -> String { let dt: DateTime = timestamp.into(); let date = dt.format("%Y%m%d"); format!("{date}/{app_slug}/{env_name}/{key_id}") } struct CredentialComponents { key_id: u32, app_slug: String, env_name: String, date: String, } fn parse_credential_string(s: &str) -> Result { let mut parts = s.split('/'); let date = parts .next() .ok_or(InvalidSignature::InvalidCredentialString)? .to_string(); let app_slug = parts .next() .ok_or(InvalidSignature::InvalidCredentialString)? .to_string(); let env_name = parts .next() .ok_or(InvalidSignature::InvalidCredentialString)? .to_string(); let key_id = parts .next() .ok_or(InvalidSignature::InvalidCredentialString)? .parse::() .map_err(|_| InvalidSignature::InvalidCredentialString)?; if parts.next().is_some() { return Err(InvalidSignature::InvalidCredentialString); } Ok(CredentialComponents { key_id, app_slug, env_name, date, }) } /// The request digest represents the request that we want to make /// and is the data we will sign. /// /// It is a newline separated string of the following: /// /// - The auth scheme being used. /// - Timestamp in RFC3339 format. /// - App slug and environment name. /// - The operation hash. fn build_request_digest( timestamp: SystemTime, credentials: &str, operation: &OperationHash, ) -> String { let dt: DateTime = timestamp.into(); let timestamp = dt.to_rfc3339_opts(SecondsFormat::Secs, true); format!( "{}\n{}\n{}\n{}", AUTH_SCHEME, timestamp, credentials, operation.as_hex(), ) } /// The signing key is a HMAC-SHA3-256 hash of the following, where each component is hashed in order, /// and the result of each hash is used as the key for the next hash: /// - Signature version. /// - The shared secret between the app and Encore. /// - The date in YYYYMMDD format. /// - The application slug. /// - The environment name. /// - The string "encore_request". fn derive_signing_key( key_data: &[u8], timestamp: SystemTime, app_slug: &str, env_name: &str, ) -> hmac::digest::CtOutput { let base_key = { let mut bytes = BytesMut::with_capacity(SIGNATURE_VERSION.len() + key_data.len()); bytes.put_slice(SIGNATURE_VERSION.as_bytes()); bytes.put_slice(key_data); bytes.to_vec() }; let date_key = { let dt: DateTime = timestamp.into(); let timestamp = dt.format("%Y%m%d").to_string(); hash_hmac(&base_key, timestamp.as_bytes()).into_bytes() }; let app_key = hash_hmac(&date_key, app_slug.as_bytes()).into_bytes(); let env_key = hash_hmac(&app_key, env_name.as_bytes()).into_bytes(); hash_hmac(&env_key, b"encore_request") } type HmacSha3_256 = Hmac; fn hash_hmac(key: &[u8], data: &[u8]) -> hmac::digest::CtOutput { HmacSha3_256::new_from_slice(key) .expect("hmac can accept keys of any size") .chain_update(data) .finalize() } ================================================ FILE: runtimes/core/src/api/reqauth/meta.rs ================================================ use std::str::{self, FromStr}; use http::HeaderValue; pub trait HeaderValueExt { fn to_utf8_str(&self) -> Result<&str, std::str::Utf8Error>; } impl HeaderValueExt for HeaderValue { // Some header values may contain utf8 characters (e.g authdata) so we use `str::from_utf8` // rather than using `HeaderValues::to_str` which errors on non-visible ASCII characters. fn to_utf8_str(&self) -> Result<&str, std::str::Utf8Error> { std::str::from_utf8(self.as_bytes()) } } #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum MetaKey { TraceParent, TraceState, XCorrelationId, Version, UserId, UserData, Caller, Callee, SvcAuthMethod, SvcAuthEncoreAuthHash, SvcAuthEncoreAuthDate, } impl MetaKey { pub fn header_key(&self) -> &'static str { use MetaKey::*; match self { TraceParent => "traceparent", TraceState => "tracestate", XCorrelationId => "x-correlation-id", Version => "x-encore-meta-version", UserId => "x-encore-meta-userid", UserData => "x-encore-meta-authdata", Caller => "x-encore-meta-caller", Callee => "x-encore-meta-callee", SvcAuthMethod => "x-encore-meta-svc-auth-method", SvcAuthEncoreAuthHash => "x-encore-meta-svc-auth", SvcAuthEncoreAuthDate => "x-encore-meta-date", } } } pub struct NotMetaKey; impl FromStr for MetaKey { type Err = NotMetaKey; fn from_str(value: &str) -> Result { use MetaKey::*; Ok(match value { "traceparent" => TraceParent, "tracestate" => TraceState, "x-correlation-id" => XCorrelationId, "x-encore-meta-version" => Version, "x-encore-meta-userid" => UserId, "x-encore-meta-authdata" => UserData, "x-encore-meta-caller" => Caller, "x-encore-meta-callee" => Callee, "x-encore-meta-svc-auth-method" => SvcAuthMethod, "x-encore-meta-svc-auth" => SvcAuthEncoreAuthHash, "x-encore-meta-date" => SvcAuthEncoreAuthDate, _ => return Err(NotMetaKey), }) } } pub trait MetaMapMut: MetaMap { fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()>; } pub trait MetaMap { fn get_meta(&self, key: MetaKey) -> Option<&str>; fn meta_values<'a>(&'a self, key: MetaKey) -> Box + 'a>; /// Returns all meta keys, sorted alphabetically based on MetaKey::header_key. fn sorted_meta_keys(&self) -> Vec; } impl MetaMapMut for reqwest::header::HeaderMap { fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()> { self.insert(key.header_key(), value.parse()?); Ok(()) } } impl MetaMap for axum::http::HeaderMap { fn get_meta(&self, key: MetaKey) -> Option<&str> { self.get(key.header_key()) .and_then(|val| val.to_utf8_str().ok()) } fn meta_values<'a>(&'a self, key: MetaKey) -> Box + 'a> { Box::new( self.get_all(key.header_key()) .iter() .filter_map(|v| v.to_utf8_str().ok()), ) } fn sorted_meta_keys(&self) -> Vec { let mut keys: Vec<_> = self .keys() .filter_map(|k| MetaKey::from_str(k.as_str()).ok()) .collect(); keys.sort_by_key(|k| k.header_key()); keys } } impl MetaMapMut for pingora::http::RequestHeader { fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()> { self.insert_header(key.header_key(), value)?; Ok(()) } } impl MetaMap for pingora::http::RequestHeader { fn get_meta(&self, key: MetaKey) -> Option<&str> { self.headers .get(key.header_key()) .and_then(|v| v.to_utf8_str().ok()) } fn meta_values<'a>(&'a self, key: MetaKey) -> Box + 'a> { Box::new( self.headers .get_all(key.header_key()) .iter() .filter_map(|v| v.to_utf8_str().ok()), ) } fn sorted_meta_keys(&self) -> Vec { let mut keys: Vec<_> = self .headers .keys() .filter_map(|k| MetaKey::from_str(k.as_str()).ok()) .collect(); keys.sort_by_key(|k| k.header_key()); keys } } ================================================ FILE: runtimes/core/src/api/reqauth/mod.rs ================================================ use crate::api::jsonschema::DecodeConfig; use crate::api::reqauth::caller::Caller; use crate::api::reqauth::meta::MetaMap; use crate::api::APIResult; use crate::encore::runtime::v1 as pb; use crate::{api, model, secrets}; use anyhow::Context; use std::collections::HashMap; use std::sync::Arc; use std::time::SystemTime; use super::{jsonschema, PValues}; pub mod caller; mod encoreauth; pub mod meta; pub mod platform; pub mod svcauth; /// Computes the service auth method to use for communicating with a given service. pub fn service_auth_method( secrets: &secrets::Manager, env: &pb::Environment, auth_method: pb::ServiceAuth, ) -> anyhow::Result> { let obj: Arc = match auth_method.auth_method { None | Some(pb::service_auth::AuthMethod::Noop(_)) => Arc::new(svcauth::Noop), Some(pb::service_auth::AuthMethod::EncoreAuth(ea)) => { let auth_keys = ea .auth_keys .into_iter() .filter_map(|k| { let data = k.data?; Some(svcauth::EncoreAuthKey { key_id: k.id, data: secrets.load(data), }) }) .collect::>(); if auth_keys.is_empty() { anyhow::bail!("no auth keys provided for encore-auth method"); } Arc::new(svcauth::EncoreAuth::new( env.app_slug.clone(), env.env_name.clone(), auth_keys, )) } }; Ok(obj) } #[derive(Debug, Clone)] pub struct CallMeta { /// The trace id to use. Equal to caller_trace_id if set, and generated otherwise. pub trace_id: model::TraceId, /// The trace id of the caller; None if not traced. pub caller_trace_id: Option, /// The span id of the caller (None if there's no parent). pub parent_span_id: Option, /// The span id of THIS request, if predefined by the caller (None in most cases). pub this_span_id: Option, /// The event id which started the API call (None if there's no parent). pub parent_event_id: Option, /// Correlation id to use. pub ext_correlation_id: Option, /// Whether the caller sampled trace info /// None if there's no parent span or if the sampled flag couldn't be parsed. pub trace_sampled: Option, /// Information about an internal call, if any. /// If set it can be trusted as it has been authenticated. pub internal: Option, } #[derive(Debug, Clone)] pub struct InternalCallMeta { /// The source of the call. pub caller: Caller, /// The authenticated user id, if any. pub auth_uid: Option, /// The user data, if any. pub auth_data: Option, } impl CallMeta { pub fn parse_with_caller( auth: &[Arc], headers: &axum::http::HeaderMap, auth_data_schemas: &HashMap>, ) -> APIResult { Self::parse(headers, auth, true, Some(auth_data_schemas)) } pub fn parse_without_caller(headers: &axum::http::HeaderMap) -> APIResult { Self::parse(headers, &[], false, None) } fn parse( headers: &axum::http::HeaderMap, auth: &[Arc], parse_caller: bool, auth_data_schemas: Option<&HashMap>>, ) -> APIResult { let do_parse = move || -> anyhow::Result { use meta::MetaKey; if let Some(version) = headers.get_meta(MetaKey::Version) { if version != "1" { anyhow::bail!("unknown encore meta version"); } } let mut meta = CallMeta { trace_id: model::TraceId::generate(), caller_trace_id: None, parent_span_id: None, this_span_id: None, parent_event_id: None, ext_correlation_id: None, trace_sampled: None, internal: None, }; // If it was an internal call, parse it. if parse_caller { if let Some(caller) = headers.get_meta(MetaKey::Caller) { // Find the auth method. let auth_method = headers.get_meta(MetaKey::SvcAuthMethod); let Some(auth) = auth.iter().find(|a| auth_method == Some(a.name())) else { anyhow::bail!("unknown service auth method"); }; // Verify the caller's signature. auth.verify(headers, SystemTime::now()) .context("invalid service authentication data")?; let caller = caller.parse().context("invalid meta caller")?; // TODO: Currently we assume a single auth data schema. // When we support multiple gateways with distinct schemas we'll need to change this. let auth_data_schema = auth_data_schemas .and_then(|s| s.values().filter_map(|s| s.as_ref()).next()); // Parse the auth data, if provided. let auth_data = match (headers.get_meta(MetaKey::UserData), &auth_data_schema) { (None, _) => None, (Some(data), None) => { // Hack: temporarily work around the absence of a schema in certain situations. let any_schema = { use crate::encore::parser::meta::v1 as meta; let md = meta::Data::default(); let mut builder = jsonschema::Builder::new(&md); let idx = builder.register_value(jsonschema::Value::Basic( jsonschema::Basic::Any, )); let registry = builder.build(); registry.schema(idx) }; let mut jsonde = serde_json::Deserializer::from_str(data); let data = any_schema .deserialize(&mut jsonde, DecodeConfig::default()) .context("invalid auth data")?; Some(data) } (Some(data), Some(schema)) => { let mut jsonde = serde_json::Deserializer::from_str(data); let data = schema .deserialize(&mut jsonde, DecodeConfig::default()) .context("invalid auth data")?; Some(data) } }; meta.internal = Some(InternalCallMeta { caller, auth_uid: headers.get_meta(MetaKey::UserId).map(|s| s.to_string()), auth_data, }); }; } // For now we only read the traceparent for internal-to-internal calls, this is because CloudRun // is adding a traceparent header to all requests, which is causing our trace system to get confused // and think that the initial request is a child of another already traced request // // In the future we should be able to remove this check and read the traceparent header for all requests // to interopt with other tracing systems. if meta.internal.is_some() { if let Some(traceparent) = headers.get_meta(MetaKey::TraceParent) { // Parse the traceparent. if let Ok((trace_id, parent_span_id, sampled)) = parse_traceparent(traceparent) { meta.trace_id = trace_id; meta.caller_trace_id = Some(trace_id); meta.parent_span_id = Some(parent_span_id); meta.trace_sampled = Some(sampled); }; // If the caller is a gateway, ignore the parent span id as gateways don't currently record a span. // If we include it the root request won't be tagged as such. if let Some(internal) = &meta.internal { if matches!(internal.caller, Caller::Gateway { .. }) { meta.parent_span_id = None; } } // Parse the trace state. if let (Some(event_id), parent_span) = parse_tracestate(headers.meta_values(MetaKey::TraceState)) { meta.parent_event_id = Some(event_id); // If we where given a parent span ID, use that instead of the one from the traceparent header // This is because GCP Cloud Run will add it's own spans in before the application code is run // and thus we lose the parent span ID from the traceparent header if let Some(parent_span) = parent_span { meta.parent_span_id = Some(parent_span); } } } } meta.ext_correlation_id = headers.get_meta(MetaKey::XCorrelationId).map(|s| { // Limit the maximum length the correlation id can have. s[..s.len().min(64)].to_string() }); Ok(meta) }; do_parse().map_err(|e| api::Error::invalid_argument("unable to parse request", e)) } } fn parse_traceparent(s: &str) -> anyhow::Result<(model::TraceId, model::SpanId, bool)> { let version = "00"; let trace_id_len = 32; let span_id_len = 16; let trace_flags_len = 2; let ver_start = 0; let ver_end = ver_start + version.len(); let ver_sep = ver_end; let trace_id_start = ver_sep + 1; let trace_id_end = trace_id_start + trace_id_len; let trace_id_sep = trace_id_end; let span_id_start = trace_id_sep + 1; let span_id_end = span_id_start + span_id_len; let span_id_sep = span_id_end; let trace_flags_start = span_id_sep + 1; let trace_flags_end = trace_flags_start + trace_flags_len; let total_len = trace_flags_end; if s.len() != total_len { anyhow::bail!("invalid traceparent length"); } else if &s[ver_start..ver_end] != version { anyhow::bail!("invalid traceparent version"); } else if &s[ver_sep..ver_sep + 1] != "-" { anyhow::bail!("invalid traceparent version separator"); } else if &s[trace_id_sep..trace_id_sep + 1] != "-" { anyhow::bail!("invalid traceparent trace id separator"); } else if &s[span_id_sep..span_id_sep + 1] != "-" { anyhow::bail!("invalid traceparent span id separator"); } let trace_id = &s[trace_id_start..trace_id_end]; let trace_id = model::TraceId::parse_std(trace_id).context("invalid trace id")?; let span_id = &s[span_id_start..span_id_end]; let span_id = model::SpanId::parse_std(span_id).context("invalid span id")?; // Parse trace flags - bit 0 (0x01) indicates "sampled" let trace_flags = &s[trace_flags_start..trace_flags_end]; let trace_flags = u8::from_str_radix(trace_flags, 16).context("invalid trace flags")?; let sampled = trace_flags & 0x01 != 0; Ok((trace_id, span_id, sampled)) } fn parse_tracestate<'a>( vals: impl Iterator, ) -> (Option, Option) { enum Data { EventId(model::TraceEventId), SpanId(model::SpanId), } let parse_entry = |val: &str| -> Option { let (key, val) = val.split_once('=')?; match key { "encore/event-id" => Some(Data::EventId(val.parse().ok()?)), "encore/span-id" => Some(Data::SpanId(model::SpanId::parse_std(val).ok()?)), _ => None, } }; let mut event_id = None; let mut span_id = None; for val in vals { for field in val.split(',') { match parse_entry(field) { Some(Data::EventId(id)) => event_id = Some(id), Some(Data::SpanId(id)) => span_id = Some(id), None => (), } } } (event_id, span_id) } ================================================ FILE: runtimes/core/src/api/reqauth/platform.rs ================================================ use crate::{encore, secrets}; use anyhow::Context; use base64::engine::general_purpose; use base64::Engine; use encore::runtime::v1 as pb; use hmac::Mac; use percent_encoding::percent_decode_str; use std::fmt::Display; use std::time::SystemTime; pub struct RequestValidator { keys: Box<[SigningKey]>, } impl std::fmt::Debug for RequestValidator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RequestValidator").finish() } } pub struct ValidationData<'a> { pub request_path: &'a str, pub date_header: &'a str, pub x_encore_auth_header: &'a str, } /// A seal of approval is a record that the request originated from the Encore Platform. #[derive(Debug)] pub struct SealOfApproval; impl RequestValidator { pub fn new(secrets: &secrets::Manager, keys: Vec) -> Self { let keys = keys .into_iter() .filter_map(|k| match k.data { Some(data) => Some(SigningKey { id: k.id, data: secrets.load(data), }), None => None, }) .collect(); Self { keys } } #[cfg(test)] pub fn new_mock() -> Self { use crate::secrets::Secret; Self { keys: [SigningKey { id: 123, data: Secret::new_for_test("secret data"), }] .into(), } } pub fn validate_platform_request( &self, req: &ValidationData, ) -> Result { let decoded_auth_header = BASE64 .decode(req.x_encore_auth_header.as_bytes()) .map_err(|_| ValidationError::InvalidMac)?; // Pull out key ID from hmac prefix const KEY_ID_LEN: usize = 4; if decoded_auth_header.len() < KEY_ID_LEN { return Err(ValidationError::InvalidMac); } let key_id = u32::from_be_bytes(decoded_auth_header[..KEY_ID_LEN].try_into().unwrap()); let received_mac = &decoded_auth_header[KEY_ID_LEN..]; for k in self.keys.iter() { if k.id == key_id { let secret_data = k.data.get().map_err(ValidationError::SecretResolve)?; return check_auth_key(secret_data, req, received_mac); } } Err(ValidationError::UnknownMacKey) } pub fn sign_outgoing_request(&self, req: &mut reqwest::Request) -> anyhow::Result<()> { let path = percent_decode_str(req.url().path()) .decode_utf8_lossy() .to_string(); let date_str = req .headers_mut() .entry(reqwest::header::DATE) .or_insert_with(|| { let date_str = httpdate::fmt_http_date(SystemTime::now()); date_str.parse().unwrap() }); let key = &self.keys[0]; let key_data = key.data.get().context("unable to resolve signing key")?; let mut mac = hmac::Hmac::::new_from_slice(key_data).unwrap(); mac.update(date_str.as_bytes()); mac.update(b"\x00"); mac.update(path.as_bytes()); let mac_bytes = mac.finalize().into_bytes(); let combined = [key.id.to_be_bytes().as_slice(), mac_bytes.as_slice()].concat(); let auth_header = BASE64.encode(combined); req.headers_mut().insert( reqwest::header::HeaderName::from_static("x-encore-auth"), reqwest::header::HeaderValue::from_str(&auth_header).context("invalid auth header")?, ); Ok(()) } } struct SigningKey { id: u32, data: secrets::Secret, } const BASE64: general_purpose::GeneralPurpose = general_purpose::STANDARD_NO_PAD; fn check_auth_key( decryption_key: &[u8], req: &ValidationData, received_mac: &[u8], ) -> Result { let request_date = httpdate::parse_http_date(req.date_header) .map_err(|_| ValidationError::InvalidDateHeader)?; let now = SystemTime::now(); let diff = now .duration_since(request_date) .unwrap_or_else(|e| e.duration()); const THRESHOLD: u64 = 15 * 60; if diff.as_secs() > THRESHOLD { return Err(ValidationError::TimeSkew); } // Compute the MAC. type HmacSha256 = hmac::Hmac; let mut computed_mac = HmacSha256::new_from_slice(decryption_key).map_err(|_| ValidationError::InvalidMacKey)?; computed_mac.update(req.date_header.as_bytes()); computed_mac.update(b"\x00"); computed_mac.update(req.request_path.as_bytes()); computed_mac .verify_slice(received_mac) .map_err(|_| ValidationError::InvalidMac)?; Ok(SealOfApproval) } #[derive(Debug)] pub enum ValidationError { InvalidMac, UnknownMacKey, InvalidMacKey, InvalidDateHeader, TimeSkew, SecretResolve(secrets::ResolveError), } impl Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ValidationError::InvalidMac => write!(f, "invalid mac"), ValidationError::UnknownMacKey => write!(f, "unknown mac key"), ValidationError::InvalidMacKey => write!(f, "invalid mac key"), ValidationError::InvalidDateHeader => write!(f, "invalid or missing date header"), ValidationError::TimeSkew => write!(f, "time skew"), ValidationError::SecretResolve(e) => { write!(f, "resolve secret: {e}") } } } } impl std::error::Error for ValidationError {} ================================================ FILE: runtimes/core/src/api/reqauth/svcauth.rs ================================================ use std::fmt::{Debug, Display}; use std::time::SystemTime; use anyhow::Context; use sha3::digest::Digest; use subtle::ConstantTimeEq; use crate::api::reqauth::encoreauth; use crate::api::reqauth::encoreauth::{OperationHash, SignatureComponents}; use crate::api::reqauth::meta::{MetaKey, MetaMapMut}; use crate::secrets; use crate::secrets::Secret; use super::meta::MetaMap; pub trait ServiceAuthMethod: Debug + Send + Sync + 'static { fn name(&self) -> &'static str; fn sign(&self, headers: &mut dyn MetaMapMut, now: SystemTime) -> anyhow::Result<()>; fn verify(&self, headers: &dyn MetaMap, now: SystemTime) -> Result<(), VerifyError>; } #[derive(Debug)] pub struct Noop; impl ServiceAuthMethod for Noop { fn name(&self) -> &'static str { "noop" } fn sign(&self, _headers: &mut dyn MetaMapMut, _now: SystemTime) -> anyhow::Result<()> { Ok(()) } fn verify(&self, _headers: &dyn MetaMap, _now: SystemTime) -> Result<(), VerifyError> { Ok(()) } } pub struct EncoreAuthKey { pub key_id: u32, pub data: Secret, } pub struct EncoreAuth { app_slug: String, env_name: String, keys: Vec, latest_idx: usize, // index into keys } impl Debug for EncoreAuth { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EncoreAuth") .field("app_slug", &self.app_slug) .field("env_name", &self.env_name) .finish() } } impl EncoreAuth { pub fn new(app_slug: String, env_name: String, keys: Vec) -> Self { if keys.is_empty() { panic!("auth keys must not be empty"); } let latest_idx = { let mut max_id = keys[0].key_id; let mut max_idx = 0; for (idx, k) in keys.iter().enumerate() { if k.key_id > max_id { max_idx = idx; max_id = k.key_id; } } max_idx }; Self { app_slug, env_name, keys, latest_idx, } } } #[derive(Debug)] pub enum VerifyError { NoAuthorizationHeader, NoDateHeader, InvalidHeader(encoreauth::InvalidSignature), SignatureMismatch, DateSkew, UnknownKey, ResolveKeyData(secrets::ResolveError), } impl Display for VerifyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use VerifyError::*; match self { NoAuthorizationHeader => write!(f, "no authorization header"), NoDateHeader => write!(f, "no date header"), InvalidHeader(e) => write!(f, "invalid header: {e}"), SignatureMismatch => write!(f, "signature mismatch"), DateSkew => write!(f, "date skew"), UnknownKey => write!(f, "unknown key"), ResolveKeyData(e) => write!(f, "unable to resolve secret key data: {e}"), } } } impl std::error::Error for VerifyError {} impl ServiceAuthMethod for EncoreAuth { fn name(&self) -> &'static str { "encore-auth" } fn sign(&self, headers: &mut dyn MetaMapMut, now: SystemTime) -> anyhow::Result<()> { let op_hash = self.build_op_hash(headers); let key = &self.keys[self.latest_idx]; let key_data = key.data.get().context("unable to resolve auth key data")?; let hash = encoreauth::sign( (key.key_id, key_data), &self.app_slug, &self.env_name, now, &op_hash, ); headers .set(MetaKey::SvcAuthEncoreAuthHash, hash) .context("set auth hash header")?; headers .set(MetaKey::SvcAuthEncoreAuthDate, httpdate::fmt_http_date(now)) .context("set auth date header")?; Ok(()) } fn verify(&self, headers: &dyn MetaMap, now: SystemTime) -> Result<(), VerifyError> { let auth_header = headers .get_meta(MetaKey::SvcAuthEncoreAuthHash) .ok_or(VerifyError::NoAuthorizationHeader)?; let date_header = headers .get_meta(MetaKey::SvcAuthEncoreAuthDate) .ok_or(VerifyError::NoDateHeader)?; let components = SignatureComponents::parse(auth_header, date_header) .map_err(VerifyError::InvalidHeader)?; let diff = now .duration_since(components.timestamp) .unwrap_or_else(|e| e.duration()); if diff.as_secs() > 120 { return Err(VerifyError::DateSkew); } let key = self .keys .iter() .find(|k| k.key_id == components.key_id) .ok_or(VerifyError::UnknownKey)?; let key_data = key.data.get().map_err(VerifyError::ResolveKeyData)?; let expected_signature = encoreauth::sign_for_verification( (key.key_id, key_data), &components.app_slug, &components.env_name, components.timestamp, &components.operation_hash, ); let signature_match: bool = expected_signature .as_bytes() .ct_eq(auth_header.as_bytes()) .into(); if !signature_match { return Err(VerifyError::SignatureMismatch); } let expected_op_hash = self.build_op_hash(headers); if !expected_op_hash.ct_eq(&components.operation_hash) { return Err(VerifyError::SignatureMismatch); } Ok(()) } } impl EncoreAuth { fn build_op_hash(&self, req: &R) -> OperationHash { // Build a deterministic hash of the meta keys and values. let mut hash = ::new(); for key in req.sorted_meta_keys() { use MetaKey::*; match key { SvcAuthMethod | SvcAuthEncoreAuthHash | SvcAuthEncoreAuthDate => { // Skip these headers, as they are part of the auth mechanism itself. } TraceParent | TraceState => { // Skip these headers, as they are part of the tracing mechanism and could be changed // by things like load balancers. } XCorrelationId | Version | UserId | UserData | Caller | Callee => { // Read all values for this key, and sort them. let mut values = req.meta_values(key).collect::>(); values.sort(); for value in values { hash.update(key.header_key()); hash.update(b"="); hash.update(value.as_bytes()); hash.update(b"\n"); } } } } let payload = hash.finalize(); OperationHash::new( "internal-api".as_bytes(), "call".as_bytes(), Some(payload.as_slice()), std::iter::empty(), ) } } #[cfg(test)] mod tests { use crate::api::reqauth::meta::MetaMap; use super::*; fn metas(req: &R) -> Vec<(MetaKey, Vec)> { let keys = req.sorted_meta_keys(); keys.into_iter() .map(|k| { ( k, req.meta_values(k) .map(|s| s.to_string()) .collect::>(), ) }) .collect() } fn convert_header_map(src: reqwest::header::HeaderMap) -> axum::http::HeaderMap { let mut dst = axum::http::HeaderMap::new(); for entry in src.iter() { let key: axum::http::HeaderName = entry.0.as_str().parse().unwrap(); let value = entry.1.to_str().unwrap().parse().unwrap(); dst.insert(key, value); } dst } #[test] fn test_encore_auth() -> anyhow::Result<()> { let mut headers = reqwest::header::HeaderMap::new(); let auth = EncoreAuth { app_slug: "app".into(), env_name: "env".into(), keys: vec![EncoreAuthKey { key_id: 123, data: Secret::new_for_test("secret data"), }], latest_idx: 0, }; let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1234567890); auth.sign(&mut headers, now) .context("unable to sign request")?; let out_headers = convert_header_map(headers); auth.verify(&out_headers, now) .context("unable to verify request")?; assert_eq!( metas(&out_headers), vec![ ( MetaKey::SvcAuthEncoreAuthDate, vec!["Fri, 13 Feb 2009 23:31:30 GMT".to_string()] ), (MetaKey::SvcAuthEncoreAuthHash, vec![r#"ENCORE1-HMAC-SHA3-256 cred="20090213/app/env/123", op=f3c70a419394ce9d56efafad2208154b92c8596d7396b3a2b4ea7fd925d28dc2, sig=fc0c88b47c13d999353ecc8681d91d9c03209a1f05583b92d84e429fedfe387a"#.to_string()]), ] ); Ok(()) } } ================================================ FILE: runtimes/core/src/api/schema/body.rs ================================================ use axum::http::HeaderValue; use bytes::Bytes; use crate::api::jsonschema::DecodeConfig; use crate::api::schema::{JSONPayload, ToOutgoingRequest}; use crate::api::{self, PValues}; use crate::api::{jsonschema, APIResult}; use http_body_util::BodyExt; use reqwest::header::CONTENT_TYPE; #[derive(Debug, Clone)] pub struct Body { schema: jsonschema::JSONSchema, } impl Body { pub fn new(schema: jsonschema::JSONSchema) -> Self { Self { schema } } pub async fn parse_incoming_request_body( &self, body: axum::body::Body, ) -> APIResult> { // Collect the bytes of the request body. // Serde doesn't support async streaming reads (at least not yet). let bytes = body .collect() .await .map_err(|e| api::Error::invalid_argument("unable to read request body", e))? .to_bytes(); let mut jsonde = serde_json::Deserializer::from_slice(&bytes); let cfg = DecodeConfig { coerce_strings: false, arrays_as_repeated_fields: false, }; let value = self .schema .deserialize(&mut jsonde, cfg) .map_err(|e| api::Error::invalid_argument("unable to decode request body", e))?; Ok(Some(value)) } pub async fn parse_response_body(&self, body: Bytes) -> APIResult> { let mut jsonde = serde_json::Deserializer::from_slice(&body); let cfg = DecodeConfig { coerce_strings: false, arrays_as_repeated_fields: false, }; let value = self .schema .deserialize(&mut jsonde, cfg) .map_err(|e| api::Error::invalid_argument("unable to decode response body", e))?; Ok(Some(value)) } } impl ToOutgoingRequest for Body { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut reqwest::Request, ) -> APIResult<()> { if payload.is_none() { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing body payload".to_string(), internal_message: None, stack: None, details: None, }); }; let body = self.schema.to_vec(payload).map_err(api::Error::internal)?; if !req.headers().contains_key(CONTENT_TYPE) { req.headers_mut().insert( CONTENT_TYPE, reqwest::header::HeaderValue::from_static("application/json"), ); } *req.body_mut() = Some(body.into()); Ok(()) } } impl Body { pub fn to_outgoing_payload(&self, payload: &JSONPayload) -> APIResult> { if payload.is_none() { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing body payload".to_string(), internal_message: None, stack: None, details: None, }); }; let body = self.schema.to_vec(payload).map_err(api::Error::internal)?; Ok(body) } pub fn to_response( &self, payload: &JSONPayload, resp: axum::http::response::Builder, ) -> APIResult> { let buf = self.schema.to_vec(payload).map_err(api::Error::internal)?; let resp = resp .header( axum::http::header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), ) .body(axum::body::Body::from(buf)) .map_err(api::Error::internal)?; Ok(resp) } } ================================================ FILE: runtimes/core/src/api/schema/cookie.rs ================================================ use http::{ header::{COOKIE, SET_COOKIE}, HeaderValue, }; use crate::api::{self, jsonschema, schema::ToResponse, APIResult, PValue, PValues}; use super::{HTTPHeaders, JSONPayload, ToHeaderStr, ToOutgoingRequest}; #[derive(Debug, Clone)] pub struct Cookie { schema: jsonschema::JSONSchema, } impl Cookie { pub fn new(schema: jsonschema::JSONSchema) -> Self { Self { schema } } pub fn contains_any(&self, headers: &impl HTTPHeaders) -> bool { let mut jar = cookie::CookieJar::new(); for raw in headers.get_all(axum::http::header::COOKIE.as_str()) { if let Ok(raw) = raw.to_str() { for c in cookie::Cookie::split_parse(raw).flatten() { jar.add_original(c.into_owned()); } } } for (name, field) in self.schema.root().fields.iter() { let cookie_name = field.name_override.as_deref().unwrap_or(name.as_str()); if let Some(c) = jar.get(cookie_name) { // Only consider non-empty values to be present. if !c.value().is_empty() { return true; } } } false } pub fn fields(&self) -> impl Iterator { self.schema.root().fields.iter() } pub fn parse_incoming_request_parts( &self, req: &axum::http::request::Parts, ) -> APIResult> { self.parse_req(&req.headers) } pub fn parse_req(&self, headers: &axum::http::HeaderMap) -> APIResult> { if self.schema.root().is_empty() { return Ok(None); } let mut jar = cookie::CookieJar::new(); headers .get_all(COOKIE) .iter() .filter_map(|raw| raw.to_str().ok()) .flat_map(cookie::Cookie::split_parse) .flatten() .for_each(|c| jar.add_original(c.into_owned())); match self.schema.parse(jar) { Ok(decoded) => Ok(Some(decoded)), Err(err) => Err(err), } } pub fn parse_resp(&self, headers: &axum::http::HeaderMap) -> APIResult> { if self.schema.root().is_empty() { return Ok(None); } let mut jar = cookie::CookieJar::new(); headers .get_all(SET_COOKIE) .iter() .filter_map(|raw| raw.to_str().ok()) .flat_map(cookie::Cookie::parse) .for_each(|c| jar.add_original(c.into_owned())); match self.schema.parse(jar) { Ok(decoded) => Ok(Some(decoded)), Err(err) => Err(err), } } } impl ToOutgoingRequest for Cookie { fn to_outgoing_request( &self, payload: &mut JSONPayload, headers: &mut http::HeaderMap, ) -> APIResult<()> { if self.schema.root().is_empty() { return Ok(()); } let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing cookie parameters".to_string(), internal_message: Some("missing cookie parameters".to_string()), stack: None, details: None, }); }; for (key, field) in self.schema.root().fields.iter() { let Some(PValue::Cookie(c)) = payload.get(key) else { if field.optional { continue; } return Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("missing cookie parameter: {key}"), internal_message: Some(format!("missing cookie parameter: {key}")), stack: None, details: None, }); }; let header_value = HeaderValue::from_str(&c.to_string()).map_err(api::Error::internal)?; headers.append(http::header::COOKIE, header_value); } Ok(()) } } impl ToOutgoingRequest for Cookie { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut reqwest::Request, ) -> APIResult<()> { let headers = req.headers_mut(); self.to_outgoing_request(payload, headers) } } impl ToOutgoingRequest> for Cookie { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut http::Request, ) -> APIResult<()> { let headers = req.headers_mut(); self.to_outgoing_request(payload, headers) } } impl ToResponse for Cookie { fn to_response( &self, payload: &super::JSONPayload, mut resp: axum::http::response::Builder, ) -> APIResult { if self.schema.root().is_empty() { return Ok(resp); } let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing cookie parameters".to_string(), internal_message: Some("missing cookie parameters".to_string()), stack: None, details: None, }); }; let schema = self.schema.root(); for (key, value) in payload.iter() { let key = key.as_str(); if !schema.fields.contains_key(key) { continue; }; let PValue::Cookie(c) = value else { continue; }; resp = resp.header(SET_COOKIE, c.to_string()); } Ok(resp) } } ================================================ FILE: runtimes/core/src/api/schema/encoding.rs ================================================ use crate::api::jsonschema; use crate::api::schema::{Body, Cookie, Header, HttpStatus, Method, Path, Query}; use crate::encore::parser::meta::v1 as meta; use crate::encore::parser::meta::v1::path_segment::SegmentType; use crate::encore::parser::schema::v1 as schema; use crate::encore::parser::schema::v1::r#type::Typ; use anyhow::Context; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::sync::Arc; #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub enum DefaultLoc { Body, Query, } impl DefaultLoc { pub fn into_wire_loc(self) -> WireLoc { match self { DefaultLoc::Body => WireLoc::Body, DefaultLoc::Query => WireLoc::Query, } } } #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub enum WireLoc { Body, Query, Header(String), Path, Cookie(String), } pub struct EncodingConfig<'a, 'b> { pub meta: &'a meta::Data, pub registry_builder: &'a mut jsonschema::Builder<'b>, pub default_loc: Option, pub rpc_path: Option<&'a meta::Path>, pub supports_body: bool, pub supports_query: bool, pub supports_header: bool, pub supports_path: bool, pub supports_http_status: bool, } #[derive(Debug)] pub struct SchemaUnderConstruction { combined: Option, body: Option, query: Option, header: Option, cookie: Option, http_status: Option, rpc_path: Option, } impl SchemaUnderConstruction { pub fn build(self, reg: &Arc) -> anyhow::Result { Ok(Schema { combined: self.combined.map(|v| reg.schema(v)), body: self.body.map(|v| Body::new(reg.schema(v))), query: self.query.map(|v| Query::new(reg.schema(v))), header: self.header.map(|v| Header::new(reg.schema(v))), cookie: self.cookie.map(|v| Cookie::new(reg.schema(v))), http_status: self.http_status, path: self.rpc_path.as_ref().map(Path::from_meta).transpose()?, }) } } #[derive(Debug, Clone)] pub struct Schema { pub combined: Option, pub query: Option, pub header: Option

, pub body: Option, pub path: Option, pub cookie: Option, pub http_status: Option, } impl EncodingConfig<'_, '_> { pub fn compute(&mut self, typ: &schema::Type) -> anyhow::Result { let typ = typ.typ.as_ref().context("type without type")?; let typ = resolve_type(self.meta, typ)?; let Typ::Struct(st) = typ.as_ref() else { return Ok(SchemaUnderConstruction { combined: None, body: None, query: None, header: None, cookie: None, http_status: None, rpc_path: self.rpc_path.cloned(), }); }; // Determine which fields belong to the path, if any. let path_fields = { let mut path_fields = HashSet::new(); if let Some(rpc_path) = self.rpc_path { for seg in &rpc_path.segments { let typ = SegmentType::try_from(seg.r#type).context("invalid segment type")?; match typ { SegmentType::Literal => {} SegmentType::Param | SegmentType::Wildcard | SegmentType::Fallback => { path_fields.insert(seg.value.as_str()); } } } } path_fields }; let mut combined = jsonschema::Struct::default(); let mut body: Option = None; let mut query: Option = None; let mut header: Option = None; let mut cookie: Option = None; let mut http_status: Option = None; for f in &st.fields { // If it's a path field, skip it. We handle it separately in Path::from_meta. if path_fields.contains(f.name.as_str()) { continue; } let (name, mut field) = self.registry_builder.struct_field(f)?; combined.fields.insert(name.to_owned(), field.clone()); let default_loc = || -> anyhow::Result { self.default_loc .with_context(|| format!("no location defined for field {}", f.name)) }; // Resolve which location the field should be in. let loc = f.wire.as_ref().and_then(|w| w.location.as_ref()); let wire_loc = match loc { None => default_loc()?.into_wire_loc(), Some(schema::wire_spec::Location::Header(hdr)) => { WireLoc::Header(hdr.name.as_ref().unwrap_or(&f.name).clone()) } Some(schema::wire_spec::Location::Query(_)) => WireLoc::Query, Some(schema::wire_spec::Location::Cookie(c)) => { WireLoc::Cookie(c.name.as_ref().unwrap_or(&f.name).clone()) } Some(schema::wire_spec::Location::HttpStatus(_)) => { if self.supports_http_status { http_status = Some(HttpStatus::new(f.name.clone())); continue; } else { default_loc()?.into_wire_loc() } } }; // Add the field to the appropriate struct. let (dst, name_override) = match wire_loc { WireLoc::Body => (&mut body, None), WireLoc::Query => (&mut query, None), WireLoc::Header(s) => (&mut header, Some(s)), WireLoc::Path => unreachable!(), WireLoc::Cookie(s) => (&mut cookie, Some(s)), }; field.name_override = name_override; match dst { Some(dst) => { dst.fields.insert(name.to_owned(), field); } None => { *dst = Some(jsonschema::Struct { fields: { let mut fields = HashMap::new(); fields.insert(name.to_owned(), field); fields }, }); } } } let mut build = |s| { self.registry_builder .register_value(jsonschema::Value::Struct(s)) }; Ok(SchemaUnderConstruction { combined: Some(build(combined)), body: body.map(&mut build), query: query.map(&mut build), header: header.map(&mut build), cookie: cookie.map(&mut build), http_status, rpc_path: self.rpc_path.cloned(), }) } #[allow(dead_code)] fn resolve_struct<'b>( &'b self, typ: &'b schema::Type, ) -> anyhow::Result> { let typ = typ.typ.as_ref().context("type without type")?; match typ { Typ::Struct(s) => Ok(Cow::Borrowed(s)), Typ::Pointer(ptr) => { let base = ptr.base.as_ref().context("pointer without base")?; self.resolve_struct(base) } Typ::Named(named) => { let decl = &self.meta.decls[named.id as usize]; let typ = decl.r#type.as_ref().context("decl without type")?; self.resolve_struct(typ) } _ => anyhow::bail!("expected struct, got {:?}", typ), } } } fn resolve_type<'a>(meta: &'a meta::Data, typ: &'a Typ) -> anyhow::Result> { let resolver = TypeArgResolver { meta, resolved_args: vec![], decls: vec![], }; resolver.resolve(typ) } struct TypeArgResolver<'a> { meta: &'a meta::Data, resolved_args: Vec>, /// List of declarations being processed. /// Used to detect cycles. decls: Vec, } impl<'a> TypeArgResolver<'a> { fn resolve(&self, typ: &'a Typ) -> anyhow::Result> { match typ { Typ::Named(named) => { let decl = &self.meta.decls[named.id as usize]; if self.decls.contains(&decl.id) { // Return it unmodified. return Ok(Cow::Borrowed(typ)); } let args = self.resolve_types(&named.type_arguments)?; let nested = TypeArgResolver { meta: self.meta, resolved_args: args, decls: { let mut decls = self.decls.clone(); decls.push(decl.id); decls }, }; let typ = decl.r#type.as_ref().context("decl without type")?; let typ = typ.typ.as_ref().context("type without type")?; nested.resolve(typ) } Typ::Struct(strukt) => { let mut cows = Vec::with_capacity(strukt.fields.len()); for field in &strukt.fields { let t = field.typ.as_ref().context("field without type")?; let typ = t.typ.as_ref().context("type without type")?; let resolved = self.resolve(typ)?; cows.push((resolved, t.validation.as_ref())); } let mut fields = Vec::with_capacity(strukt.fields.len()); for (field, (typ, v)) in strukt.fields.iter().zip(cows) { fields.push(schema::Field { typ: Some(schema::Type { typ: Some(typ.into_owned()), validation: v.cloned(), }), ..field.clone() }); } Ok(Cow::Owned(Typ::Struct(schema::Struct { fields }))) } Typ::Map(map) => { let key = map.key.as_ref().context("map without key")?; let key_typ = key.typ.as_ref().context("key without type")?; let value = map.value.as_ref().context("map without value")?; let val_typ = value.typ.as_ref().context("value without type")?; let key_typ = self.resolve(key_typ)?; let val_typ = self.resolve(val_typ)?; if matches!((&key_typ, &val_typ), (Cow::Borrowed(_), Cow::Borrowed(_))) { Ok(Cow::Borrowed(typ)) } else { Ok(Cow::Owned(Typ::Map(Box::new(schema::Map { key: Some(Box::new(schema::Type { typ: Some(key_typ.into_owned()), validation: key.validation.clone(), })), value: Some(Box::new(schema::Type { typ: Some(val_typ.into_owned()), validation: value.validation.clone(), })), })))) } } Typ::List(list) => { let elem = list.elem.as_ref().context("list without elem")?; let elem_typ = elem.typ.as_ref().context("elem without type")?; let elem_typ = self.resolve(elem_typ)?; if matches!(elem_typ, Cow::Borrowed(_)) { Ok(Cow::Borrowed(typ)) } else { Ok(Cow::Owned(Typ::List(Box::new(schema::List { elem: Some(Box::new(schema::Type { typ: Some(elem_typ.into_owned()), validation: elem.validation.clone(), })), })))) } } Typ::Union(union) => { let types = self.resolve_types(&union.types)?; let types = types .into_iter() .zip(&union.types) .map(|(typ, t)| schema::Type { typ: Some(typ.into_owned()), validation: t.validation.clone(), }) .collect::>(); Ok(Cow::Owned(Typ::Union(schema::Union { types }))) } Typ::Builtin(_) => Ok(Cow::Borrowed(typ)), Typ::Literal(_) => Ok(Cow::Borrowed(typ)), Typ::Pointer(ptr) => { let base = ptr.base.as_ref().context("pointer without base")?; let typ = base.typ.as_ref().context("base without type")?; self.resolve(typ) } Typ::Option(opt) => { let value = opt.value.as_ref().context("option without value")?; let inner = value.typ.as_ref().context("value without type")?; match self.resolve(inner)? { Cow::Borrowed(_) => Ok(Cow::Borrowed(typ)), Cow::Owned(inner) => Ok(Cow::Owned(Typ::Option(Box::new(schema::Option { value: Some(Box::new(schema::Type { typ: Some(inner), validation: value.validation.clone(), })), })))), } } Typ::TypeParameter(param) => { let idx = param.param_idx as usize; let typ = &self.resolved_args[idx]; Ok(typ.clone()) } Typ::Config(_cfg) => { anyhow::bail!("config types are not supported") } } } fn resolve_types(&self, types: &'a [schema::Type]) -> anyhow::Result>> { types .iter() .map(|typ| { let typ = typ.typ.as_ref().context("type without type")?; self.resolve(typ) }) .collect() } } pub struct ReqSchemaUnderConstruction { pub methods: Vec, pub schema: SchemaUnderConstruction, } impl ReqSchemaUnderConstruction { pub fn build(self, reg: &Arc) -> anyhow::Result { Ok(ReqSchema { methods: self.methods, schema: self.schema.build(reg)?, }) } } pub struct ReqSchema { pub methods: Vec, pub schema: Schema, } pub struct HandshakeSchemaUnderConstruction { pub parse_data: bool, pub schema: SchemaUnderConstruction, } impl HandshakeSchemaUnderConstruction { pub fn build(self, reg: &Arc) -> anyhow::Result { Ok(HandshakeSchema { parse_data: self.parse_data, schema: self.schema.build(reg)?, }) } } pub struct HandshakeSchema { pub parse_data: bool, pub schema: Schema, } /// Computes the handshake encoding for the given rpc. pub fn handshake_encoding( registry_builder: &mut jsonschema::Builder, meta: &meta::Data, rpc: &meta::Rpc, ) -> anyhow::Result> { // Only streaming endpoints have a handshake schema if !rpc.streaming_request && !rpc.streaming_response { return Ok(None); } let default_path = meta::Path { segments: vec![meta::PathSegment { value: format!("{}.{}", rpc.service_name, rpc.name), r#type: SegmentType::Literal as i32, value_type: meta::path_segment::ParamType::String as i32, validation: None, }], r#type: meta::path::Type::Url as i32, }; let rpc_path = rpc.path.as_ref().unwrap_or(&default_path); let Some(handshake_schema) = &rpc.handshake_schema else { let parse_data = rpc.path.as_ref().is_some_and(|path| { path.segments .iter() .any(|segment| segment.r#type() != SegmentType::Literal) }); return Ok(Some(HandshakeSchemaUnderConstruction { parse_data, schema: SchemaUnderConstruction { combined: None, http_status: None, body: None, query: None, header: None, cookie: None, rpc_path: Some(rpc_path.clone()), }, })); }; let mut config = EncodingConfig { meta, registry_builder, default_loc: Some(DefaultLoc::Query), rpc_path: Some(rpc_path), supports_body: false, supports_query: true, supports_header: true, supports_path: true, supports_http_status: false, }; let schema = config.compute(handshake_schema)?; Ok(Some(HandshakeSchemaUnderConstruction { parse_data: true, schema, })) } /// Computes the request encoding for the given rpc. pub fn request_encoding( registry_builder: &mut jsonschema::Builder, meta: &meta::Data, rpc: &meta::Rpc, ) -> anyhow::Result> { // Streaming request can only have a body schema if rpc.streaming_request || rpc.streaming_response { let Some(request_schema) = &rpc.request_schema else { return Ok(vec![ReqSchemaUnderConstruction { methods: vec![Method::GET], schema: SchemaUnderConstruction { combined: None, http_status: None, body: None, query: None, header: None, cookie: None, rpc_path: None, }, }]); }; let mut config = EncodingConfig { meta, registry_builder, default_loc: Some(DefaultLoc::Body), rpc_path: None, supports_body: true, supports_query: false, supports_header: false, supports_path: false, supports_http_status: false, }; let schema = config.compute(request_schema)?; return Ok(vec![ReqSchemaUnderConstruction { methods: vec![Method::GET], schema, }]); } // Compute the set of methods. let methods = { let methods: anyhow::Result> = rpc .http_methods .iter() .map(|m| Method::try_from(m.as_str())) .collect(); methods.context("unable to parse http methods")? }; let default_path = meta::Path { segments: vec![meta::PathSegment { value: format!("{}.{}", rpc.service_name, rpc.name), r#type: SegmentType::Literal as i32, value_type: meta::path_segment::ParamType::String as i32, validation: None, }], r#type: meta::path::Type::Url as i32, }; let rpc_path = rpc.path.as_ref().unwrap_or(&default_path); // If there is no request schema, use the same encoding for all methods. let Some(request_schema) = &rpc.request_schema else { return Ok(vec![ReqSchemaUnderConstruction { methods, schema: SchemaUnderConstruction { combined: None, http_status: None, body: None, query: None, header: None, cookie: None, rpc_path: Some(rpc_path.clone()), }, }]); }; let mut schemas = Vec::new(); for default_loc in split_by_loc(&methods) { let mut config = EncodingConfig { meta, registry_builder, default_loc: Some(default_loc.0), rpc_path: Some(rpc_path), supports_body: true, supports_query: true, supports_header: true, supports_path: true, supports_http_status: false, }; let schema = config.compute(request_schema)?; schemas.push(ReqSchemaUnderConstruction { methods: default_loc.1.clone(), schema, }); } Ok(schemas) } /// Computes the request encoding for the given rpc. pub fn response_encoding( registry_builder: &mut jsonschema::Builder, meta: &meta::Data, rpc: &meta::Rpc, ) -> anyhow::Result { let Some(response_schema) = &rpc.response_schema else { return Ok(SchemaUnderConstruction { combined: None, body: None, query: None, header: None, cookie: None, http_status: None, rpc_path: None, }); }; let mut config = EncodingConfig { meta, registry_builder, default_loc: Some(DefaultLoc::Body), rpc_path: None, supports_body: true, supports_query: false, supports_header: true, supports_path: false, supports_http_status: true, }; config.compute(response_schema) } fn split_by_loc(methods: &[Method]) -> impl Iterator)> { let mut locs = HashMap::new(); for m in methods { let loc = if m.supports_body() { DefaultLoc::Body } else { DefaultLoc::Query }; locs.entry(loc).or_insert(Vec::new()).push(*m); } locs.into_iter() } ================================================ FILE: runtimes/core/src/api/schema/header.rs ================================================ use crate::api::reqauth::meta::HeaderValueExt; use crate::api::schema::{JSONPayload, ParseResponse, ToOutgoingRequest, ToResponse}; use crate::api::{self, PValue, PValues}; use crate::api::{jsonschema, APIResult}; use std::str::FromStr; #[derive(Debug, Clone)] pub struct Header { schema: jsonschema::JSONSchema, } impl Header { pub fn new(schema: jsonschema::JSONSchema) -> Self { Self { schema } } pub fn contains_any(&self, headers: &impl HTTPHeaders) -> bool { for (name, field) in self.schema.root().fields.iter() { let header_name = field.name_override.as_deref().unwrap_or(name.as_str()); if let Some(val) = headers.get(header_name) { // Only consider non-empty values to be present. if !val.is_empty() { return true; } } } false } pub fn contains_key(&self, key: &str) -> bool { self.schema.root().fields.contains_key(key) } pub fn len(&self) -> usize { self.schema.root().fields.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn fields(&self) -> impl Iterator { self.schema.root().fields.iter() } /// Returns an iterator that yields the header names that are expected by the schema. pub fn header_names(&self) -> impl Iterator + '_ { self.schema .root() .fields .iter() .filter_map(|(name, field)| { let header_name = field.name_override.as_deref().unwrap_or(name.as_str()); axum::http::HeaderName::from_str(header_name).ok() }) } } pub trait AsStr { fn as_str(&self) -> &str; } pub trait ToHeaderStr { type Error: std::error::Error; fn to_str(&self) -> Result<&str, Self::Error>; fn is_empty(&self) -> bool; } impl AsStr for &axum::http::header::HeaderName { fn as_str(&self) -> &str { ::as_str(self) } } impl ToHeaderStr for &axum::http::header::HeaderValue { type Error = std::str::Utf8Error; fn to_str(&self) -> Result<&str, Self::Error> { ::to_utf8_str(self) } fn is_empty(&self) -> bool { ::is_empty(self) } } pub trait HTTPHeaders { type Name: AsStr; type Value: ToHeaderStr; type Iter: Iterator; type GetAll: Iterator; fn headers(&self) -> Self::Iter; fn get(&self, key: &str) -> Option; fn get_all(&self, key: &str) -> Self::GetAll; fn contains_key(&self, key: &str) -> bool; } impl<'a> HTTPHeaders for &'a axum::http::HeaderMap { type Name = &'a axum::http::header::HeaderName; type Value = &'a axum::http::header::HeaderValue; type Iter = axum::http::header::Iter<'a, axum::http::header::HeaderValue>; type GetAll = axum::http::header::ValueIter<'a, axum::http::header::HeaderValue>; fn headers(&self) -> Self::Iter { self.iter() } fn get(&self, key: &str) -> Option { ::get(self, key) } fn get_all(&self, key: &str) -> Self::GetAll { ::get_all(self, key).iter() } fn contains_key(&self, key: &str) -> bool { ::contains_key(self, key) } } impl Header { pub fn parse_incoming_request_parts( &self, req: &axum::http::request::Parts, ) -> APIResult> { self.parse(&req.headers) } pub fn parse(&self, headers: &axum::http::HeaderMap) -> APIResult> { if self.schema.root().is_empty() { return Ok(None); } match self.schema.parse(headers) { Ok(decoded) => Ok(Some(decoded)), Err(err) => Err(err), } } } impl ParseResponse for Header { type Output = Option; fn parse_response(&self, resp: &mut reqwest::Response) -> APIResult { if self.schema.root().is_empty() { return Ok(None); } match self.schema.parse(resp.headers()) { Ok(decoded) => Ok(Some(decoded)), Err(err) => Err(err), } } } impl ToOutgoingRequest for Header { fn to_outgoing_request( &self, payload: &mut JSONPayload, headers: &mut http::HeaderMap, ) -> APIResult<()> { if self.schema.root().is_empty() { return Ok(()); } let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing header parameters".to_string(), internal_message: Some("missing header parameters".to_string()), stack: None, details: None, }); }; for (key, field) in self.schema.root().fields.iter() { let Some(value) = payload.get(key) else { if field.optional { continue; } return Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("missing header parameter: {key}"), internal_message: Some(format!("missing header parameter: {key}")), stack: None, details: None, }); }; let header_name = field.name_override.as_deref().unwrap_or(key); let header_name = reqwest::header::HeaderName::from_str(header_name).map_err(api::Error::internal)?; match to_reqwest_header_value(value)? { ReqwestHeaders::Single(value) => { headers.append(header_name, value); } ReqwestHeaders::Multi(values) => { for value in values { headers.append(header_name.clone(), value); } } } } Ok(()) } } impl ToOutgoingRequest for Header { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut reqwest::Request, ) -> APIResult<()> { let headers = req.headers_mut(); self.to_outgoing_request(payload, headers) } } impl ToOutgoingRequest> for Header { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut http::Request, ) -> APIResult<()> { let headers = req.headers_mut(); self.to_outgoing_request(payload, headers) } } impl ToResponse for Header { fn to_response( &self, payload: &JSONPayload, mut resp: axum::http::response::Builder, ) -> APIResult { if self.schema.root().is_empty() { return Ok(resp); } let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing header parameters".to_string(), internal_message: Some("missing header parameters".to_string()), stack: None, details: None, }); }; let schema = self.schema.root(); for (key, value) in payload.iter() { let key = key.as_str(); let Some(field) = schema.fields.get(key) else { continue; // Not a header. }; let header_name = field.name_override.as_deref().unwrap_or(key); let header_name = axum::http::header::HeaderName::from_str(header_name) .map_err(api::Error::internal)?; match to_axum_header_value(value)? { AxumHeaders::Single(value) => resp = resp.header(header_name, value), AxumHeaders::Multi(values) => { for value in values { resp = resp.header(header_name.clone(), value); } } } } Ok(resp) } } enum ReqwestHeaders { Single(reqwest::header::HeaderValue), Multi(Vec), } fn to_reqwest_header_value(value: &PValue) -> APIResult { use PValue::*; use ReqwestHeaders::*; Ok(Single(match value { Null => reqwest::header::HeaderValue::from_static("null"), Bool(bool) => { reqwest::header::HeaderValue::from_static(if *bool { "true" } else { "false" }) } String(str) => reqwest::header::HeaderValue::from_str(str).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert string to header value".to_string(), internal_message: Some(format!("unable to convert string to header value: {e}")), stack: None, details: None, })?, DateTime(dt) => { let s = dt.to_rfc3339(); reqwest::header::HeaderValue::from_str(&s).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert datetime to header value".to_string(), internal_message: Some(format!("unable to convert datetime to header value: {e}")), stack: None, details: None, })? } Number(num) => { let str = num.to_string(); reqwest::header::HeaderValue::from_str(&str).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert number to header value".to_string(), internal_message: Some(format!("unable to convert number to header value: {e}")), stack: None, details: None, })? } Decimal(d) => { reqwest::header::HeaderValue::from_str(&d.to_string()).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert decimal to header value".to_string(), internal_message: Some(format!("unable to convert decimal to header value: {e}")), stack: None, details: None, })? } Array(arr) => { let mut values = Vec::with_capacity(arr.len()); for value in arr.iter() { match to_reqwest_header_value(value)? { Single(value) => values.push(value), Multi(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "nested array type unsupported as header value".into(), internal_message: None, stack: None, details: None, }) } } } return Ok(Multi(values)); } Object(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "map type unsupported as header value".into(), internal_message: None, stack: None, details: None, }); } Cookie(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "cookie type unsupported as header value".into(), internal_message: None, stack: None, details: None, }) } })) } enum AxumHeaders { Single(axum::http::HeaderValue), Multi(Vec), } fn to_axum_header_value(value: &PValue) -> APIResult { use PValue::*; Ok(AxumHeaders::Single(match value { Null => axum::http::HeaderValue::from_static("null"), Bool(bool) => axum::http::HeaderValue::from_static(if *bool { "true" } else { "false" }), String(str) => axum::http::HeaderValue::from_str(str).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert string to header value".to_string(), internal_message: Some(format!("unable to convert string to header value: {e}")), stack: None, details: None, })?, DateTime(dt) => { let s = dt.to_rfc3339(); axum::http::HeaderValue::from_str(&s).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert datetime to header value".to_string(), internal_message: Some(format!("unable to convert datetime to header value: {e}")), stack: None, details: None, })? } Number(num) => { let str = num.to_string(); axum::http::HeaderValue::from_str(&str).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert number to header value".to_string(), internal_message: Some(format!("unable to convert number to header value: {e}")), stack: None, details: None, })? } Decimal(d) => { axum::http::HeaderValue::from_str(&d.to_string()).map_err(|e| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to convert decimal to header value".to_string(), internal_message: Some(format!("unable to convert decimal to header value: {e}")), stack: None, details: None, })? } Array(arr) => { let mut values = Vec::with_capacity(arr.len()); for value in arr.iter() { match to_axum_header_value(value)? { AxumHeaders::Single(value) => values.push(value), AxumHeaders::Multi(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "nested array type unsupported as header value".into(), internal_message: None, stack: None, details: None, }) } } } return Ok(AxumHeaders::Multi(values)); } Object(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "map type unsupported as header value".into(), internal_message: None, stack: None, details: None, }); } Cookie(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "cookie type unsupported as header value".into(), internal_message: None, stack: None, details: None, }) } })) } ================================================ FILE: runtimes/core/src/api/schema/httpstatus.rs ================================================ use crate::api::{APIResult, PValues}; use axum::http::response::Builder as ResponseBuilder; /// HTTP status code parameter specification. #[derive(Debug, Clone)] pub struct HttpStatus { pub name: String, } impl HttpStatus { pub fn new(name: String) -> Self { Self { name } } /// Extract HTTP status code from response payload and apply it to the response builder. pub fn to_response( &self, payload: &Option, mut resp_builder: ResponseBuilder, ) -> APIResult { if let Some(payload) = payload { if let Some(status_value) = payload.get(&self.name) { let status_code = self.extract_status_code(status_value)?; resp_builder = resp_builder.status(status_code); } } Ok(resp_builder) } /// Extract and validate HTTP status code from a PValue. fn extract_status_code(&self, status_value: &crate::api::PValue) -> APIResult { let status_code = match status_value { crate::api::PValue::Number(n) => n.as_u64().ok_or_else(|| { crate::api::Error::invalid_argument( "invalid http status code", anyhow::anyhow!( "HTTP status field '{}' must be a positive integer", self.name ), ) })?, _ => { return Err(crate::api::Error::invalid_argument( "invalid http status code", anyhow::anyhow!( "HTTP status field '{}' must be a number, got: {}", self.name, status_value.type_name() ), )); } }; if !(100..=599).contains(&status_code) { return Err(crate::api::Error::invalid_argument( "invalid http status code", anyhow::anyhow!( "HTTP status field '{}' must be a between 100 and 599, got: {status_code}", self.name, ), )); } Ok(status_code as u16) } } impl super::ToResponse for HttpStatus { fn to_response( &self, payload: &super::JSONPayload, resp: axum::http::response::Builder, ) -> APIResult { self.to_response(payload, resp) } } ================================================ FILE: runtimes/core/src/api/schema/method.rs ================================================ #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Method { GET, HEAD, POST, PUT, DELETE, // CONNECT, OPTIONS, TRACE, PATCH, } impl Method { pub fn as_str(self) -> &'static str { match self { Method::GET => "GET", Method::HEAD => "HEAD", Method::POST => "POST", Method::PUT => "PUT", Method::DELETE => "DELETE", // Method::CONNECT => "CONNECT", Method::OPTIONS => "OPTIONS", Method::TRACE => "TRACE", Method::PATCH => "PATCH", } } /// Whether the method supports a request body. pub fn supports_body(&self) -> bool { match self { Self::POST | Self::PUT | Self::PATCH /* | Self::CONNECT */ => true, Self::GET | Self::DELETE | Self::HEAD | Self::OPTIONS | Self::TRACE => false, } } } impl TryFrom<&str> for Method { type Error = anyhow::Error; fn try_from(s: &str) -> Result { match s { "GET" => Ok(Method::GET), "HEAD" => Ok(Method::HEAD), "POST" => Ok(Method::POST), "PUT" => Ok(Method::PUT), "DELETE" => Ok(Method::DELETE), // "CONNECT" => Ok(Method::CONNECT), "OPTIONS" => Ok(Method::OPTIONS), "TRACE" => Ok(Method::TRACE), "PATCH" => Ok(Method::PATCH), _ => Err(anyhow::anyhow!("invalid method: {}", s)), } } } impl From for axum::http::Method { fn from(val: Method) -> Self { match val { Method::GET => axum::http::Method::GET, Method::HEAD => axum::http::Method::HEAD, Method::POST => axum::http::Method::POST, Method::PUT => axum::http::Method::PUT, Method::DELETE => axum::http::Method::DELETE, // Method::CONNECT => http::Method::CONNECT, Method::OPTIONS => axum::http::Method::OPTIONS, Method::TRACE => axum::http::Method::TRACE, Method::PATCH => axum::http::Method::PATCH, } } } impl TryFrom for Method { type Error = anyhow::Error; fn try_from(m: axum::http::Method) -> anyhow::Result { Ok(match m.as_str() { "GET" => Method::GET, "HEAD" => Method::HEAD, "POST" => Method::POST, "PUT" => Method::PUT, "DELETE" => Method::DELETE, // "CONNECT" => Method::CONNECT, "OPTIONS" => Method::OPTIONS, "TRACE" => Method::TRACE, "PATCH" => Method::PATCH, x => anyhow::bail!("invalid method: {}", x), }) } } pub fn method_filter(methods: M) -> Option where M: Iterator, { use axum::routing::MethodFilter; let mut filter = None; for method in methods { let method_filter = match method { Method::GET => MethodFilter::GET, Method::HEAD => MethodFilter::HEAD, Method::POST => MethodFilter::POST, Method::PUT => MethodFilter::PUT, Method::DELETE => MethodFilter::DELETE, // Method::CONNECT => MethodFilter::CONNECT, Method::OPTIONS => MethodFilter::OPTIONS, Method::TRACE => MethodFilter::TRACE, Method::PATCH => MethodFilter::PATCH, }; filter = Some(filter.unwrap_or(method_filter).or(method_filter)); } filter } ================================================ FILE: runtimes/core/src/api/schema/mod.rs ================================================ use crate::api; pub use body::*; pub use cookie::*; pub use header::*; pub use httpstatus::*; pub use method::*; pub use path::*; pub use query::*; use std::sync::Arc; use crate::api::{endpoint, APIResult, PValues, RequestPayload}; use super::ResponsePayload; mod body; mod cookie; pub mod encoding; mod header; mod httpstatus; mod method; mod path; mod query; pub type JSONPayload = Option; pub trait ToOutgoingRequest { fn to_outgoing_request(&self, payload: &mut JSONPayload, req: &mut Request) -> APIResult<()>; } pub trait ToResponse { fn to_response( &self, payload: &JSONPayload, resp: axum::http::response::Builder, ) -> APIResult; } pub trait ParseResponse { type Output: Sized; fn parse_response(&self, resp: &mut reqwest::Response) -> APIResult; } /// The request schema for a set of methods. #[derive(Debug)] pub struct Request { /// The methods the schema is applicable for. pub methods: Vec, /// Path to reach the endpoint. pub path: Path, /// Header names used by the endpoint. pub header: Option
, /// Cookie names used by the endpoint. pub cookie: Option, /// Query string names used by the endpoint. pub query: Option, /// Request body. pub body: RequestBody, /// If this is a streamed request pub stream: bool, } #[derive(Debug)] pub enum RequestBody { Typed(Option), Raw, } impl Request { pub async fn extract( &self, parts: &mut axum::http::request::Parts, body: axum::body::Body, ) -> APIResult> { let path = self.path.parse_incoming_request_parts(parts)?; let query = match &self.query { None => None, Some(q) => q.parse_incoming_request_parts(parts)?, }; let header = match &self.header { None => None, Some(h) => h.parse_incoming_request_parts(parts)?, }; let cookie = match &self.cookie { None => None, Some(c) => c.parse_incoming_request_parts(parts)?, }; let body = match &self.body { RequestBody::Raw => endpoint::Body::Raw(Arc::new(std::sync::Mutex::new(Some(body)))), RequestBody::Typed(None) => endpoint::Body::Typed(None), RequestBody::Typed(Some(b)) => { endpoint::Body::Typed(b.parse_incoming_request_body(body).await?) } }; Ok(Some(RequestPayload { path, query, header, cookie, body, })) } } /// The response schema for an endpoint. #[derive(Debug)] pub struct Response { /// Response header names returned by the endpoint. pub header: Option
, /// Response cookie names returned by the endpoint. pub cookie: Option, /// Response body, if any. pub body: Option, /// HTTP status code field, if any. pub http_status: Option, /// If this is a streamed response pub stream: bool, } impl Response { pub fn encode( &self, payload: &JSONPayload, status: u16, ) -> APIResult> { let mut bld = axum::http::Response::builder().status(status); if let Some(hdr) = &self.header { bld = hdr.to_response(payload, bld)? }; if let Some(c) = &self.cookie { bld = c.to_response(payload, bld)?; } if let Some(hs) = &self.http_status { bld = hs.to_response(payload, bld)?; } match &self.body { Some(body) => body.to_response(payload, bld), None => bld .body(axum::body::Body::empty()) .map_err(api::Error::internal), } } pub async fn extract(&self, resp: reqwest::Response) -> APIResult { let header = match &self.header { None => None, Some(h) => h.parse(resp.headers())?, }; let cookie = match &self.cookie { None => None, Some(c) => c.parse_resp(resp.headers())?, }; // Do we have a body schema? let body = endpoint::Body::Typed(match &self.body { None => None, Some(body_schema) => { // If so we expect a JSON body. match resp.headers().get(axum::http::header::CONTENT_TYPE) { Some(content_type) if content_type == "application/json" => { // Collect the bytes of the request body. // Serde doesn't support async streaming reads (at least not yet). let bytes = resp.bytes().await.map_err(|e| { api::Error::invalid_argument("unable to read response body", e) })?; body_schema.parse_response_body(bytes).await? } _ => { // We didn't get a JSON body, so return an error. return Err(api::Error::internal(anyhow::anyhow!("expected json body"))); } } } }); Ok(ResponsePayload { header, body, cookie, }) } } pub struct Stream { incoming: Box, outgoing: Box, } impl Stream { pub fn new(incoming: I, outgoing: O) -> Self where I: StreamMessage, O: StreamMessage, { Stream { incoming: Box::new(incoming), outgoing: Box::new(outgoing), } } pub fn to_outgoing_message(&self, msg: PValues) -> APIResult> { let body_schema = self.outgoing.body().ok_or_else(|| { super::Error::internal(anyhow::anyhow!("outgoing message body can't be empty")) })?; body_schema.to_outgoing_payload(&Some(msg)) } pub async fn parse_incoming_message(&self, bytes: &[u8]) -> APIResult { let Some(body) = self.incoming.body() else { return Err(super::Error { code: super::ErrCode::InvalidArgument, message: "invalid streaming body type in schema".to_string(), internal_message: None, stack: None, details: None, }); }; let value = body .parse_incoming_request_body(bytes.to_vec().into()) .await? .ok_or_else(|| super::Error { code: super::ErrCode::InvalidArgument, message: "missing payload".to_string(), internal_message: None, stack: None, details: None, })?; Ok(value) } } pub trait StreamMessage: Send + Sync + 'static { fn body(&self) -> Option<&Body>; } impl StreamMessage for Arc { fn body(&self) -> Option<&Body> { self.body.as_ref() } } impl StreamMessage for Arc { fn body(&self) -> Option<&Body> { if let RequestBody::Typed(body) = &self.body { body.as_ref() } else { None } } } ================================================ FILE: runtimes/core/src/api/schema/path.rs ================================================ use std::future::Future; use std::pin::Pin; use std::ptr; use std::str::FromStr; use std::task::{Poll, RawWaker, RawWakerVTable, Waker}; use anyhow::Context; use axum::extract::rejection::PathRejection; use axum::extract::FromRequestParts; use axum::http::request::Parts; use indexmap::IndexMap; use crate::api::jsonschema::Basic; use crate::api::schema::JSONPayload; use crate::api::{self, pvalue::PValue}; use crate::api::{jsonschema, APIResult}; use crate::encore::parser::meta::v1 as meta; use crate::encore::parser::meta::v1::path_segment::ParamType; /// The URL path to an endpoint, e.g. ("/foo/bar/:id"). #[derive(Debug, Clone)] pub struct Path { /// The path segments. segments: Vec, dynamic_segments: Vec, /// The capacity to use for generating requests. capacity: usize, } #[derive(Debug, Clone)] struct DynamicSegment { name: Box, typ: Basic, validation: Option, } impl Path { pub fn from_meta(path: &meta::Path) -> anyhow::Result { let mut segments = Vec::with_capacity(path.segments.len()); for seg in &path.segments { use meta::path_segment::SegmentType; let validation = seg .validation .as_ref() .map(|v| { jsonschema::validation::Expr::try_from(v) .context("invalid path segment validation") }) .transpose()?; match SegmentType::try_from(seg.r#type).context("invalid path segment type")? { SegmentType::Literal => { segments.push(Segment::Literal(seg.value.clone().into_boxed_str())) } SegmentType::Param => { let name = &seg.value; let typ = match ParamType::try_from(seg.value_type) .context("invalid path parameter type")? { ParamType::String => Basic::String, ParamType::Bool => Basic::Bool, ParamType::Uuid => Basic::String, ParamType::Int | ParamType::Int8 | ParamType::Int16 | ParamType::Int32 | ParamType::Int64 | ParamType::Uint | ParamType::Uint8 | ParamType::Uint16 | ParamType::Uint32 | ParamType::Uint64 => Basic::Number, }; segments.push(Segment::Param { name: name.clone().into_boxed_str(), typ, validation, }); } SegmentType::Wildcard => segments.push(Segment::Wildcard { name: seg.clone().value.into_boxed_str(), validation, }), SegmentType::Fallback => segments.push(Segment::Fallback { name: seg.clone().value.into_boxed_str(), validation, }), } } Ok(Self::from_segments(segments)) } pub fn from_segments(segments: Vec) -> Self { let mut capacity = 0; let mut dynamic_segments = Vec::new(); for seg in segments.iter() { use Segment::*; capacity += 1; // slash match seg { Literal(lit) => capacity += lit.len(), Param { name, typ, validation, } => { capacity += 10; // assume path parameters on average are 10 characters long dynamic_segments.push(DynamicSegment { name: name.clone(), typ: *typ, validation: validation.clone(), }); } Wildcard { name, validation } | Fallback { name, validation } => { // Assume path parameters on average are 10 characters long. capacity += 10; dynamic_segments.push(DynamicSegment { name: name.clone(), typ: jsonschema::Basic::String, validation: validation.clone(), }); } } } Self { segments, dynamic_segments, capacity, } } } /// Represents a path segment. #[derive(Debug, Clone)] pub enum Segment { Literal(Box), Param { name: Box, /// The type of the path parameter. typ: jsonschema::Basic, validation: Option, }, Wildcard { name: Box, validation: Option, }, Fallback { name: Box, validation: Option, }, } impl Path { /// Computes the request path to use for making an API call to this path with the given payload. pub fn to_request_path(&self, payload: &mut JSONPayload) -> Result { let mut path = String::with_capacity(self.capacity); for seg in self.segments.iter() { path.push('/'); use Segment::*; match seg { Literal(lit) => path.push_str(lit), Param { name, .. } | Wildcard { name, .. } | Fallback { name, .. } => { let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing field in request payload".into(), internal_message: Some(format!( "missing field in request payload: {}", &name )), stack: None, details: None, }); }; // Find the data in the payload. let Some(value) = payload.get(name.as_ref()) else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing field in request payload".into(), internal_message: Some(format!( "missing field in request payload: {}", &name )), stack: None, details: None, }); }; match value { PValue::String(str) => { // URL-encode the string, so it doesn't get reinterpreted // as multiple path segments. let encoded = urlencoding::encode(str); path.push_str(&encoded); } PValue::Null => path.push_str("null"), PValue::Bool(bool) => path.push_str(if *bool { "true" } else { "false" }), PValue::Number(num) => { let str = num.to_string(); path.push_str(&str); } PValue::Decimal(d) => { let str = d.to_string(); path.push_str(&str); } PValue::DateTime(dt) => { let encoded = dt.to_rfc3339(); path.push_str(&encoded); } PValue::Array(_) | PValue::Object(_) | PValue::Cookie(_) => { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "unsupported type in request payload".into(), internal_message: Some(format!( "unsupported type in request payload for field {}", &name )), stack: None, details: None, }) } } } } } Ok(path) } } impl Path { pub fn parse_incoming_request_parts( &self, req: &mut Parts, ) -> APIResult>> { if self.dynamic_segments.is_empty() { return Ok(None); } let fut = axum::extract::Path::>::from_request_parts(req, &()); let result = resolve_immediate_future(fut).map_err(|_| api::Error { code: api::ErrCode::InvalidArgument, message: "unable to parse path params".into(), internal_message: Some("polling path params returned pending".into()), stack: None, details: None, })?; match result { Ok(axum::extract::Path(params)) => { let mut map = IndexMap::with_capacity(params.len()); // For each param, find the corresponding segment and deserialize it. for (idx, (_axum_name, val)) in params.into_iter().enumerate() { if let Some(DynamicSegment { name, typ, validation, }) = self.dynamic_segments.get(idx) { // Decode it into the correct type based on the type. let val = match &typ { // For strings and any, use the value directly. Basic::String | Basic::Any => PValue::String(val), // For numbers and booleans, use the JSON parser. Basic::Number => { let val = serde_json::from_str::(&val) .map_err(|err| api::Error { code: api::ErrCode::InvalidArgument, message: "path parameter is not a valid number".into(), internal_message: Some(err.to_string()), stack: None, details: None, })?; PValue::Number(val) } Basic::Bool => { let val = serde_json::from_str::(&val).map_err(|err| { api::Error { code: api::ErrCode::InvalidArgument, message: "path parameter is not a valid boolean".into(), internal_message: Some(err.to_string()), stack: None, details: None, } })?; PValue::Bool(val) } Basic::DateTime => { let val = api::DateTime::parse_from_rfc3339(&val).map_err(|err| { api::Error { code: api::ErrCode::InvalidArgument, message: "path parameter is not a valid datetime" .into(), internal_message: Some(err.to_string()), stack: None, details: None, } })?; PValue::DateTime(val) } Basic::Decimal => { let val = api::Decimal::from_str(&val).map_err(|err| api::Error { code: api::ErrCode::InvalidArgument, message: "path parameter is not a valid decimal".into(), internal_message: Some(err.to_string()), stack: None, details: None, })?; PValue::Decimal(val) } // We shouldn't have null here, but handle it just in case. Basic::Null => PValue::Null, }; // Validate the value, if we have a validation expression. if let Some(validation) = validation.as_ref() { if let Err(err) = validation.validate_pval(&val) { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: format!("invalid path parameter {name}: {err}"), internal_message: None, stack: None, details: None, }); } } map.insert(name.to_string(), val); } } Ok(Some(map)) } Err(err) => Err(match err { PathRejection::FailedToDeserializePathParams(err) => api::Error { code: api::ErrCode::InvalidArgument, message: "unable to parse path params".into(), internal_message: Some(err.to_string()), stack: None, details: None, }, PathRejection::MissingPathParams(err) => api::Error { code: api::ErrCode::InvalidArgument, message: "missing path params".into(), internal_message: Some(err.to_string()), stack: None, details: None, }, err => api::Error { code: api::ErrCode::Internal, message: "unable to parse path params".into(), internal_message: Some(err.to_string()), stack: None, details: None, }, }), } } } struct FuturePendingError; fn resolve_immediate_future(mut fut: F) -> Result where F: Future + Unpin, { let waker = noop_waker(); let mut cx = std::task::Context::from_waker(&waker); match Pin::new(&mut fut).poll(&mut cx) { Poll::Ready(value) => Ok(value), Poll::Pending => Err(FuturePendingError), } } fn noop_waker() -> Waker { const VTABLE: RawWakerVTable = RawWakerVTable::new( // Cloning just returns a new no-op raw waker |_| RAW, // `wake` does nothing |_| {}, // `wake_by_ref` does nothing |_| {}, // Dropping does nothing as we don't allocate anything |_| {}, ); const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE); // SAFETY: This is copied from https://github.com/rust-lang/rust/pull/96875. unsafe { Waker::from_raw(RAW) } } ================================================ FILE: runtimes/core/src/api/schema/query.rs ================================================ use std::str::FromStr; use crate::api::jsonschema::DecodeConfig; use crate::api::schema::{JSONPayload, ToOutgoingRequest}; use crate::api::{self, PValues}; use crate::api::{jsonschema, APIResult}; use serde::Serialize; use url::Url; #[derive(Debug, Clone)] pub struct Query { schema: jsonschema::JSONSchema, } impl Query { pub fn new(schema: jsonschema::JSONSchema) -> Self { Self { schema } } pub fn parse_incoming_request_parts( &self, req: &axum::http::request::Parts, ) -> APIResult> { self.parse(req.uri.query()) } pub fn parse(&self, query: Option<&str>) -> APIResult> { let parsed = form_urlencoded::parse(query.unwrap_or_default().as_bytes()); let de = serde_urlencoded::Deserializer::new(parsed); let cfg = DecodeConfig { coerce_strings: true, arrays_as_repeated_fields: true, }; let decoded = self .schema .deserialize(de, cfg) .map_err(|e| api::Error::invalid_argument("unable to decode query string", e))?; Ok(Some(decoded)) } pub fn contains_name(&self, name: &str) -> bool { self.schema.root().contains_name(name) } pub fn contains_any(&self, query: &[u8]) -> bool { let schema = &self.schema.root(); if schema.is_empty() { return false; } let parsed = form_urlencoded::parse(query); for (key, val) in parsed { // Only consider non-empty values to be present. if !val.is_empty() && schema.contains_name(key.as_ref()) { return true; } } false } pub fn len(&self) -> usize { self.schema.root().fields.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn fields(&self) -> impl Iterator { self.schema.root().fields.iter() } } impl ToOutgoingRequest> for Query { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut http::Request<()>, ) -> APIResult<()> { let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing query parameters".to_string(), internal_message: Some("missing query parameters".to_string()), stack: None, details: None, }); }; // Serialize the payload. let mut url = Url::parse(&req.uri().to_string()).map_err(api::Error::internal)?; { let mut pairs = url.query_pairs_mut(); let serializer = serde_urlencoded::Serializer::new(&mut pairs); flatten_payload(payload) .serialize(serializer) .map_err(api::Error::internal)?; } *req.uri_mut() = http::Uri::from_str(url.as_str()).map_err(api::Error::internal)?; Ok(()) } } impl ToOutgoingRequest for Query { fn to_outgoing_request( &self, payload: &mut JSONPayload, req: &mut reqwest::Request, ) -> APIResult<()> { if self.schema.root().is_empty() { return Ok(()); } let Some(payload) = payload else { return Err(api::Error { code: api::ErrCode::InvalidArgument, message: "missing query parameters".to_string(), internal_message: Some("missing query parameters".to_string()), stack: None, details: None, }); }; // Serialize the payload. { let url = req.url_mut(); let mut pairs = url.query_pairs_mut(); let serializer = serde_urlencoded::Serializer::new(&mut pairs); flatten_payload(payload) .serialize(serializer) .map_err(api::Error::internal)?; } // If the query string is now empty, set it to None. if let Some("") = req.url().query() { req.url_mut().set_query(None); } Ok(()) } } // Flatten arrays into multi-fields for serde_urlencoded fn flatten_payload(payload: &PValues) -> Vec<(&String, &api::PValue)> { payload .iter() .flat_map(|(k, v)| { if let api::PValue::Array(vec) = v { vec.iter().map(|val| (k, val)).collect() } else { vec![(k, v)] } }) .collect() } ================================================ FILE: runtimes/core/src/api/server.rs ================================================ use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; use std::pin::Pin; use std::sync::atomic::AtomicUsize; use std::sync::{Arc, Mutex, RwLock}; use crate::api::endpoint::{EndpointHandler, SharedEndpointData}; use crate::api::paths::Pather; use crate::api::reqauth::svcauth; use crate::api::static_assets::StaticAssetsHandler; use crate::api::{self, ToResponse}; use crate::api::{paths, reqauth, schema, BoxedHandler, EndpointMap}; use crate::encore::parser::meta::v1 as meta; use crate::names::EndpointName; use crate::trace; use super::jsonschema::JSONSchema; /// An alias for the concrete type of a server handler. type ServerHandler = ReplaceableHandler; /// Server is an API server. It serves the registered API endpoints. /// /// When running tests there's not a single entrypoint, so the server /// is designed to support incrementally adding endpoints. /// /// We handle this by registering all handlers with axum up-front, and add /// the handler once it has been registered. #[derive(Debug)] pub struct Server { endpoints: Arc, hosted_endpoints: Mutex>, router: Mutex>, /// Data shared between all endpoints. shared: Arc, /// Metrics registry for creating metrics metrics_registry: Arc, } impl Server { pub fn new( endpoints: Arc, hosted_endpoints: Vec, platform_auth: Arc, inbound_svc_auth: Vec>, tracer: trace::Tracer, auth_data_schemas: HashMap>, metrics_registry: Arc, ) -> anyhow::Result { // Register the routes, and track the handlers in a map so we can easily // set the request handler when registered. let mut router = axum::Router::new(); async fn not_found_handler( req: axum::http::Request, ) -> axum::response::Response { api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".to_string(), internal_message: Some(format!("no such endpoint exists: {}", req.uri().path())), stack: None, details: None, } .to_response(None) } let mut fallback_router = axum::Router::new(); fallback_router = fallback_router.fallback(not_found_handler); let mut handler_map = HashMap::with_capacity(hosted_endpoints.len()); let path_set = paths::compute(hosted_endpoints.iter().map(|ep| EndpointPathResolver { ep: endpoints.get(ep).unwrap().to_owned(), })); let shared = Arc::new(SharedEndpointData { tracer, platform_auth, inbound_svc_auth, auth_data_schemas, }); let mut register = |paths: &[(Arc, Vec)], mut router: axum::Router| -> axum::Router { for (ep, paths) in paths { match schema::method_filter(ep.methods()) { Some(filter) => { let server_handler = ServerHandler::default(); if let Some(assets) = &ep.static_assets { // For static asset routes, configure the static asset handler directly. // There's no need to defer it for dynamic runtime registration. let static_handler = StaticAssetsHandler::new(assets); let requests_total = crate::metrics::requests_total_counter( &metrics_registry, ep.name.service(), ep.name.endpoint(), ); let handler = EndpointHandler { endpoint: ep.clone(), handler: Arc::new(static_handler), shared: shared.clone(), requests_total, }; server_handler.set(handler); } let handler = axum::routing::on(filter, server_handler.clone()); for path in paths { router = router.route(path, handler.clone()); } handler_map.insert(ep.name.clone(), server_handler); } None => { log::warn!("no methods for endpoint {}, skipping", ep.name); } } } router }; if let Some(main_paths) = path_set.main.get(&()) { router = register(main_paths, router); } if let Some(fallback_paths) = path_set.fallback.get(&()) { fallback_router = register(fallback_paths, fallback_router); } // Register our fallback route. router = router.fallback_service(fallback_router); Ok(Self { endpoints, hosted_endpoints: Mutex::new(handler_map), router: Mutex::new(Some(router)), shared, metrics_registry, }) } pub fn router(&self) -> axum::Router { self.router.lock().unwrap().as_ref().unwrap().clone() } /// Registers a handler for the given endpoint. /// Reports an error if the handler was not found. pub fn register_handler( &self, endpoint_name: EndpointName, handler: Arc, ) -> anyhow::Result<()> { match self .hosted_endpoints .lock() .unwrap() .get_mut(&endpoint_name) { None => Ok(()), // anyhow::bail!("no handler found for endpoint: {}", endpoint_name), Some(h) => { let endpoint = self.endpoints.get(&endpoint_name).unwrap().to_owned(); let requests_total = crate::metrics::requests_total_counter( &self.metrics_registry, endpoint.name.service(), endpoint.name.endpoint(), ); let handler = EndpointHandler { endpoint, handler, shared: self.shared.clone(), requests_total, }; h.add(handler); Ok(()) } } } } struct EndpointPathResolver { ep: Arc, } impl Pather for EndpointPathResolver { type Key = (); type Value = Arc; fn key(&self) {} fn value(&self) -> Self::Value { self.ep.clone() } fn path(&self) -> &meta::Path { &self.ep.path } } #[derive(Debug)] struct LoadBalancingHandler { handlers: Vec, counter: AtomicUsize, } impl Default for LoadBalancingHandler { fn default() -> Self { Self { handlers: Vec::new(), counter: AtomicUsize::new(0), } } } impl LoadBalancingHandler { pub fn single(handler: H) -> Self { Self { handlers: vec![handler], counter: AtomicUsize::new(1), } } pub fn add(&mut self, handler: H) { self.handlers.push(handler); } pub fn len(&self) -> usize { self.handlers.len() } pub fn next(&self) -> &H { let n = self.handlers.len(); // If we have a single handler, skip the increment and modulo steps. if n == 1 { return &self.handlers[0]; } // Atomically increment the counter, and then get the next handler. let idx = self .counter .fetch_add(1, std::sync::atomic::Ordering::Relaxed); &self.handlers[idx % n] } } /// A replaceable handler is a handler that can be replaced at runtime. /// It is used to support incremental registration of endpoints. #[derive(Clone)] struct ReplaceableHandler { /// Underlying handler. The RwLock is used to be able to inject the underlying handler. handler: Arc>>, } impl Debug for ReplaceableHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ReplaceableHandler").finish() } } impl Default for ReplaceableHandler { fn default() -> Self { Self::new() } } impl ReplaceableHandler { pub fn new() -> Self { Self { handler: Arc::new(RwLock::default()), } } /// Set sets the handler. pub fn set(&self, handler: H) { *self.handler.write().unwrap() = LoadBalancingHandler::single(handler); } /// Set sets the handler. pub fn add(&self, handler: H) { self.handler.write().unwrap().add(handler); } } impl axum::handler::Handler<(), ()> for ReplaceableHandler where H: axum::handler::Handler<(), ()> + Sync, { type Future = MaybeHandlerFuture; fn call(self, req: axum::extract::Request, state: ()) -> Self::Future { let handlers = self.handler.read().unwrap(); match handlers.len() { 0 => MaybeHandlerFuture { fut: None }, _ => { let handler = handlers.next().clone(); MaybeHandlerFuture { fut: Some(Box::pin(handler.call(req, state))), } } } } } /// A MaybeHandlerFuture is a future that may or may not have a future. /// If there is no future, it returns a 404 response. struct MaybeHandlerFuture { fut: Option>>, } impl Future for MaybeHandlerFuture where F: Future + Send + 'static, { type Output = axum::response::Response; fn poll( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { match self.fut.as_mut() { // If we have a future, poll it. Some(fut) => fut.as_mut().poll(cx), // Otherwise we return a 404 response. None => { let resp = api::Error { code: api::ErrCode::NotFound, message: "endpoint not found".to_string(), internal_message: Some("no handler registered for endpoint".to_string()), stack: None, details: None, } .to_response(None); std::task::Poll::Ready(resp) } } } } ================================================ FILE: runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__basic.snap ================================================ --- source: runtimes/core/src/api/paths.rs expression: paths --- main: a: - - one - - /foo - /foo/ - - two - - /foo/bar - /foo/bar/ fallback: {} ================================================ FILE: runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__fallback.snap ================================================ --- source: runtimes/core/src/api/paths.rs expression: paths --- main: {} fallback: a: - - one - - / - /*_ ================================================ FILE: runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__paths_to_register.snap ================================================ --- source: runtimes/core/src/api/paths.rs expression: paths --- main: a: - endpoint: key: a path: /a routes: - /a - /a/ fallback: {} ================================================ FILE: runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__tsr_conflict.snap ================================================ --- source: runtimes/core/src/api/paths.rs expression: paths --- main: a: - - one - - /foo/ b: - - two - - /foo fallback: {} ================================================ FILE: runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__wildcard.snap ================================================ --- source: runtimes/core/src/api/paths.rs expression: paths --- main: a: - - one - - / - /*_ fallback: {} ================================================ FILE: runtimes/core/src/api/static_assets.rs ================================================ use std::{convert::Infallible, future::Future, path::PathBuf, pin::Pin, sync::Arc}; use http::{HeaderName, HeaderValue, StatusCode}; use http_body_util::Empty; use std::fmt::Debug; use std::io; use tower_http::services::{fs::ServeDir, ServeFile}; use tower_service::Service; use crate::{encore::parser::meta::v1 as meta, model::RequestData}; use super::{BoxedHandler, Error, HandlerRequest, ResponseData}; #[derive(Clone, Debug)] pub struct StaticAssetsHandler { service: Arc, not_found_handler: bool, not_found_status: StatusCode, headers: Vec<(HeaderName, HeaderValue)>, } impl StaticAssetsHandler { pub fn new(cfg: &meta::rpc::StaticAssets) -> Self { let service = ServeDir::new(PathBuf::from(&cfg.dir_rel_path)); let not_found_status = cfg .not_found_status .and_then(|c| StatusCode::from_u16(c as u16).ok()) .unwrap_or(StatusCode::NOT_FOUND); let not_found = cfg .not_found_rel_path .as_ref() .map(|p| ServeFile::new(PathBuf::from(p))); let not_found_handler = not_found.is_some(); let service: Arc = match not_found { Some(not_found) => Arc::new(service.not_found_service(not_found)), None => Arc::new(service), }; let headers: Vec<(HeaderName, HeaderValue)> = cfg .headers .iter() .flat_map(|(key, header_values)| { HeaderName::from_bytes(key.as_bytes()) .inspect_err(|e| { log::error!("skipping header: '{}' - {}", key, e); }) .ok() .map(|header_name| { header_values.values.iter().filter_map(move |value| { HeaderValue::from_bytes(value.as_bytes()) .inspect_err(|e| { log::error!("skipping header '{}': '{}' - {}", key, value, e); }) .ok() .map(|header_value| (header_name.clone(), header_value)) }) }) .into_iter() .flatten() }) .collect(); StaticAssetsHandler { service, not_found_handler, not_found_status, headers, } } } impl BoxedHandler for StaticAssetsHandler { fn call( self: Arc, req: HandlerRequest, ) -> Pin + Send + 'static>> { Box::pin(async move { let RequestData::RPC(data) = &req.data else { return ResponseData::Typed(Err(Error::internal(anyhow::anyhow!( "invalid request data type" )))); }; // Find the file path from the request. let file_path = match &data.path_params { Some(params) => params .values() .next() .and_then(|v| v.as_str()) .map(|s| format!("/{s}")) .unwrap_or("/".to_string()), None => "/".to_string(), }; let httpreq = { let mut b = axum::http::request::Request::builder(); { // Copy headers into request. let headers = b.headers_mut().unwrap(); for (k, v) in &data.req_headers { headers.append(k.clone(), v.clone()); } } match b .method(data.method) .uri(file_path) .body(Empty::::new()) { Ok(req) => req, Err(e) => { return ResponseData::Typed(Err(Error::invalid_argument( "invalid file path", e, ))); } } }; match self.service.serve(httpreq).await { Ok(mut resp) => { let resp_headers = resp.headers_mut(); for (name, value) in &self.headers { resp_headers.append(name.clone(), value.clone()); } match resp.status() { // 1xx, 2xx, 3xx are all considered successful. code if code.is_informational() || code.is_success() || code.is_redirection() => { ResponseData::Raw(resp.map(axum::body::Body::new)) } axum::http::StatusCode::NOT_FOUND => { // If we have a not found handler, use that directly. if self.not_found_handler { *resp.status_mut() = self.not_found_status; ResponseData::Raw(resp.map(axum::body::Body::new)) } else { // Otherwise return our standard not found error. ResponseData::Typed(Err(Error::not_found("file not found"))) } } axum::http::StatusCode::METHOD_NOT_ALLOWED => { ResponseData::Typed(Err(Error { code: super::ErrCode::InvalidArgument, internal_message: None, message: "method not allowed".to_string(), stack: None, details: None, })) } axum::http::StatusCode::INTERNAL_SERVER_ERROR => { ResponseData::Typed(Err(Error { code: super::ErrCode::Internal, internal_message: None, message: "failed to serve static asset".to_string(), stack: None, details: None, })) } code => ResponseData::Typed(Err(Error::internal(anyhow::anyhow!( "failed to serve static asset: {}", code, )))), } } Err(e) => ResponseData::Typed(Err(Error::internal(e))), } }) } } trait FileServer: Sync + Send + Debug { fn serve( &self, req: axum::http::Request>, ) -> Pin> + Send + 'static>>; } type FileReq = axum::http::Request>; type FileRes = axum::http::Response; impl FileServer for ServeDir where F: Service + Debug + Clone + Sync + Send + 'static, F::Future: Send + 'static, { fn serve( &self, req: axum::http::Request>, ) -> Pin> + Send + 'static>> { let mut this = self.clone(); Box::pin(async move { this.try_call(req).await }) } } ================================================ FILE: runtimes/core/src/api/websocket.rs ================================================ use std::sync::Arc; use anyhow::{anyhow, Context}; use axum::extract::ws::{Message, WebSocket}; use futures::Future; use tokio::sync::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, watch, }; use crate::model::{self, Request, RequestData}; use super::{schema, APIResult, HandlerResponse, HandlerResponseInner, PValues}; pub enum StreamMessagePayload { InOut(Socket), Out(Sink), In(Stream), } pub fn upgrade_request( req: Arc, callback: C, ) -> APIResult where C: FnOnce(Arc, StreamMessagePayload, UnboundedSender) -> Fut + Send + 'static, Fut: Future + Send + 'static, { let RequestData::Stream(ref data) = req.data else { return Err(super::Error::internal(anyhow!( "wrong request data type for stream" ))); }; let req_schema = data .endpoint .request .first() .context("no request schema") .map_err(super::Error::internal)? .clone(); let resp_schema = data.endpoint.response.clone(); let upgrade = { if let Some(upgrade) = data .websocket_upgrade .lock() .expect("mutex poisoned") .take() { upgrade } else { return Err(super::Error::internal(anyhow!( "websocket already upgraded" ))); } }; let (tx, mut rx) = mpsc::unbounded_channel::(); let direction = data.direction; Ok(upgrade .protocols(["encore-ws"]) .on_failed_upgrade(|err| log::debug!("websocket upgrade failed: {err}")) .on_upgrade(move |ws| async move { let socket = Socket::new(ws, schema::Stream::new(req_schema, resp_schema).into()); let payload = match direction { model::StreamDirection::InOut => StreamMessagePayload::InOut(socket), model::StreamDirection::In => { let (sink, stream) = socket.split(); tokio::spawn(async move { match rx.recv().await { Some(resp) => match resp { Ok(HandlerResponseInner { payload: Some(resp), .. }) => { if sink.send(resp).is_err() { log::debug!("sink channel closed"); } } Ok(HandlerResponseInner { payload: None, .. }) => { log::warn!("responded with empty response") } Err(err) => log::warn!("responded with error: {err:?}"), }, None => log::debug!("response channel closed"), }; }); StreamMessagePayload::In(stream) } model::StreamDirection::Out => { let (sink, _stream) = socket.split(); StreamMessagePayload::Out(sink) } }; (callback)(req, payload, tx).await })) } pub struct Socket { outgoing_message_tx: UnboundedSender, incoming_message_rx: tokio::sync::Mutex>, shutdown: watch::Sender, } impl Socket { fn new(mut websocket: WebSocket, schema: Arc) -> Self { let (shutdown, mut shutdown_watch) = watch::channel(false); let (outgoing_message_tx, mut outgoing_messages_rx) = mpsc::unbounded_channel(); let (incoming_messages_tx, incoming_message_rx) = mpsc::unbounded_channel(); tokio::spawn({ async move { loop { tokio::select! { msg = websocket.recv() => match msg { None => { log::trace!("websocket closed"); break }, Some(Ok(msg)) => { if let Err(e) = Socket::handle_incoming_message( &schema, &incoming_messages_tx, msg, ) .await { log::warn!("failed handling incoming message: {e}"); break; } }, Some(Err(e)) => { log::debug!("websocket receive failed: {e}"); break; } }, msg = outgoing_messages_rx.recv() => { match msg { None => { _ = websocket.close().await; log::trace!("websocket closed"); break; } Some(msg) => Socket::handle_outgoing_message(&schema, &mut websocket, msg).await } }, _ = shutdown_watch.changed() => { // gracefully shutdown, wait for all messages to be read on out channel // before closing the websocket outgoing_messages_rx.close(); } } } log::trace!("socket closed"); } }); Socket { outgoing_message_tx, incoming_message_rx: tokio::sync::Mutex::new(incoming_message_rx), shutdown, } } pub fn send(&self, msg: PValues) -> anyhow::Result<()> { self.outgoing_message_tx.send(msg)?; Ok(()) } pub async fn recv(&self) -> Option { self.incoming_message_rx.lock().await.recv().await } pub fn close(&self) { _ = self.shutdown.send(true); } pub fn split(self) -> (Sink, Stream) { let Self { outgoing_message_tx: tx, incoming_message_rx: rx, shutdown, } = self; let sink = Sink { tx, shutdown }; let stream = Stream { rx }; (sink, stream) } async fn handle_outgoing_message( schema: &schema::Stream, websocket: &mut WebSocket, msg: PValues, ) { let msg = schema .to_outgoing_message(msg) .and_then(|msg| String::from_utf8(msg).map_err(super::Error::internal)); match msg { Ok(msg) => { if let Err(e) = websocket.send(Message::Text(msg)).await { log::debug!("failed to send message to socket: {e}") } } Err(e) => log::warn!("failed to send message to socket: {e}"), } } async fn handle_incoming_message( schema: &schema::Stream, incoming: &UnboundedSender, msg: M, ) -> anyhow::Result<()> where M: MessagePayload, { if let Some(data) = msg.payload() { match schema.parse_incoming_message(data).await { Ok(msg) => { if let Err(e) = incoming.send(msg) { return Err(anyhow!("tried to send on closed channel: {e}")); } } Err(e) => log::warn!("failed to parse incoming message: {e}"), }; } Ok(()) } } trait MessagePayload { fn payload(&self) -> Option<&[u8]>; } impl MessagePayload for axum::extract::ws::Message { fn payload(&self) -> Option<&[u8]> { match self { Message::Text(text) => Some(text.as_bytes()), Message::Binary(data) => Some(data), // these message types are handled by axum Message::Ping(_) | Message::Pong(_) | Message::Close(_) => None, } } } pub struct Sink { tx: UnboundedSender, shutdown: watch::Sender, } impl Sink { pub fn send(&self, msg: PValues) -> anyhow::Result<()> { self.tx.send(msg)?; Ok(()) } pub fn close(&self) { _ = self.shutdown.send(true); } } pub struct Stream { rx: tokio::sync::Mutex>, } impl Stream { pub async fn recv(&self) -> Option { self.rx.lock().await.recv().await } } ================================================ FILE: runtimes/core/src/api/websocket_client.rs ================================================ use std::sync::Arc; use futures::sink::SinkExt; use futures::stream::SplitSink; use futures::stream::SplitStream; use futures::stream::StreamExt; use tokio::net::TcpStream; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::watch; use tokio::sync::Mutex; use tokio_tungstenite::MaybeTlsStream; use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; use super::schema; use super::APIResult; use super::PValues; pub struct WebSocketClient { send_channel: UnboundedSender, receive_channel: Mutex>, shutdown: watch::Sender, schema: Arc, } impl WebSocketClient { pub async fn connect( request: http::Request<()>, schema: schema::Stream, ) -> APIResult { let (connection, _resp) = tokio_tungstenite::connect_async(request) .await .map_err(|e| super::Error { code: super::ErrCode::Unknown, message: "failed connecting to websocket endpoint".to_string(), internal_message: Some(e.to_string()), stack: None, details: None, })?; let (ws_write, ws_read) = connection.split(); let (send_channel_tx, send_channel_rx) = tokio::sync::mpsc::unbounded_channel(); let (receive_channel_tx, receive_channel_rx) = tokio::sync::mpsc::unbounded_channel(); let (shutdown, shutdown_watch) = watch::channel(false); tokio::spawn(send_to_ws(send_channel_rx, ws_write, shutdown_watch)); tokio::spawn(ws_to_receive(ws_read, receive_channel_tx)); Ok(WebSocketClient { send_channel: send_channel_tx, receive_channel: Mutex::new(receive_channel_rx), shutdown, schema: schema.into(), }) } pub fn send(&self, msg: PValues) -> APIResult<()> { let msg = self.schema.to_outgoing_message(msg)?; let msg = String::from_utf8(msg).map_err(super::Error::internal)?; self.send_channel .send(Message::Text(msg)) .map_err(super::Error::internal)?; Ok(()) } pub async fn recv(&self) -> Option> { loop { let msg = self.receive_channel.lock().await.recv().await; let bytes: bytes::Bytes = match msg { Some(Message::Text(msg)) => msg.into(), Some(Message::Binary(vec)) => vec.into(), Some(_msg) => continue, None => return None, }; return Some(self.schema.parse_incoming_message(&bytes).await); } } pub fn close(&self) { if let Err(err) = self.shutdown.send(true) { log::trace!("error sending shutdown signal: {err}"); } } } async fn send_to_ws( mut rx: UnboundedReceiver, mut ws: SplitSink>, Message>, mut shutdown: watch::Receiver, ) { loop { tokio::select! { _ = shutdown.changed() => { rx.close() }, msg = rx.recv() => match msg { Some(msg) => { if let Err(err) = ws.send(msg).await { log::debug!("failed sending over websocket: {err}"); } }, None => { log::trace!("receive channel closed, shutting down"); if let Err(err) = ws.close().await { log::trace!("closing websocket failed: {err}"); } break; }, }, } } } async fn ws_to_receive( mut ws: SplitStream>>, tx: UnboundedSender, ) { loop { let msg = ws.next().await; match msg { Some(Ok(msg)) => { if let Err(err) = tx.send(msg) { log::warn!("failed sending to receive channel: {err}"); break; } } Some(Err(err)) => { log::debug!("received an error from websocket: {err}"); break; } None => { log::trace!("websocket closed, shutting down"); break; } } } } ================================================ FILE: runtimes/core/src/base32.rs ================================================ use std::cmp::min; #[derive(Copy, Clone)] pub enum Alphabet { #[allow(dead_code)] RFC4648 { padding: bool, }, #[allow(dead_code)] Crockford, Encore, } const RFC4648_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const CROCKFORD_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; const ENCORE_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuv"; pub fn encode(alphabet: Alphabet, data: &[u8]) -> String { let (alphabet, padding) = match alphabet { Alphabet::RFC4648 { padding } => (RFC4648_ALPHABET, padding), Alphabet::Crockford => (CROCKFORD_ALPHABET, false), Alphabet::Encore => (ENCORE_ALPHABET, false), }; let mut ret = Vec::with_capacity(data.len().div_ceil(4) * 5); for chunk in data.chunks(5) { let buf = { let mut buf = [0u8; 5]; for (i, &b) in chunk.iter().enumerate() { buf[i] = b; } buf }; ret.push(alphabet[((buf[0] & 0xF8) >> 3) as usize]); ret.push(alphabet[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize]); ret.push(alphabet[((buf[1] & 0x3E) >> 1) as usize]); ret.push(alphabet[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize]); ret.push(alphabet[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize]); ret.push(alphabet[((buf[3] & 0x7C) >> 2) as usize]); ret.push(alphabet[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize]); ret.push(alphabet[(buf[4] & 0x1F) as usize]); } if !data.len().is_multiple_of(5) { let len = ret.len(); let num_extra = 8 - (data.len() % 5 * 8).div_ceil(5); if padding { for i in 1..num_extra + 1 { ret[len - i] = b'='; } } else { ret.truncate(len - num_extra); } } String::from_utf8(ret).unwrap() } const RFC4648_INV_ALPHABET: [i8; 43] = [ -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, ]; const CROCKFORD_INV_ALPHABET: [i8; 43] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1, 18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31, ]; const ENCORE_INV_ALPHABET: [i8; 43] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, ]; pub fn decode(alphabet: Alphabet, data: &str) -> Option> { if !data.is_ascii() { return None; } let data = data.as_bytes(); let alphabet = match alphabet { Alphabet::RFC4648 { .. } => RFC4648_INV_ALPHABET, Alphabet::Crockford => CROCKFORD_INV_ALPHABET, Alphabet::Encore => ENCORE_INV_ALPHABET, }; let mut unpadded_data_length = data.len(); for i in 1..min(6, data.len()) + 1 { if data[data.len() - i] != b'=' { break; } unpadded_data_length -= 1; } let output_length = unpadded_data_length * 5 / 8; let mut ret = Vec::with_capacity(output_length.div_ceil(5) * 5); for chunk in data.chunks(8) { let buf = { let mut buf = [0u8; 8]; for (i, &c) in chunk.iter().enumerate() { match alphabet.get(c.to_ascii_uppercase().wrapping_sub(b'0') as usize) { Some(&-1) | None => return None, Some(&value) => buf[i] = value as u8, }; } buf }; ret.push((buf[0] << 3) | (buf[1] >> 2)); ret.push((buf[1] << 6) | (buf[2] << 1) | (buf[3] >> 4)); ret.push((buf[3] << 4) | (buf[4] >> 1)); ret.push((buf[4] << 7) | (buf[5] << 2) | (buf[6] >> 3)); ret.push((buf[6] << 5) | buf[7]); } ret.truncate(output_length); Some(ret) } #[cfg(test)] #[allow(dead_code, unused_attributes)] mod test { use super::Alphabet::{Crockford, Encore, RFC4648}; use super::{decode, encode}; #[derive(Clone)] struct B32 { c: u8, } impl quickcheck::Arbitrary for B32 { fn arbitrary(g: &mut quickcheck::Gen) -> B32 { let alphabet = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; B32 { c: *g.choose(alphabet).unwrap(), } } } impl std::fmt::Debug for B32 { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { (self.c as char).fmt(f) } } #[test] fn masks_crockford() { assert_eq!( encode(Crockford, &[0xF8, 0x3E, 0x0F, 0x83, 0xE0]), "Z0Z0Z0Z0" ); assert_eq!( encode(Crockford, &[0x07, 0xC1, 0xF0, 0x7C, 0x1F]), "0Z0Z0Z0Z" ); assert_eq!( decode(Crockford, "Z0Z0Z0Z0").unwrap(), [0xF8, 0x3E, 0x0F, 0x83, 0xE0] ); assert_eq!( decode(Crockford, "0Z0Z0Z0Z").unwrap(), [0x07, 0xC1, 0xF0, 0x7C, 0x1F] ); } #[test] fn masks_rfc4648() { assert_eq!( encode(RFC4648 { padding: true }, &[0xF8, 0x3E, 0x7F, 0x83, 0xE7]), "7A7H7A7H" ); assert_eq!( encode(RFC4648 { padding: true }, &[0x77, 0xC1, 0xF7, 0x7C, 0x1F]), "O7A7O7A7" ); assert_eq!( decode(RFC4648 { padding: true }, "7A7H7A7H").unwrap(), [0xF8, 0x3E, 0x7F, 0x83, 0xE7] ); assert_eq!( decode(RFC4648 { padding: true }, "O7A7O7A7").unwrap(), [0x77, 0xC1, 0xF7, 0x7C, 0x1F] ); assert_eq!( encode(RFC4648 { padding: true }, &[0xF8, 0x3E, 0x7F, 0x83]), "7A7H7AY=" ); } #[test] fn masks_unpadded_rfc4648() { assert_eq!( encode(RFC4648 { padding: false }, &[0xF8, 0x3E, 0x7F, 0x83, 0xE7]), "7A7H7A7H" ); assert_eq!( encode(RFC4648 { padding: false }, &[0x77, 0xC1, 0xF7, 0x7C, 0x1F]), "O7A7O7A7" ); assert_eq!( decode(RFC4648 { padding: false }, "7A7H7A7H").unwrap(), [0xF8, 0x3E, 0x7F, 0x83, 0xE7] ); assert_eq!( decode(RFC4648 { padding: false }, "O7A7O7A7").unwrap(), [0x77, 0xC1, 0xF7, 0x7C, 0x1F] ); assert_eq!( encode(RFC4648 { padding: false }, &[0xF8, 0x3E, 0x7F, 0x83]), "7A7H7AY" ); } #[test] fn padding() { let num_padding = [0, 6, 4, 3, 1]; for i in 1..6 { let encoded = encode( RFC4648 { padding: true }, (0..(i as u8)).collect::>().as_ref(), ); assert_eq!(encoded.len(), 8); for j in 0..(num_padding[i % 5]) { assert_eq!(encoded.as_bytes()[encoded.len() - j - 1], b'='); } for j in 0..(8 - num_padding[i % 5]) { assert!(encoded.as_bytes()[j] != b'='); } } } #[test] fn invertible_encore() { fn test(data: Vec) -> bool { decode(Encore, encode(Encore, data.as_ref()).as_ref()).unwrap() == data } quickcheck::quickcheck(test as fn(Vec) -> bool) } #[test] fn invertible_crockford() { fn test(data: Vec) -> bool { decode(Crockford, encode(Crockford, data.as_ref()).as_ref()).unwrap() == data } quickcheck::quickcheck(test as fn(Vec) -> bool) } #[test] fn invertible_rfc4648() { fn test(data: Vec) -> bool { decode( RFC4648 { padding: true }, encode(RFC4648 { padding: true }, data.as_ref()).as_ref(), ) .unwrap() == data } quickcheck::quickcheck(test as fn(Vec) -> bool) } #[test] fn invertible_unpadded_rfc4648() { fn test(data: Vec) -> bool { decode( RFC4648 { padding: false }, encode(RFC4648 { padding: false }, data.as_ref()).as_ref(), ) .unwrap() == data } quickcheck::quickcheck(test as fn(Vec) -> bool) } #[test] fn lower_case() { fn test(data: Vec) -> bool { let data: String = data.iter().map(|e| e.c as char).collect(); decode(Crockford, data.as_ref()) == decode(Crockford, data.to_ascii_lowercase().as_ref()) } quickcheck::quickcheck(test as fn(Vec) -> bool) } #[test] #[allow(non_snake_case)] fn iIlL1_oO0() { assert_eq!(decode(Crockford, "IiLlOo"), decode(Crockford, "111100")); } #[test] fn invalid_chars_crockford() { assert_eq!(decode(Crockford, ","), None) } #[test] fn invalid_chars_rfc4648() { assert_eq!(decode(RFC4648 { padding: true }, ","), None) } #[test] fn invalid_chars_unpadded_rfc4648() { assert_eq!(decode(RFC4648 { padding: false }, ","), None) } } ================================================ FILE: runtimes/core/src/cache/client.rs ================================================ use std::time::{SystemTime, UNIX_EPOCH}; use bb8::{ErrorSink, Pool as Bb8Pool, RunError}; use bb8_redis::redis::{self as redis, RedisResult}; use bb8_redis::RedisConnectionManager; use redis::{FromRedisValue, SetExpiry, ToSingleRedisArg}; use crate::cache::error::{Error, OpResult, Result}; use crate::cache::tracer::CacheTracer; use crate::model::Request; use crate::trace::Tracer; /// TTL operation for cache write commands. #[derive(Debug, Clone, Copy)] pub enum TtlOp { /// Preserve the existing TTL (KEEPTTL for SET; no-op for others). Keep, /// Set TTL in milliseconds (PX for SET; atomic PEXPIREAT for others). SetMs(u64), /// Remove TTL / never expire (no TTL flags for SET; atomic PERSIST for others). Persist, } /// Direction for list operations. #[derive(Debug, Clone, Copy)] pub enum ListDirection { Left, Right, } impl From for redis::Direction { fn from(value: ListDirection) -> Self { match value { ListDirection::Left => Self::Left, ListDirection::Right => Self::Right, } } } enum LRemOp { All, First(u64), Last(u64), } impl LRemOp { fn name(&self) -> &'static str { match self { LRemOp::All => "remove all", LRemOp::First(_) => "remove first", LRemOp::Last(_) => "remove last", } } } /// Converts a relative TTL in milliseconds to an absolute PEXPIREAT timestamp. fn expire_at_ms(relative_ms: u64) -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis() as u64 + relative_ms } /// Builds an expiration command for a key based on the TTL operation. fn exp_cmd(key: &str, ttl: Option) -> Option { match ttl? { TtlOp::Keep => None, TtlOp::SetMs(ms) => Some( redis::cmd("PEXPIREAT") .arg(key) .arg(expire_at_ms(ms)) .take(), ), TtlOp::Persist => Some(redis::cmd("PERSIST").arg(key).take()), } } #[derive(Debug, Clone)] struct RedisErrorSink { cluster_name: String, } impl ErrorSink for RedisErrorSink { fn sink(&self, err: redis::RedisError) { log::error!( "cache cluster {}: connection pool error: {:?}", self.cluster_name, err ); } fn boxed_clone(&self) -> Box> { Box::new(self.clone()) } } struct RedisBackend { pool: Bb8Pool, } impl RedisBackend { fn new( client: redis::Client, cluster_name: String, min_conns: u32, max_conns: u32, ) -> anyhow::Result { let conn_info = client.get_connection_info().clone(); let mgr = RedisConnectionManager::new(conn_info)?; let mut pool = Bb8Pool::builder() .error_sink(Box::new(RedisErrorSink { cluster_name })) .max_size(if max_conns > 0 { max_conns } else { (std::thread::available_parallelism() .map(|n| n.get()) .unwrap_or(4) * 10) as u32 }) .connection_timeout(std::time::Duration::from_secs(10)); if min_conns > 0 { pool = pool.min_idle(Some(min_conns)); } let pool = pool.build_unchecked(mgr); Ok(Self { pool }) } async fn conn(&self) -> Result> { self.pool.get().await.map_err(|e| match e { RunError::User(err) => Error::Redis(err), RunError::TimedOut => Error::PoolTimeout, }) } /// Execute a single Redis command, mapping nil to Error::Miss. async fn query(&self, f: F) -> Result where F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline, T: FromRedisValue, { let mut pipe = redis::pipe(); let mut conn = self.conn().await?; let res: RedisResult<(Option,)> = f(&mut pipe).query_async(&mut *conn).await; match res?.0 { None => Err(Error::Miss), Some(v) => Ok(v), } } /// Execute a Redis command atomically with TTL management for one key. async fn query_with_ttl(&self, key: &str, ttl: Option, f: F) -> Result where F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline, T: FromRedisValue, { let exp = exp_cmd(key, ttl); if exp.is_none() { return self.query(f).await; } let mut pipe = redis::pipe(); let mut conn = self.conn().await?; pipe.atomic(); f(&mut pipe); if let Some(exp) = exp { pipe.add_command(exp).ignore(); } let res: RedisResult<(Option,)> = pipe.query_async(&mut *conn).await; match res?.0 { None => Err(Error::Miss), Some(v) => Ok(v), } } /// Execute a Redis command atomically with TTL management for two keys. async fn query_with_ttl2(&self, keys: (&str, &str), ttl: Option, f: F) -> Result where F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline, T: FromRedisValue, { let exp_a = exp_cmd(keys.0, ttl); let exp_b = exp_cmd(keys.1, ttl); if exp_a.is_none() && exp_b.is_none() { return self.query(f).await; } let mut pipe = redis::pipe(); let mut conn = self.conn().await?; pipe.atomic(); f(&mut pipe); if let Some(exp) = exp_a { pipe.add_command(exp).ignore(); } if let Some(exp) = exp_b { pipe.add_command(exp).ignore(); } let res: RedisResult<(Option,)> = pipe.query_async(&mut *conn).await; match res?.0 { None => Err(Error::Miss), Some(v) => Ok(v), } } async fn _set( &self, key: &str, ttl: Option, set_cond: Option, get: bool, value: V, ) -> Result where V: ToSingleRedisArg + Sync + Send, T: FromRedisValue, { let mut opts = redis::SetOptions::default().get(get); if let Some(set_cond) = set_cond { opts = opts.conditional_set(set_cond); } if let Some(set_exp) = ttl.and_then(|t| match t { TtlOp::Keep => Some(SetExpiry::KEEPTTL), TtlOp::SetMs(ms) => Some(SetExpiry::PX(ms)), TtlOp::Persist => None, }) { opts = opts.with_expiration(set_exp); } match self.query(|pipe| pipe.set_options(key, value, opts)).await { Err(Error::Miss) if matches!(set_cond, Some(redis::ExistenceCheck::NX)) => { Err(Error::KeyExist) } other => other, } } async fn get(&self, key: &str) -> Result> { self.query(|pipe| pipe.get(key)).await } async fn set(&self, key: &str, value: &[u8], ttl: Option) -> Result<()> { self._set(key, ttl, None, false, value).await } async fn set_if_not_exists(&self, key: &str, value: &[u8], ttl: Option) -> Result<()> { self._set(key, ttl, Some(redis::ExistenceCheck::NX), false, value) .await } async fn replace(&self, key: &str, value: &[u8], ttl: Option) -> Result<()> { self._set(key, ttl, Some(redis::ExistenceCheck::XX), false, value) .await } async fn get_and_set(&self, key: &str, value: &[u8], ttl: Option) -> Result> { self._set(key, ttl, None, true, value).await } async fn get_and_delete(&self, key: &str) -> Result> { self.query(|pipe| pipe.get_del(key)).await } async fn delete(&self, keys: &[&str]) -> Result { self.query(|pipe| pipe.del(keys)).await } async fn mget(&self, keys: &[&str]) -> Result>>> { self.query(|pipe| pipe.mget(keys)).await } async fn append(&self, key: &str, value: &[u8], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.append(key, value)) .await } async fn get_range(&self, key: &str, start: i64, end: i64) -> Result> { self.query(|pipe| pipe.getrange(key, start as isize, end as isize)) .await } async fn set_range( &self, key: &str, offset: i64, value: &[u8], ttl: Option, ) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.setrange(key, offset as isize, value)) .await } async fn strlen(&self, key: &str) -> Result { self.query(|pipe| pipe.strlen(key)).await } async fn incr_by(&self, key: &str, delta: i64, ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.incr(key, delta)) .await } async fn incr_by_float(&self, key: &str, delta: f64, ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.incr(key, delta)) .await } async fn lpush(&self, key: &str, values: &[&[u8]], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.lpush(key, values)) .await } async fn rpush(&self, key: &str, values: &[&[u8]], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.rpush(key, values)) .await } async fn lpop(&self, key: &str, ttl: Option) -> Result> { self.query_with_ttl(key, ttl, |pipe| pipe.lpop(key, None)) .await } async fn rpop(&self, key: &str, ttl: Option) -> Result> { self.query_with_ttl(key, ttl, |pipe| pipe.rpop(key, None)) .await } async fn lindex(&self, key: &str, index: i64) -> Result> { self.query(|pipe| pipe.lindex(key, index as isize)).await } async fn lset(&self, key: &str, index: i64, value: &[u8], _ttl: Option) -> Result<()> { self.query(|pipe| pipe.lset(key, index as isize, value)) .await } async fn lrange(&self, key: &str, start: i64, stop: i64) -> Result>> { self.query(|pipe| pipe.lrange(key, start as isize, stop as isize)) .await } async fn ltrim(&self, key: &str, start: i64, stop: i64, _ttl: Option) -> Result<()> { self.query(|pipe| pipe.ltrim(key, start as isize, stop as isize)) .await } async fn linsert_before( &self, key: &str, pivot: &[u8], value: &[u8], ttl: Option, ) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.linsert_before(key, pivot, value)) .await } async fn linsert_after( &self, key: &str, pivot: &[u8], value: &[u8], ttl: Option, ) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.linsert_after(key, pivot, value)) .await } async fn lrem(&self, key: &str, count: i64, value: &[u8], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.lrem(key, count as isize, value)) .await } async fn lmove( &self, src: &str, dst: &str, src_dir: ListDirection, dst_dir: ListDirection, ttl: Option, ) -> Result> { self.query_with_ttl2((src, dst), ttl, |pipe| { pipe.lmove(src, dst, src_dir.into(), dst_dir.into()) }) .await } async fn llen(&self, key: &str) -> Result { self.query(|pipe| pipe.llen(key)).await } async fn sadd(&self, key: &str, members: &[&[u8]], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.sadd(key, members)) .await } async fn srem(&self, key: &str, members: &[&[u8]], ttl: Option) -> Result { self.query_with_ttl(key, ttl, |pipe| pipe.srem(key, members)) .await } async fn sismember(&self, key: &str, member: &[u8]) -> Result { self.query(|pipe| pipe.sismember(key, member)).await } async fn spop(&self, key: &str, ttl: Option) -> Result> { self.query_with_ttl(key, ttl, |pipe| pipe.spop(key)).await } async fn spop_n(&self, key: &str, count: usize, ttl: Option) -> Result>> { self.query_with_ttl(key, ttl, |pipe| pipe.spop(key).arg(count)) .await } async fn srandmember(&self, key: &str) -> Result> { self.query(|pipe| pipe.srandmember(key)).await } async fn srandmember_n(&self, key: &str, count: i64) -> Result>> { self.query(|pipe| pipe.srandmember_multiple(key, count as isize)) .await } async fn smembers(&self, key: &str) -> Result>> { self.query(|pipe| pipe.smembers(key)).await } async fn scard(&self, key: &str) -> Result { self.query(|pipe| pipe.scard(key)).await } async fn sdiff(&self, keys: &[&str]) -> Result>> { self.query(|pipe| pipe.sdiff(keys)).await } async fn sdiffstore(&self, dest: &str, keys: &[&str], ttl: Option) -> Result { self.query_with_ttl(dest, ttl, |pipe| pipe.sdiffstore(dest, keys)) .await } async fn sinter(&self, keys: &[&str]) -> Result>> { self.query(|pipe| pipe.sinter(keys)).await } async fn sinterstore(&self, dest: &str, keys: &[&str], ttl: Option) -> Result { self.query_with_ttl(dest, ttl, |pipe| pipe.sinterstore(dest, keys)) .await } async fn sunion(&self, keys: &[&str]) -> Result>> { self.query(|pipe| pipe.sunion(keys)).await } async fn sunionstore(&self, dest: &str, keys: &[&str], ttl: Option) -> Result { self.query_with_ttl(dest, ttl, |pipe| pipe.sunionstore(dest, keys)) .await } async fn smove(&self, src: &str, dst: &str, member: &[u8], ttl: Option) -> Result { self.query_with_ttl2((src, dst), ttl, |pipe| pipe.smove(src, dst, member)) .await } } /// A cache client for a Redis-compatible cluster. /// Handles key prefixing, tracing, and dispatching to the Redis backend. pub struct Client { backend: RedisBackend, tracer: CacheTracer, key_prefix: Option, } impl Client { pub(crate) fn new( client: redis::Client, key_prefix: Option, tracer: Tracer, min_conns: u32, max_conns: u32, ) -> anyhow::Result { let cluster_name = key_prefix.clone().unwrap_or_else(|| "default".to_string()); let backend = RedisBackend::new(client, cluster_name, min_conns, max_conns)?; Ok(Self { backend, tracer: CacheTracer::new(tracer), key_prefix, }) } fn prefixed_key(&self, key: &str) -> String { match &self.key_prefix { Some(prefix) => format!("{}{}", prefix, key), None => key.to_string(), } } /// Get a value by key. pub async fn get(&self, key: &str, source: Option<&Request>) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "get", false, &[&key], async || { self.backend.get(&key).await }) .await } /// Set a value by key with optional TTL operation. pub async fn set( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult<()> { let key = self.prefixed_key(key); self.tracer .trace(source, "set", true, &[&key], async || { self.backend.set(&key, value, ttl).await }) .await } /// Set a value only if the key doesn't exist (SET NX). pub async fn set_if_not_exists( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult<()> { let key = self.prefixed_key(key); self.tracer .trace(source, "set if not exists", true, &[&key], async || { self.backend.set_if_not_exists(&key, value, ttl).await }) .await } /// Replace a value only if the key exists (SET XX). pub async fn replace( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult<()> { let key = self.prefixed_key(key); self.tracer .trace(source, "replace", true, &[&key], async || { self.backend.replace(&key, value, ttl).await }) .await } /// Get old value and set new value atomically (SET GET). pub async fn get_and_set( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "get and set", true, &[&key], async || { self.backend.get_and_set(&key, value, ttl).await }) .await } /// Get value and delete key atomically (GETDEL). pub async fn get_and_delete(&self, key: &str, source: Option<&Request>) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "get and delete", true, &[&key], async || { self.backend.get_and_delete(&key).await }) .await } /// Delete one or more keys. pub async fn delete(&self, keys: &[&str], source: Option<&Request>) -> OpResult { let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); self.tracer .trace(source, "delete", true, &key_refs, async || { self.backend.delete(&key_refs).await }) .await } /// Get multiple values (MGET). pub async fn mget( &self, keys: &[&str], source: Option<&Request>, ) -> OpResult>>> { let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); self.tracer .trace(source, "multi get", false, &key_refs, async || { self.backend.mget(&key_refs).await }) .await } /// Append to a string value. pub async fn append( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "append", true, &[&key], async || { self.backend.append(&key, value, ttl).await }) .await } /// Get a substring of a string value. pub async fn get_range( &self, key: &str, start: i64, end: i64, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "get range", false, &[&key], async || { self.backend.get_range(&key, start, end).await }) .await } /// Set a substring at a specific offset. pub async fn set_range( &self, key: &str, offset: i64, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "set range", true, &[&key], async || { self.backend.set_range(&key, offset, value, ttl).await }) .await } /// Get string length. pub async fn strlen(&self, key: &str, source: Option<&Request>) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "len", false, &[&key], async || { self.backend.strlen(&key).await }) .await } /// Increment an integer value. pub async fn incr_by( &self, key: &str, delta: i64, ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "increment", true, &[&key], async || { self.backend.incr_by(&key, delta, ttl).await }) .await } /// Decrement an integer value. pub async fn decr_by( &self, key: &str, delta: i64, ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "decrement", true, &[&key], async || { self.backend.incr_by(&key, -delta, ttl).await }) .await } /// Increment a float value. pub async fn incr_by_float( &self, key: &str, delta: f64, ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "increment", true, &[&key], async || { self.backend.incr_by_float(&key, delta, ttl).await }) .await } /// Push values to the left (head) of a list. pub async fn lpush( &self, key: &str, values: &[&[u8]], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "push left", true, &[&key], async || { self.backend.lpush(&key, values, ttl).await }) .await } /// Push values to the right (tail) of a list. pub async fn rpush( &self, key: &str, values: &[&[u8]], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "push right", true, &[&key], async || { self.backend.rpush(&key, values, ttl).await }) .await } /// Pop value from the left (head) of a list. pub async fn lpop( &self, key: &str, ttl: Option, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "pop left", true, &[&key], async || { self.backend.lpop(&key, ttl).await }) .await } /// Pop value from the right (tail) of a list. pub async fn rpop( &self, key: &str, ttl: Option, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "pop right", true, &[&key], async || { self.backend.rpop(&key, ttl).await }) .await } /// Get element at index from a list. pub async fn lindex( &self, key: &str, index: i64, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "list get", false, &[&key], async || { self.backend.lindex(&key, index).await }) .await } /// Set element at index in a list. pub async fn lset( &self, key: &str, index: i64, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult<()> { let key = self.prefixed_key(key); self.tracer .trace(source, "list set", true, &[&key], async || { self.backend.lset(&key, index, value, ttl).await }) .await } /// Get a range of elements from a list. pub async fn lrange( &self, key: &str, start: i64, stop: i64, source: Option<&Request>, ) -> OpResult>> { let key = self.prefixed_key(key); self.tracer .trace(source, "get range", false, &[&key], async || { self.backend.lrange(&key, start, stop).await }) .await } /// Get all elements of a list. Equivalent to LRANGE 0 -1 but traced as "items". pub async fn lrange_all(&self, key: &str, source: Option<&Request>) -> OpResult>> { let key = self.prefixed_key(key); self.tracer .trace(source, "items", false, &[&key], async || { self.backend.lrange(&key, 0, -1).await }) .await } /// Trim list to specified range. pub async fn ltrim( &self, key: &str, start: i64, stop: i64, ttl: Option, source: Option<&Request>, ) -> OpResult<()> { let key = self.prefixed_key(key); self.tracer .trace(source, "list trim", true, &[&key], async || { self.backend.ltrim(&key, start, stop, ttl).await }) .await } /// Insert element before pivot in list. pub async fn linsert_before( &self, key: &str, pivot: &[u8], value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "insert before", true, &[&key], async || { let result = self.backend.linsert_before(&key, pivot, value, ttl).await; match result { Ok(n) if n < 0 => Err(Error::Miss), other => other, } }) .await } /// Insert element after pivot in list. pub async fn linsert_after( &self, key: &str, pivot: &[u8], value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "insert after", true, &[&key], async || { let result = self.backend.linsert_after(&key, pivot, value, ttl).await; match result { Ok(n) if n < 0 => Err(Error::Miss), other => other, } }) .await } /// Remove elements from list. pub async fn lrem_all( &self, key: &str, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { self._lrem(key, LRemOp::All, value, ttl, source).await } /// Remove elements from list. pub async fn lrem_first( &self, key: &str, count: u64, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { self._lrem(key, LRemOp::First(count), value, ttl, source) .await } /// Remove elements from list. pub async fn lrem_last( &self, key: &str, count: u64, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { self._lrem(key, LRemOp::Last(count), value, ttl, source) .await } async fn _lrem( &self, key: &str, op: LRemOp, value: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, op.name(), true, &[&key], async || { let count: i64 = { use LRemOp::*; match &op { First(0) | Last(0) => return Ok(0), All => 0, First(count) => *count as i64, Last(count) => -(*count as i64), } }; self.backend.lrem(&key, count, value, ttl).await }) .await } /// Move element between lists. pub async fn lmove( &self, src: &str, dst: &str, src_dir: ListDirection, dst_dir: ListDirection, ttl: Option, source: Option<&Request>, ) -> OpResult> { let src_key = self.prefixed_key(src); let dst_key = self.prefixed_key(dst); self.tracer .trace( source, "list move", true, &[&src_key, &dst_key], async || { self.backend .lmove(&src_key, &dst_key, src_dir, dst_dir, ttl) .await }, ) .await } /// Get list length. pub async fn llen(&self, key: &str, source: Option<&Request>) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "list len", false, &[&key], async || { self.backend.llen(&key).await }) .await } /// Add members to a set. pub async fn sadd( &self, key: &str, members: &[&[u8]], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "set add", true, &[&key], async || { self.backend.sadd(&key, members, ttl).await }) .await } /// Remove members from a set. pub async fn srem( &self, key: &str, members: &[&[u8]], ttl: Option, source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "set remove", true, &[&key], async || { self.backend.srem(&key, members, ttl).await }) .await } /// Check if member exists in set. pub async fn sismember( &self, key: &str, member: &[u8], source: Option<&Request>, ) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "set contains", false, &[&key], async || { self.backend.sismember(&key, member).await }) .await } /// Pop a single random member from a set (SPOP without count). pub async fn spop( &self, key: &str, ttl: Option, source: Option<&Request>, ) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "set pop one", true, &[&key], async || { self.backend.spop(&key, ttl).await }) .await } /// Pop random members from a set (SPOP with count). pub async fn spop_n( &self, key: &str, count: usize, ttl: Option, source: Option<&Request>, ) -> OpResult>> { let key = self.prefixed_key(key); self.tracer .trace(source, "set pop", true, &[&key], async || { self.backend.spop_n(&key, count, ttl).await }) .await } /// Get a single random member from a set without removing (SRANDMEMBER). pub async fn srandmember(&self, key: &str, source: Option<&Request>) -> OpResult> { let key = self.prefixed_key(key); self.tracer .trace(source, "set sample one", false, &[&key], async || { self.backend.srandmember(&key).await }) .await } /// Get random members from a set without removing (SRANDMEMBER). pub async fn srandmember_n( &self, key: &str, count: i64, source: Option<&Request>, ) -> OpResult>> { let key = self.prefixed_key(key); let op = if count < 0 { "set sample with replacement" } else { "set sample" }; self.tracer .trace(source, op, false, &[&key], async || { self.backend.srandmember_n(&key, count).await }) .await } /// Get all members of a set. pub async fn smembers(&self, key: &str, source: Option<&Request>) -> OpResult>> { let key = self.prefixed_key(key); self.tracer .trace(source, "set items", false, &[&key], async || { self.backend.smembers(&key).await }) .await } /// Get set cardinality. pub async fn scard(&self, key: &str, source: Option<&Request>) -> OpResult { let key = self.prefixed_key(key); self.tracer .trace(source, "set len", false, &[&key], async || { self.backend.scard(&key).await }) .await } /// Set difference. pub async fn sdiff(&self, keys: &[&str], source: Option<&Request>) -> OpResult>> { let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); self.tracer .trace(source, "set diff", false, &key_refs, async || { self.backend.sdiff(&key_refs).await }) .await } /// Store set difference. pub async fn sdiffstore( &self, dest: &str, keys: &[&str], ttl: Option, source: Option<&Request>, ) -> OpResult { let dest_key = self.prefixed_key(dest); let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let mut all_keys: Vec<&str> = vec![dest_key.as_str()]; let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); all_keys.extend(key_refs.iter().copied()); self.tracer .trace(source, "store set diff", true, &all_keys, async || { self.backend.sdiffstore(&dest_key, &key_refs, ttl).await }) .await } /// Set intersection. pub async fn sinter(&self, keys: &[&str], source: Option<&Request>) -> OpResult>> { let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); self.tracer .trace(source, "intersect", false, &key_refs, async || { self.backend.sinter(&key_refs).await }) .await } /// Store set intersection. pub async fn sinterstore( &self, dest: &str, keys: &[&str], ttl: Option, source: Option<&Request>, ) -> OpResult { let dest_key = self.prefixed_key(dest); let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let mut all_keys: Vec<&str> = vec![dest_key.as_str()]; let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); all_keys.extend(key_refs.iter().copied()); self.tracer .trace(source, "store set intersect", true, &all_keys, async || { self.backend.sinterstore(&dest_key, &key_refs, ttl).await }) .await } /// Set union. pub async fn sunion(&self, keys: &[&str], source: Option<&Request>) -> OpResult>> { let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); self.tracer .trace(source, "union", false, &key_refs, async || { self.backend.sunion(&key_refs).await }) .await } /// Store set union. pub async fn sunionstore( &self, dest: &str, keys: &[&str], ttl: Option, source: Option<&Request>, ) -> OpResult { let dest_key = self.prefixed_key(dest); let prefixed: Vec = keys.iter().map(|k| self.prefixed_key(k)).collect(); let mut all_keys: Vec<&str> = vec![dest_key.as_str()]; let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect(); all_keys.extend(key_refs.iter().copied()); self.tracer .trace(source, "store set union", true, &all_keys, async || { self.backend.sunionstore(&dest_key, &key_refs, ttl).await }) .await } /// Move member between sets. pub async fn smove( &self, src: &str, dst: &str, member: &[u8], ttl: Option, source: Option<&Request>, ) -> OpResult { let src_key = self.prefixed_key(src); let dst_key = self.prefixed_key(dst); self.tracer .trace(source, "move", true, &[&src_key, &dst_key], async || { self.backend.smove(&src_key, &dst_key, member, ttl).await }) .await } } #[cfg(test)] #[path = "client_tests.rs"] mod client_tests; ================================================ FILE: runtimes/core/src/cache/client_tests.rs ================================================ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::OnceLock; use crate::cache::client::{Client, ListDirection, TtlOp}; use crate::cache::error::Error; use crate::cache::miniredis::MiniredisServer; use crate::trace::Tracer; static TEST_MINIREDIS: OnceLock = OnceLock::new(); static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); fn new_test_pool() -> Client { let server = TEST_MINIREDIS.get_or_init(|| { // Spawn a dedicated thread with its own runtime for miniredis, // since we can't create a Runtime from within a #[tokio::test]. let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().expect("failed to create runtime"); let server = rt .block_on(MiniredisServer::start()) .expect("failed to start miniredis for tests"); tx.send(server).expect("failed to send server"); // Park forever to keep the runtime (and its server task) alive. loop { std::thread::park(); } }); rx.recv().expect("failed to receive miniredis server") }); let url = format!("redis://{}", server.addr()); let client = bb8_redis::redis::Client::open(url).expect("failed to create redis client"); // Use a unique key prefix per test to avoid interference between parallel tests. let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed); let prefix = format!("test{}:", id); Client::new(client, Some(prefix), Tracer::noop(), 0, 10).expect("failed to create cache client") } fn is_miss(err: &crate::cache::OpError) -> bool { matches!(err.source, Error::Miss) } fn is_key_exist(err: &crate::cache::OpError) -> bool { matches!(err.source, Error::KeyExist) } #[tokio::test] async fn test_set_get_delete() { let p = new_test_pool(); p.set("k", b"hello", None, None).await.unwrap(); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"hello".to_vec()); let deleted = p.delete(&["k"], None).await.unwrap(); assert_eq!(deleted, 1); let err = p.get("k", None).await.unwrap_err(); assert!(is_miss(&err)); } #[tokio::test] async fn test_set_overwrites() { let p = new_test_pool(); p.set("k", b"v1", None, None).await.unwrap(); p.set("k", b"v2", None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_get_missing_key() { let p = new_test_pool(); let err = p.get("missing", None).await.unwrap_err(); assert!(is_miss(&err)); } #[tokio::test] async fn test_set_empty_value() { let p = new_test_pool(); p.set("k", b"", None, None).await.unwrap(); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"".to_vec()); } #[tokio::test] async fn test_set_binary_value() { let p = new_test_pool(); let binary = vec![0u8, 1, 2, 255, 254, 0, 128]; p.set("k", &binary, None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), binary); } #[tokio::test] async fn test_set_large_value() { let p = new_test_pool(); let large = vec![b'x'; 100_000]; p.set("k", &large, None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), large); } #[tokio::test] async fn test_delete_multiple() { let p = new_test_pool(); p.set("a", b"1", None, None).await.unwrap(); p.set("b", b"2", None, None).await.unwrap(); p.set("c", b"3", None, None).await.unwrap(); let deleted = p.delete(&["a", "c", "missing"], None).await.unwrap(); assert_eq!(deleted, 2); assert!(is_miss(&p.get("a", None).await.unwrap_err())); assert_eq!(p.get("b", None).await.unwrap(), b"2".to_vec()); assert!(is_miss(&p.get("c", None).await.unwrap_err())); } #[tokio::test] async fn test_delete_all_missing() { let p = new_test_pool(); let deleted = p.delete(&["x", "y", "z"], None).await.unwrap(); assert_eq!(deleted, 0); } #[tokio::test] async fn test_delete_single() { let p = new_test_pool(); p.set("k", b"v", None, None).await.unwrap(); assert_eq!(p.delete(&["k"], None).await.unwrap(), 1); assert!(is_miss(&p.get("k", None).await.unwrap_err())); } #[tokio::test] async fn test_delete_idempotent() { let p = new_test_pool(); p.set("k", b"v", None, None).await.unwrap(); assert_eq!(p.delete(&["k"], None).await.unwrap(), 1); assert_eq!(p.delete(&["k"], None).await.unwrap(), 0); } #[tokio::test] async fn test_set_if_not_exists() { let p = new_test_pool(); p.set_if_not_exists("k", b"v1", None, None).await.unwrap(); let err = p .set_if_not_exists("k", b"v2", None, None) .await .unwrap_err(); assert!(is_key_exist(&err)); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"v1".to_vec()); } #[tokio::test] async fn test_set_if_not_exists_with_ttl() { let p = new_test_pool(); p.set_if_not_exists("k", b"v", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v".to_vec()); } #[tokio::test] async fn test_set_if_not_exists_after_delete() { let p = new_test_pool(); p.set_if_not_exists("k", b"v1", None, None).await.unwrap(); p.delete(&["k"], None).await.unwrap(); p.set_if_not_exists("k", b"v2", None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_replace() { let p = new_test_pool(); let err = p.replace("k", b"v1", None, None).await.unwrap_err(); assert!(is_miss(&err)); p.set("k", b"v1", None, None).await.unwrap(); p.replace("k", b"v2", None, None).await.unwrap(); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"v2".to_vec()); } #[tokio::test] async fn test_replace_with_ttl() { let p = new_test_pool(); p.set("k", b"v1", None, None).await.unwrap(); p.replace("k", b"v2", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_replace_multiple_times() { let p = new_test_pool(); p.set("k", b"v1", None, None).await.unwrap(); p.replace("k", b"v2", None, None).await.unwrap(); p.replace("k", b"v3", None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v3".to_vec()); } #[tokio::test] async fn test_get_and_set() { let p = new_test_pool(); // get_and_set on missing key returns Miss but still sets the value. let err = p.get_and_set("k", b"v1", None, None).await.unwrap_err(); assert!(is_miss(&err)); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"v1".to_vec()); let old = p.get_and_set("k", b"v2", None, None).await.unwrap(); assert_eq!(old, b"v1".to_vec()); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"v2".to_vec()); } #[tokio::test] async fn test_get_and_set_with_ttl() { let p = new_test_pool(); p.set("k", b"v1", None, None).await.unwrap(); let old = p .get_and_set("k", b"v2", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(old, b"v1".to_vec()); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_get_and_set_chain() { let p = new_test_pool(); p.set("k", b"v1", None, None).await.unwrap(); let old1 = p.get_and_set("k", b"v2", None, None).await.unwrap(); let old2 = p.get_and_set("k", b"v3", None, None).await.unwrap(); assert_eq!(old1, b"v1".to_vec()); assert_eq!(old2, b"v2".to_vec()); assert_eq!(p.get("k", None).await.unwrap(), b"v3".to_vec()); } #[tokio::test] async fn test_get_and_delete() { let p = new_test_pool(); p.set("k", b"val", None, None).await.unwrap(); let old = p.get_and_delete("k", None).await.unwrap(); assert_eq!(old, b"val".to_vec()); let err = p.get("k", None).await.unwrap_err(); assert!(is_miss(&err)); // Double delete returns Miss. let err = p.get_and_delete("k", None).await.unwrap_err(); assert!(is_miss(&err)); } #[tokio::test] async fn test_get_and_delete_missing_key() { let p = new_test_pool(); let err = p.get_and_delete("missing", None).await.unwrap_err(); assert!(is_miss(&err)); } #[tokio::test] async fn test_mget() { let p = new_test_pool(); p.set("a", b"1", None, None).await.unwrap(); p.set("b", b"2", None, None).await.unwrap(); let vals = p.mget(&["a", "b", "c"], None).await.unwrap(); assert_eq!(vals.len(), 3); assert_eq!(vals[0], Some(b"1".to_vec())); assert_eq!(vals[1], Some(b"2".to_vec())); assert_eq!(vals[2], None); } #[tokio::test] async fn test_mget_all_missing() { let p = new_test_pool(); let vals = p.mget(&["x", "y"], None).await.unwrap(); assert_eq!(vals, vec![None, None]); } #[tokio::test] async fn test_mget_single_key() { let p = new_test_pool(); p.set("k", b"v", None, None).await.unwrap(); let vals = p.mget(&["k"], None).await.unwrap(); assert_eq!(vals, vec![Some(b"v".to_vec())]); } #[tokio::test] async fn test_mget_duplicate_keys() { let p = new_test_pool(); p.set("k", b"v", None, None).await.unwrap(); let vals = p.mget(&["k", "k"], None).await.unwrap(); assert_eq!(vals, vec![Some(b"v".to_vec()), Some(b"v".to_vec())]); } #[tokio::test] async fn test_append() { let p = new_test_pool(); // Append to non-existent key creates it. let len = p.append("k", b"hello", None, None).await.unwrap(); assert_eq!(len, 5); let len = p.append("k", b" world", None, None).await.unwrap(); assert_eq!(len, 11); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"hello world".to_vec()); } #[tokio::test] async fn test_append_empty() { let p = new_test_pool(); p.set("k", b"hi", None, None).await.unwrap(); let len = p.append("k", b"", None, None).await.unwrap(); assert_eq!(len, 2); assert_eq!(p.get("k", None).await.unwrap(), b"hi".to_vec()); } #[tokio::test] async fn test_append_with_ttl() { let p = new_test_pool(); let len = p .append("k", b"hello", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(len, 5); assert_eq!(p.get("k", None).await.unwrap(), b"hello".to_vec()); } #[tokio::test] async fn test_append_binary() { let p = new_test_pool(); p.append("k", &[0u8, 1, 2], None, None).await.unwrap(); p.append("k", &[3u8, 4], None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), vec![0, 1, 2, 3, 4]); } #[tokio::test] async fn test_get_range() { let p = new_test_pool(); p.set("k", b"hello world", None, None).await.unwrap(); assert_eq!( p.get_range("k", 0, 4, None).await.unwrap(), b"hello".to_vec() ); assert_eq!( p.get_range("k", 6, 10, None).await.unwrap(), b"world".to_vec() ); // Negative indices. assert_eq!( p.get_range("k", -5, -1, None).await.unwrap(), b"world".to_vec() ); // Missing key returns empty. assert_eq!( p.get_range("missing", 0, 10, None).await.unwrap(), b"".to_vec() ); } #[tokio::test] async fn test_get_range_full_string() { let p = new_test_pool(); p.set("k", b"hello", None, None).await.unwrap(); assert_eq!( p.get_range("k", 0, -1, None).await.unwrap(), b"hello".to_vec() ); } #[tokio::test] async fn test_get_range_single_char() { let p = new_test_pool(); p.set("k", b"abc", None, None).await.unwrap(); assert_eq!(p.get_range("k", 1, 1, None).await.unwrap(), b"b".to_vec()); } #[tokio::test] async fn test_get_range_beyond_end() { let p = new_test_pool(); p.set("k", b"hi", None, None).await.unwrap(); // Range past end returns what's available. assert_eq!( p.get_range("k", 0, 100, None).await.unwrap(), b"hi".to_vec() ); } #[tokio::test] async fn test_set_range() { let p = new_test_pool(); p.set("k", b"hello world", None, None).await.unwrap(); let new_len = p.set_range("k", 6, b"rust!", None, None).await.unwrap(); assert_eq!(new_len, 11); assert_eq!(p.get("k", None).await.unwrap(), b"hello rust!".to_vec()); // Set range past end extends the string. let new_len = p.set_range("k", 11, b"!!", None, None).await.unwrap(); assert_eq!(new_len, 13); assert_eq!(p.get("k", None).await.unwrap(), b"hello rust!!!".to_vec()); } #[tokio::test] async fn test_set_range_with_gap() { let p = new_test_pool(); // Setting at offset past current length pads with zeros. let len = p.set_range("k", 5, b"hi", None, None).await.unwrap(); assert_eq!(len, 7); let val = p.get("k", None).await.unwrap(); assert_eq!(&val[..5], &[0, 0, 0, 0, 0]); assert_eq!(&val[5..], b"hi"); } #[tokio::test] async fn test_set_range_on_missing_key() { let p = new_test_pool(); // SETRANGE on missing key at offset 0 creates the key. let len = p.set_range("k", 0, b"abc", None, None).await.unwrap(); assert_eq!(len, 3); assert_eq!(p.get("k", None).await.unwrap(), b"abc".to_vec()); } #[tokio::test] async fn test_set_range_empty_value() { let p = new_test_pool(); p.set("k", b"hello", None, None).await.unwrap(); let len = p.set_range("k", 2, b"", None, None).await.unwrap(); assert_eq!(len, 5); assert_eq!(p.get("k", None).await.unwrap(), b"hello".to_vec()); } #[tokio::test] async fn test_set_range_with_ttl() { let p = new_test_pool(); p.set("k", b"hello", None, None).await.unwrap(); let len = p .set_range("k", 0, b"HE", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(len, 5); assert_eq!(p.get("k", None).await.unwrap(), b"HEllo".to_vec()); } #[tokio::test] async fn test_strlen() { let p = new_test_pool(); assert_eq!(p.strlen("missing", None).await.unwrap(), 0); p.set("k", b"hello", None, None).await.unwrap(); assert_eq!(p.strlen("k", None).await.unwrap(), 5); } #[tokio::test] async fn test_strlen_empty_value() { let p = new_test_pool(); p.set("k", b"", None, None).await.unwrap(); assert_eq!(p.strlen("k", None).await.unwrap(), 0); } #[tokio::test] async fn test_strlen_after_append() { let p = new_test_pool(); p.set("k", b"hi", None, None).await.unwrap(); p.append("k", b"!!!", None, None).await.unwrap(); assert_eq!(p.strlen("k", None).await.unwrap(), 5); } #[tokio::test] async fn test_incr_by() { let p = new_test_pool(); p.set("k", b"10", None, None).await.unwrap(); let v = p.incr_by("k", 5, None, None).await.unwrap(); assert_eq!(v, 15); let v = p.decr_by("k", 3, None, None).await.unwrap(); assert_eq!(v, 12); // Negative delta acts as decrement. let v = p.incr_by("k", -2, None, None).await.unwrap(); assert_eq!(v, 10); } #[tokio::test] async fn test_incr_creates_key() { let p = new_test_pool(); let v = p.incr_by("k", 7, None, None).await.unwrap(); assert_eq!(v, 7); let v = p.get("k", None).await.unwrap(); assert_eq!(v, b"7".to_vec()); } #[tokio::test] async fn test_incr_by_invalid_value() { let p = new_test_pool(); p.set("k", b"not-a-number", None, None).await.unwrap(); assert!(p.incr_by("k", 1, None, None).await.is_err()); } #[tokio::test] async fn test_decr_creates_key() { let p = new_test_pool(); let v = p.decr_by("k", 3, None, None).await.unwrap(); assert_eq!(v, -3); assert_eq!(p.get("k", None).await.unwrap(), b"-3".to_vec()); } #[tokio::test] async fn test_incr_by_zero() { let p = new_test_pool(); p.set("k", b"5", None, None).await.unwrap(); let v = p.incr_by("k", 0, None, None).await.unwrap(); assert_eq!(v, 5); } #[tokio::test] async fn test_incr_negative_value() { let p = new_test_pool(); p.set("k", b"-10", None, None).await.unwrap(); let v = p.incr_by("k", 3, None, None).await.unwrap(); assert_eq!(v, -7); } #[tokio::test] async fn test_incr_with_ttl() { let p = new_test_pool(); let v = p .incr_by("k", 1, Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(v, 1); assert_eq!(p.get("k", None).await.unwrap(), b"1".to_vec()); } #[tokio::test] async fn test_decr_with_ttl() { let p = new_test_pool(); p.set("k", b"100", None, None).await.unwrap(); let v = p .decr_by("k", 25, Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(v, 75); } #[tokio::test] async fn test_incr_by_float() { let p = new_test_pool(); let v = p.incr_by_float("k", 1.5, None, None).await.unwrap(); assert!((v - 1.5).abs() < f64::EPSILON); let v = p.incr_by_float("k", 2.5, None, None).await.unwrap(); assert!((v - 4.0).abs() < f64::EPSILON); let v = p.incr_by_float("k", -1.0, None, None).await.unwrap(); assert!((v - 3.0).abs() < f64::EPSILON); } #[tokio::test] async fn test_incr_by_float_on_invalid_value() { let p = new_test_pool(); p.set("k", b"not-a-number", None, None).await.unwrap(); assert!(p.incr_by_float("k", 1.0, None, None).await.is_err()); } #[tokio::test] async fn test_incr_by_float_on_integer_string() { let p = new_test_pool(); p.set("k", b"10", None, None).await.unwrap(); let v = p.incr_by_float("k", 0.5, None, None).await.unwrap(); assert!((v - 10.5).abs() < f64::EPSILON); } #[tokio::test] async fn test_incr_by_float_with_ttl() { let p = new_test_pool(); let v = p .incr_by_float("k", 3.125, Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert!((v - 3.125).abs() < 0.001); } #[tokio::test] async fn test_list_push_pop() { let p = new_test_pool(); let len = p.lpush("l", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(len, 2); let len = p.rpush("l", &[b"c", b"d"], None, None).await.unwrap(); assert_eq!(len, 4); // LPUSH pushes left-to-right, so [a, b] becomes [b, a] at the head. let items = p.lrange_all("l", None).await.unwrap(); assert_eq!(items, vec![b"b", b"a", b"c", b"d"]); let val = p.lpop("l", None, None).await.unwrap(); assert_eq!(val, b"b".to_vec()); let val = p.rpop("l", None, None).await.unwrap(); assert_eq!(val, b"d".to_vec()); } #[tokio::test] async fn test_lpush_single() { let p = new_test_pool(); let len = p.lpush("l", &[b"a"], None, None).await.unwrap(); assert_eq!(len, 1); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_rpush_single() { let p = new_test_pool(); let len = p.rpush("l", &[b"a"], None, None).await.unwrap(); assert_eq!(len, 1); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_lpush_extends_existing() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); let len = p.lpush("l", &[b"z"], None, None).await.unwrap(); assert_eq!(len, 3); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"z".to_vec(), b"a".to_vec(), b"b".to_vec()] ); } #[tokio::test] async fn test_rpush_extends_existing() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); let len = p.rpush("l", &[b"c"], None, None).await.unwrap(); assert_eq!(len, 3); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_lpush_with_ttl() { let p = new_test_pool(); let len = p .lpush("l", &[b"a"], Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(len, 1); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_rpush_with_ttl() { let p = new_test_pool(); let len = p .rpush("l", &[b"a"], Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(len, 1); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_lpush_multiple_ordering() { let p = new_test_pool(); // LPUSH [a, b, c] results in [c, b, a] because each is pushed to head. p.lpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"c".to_vec(), b"b".to_vec(), b"a".to_vec()] ); } #[tokio::test] async fn test_lpop_rpop_empty() { let p = new_test_pool(); assert!(is_miss(&p.lpop("missing", None, None).await.unwrap_err())); assert!(is_miss(&p.rpop("missing", None, None).await.unwrap_err())); } #[tokio::test] async fn test_lpop_until_empty() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(p.lpop("l", None, None).await.unwrap(), b"a".to_vec()); assert_eq!(p.lpop("l", None, None).await.unwrap(), b"b".to_vec()); assert!(is_miss(&p.lpop("l", None, None).await.unwrap_err())); } #[tokio::test] async fn test_rpop_until_empty() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(p.rpop("l", None, None).await.unwrap(), b"b".to_vec()); assert_eq!(p.rpop("l", None, None).await.unwrap(), b"a".to_vec()); assert!(is_miss(&p.rpop("l", None, None).await.unwrap_err())); } #[tokio::test] async fn test_lpop_with_ttl() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); let v = p .lpop("l", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(v, b"a".to_vec()); } #[tokio::test] async fn test_rpop_with_ttl() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); let v = p .rpop("l", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(v, b"b".to_vec()); } #[tokio::test] async fn test_lindex() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); assert_eq!(p.lindex("l", 0, None).await.unwrap(), b"a".to_vec()); assert_eq!(p.lindex("l", 2, None).await.unwrap(), b"c".to_vec()); assert_eq!(p.lindex("l", -1, None).await.unwrap(), b"c".to_vec()); assert_eq!(p.lindex("l", -3, None).await.unwrap(), b"a".to_vec()); // Out of range. assert!(is_miss(&p.lindex("l", 10, None).await.unwrap_err())); assert!(is_miss(&p.lindex("l", -10, None).await.unwrap_err())); // Missing key. assert!(is_miss(&p.lindex("missing", 0, None).await.unwrap_err())); } #[tokio::test] async fn test_lindex_single_element() { let p = new_test_pool(); p.rpush("l", &[b"only"], None, None).await.unwrap(); assert_eq!(p.lindex("l", 0, None).await.unwrap(), b"only".to_vec()); assert_eq!(p.lindex("l", -1, None).await.unwrap(), b"only".to_vec()); assert!(is_miss(&p.lindex("l", 1, None).await.unwrap_err())); } #[tokio::test] async fn test_lset() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.lset("l", 1, b"B", None, None).await.unwrap(); assert_eq!(p.lindex("l", 1, None).await.unwrap(), b"B".to_vec()); // Negative index. p.lset("l", -1, b"C", None, None).await.unwrap(); assert_eq!(p.lindex("l", 2, None).await.unwrap(), b"C".to_vec()); } #[tokio::test] async fn test_lset_out_of_range() { let p = new_test_pool(); p.rpush("l", &[b"a"], None, None).await.unwrap(); assert!(p.lset("l", 5, b"x", None, None).await.is_err()); } #[tokio::test] async fn test_lset_missing_key() { let p = new_test_pool(); assert!(p.lset("missing", 0, b"x", None, None).await.is_err()); } #[tokio::test] async fn test_lset_negative_out_of_range() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); assert!(p.lset("l", -5, b"x", None, None).await.is_err()); } #[tokio::test] async fn test_lset_first_and_last() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.lset("l", 0, b"A", None, None).await.unwrap(); p.lset("l", -1, b"C", None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"A".to_vec(), b"b".to_vec(), b"C".to_vec()] ); } #[tokio::test] async fn test_lrange() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c", b"d", b"e"], None, None) .await .unwrap(); assert_eq!( p.lrange("l", 0, 2, None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); assert_eq!( p.lrange("l", -2, -1, None).await.unwrap(), vec![b"d".to_vec(), b"e".to_vec()] ); // Empty range. assert_eq!( p.lrange("l", 5, 10, None).await.unwrap(), Vec::>::new() ); // Missing key. assert_eq!( p.lrange("missing", 0, -1, None).await.unwrap(), Vec::>::new() ); } #[tokio::test] async fn test_lrange_inverted() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); // start > stop returns empty list. assert_eq!( p.lrange("l", 2, 0, None).await.unwrap(), Vec::>::new() ); } #[tokio::test] async fn test_list_range_items() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c", b"d"], None, None) .await .unwrap(); let sub = p.lrange("l", 1, 2, None).await.unwrap(); assert_eq!(sub, vec![b"b".to_vec(), b"c".to_vec()]); let all = p.lrange_all("l", None).await.unwrap(); assert_eq!(all.len(), 4); } #[tokio::test] async fn test_lrange_all_empty() { let p = new_test_pool(); assert_eq!( p.lrange_all("missing", None).await.unwrap(), Vec::>::new() ); } #[tokio::test] async fn test_ltrim() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c", b"d", b"e"], None, None) .await .unwrap(); p.ltrim("l", 1, 3, None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec(), b"c".to_vec(), b"d".to_vec()] ); } #[tokio::test] async fn test_ltrim_clears_when_out_of_range() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); p.ltrim("l", 5, 10, None, None).await.unwrap(); assert_eq!(p.llen("l", None).await.unwrap(), 0); } #[tokio::test] async fn test_ltrim_negative_indices() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c", b"d"], None, None) .await .unwrap(); p.ltrim("l", -3, -2, None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_ltrim_keep_all() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.ltrim("l", 0, -1, None, None).await.unwrap(); assert_eq!(p.llen("l", None).await.unwrap(), 3); } #[tokio::test] async fn test_ltrim_to_single() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.ltrim("l", 1, 1, None, None).await.unwrap(); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec()]); } #[tokio::test] async fn test_ltrim_missing_key() { let p = new_test_pool(); // LTRIM on missing key is a no-op (no error). p.ltrim("missing", 0, 1, None, None).await.unwrap(); } #[tokio::test] async fn test_linsert_before() { let p = new_test_pool(); p.rpush("l", &[b"a", b"c"], None, None).await.unwrap(); let len = p.linsert_before("l", b"c", b"b", None, None).await.unwrap(); assert_eq!(len, 3); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); // Pivot not found returns Miss. assert!(is_miss( &p.linsert_before("l", b"z", b"x", None, None) .await .unwrap_err() )); } #[tokio::test] async fn test_linsert_after() { let p = new_test_pool(); p.rpush("l", &[b"a", b"c"], None, None).await.unwrap(); let len = p.linsert_after("l", b"a", b"b", None, None).await.unwrap(); assert_eq!(len, 3); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); // Pivot not found returns Miss. assert!(is_miss( &p.linsert_after("l", b"z", b"x", None, None) .await .unwrap_err() )); } #[tokio::test] async fn test_linsert_missing_key() { let p = new_test_pool(); // Redis LINSERT on a non-existent key returns 0 (not an error). let result = p .linsert_before("missing", b"a", b"b", None, None) .await .unwrap(); assert_eq!(result, 0); let result = p .linsert_after("missing", b"a", b"b", None, None) .await .unwrap(); assert_eq!(result, 0); } #[tokio::test] async fn test_linsert_before_first() { let p = new_test_pool(); p.rpush("l", &[b"b", b"c"], None, None).await.unwrap(); p.linsert_before("l", b"b", b"a", None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_linsert_after_last() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); p.linsert_after("l", b"b", b"c", None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_linsert_with_duplicate_pivots() { let p = new_test_pool(); p.rpush("l", &[b"a", b"a", b"b"], None, None).await.unwrap(); // LINSERT finds the first occurrence of the pivot. p.linsert_before("l", b"a", b"x", None, None).await.unwrap(); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"x".to_vec(), b"a".to_vec(), b"a".to_vec(), b"b".to_vec()] ); } #[tokio::test] async fn test_linsert_with_ttl() { let p = new_test_pool(); p.rpush("l", &[b"a", b"c"], None, None).await.unwrap(); p.linsert_before("l", b"c", b"b", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(p.llen("l", None).await.unwrap(), 3); } #[tokio::test] async fn test_lrem_first() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"a", b"c", b"a"], None, None) .await .unwrap(); let removed = p.lrem_first("l", 2, b"a", None, None).await.unwrap(); assert_eq!(removed, 2); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec(), b"c".to_vec(), b"a".to_vec()] ); } #[tokio::test] async fn test_lrem_last() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"a", b"c", b"a"], None, None) .await .unwrap(); let removed = p.lrem_last("l", 2, b"a", None, None).await.unwrap(); assert_eq!(removed, 2); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_lrem_all() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"a", b"c", b"a"], None, None) .await .unwrap(); let removed = p.lrem_all("l", b"a", None, None).await.unwrap(); assert_eq!(removed, 3); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec(), b"c".to_vec()] ); } #[tokio::test] async fn test_lrem_missing_key() { let p = new_test_pool(); assert_eq!(p.lrem_all("missing", b"a", None, None).await.unwrap(), 0); } #[tokio::test] async fn test_lrem_value_not_in_list() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); let removed = p.lrem_all("l", b"z", None, None).await.unwrap(); assert_eq!(removed, 0); assert_eq!(p.llen("l", None).await.unwrap(), 3); } #[tokio::test] async fn test_lrem_first_more_than_exist() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"a"], None, None).await.unwrap(); // count=10 but only 2 occurrences of "a". let removed = p.lrem_first("l", 10, b"a", None, None).await.unwrap(); assert_eq!(removed, 2); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec()]); } #[tokio::test] async fn test_lrem_last_more_than_exist() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"a"], None, None).await.unwrap(); let removed = p.lrem_last("l", 10, b"a", None, None).await.unwrap(); assert_eq!(removed, 2); assert_eq!(p.lrange_all("l", None).await.unwrap(), vec![b"b".to_vec()]); } #[tokio::test] async fn test_lrem_first_zero_count() { let p = new_test_pool(); p.rpush("l", &[b"a", b"a"], None, None).await.unwrap(); // count=0 in lrem_first should return 0 immediately (no-op). let removed = p.lrem_first("l", 0, b"a", None, None).await.unwrap(); assert_eq!(removed, 0); assert_eq!(p.llen("l", None).await.unwrap(), 2); } #[tokio::test] async fn test_lrem_last_zero_count() { let p = new_test_pool(); p.rpush("l", &[b"a", b"a"], None, None).await.unwrap(); let removed = p.lrem_last("l", 0, b"a", None, None).await.unwrap(); assert_eq!(removed, 0); assert_eq!(p.llen("l", None).await.unwrap(), 2); } #[tokio::test] async fn test_lrem_all_empties_list() { let p = new_test_pool(); p.rpush("l", &[b"a", b"a", b"a"], None, None).await.unwrap(); let removed = p.lrem_all("l", b"a", None, None).await.unwrap(); assert_eq!(removed, 3); assert_eq!(p.llen("l", None).await.unwrap(), 0); } #[tokio::test] async fn test_lmove() { let p = new_test_pool(); p.rpush("src", &[b"a", b"b", b"c"], None, None) .await .unwrap(); // Move left of src to right of dst. let val = p .lmove( "src", "dst", ListDirection::Left, ListDirection::Right, None, None, ) .await .unwrap(); assert_eq!(val, b"a".to_vec()); assert_eq!( p.lrange_all("src", None).await.unwrap(), vec![b"b".to_vec(), b"c".to_vec()] ); assert_eq!( p.lrange_all("dst", None).await.unwrap(), vec![b"a".to_vec()] ); // Move right of src to left of dst. let val = p .lmove( "src", "dst", ListDirection::Right, ListDirection::Left, None, None, ) .await .unwrap(); assert_eq!(val, b"c".to_vec()); assert_eq!( p.lrange_all("src", None).await.unwrap(), vec![b"b".to_vec()] ); assert_eq!( p.lrange_all("dst", None).await.unwrap(), vec![b"c".to_vec(), b"a".to_vec()] ); } #[tokio::test] async fn test_lmove_empty_source() { let p = new_test_pool(); assert!(is_miss( &p.lmove( "missing", "dst", ListDirection::Left, ListDirection::Right, None, None ) .await .unwrap_err() )); } #[tokio::test] async fn test_lmove_same_list_rotate() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); // Rotate: move right to left (rpoplpush on same key). let val = p .lmove( "l", "l", ListDirection::Right, ListDirection::Left, None, None, ) .await .unwrap(); assert_eq!(val, b"c".to_vec()); assert_eq!( p.lrange_all("l", None).await.unwrap(), vec![b"c".to_vec(), b"a".to_vec(), b"b".to_vec()] ); } #[tokio::test] async fn test_lmove_left_to_left() { let p = new_test_pool(); p.rpush("src", &[b"a", b"b"], None, None).await.unwrap(); p.rpush("dst", &[b"x"], None, None).await.unwrap(); let val = p .lmove( "src", "dst", ListDirection::Left, ListDirection::Left, None, None, ) .await .unwrap(); assert_eq!(val, b"a".to_vec()); assert_eq!( p.lrange_all("dst", None).await.unwrap(), vec![b"a".to_vec(), b"x".to_vec()] ); } #[tokio::test] async fn test_lmove_right_to_right() { let p = new_test_pool(); p.rpush("src", &[b"a", b"b"], None, None).await.unwrap(); p.rpush("dst", &[b"x"], None, None).await.unwrap(); let val = p .lmove( "src", "dst", ListDirection::Right, ListDirection::Right, None, None, ) .await .unwrap(); assert_eq!(val, b"b".to_vec()); assert_eq!( p.lrange_all("dst", None).await.unwrap(), vec![b"x".to_vec(), b"b".to_vec()] ); } #[tokio::test] async fn test_lmove_creates_destination() { let p = new_test_pool(); p.rpush("src", &[b"a"], None, None).await.unwrap(); p.lmove( "src", "dst", ListDirection::Left, ListDirection::Right, None, None, ) .await .unwrap(); assert_eq!(p.llen("src", None).await.unwrap(), 0); assert_eq!( p.lrange_all("dst", None).await.unwrap(), vec![b"a".to_vec()] ); } #[tokio::test] async fn test_lmove_with_ttl() { let p = new_test_pool(); p.rpush("src", &[b"a", b"b"], None, None).await.unwrap(); let val = p .lmove( "src", "dst", ListDirection::Left, ListDirection::Right, Some(TtlOp::SetMs(100_000)), None, ) .await .unwrap(); assert_eq!(val, b"a".to_vec()); } #[tokio::test] async fn test_llen() { let p = new_test_pool(); assert_eq!(p.llen("missing", None).await.unwrap(), 0); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); assert_eq!(p.llen("l", None).await.unwrap(), 3); } #[tokio::test] async fn test_llen_after_pop() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.lpop("l", None, None).await.unwrap(); assert_eq!(p.llen("l", None).await.unwrap(), 2); } #[tokio::test] async fn test_set_add_remove() { let p = new_test_pool(); let added = p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); assert_eq!(added, 3); // Adding duplicates. let added = p.sadd("s", &[b"b", b"c", b"d"], None, None).await.unwrap(); assert_eq!(added, 1); let removed = p.srem("s", &[b"a", b"missing"], None, None).await.unwrap(); assert_eq!(removed, 1); let len = p.scard("s", None).await.unwrap(); assert_eq!(len, 3); } #[tokio::test] async fn test_sadd_single() { let p = new_test_pool(); let added = p.sadd("s", &[b"x"], None, None).await.unwrap(); assert_eq!(added, 1); assert_eq!(p.scard("s", None).await.unwrap(), 1); } #[tokio::test] async fn test_sadd_all_duplicates() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let added = p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(added, 0); assert_eq!(p.scard("s", None).await.unwrap(), 2); } #[tokio::test] async fn test_sadd_with_ttl() { let p = new_test_pool(); let added = p .sadd("s", &[b"a"], Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(added, 1); } #[tokio::test] async fn test_srem_from_missing_key() { let p = new_test_pool(); let removed = p.srem("missing", &[b"a"], None, None).await.unwrap(); assert_eq!(removed, 0); } #[tokio::test] async fn test_srem_all_members() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let removed = p.srem("s", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(removed, 2); assert_eq!(p.scard("s", None).await.unwrap(), 0); } #[tokio::test] async fn test_srem_with_ttl() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); let removed = p .srem("s", &[b"a"], Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert_eq!(removed, 1); assert_eq!(p.scard("s", None).await.unwrap(), 2); } #[tokio::test] async fn test_set_contains() { let p = new_test_pool(); p.sadd("s", &[b"x"], None, None).await.unwrap(); assert!(p.sismember("s", b"x", None).await.unwrap()); assert!(!p.sismember("s", b"y", None).await.unwrap()); assert!(!p.sismember("missing", b"x", None).await.unwrap()); } #[tokio::test] async fn test_sismember_after_remove() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); p.srem("s", &[b"a"], None, None).await.unwrap(); assert!(!p.sismember("s", b"a", None).await.unwrap()); assert!(p.sismember("s", b"b", None).await.unwrap()); } #[tokio::test] async fn test_set_members_len() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); let mut members = p.smembers("s", None).await.unwrap(); members.sort(); assert_eq!(members, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]); assert_eq!(p.scard("s", None).await.unwrap(), 3); assert_eq!(p.scard("missing", None).await.unwrap(), 0); } #[tokio::test] async fn test_smembers_empty() { let p = new_test_pool(); assert_eq!( p.smembers("missing", None).await.unwrap(), Vec::>::new() ); } #[tokio::test] async fn test_smembers_single() { let p = new_test_pool(); p.sadd("s", &[b"only"], None, None).await.unwrap(); assert_eq!(p.smembers("s", None).await.unwrap(), vec![b"only".to_vec()]); } #[tokio::test] async fn test_spop() { let p = new_test_pool(); p.sadd("s", &[b"only"], None, None).await.unwrap(); // spop removes and returns the member. let popped = p.spop("s", None, None).await.unwrap(); assert_eq!(popped, b"only".to_vec()); // Empty set returns Miss. let err = p.spop("s", None, None).await.unwrap_err(); assert!(is_miss(&err)); } #[tokio::test] async fn test_spop_missing_key() { let p = new_test_pool(); assert!(is_miss(&p.spop("missing", None, None).await.unwrap_err())); } #[tokio::test] async fn test_spop_reduces_cardinality() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); let popped = p.spop("s", None, None).await.unwrap(); assert!(!popped.is_empty()); assert_eq!(p.scard("s", None).await.unwrap(), 2); assert!(!p.sismember("s", &popped, None).await.unwrap()); } #[tokio::test] async fn test_spop_n() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); let popped = p.spop_n("s", 2, None, None).await.unwrap(); assert_eq!(popped.len(), 2); assert_eq!(p.scard("s", None).await.unwrap(), 1); // Pop from missing key. assert_eq!( p.spop_n("missing", 1, None, None).await.unwrap(), Vec::>::new() ); } #[tokio::test] async fn test_spop_n_more_than_set_size() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let popped = p.spop_n("s", 10, None, None).await.unwrap(); assert_eq!(popped.len(), 2); assert_eq!(p.scard("s", None).await.unwrap(), 0); } #[tokio::test] async fn test_spop_n_zero() { let p = new_test_pool(); p.sadd("s", &[b"a"], None, None).await.unwrap(); let popped = p.spop_n("s", 0, None, None).await.unwrap(); assert!(popped.is_empty()); assert_eq!(p.scard("s", None).await.unwrap(), 1); } #[tokio::test] async fn test_srandmember() { let p = new_test_pool(); // Empty set returns Miss. let err = p.srandmember("s", None).await.unwrap_err(); assert!(is_miss(&err)); p.sadd("s", &[b"m"], None, None).await.unwrap(); let sampled = p.srandmember("s", None).await.unwrap(); assert_eq!(sampled, b"m".to_vec()); } #[tokio::test] async fn test_srandmember_doesnt_remove() { let p = new_test_pool(); p.sadd("s", &[b"a"], None, None).await.unwrap(); p.srandmember("s", None).await.unwrap(); assert_eq!(p.scard("s", None).await.unwrap(), 1); } #[tokio::test] async fn test_srandmember_from_larger_set() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c", b"d", b"e"], None, None) .await .unwrap(); let sampled = p.srandmember("s", None).await.unwrap(); assert!(p.sismember("s", &sampled, None).await.unwrap()); } #[tokio::test] async fn test_srandmember_n() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c"], None, None).await.unwrap(); // Positive count — distinct members. let sample = p.srandmember_n("s", 2, None).await.unwrap(); assert_eq!(sample.len(), 2); for m in &sample { assert!(p.sismember("s", m, None).await.unwrap()); } // Positive count > set size returns at most set size. let sample = p.srandmember_n("s", 10, None).await.unwrap(); assert_eq!(sample.len(), 3); // Empty/missing set. let result = p.srandmember_n("missing", 2, None).await; match result { Ok(v) => assert!(v.is_empty()), Err(e) => assert!(is_miss(&e)), } } #[tokio::test] async fn test_srandmember_negative_count() { let p = new_test_pool(); p.sadd("s", &[b"a"], None, None).await.unwrap(); // Negative count — allows duplicates, returns exactly abs(count). let sample = p.srandmember_n("s", -5, None).await.unwrap(); assert_eq!(sample.len(), 5); for m in &sample { assert_eq!(m, &b"a".to_vec()); } } #[tokio::test] async fn test_srandmember_n_distinct() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b", b"c", b"d", b"e"], None, None) .await .unwrap(); let sample = p.srandmember_n("s", 3, None).await.unwrap(); assert_eq!(sample.len(), 3); // All distinct. let mut sorted = sample.clone(); sorted.sort(); sorted.dedup(); assert_eq!(sorted.len(), 3); } #[tokio::test] async fn test_srandmember_n_negative_from_multiple() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let sample = p.srandmember_n("s", -10, None).await.unwrap(); assert_eq!(sample.len(), 10); for m in &sample { assert!(m == b"a" || m == b"b"); } } #[tokio::test] async fn test_set_diff() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.sadd("s2", &[b"b", b"c", b"d"], None, None).await.unwrap(); let mut diff = p.sdiff(&["s1", "s2"], None).await.unwrap(); diff.sort(); assert_eq!(diff, vec![b"a".to_vec()]); // Diff with missing set — returns all of first set. let mut diff = p.sdiff(&["s1", "missing"], None).await.unwrap(); diff.sort(); assert_eq!(diff, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]); let count = p .sdiffstore("dest", &["s1", "s2"], None, None) .await .unwrap(); assert_eq!(count, 1); let mut stored = p.smembers("dest", None).await.unwrap(); stored.sort(); assert_eq!(stored, vec![b"a".to_vec()]); } #[tokio::test] async fn test_sdiff_single_set() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let mut diff = p.sdiff(&["s"], None).await.unwrap(); diff.sort(); assert_eq!(diff, vec![b"a".to_vec(), b"b".to_vec()]); } #[tokio::test] async fn test_sdiff_identical_sets() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("s2", &[b"a", b"b"], None, None).await.unwrap(); let diff = p.sdiff(&["s1", "s2"], None).await.unwrap(); assert!(diff.is_empty()); } #[tokio::test] async fn test_sdiff_three_sets() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b", b"c", b"d"], None, None) .await .unwrap(); p.sadd("s2", &[b"b"], None, None).await.unwrap(); p.sadd("s3", &[b"c"], None, None).await.unwrap(); let mut diff = p.sdiff(&["s1", "s2", "s3"], None).await.unwrap(); diff.sort(); assert_eq!(diff, vec![b"a".to_vec(), b"d".to_vec()]); } #[tokio::test] async fn test_sdiffstore_overwrites_destination() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("s2", &[b"b"], None, None).await.unwrap(); p.sadd("dest", &[b"old1", b"old2", b"old3"], None, None) .await .unwrap(); let count = p .sdiffstore("dest", &["s1", "s2"], None, None) .await .unwrap(); assert_eq!(count, 1); assert_eq!(p.smembers("dest", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_set_intersect() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.sadd("s2", &[b"b", b"c", b"d"], None, None).await.unwrap(); let mut inter = p.sinter(&["s1", "s2"], None).await.unwrap(); inter.sort(); assert_eq!(inter, vec![b"b".to_vec(), b"c".to_vec()]); // Intersection with missing set — empty. assert_eq!( p.sinter(&["s1", "missing"], None).await.unwrap(), Vec::>::new() ); let count = p .sinterstore("dest", &["s1", "s2"], None, None) .await .unwrap(); assert_eq!(count, 2); let mut stored = p.smembers("dest", None).await.unwrap(); stored.sort(); assert_eq!(stored, vec![b"b".to_vec(), b"c".to_vec()]); } #[tokio::test] async fn test_sinter_single_set() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let mut inter = p.sinter(&["s"], None).await.unwrap(); inter.sort(); assert_eq!(inter, vec![b"a".to_vec(), b"b".to_vec()]); } #[tokio::test] async fn test_sinter_disjoint() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("s2", &[b"c", b"d"], None, None).await.unwrap(); let inter = p.sinter(&["s1", "s2"], None).await.unwrap(); assert!(inter.is_empty()); } #[tokio::test] async fn test_sinter_three_sets() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b", b"c"], None, None).await.unwrap(); p.sadd("s2", &[b"b", b"c", b"d"], None, None).await.unwrap(); p.sadd("s3", &[b"c", b"d", b"e"], None, None).await.unwrap(); let inter = p.sinter(&["s1", "s2", "s3"], None).await.unwrap(); assert_eq!(inter, vec![b"c".to_vec()]); } #[tokio::test] async fn test_sinterstore_overwrites_destination() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("s2", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("dest", &[b"old"], None, None).await.unwrap(); let count = p .sinterstore("dest", &["s1", "s2"], None, None) .await .unwrap(); assert_eq!(count, 2); let mut stored = p.smembers("dest", None).await.unwrap(); stored.sort(); assert_eq!(stored, vec![b"a".to_vec(), b"b".to_vec()]); } #[tokio::test] async fn test_set_union() { let p = new_test_pool(); p.sadd("s1", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("s2", &[b"b", b"c"], None, None).await.unwrap(); let mut un = p.sunion(&["s1", "s2"], None).await.unwrap(); un.sort(); assert_eq!(un, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]); let count = p .sunionstore("dest", &["s1", "s2"], None, None) .await .unwrap(); assert_eq!(count, 3); let mut stored = p.smembers("dest", None).await.unwrap(); stored.sort(); assert_eq!(stored, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]); } #[tokio::test] async fn test_sunion_single_set() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); let mut un = p.sunion(&["s"], None).await.unwrap(); un.sort(); assert_eq!(un, vec![b"a".to_vec(), b"b".to_vec()]); } #[tokio::test] async fn test_sunion_with_missing() { let p = new_test_pool(); p.sadd("s1", &[b"a"], None, None).await.unwrap(); let un = p.sunion(&["s1", "missing"], None).await.unwrap(); assert_eq!(un, vec![b"a".to_vec()]); } #[tokio::test] async fn test_sunion_three_sets() { let p = new_test_pool(); p.sadd("s1", &[b"a"], None, None).await.unwrap(); p.sadd("s2", &[b"b"], None, None).await.unwrap(); p.sadd("s3", &[b"c"], None, None).await.unwrap(); let mut un = p.sunion(&["s1", "s2", "s3"], None).await.unwrap(); un.sort(); assert_eq!(un, vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]); } #[tokio::test] async fn test_sunionstore_overwrites_destination() { let p = new_test_pool(); p.sadd("s1", &[b"a"], None, None).await.unwrap(); p.sadd("dest", &[b"old1", b"old2"], None, None) .await .unwrap(); let count = p.sunionstore("dest", &["s1"], None, None).await.unwrap(); assert_eq!(count, 1); assert_eq!(p.smembers("dest", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_set_move() { let p = new_test_pool(); p.sadd("src", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("dst", &[b"c"], None, None).await.unwrap(); let moved = p.smove("src", "dst", b"a", None, None).await.unwrap(); assert!(moved); assert!(!p.sismember("src", b"a", None).await.unwrap()); assert!(p.sismember("dst", b"a", None).await.unwrap()); // Moving non-existent member returns false. let moved = p.smove("src", "dst", b"z", None, None).await.unwrap(); assert!(!moved); } #[tokio::test] async fn test_smove_missing_source() { let p = new_test_pool(); assert!(!p.smove("missing", "dst", b"a", None, None).await.unwrap()); } #[tokio::test] async fn test_smove_creates_destination() { let p = new_test_pool(); p.sadd("src", &[b"a"], None, None).await.unwrap(); assert!(p.smove("src", "dst", b"a", None, None).await.unwrap()); assert_eq!(p.scard("src", None).await.unwrap(), 0); assert_eq!(p.scard("dst", None).await.unwrap(), 1); assert!(p.sismember("dst", b"a", None).await.unwrap()); } #[tokio::test] async fn test_smove_member_already_in_destination() { let p = new_test_pool(); p.sadd("src", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("dst", &[b"a", b"c"], None, None).await.unwrap(); // "a" is in both src and dst. Move should still succeed. let moved = p.smove("src", "dst", b"a", None, None).await.unwrap(); assert!(moved); assert!(!p.sismember("src", b"a", None).await.unwrap()); assert!(p.sismember("dst", b"a", None).await.unwrap()); // dst should still have 2 members (a was already there, not duplicated). assert_eq!(p.scard("dst", None).await.unwrap(), 2); } #[tokio::test] async fn test_smove_with_ttl() { let p = new_test_pool(); p.sadd("src", &[b"a"], None, None).await.unwrap(); let moved = p .smove("src", "dst", b"a", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); assert!(moved); } #[tokio::test] async fn test_type_mismatch_string_on_list() { let p = new_test_pool(); p.rpush("k", &[b"a"], None, None).await.unwrap(); assert!(p.get("k", None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_list_on_string() { let p = new_test_pool(); p.set("k", b"val", None, None).await.unwrap(); assert!(p.lpush("k", &[b"a"], None, None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_set_on_string() { let p = new_test_pool(); p.set("k", b"val", None, None).await.unwrap(); assert!(p.sadd("k", &[b"a"], None, None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_string_on_set() { let p = new_test_pool(); p.sadd("k", &[b"a"], None, None).await.unwrap(); assert!(p.get("k", None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_list_on_set() { let p = new_test_pool(); p.sadd("k", &[b"a"], None, None).await.unwrap(); assert!(p.lpush("k", &[b"x"], None, None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_set_on_list() { let p = new_test_pool(); p.rpush("k", &[b"a"], None, None).await.unwrap(); assert!(p.sadd("k", &[b"x"], None, None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_incr_on_list() { let p = new_test_pool(); p.rpush("k", &[b"1"], None, None).await.unwrap(); assert!(p.incr_by("k", 1, None, None).await.is_err()); } #[tokio::test] async fn test_type_mismatch_append_on_list() { let p = new_test_pool(); p.rpush("k", &[b"a"], None, None).await.unwrap(); assert!(p.append("k", b"x", None, None).await.is_err()); } #[tokio::test] async fn test_set_with_persist_ttl() { let p = new_test_pool(); p.set("k", b"v1", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); p.set("k", b"v2", Some(TtlOp::Persist), None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_ttl_keep_preserves_expiry() { let p = new_test_pool(); p.set("k", b"v1", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); p.set("k", b"v2", Some(TtlOp::Keep), None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_replace_with_keep_ttl() { let p = new_test_pool(); p.set("k", b"v1", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); p.replace("k", b"v2", Some(TtlOp::Keep), None) .await .unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_expired_key_not_returned() { let p = new_test_pool(); // Use a short TTL; miniredis FastForward advances 1s per real second. p.set("k", b"val", Some(TtlOp::SetMs(1)), None) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_millis(1500)).await; assert!(is_miss(&p.get("k", None).await.unwrap_err())); } #[tokio::test] async fn test_set_if_not_exists_on_expired_key() { let p = new_test_pool(); p.set("k", b"old", Some(TtlOp::SetMs(1)), None) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_millis(1500)).await; p.set_if_not_exists("k", b"new", None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"new".to_vec()); } #[tokio::test] async fn test_replace_on_expired_key_fails() { let p = new_test_pool(); p.set("k", b"old", Some(TtlOp::SetMs(1)), None) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_millis(1500)).await; assert!(is_miss( &p.replace("k", b"new", None, None).await.unwrap_err() )); } #[tokio::test] async fn test_set_with_ttl_then_overwrite_without() { let p = new_test_pool(); p.set("k", b"v1", Some(TtlOp::SetMs(100_000)), None) .await .unwrap(); // Overwriting without TTL should remove TTL (Persist semantics). p.set("k", b"v2", Some(TtlOp::Persist), None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v2".to_vec()); } #[tokio::test] async fn test_set_if_not_exists_with_ttl_expires() { let p = new_test_pool(); p.set_if_not_exists("k", b"v", Some(TtlOp::SetMs(1)), None) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_millis(1500)).await; assert!(is_miss(&p.get("k", None).await.unwrap_err())); } #[tokio::test] async fn test_independent_keys_different_types() { let p = new_test_pool(); p.set("str", b"hello", None, None).await.unwrap(); p.rpush("list", &[b"a", b"b"], None, None).await.unwrap(); p.sadd("set", &[b"x", b"y"], None, None).await.unwrap(); // All independent operations work. assert_eq!(p.get("str", None).await.unwrap(), b"hello".to_vec()); assert_eq!(p.llen("list", None).await.unwrap(), 2); assert_eq!(p.scard("set", None).await.unwrap(), 2); // Deleting one doesn't affect others. p.delete(&["str"], None).await.unwrap(); assert_eq!(p.llen("list", None).await.unwrap(), 2); assert_eq!(p.scard("set", None).await.unwrap(), 2); } #[tokio::test] async fn test_delete_list_key() { let p = new_test_pool(); p.rpush("l", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(p.delete(&["l"], None).await.unwrap(), 1); assert_eq!(p.llen("l", None).await.unwrap(), 0); } #[tokio::test] async fn test_delete_set_key() { let p = new_test_pool(); p.sadd("s", &[b"a", b"b"], None, None).await.unwrap(); assert_eq!(p.delete(&["s"], None).await.unwrap(), 1); assert_eq!(p.scard("s", None).await.unwrap(), 0); } #[tokio::test] async fn test_delete_mixed_types() { let p = new_test_pool(); p.set("str", b"v", None, None).await.unwrap(); p.rpush("list", &[b"a"], None, None).await.unwrap(); p.sadd("set", &[b"x"], None, None).await.unwrap(); let deleted = p .delete(&["str", "list", "set", "missing"], None) .await .unwrap(); assert_eq!(deleted, 3); } #[tokio::test] async fn test_reuse_key_string_to_list() { let p = new_test_pool(); p.set("k", b"v", None, None).await.unwrap(); p.delete(&["k"], None).await.unwrap(); p.rpush("k", &[b"a"], None, None).await.unwrap(); assert_eq!(p.lrange_all("k", None).await.unwrap(), vec![b"a".to_vec()]); } #[tokio::test] async fn test_reuse_key_list_to_set() { let p = new_test_pool(); p.rpush("k", &[b"a"], None, None).await.unwrap(); p.delete(&["k"], None).await.unwrap(); p.sadd("k", &[b"x"], None, None).await.unwrap(); assert!(p.sismember("k", b"x", None).await.unwrap()); } #[tokio::test] async fn test_reuse_key_set_to_string() { let p = new_test_pool(); p.sadd("k", &[b"x"], None, None).await.unwrap(); p.delete(&["k"], None).await.unwrap(); p.set("k", b"v", None, None).await.unwrap(); assert_eq!(p.get("k", None).await.unwrap(), b"v".to_vec()); } ================================================ FILE: runtimes/core/src/cache/error.rs ================================================ use bb8_redis::redis; use thiserror::Error; /// Result type for internal cache operations (without operation context). pub type Result = std::result::Result; /// Result type for pool operations with operation context. pub type OpResult = std::result::Result; /// Error wrapper with operation context (operation name + key). /// Analogous to Go's `cache.OpError`. #[derive(Error, Debug)] #[error("cache {operation} \"{key}\": {source}")] pub struct OpError { pub operation: &'static str, pub key: String, #[source] pub source: Error, } impl OpError { pub fn new(operation: &'static str, key: &str, source: Error) -> Self { Self { operation, key: key.to_string(), source, } } } /// Error type for cache operations. #[derive(Error, Debug, Clone)] pub enum Error { /// Miss is the error value reported when a key is missing from the cache. #[error("cache miss")] Miss, /// KeyExists is the error reported when a key already exists /// and the requested operation is specified to only apply to /// keys that do not already exist. #[error("key already exist")] KeyExist, /// Redis error. #[error("redis error: {0}")] Redis(#[from] redis::RedisError), /// Connection pool error. #[error("connection pool timeout")] PoolTimeout, } ================================================ FILE: runtimes/core/src/cache/manager.rs ================================================ use std::collections::HashMap; use std::sync::Arc; use anyhow::Context; use bb8_redis::redis; use redis::{ConnectionAddr, IntoConnectionInfo, RedisConnectionInfo, TlsCertificates}; use crate::cache::client::Client; use crate::cache::miniredis::MiniredisServer; use crate::cache::noop::NoopCluster; use crate::encore::runtime::v1 as pb; use crate::names::EncoreName; use crate::secrets; use crate::trace::Tracer; /// Manager manages cache cluster connections. pub struct Manager { clusters: Arc>>, /// Miniredis-backed cluster for testing and in-memory fallback. miniredis_cluster: Option>, /// Keeps the in-process miniredis server alive for the lifetime of the Manager. _miniredis: Option, } /// Configuration for creating a Manager. pub struct ManagerConfig<'a> { pub clusters: Vec, pub creds: &'a pb::infrastructure::Credentials, pub secrets: &'a secrets::Manager, pub tracer: Tracer, pub testing: bool, pub runtime: tokio::runtime::Handle, } impl ManagerConfig<'_> { pub fn build(self) -> anyhow::Result { // Use miniredis for testing or when any cluster has in_memory set. let needs_miniredis = self.testing || self.clusters.iter().any(|c| c.in_memory); let clusters = clusters_from_cfg(self.clusters, self.creds, self.secrets, self.tracer.clone()) .context("failed to parse Redis clusters")?; let (miniredis_cluster, miniredis) = if needs_miniredis { log::debug!("cache: starting in-process miniredis server"); let server = self .runtime .block_on(MiniredisServer::start()) .context("failed to start miniredis server")?; let url = format!("redis://{}", server.addr()); let client = redis::Client::open(url).context("failed to create miniredis client")?; let cluster = Arc::new(ClusterImpl::new( EncoreName::from("miniredis".to_string()), client, None, // no key prefix — matches Go runtime behavior self.tracer.clone(), 0, // min_conns 10, // max_conns )); (Some(cluster), Some(server)) } else { (None, None) }; Ok(Manager { clusters: Arc::new(clusters), miniredis_cluster, _miniredis: miniredis, }) } } impl Manager { /// Returns a cluster by name. /// If the cluster is not configured and miniredis is available (testing or in-memory mode), /// returns the miniredis-backed cluster. Otherwise, returns a NoopCluster /// that errors on all operations. pub fn cluster(&self, name: &EncoreName) -> Arc { match self.clusters.get(name) { Some(cluster) => cluster.clone(), None => { // If we have a miniredis cluster (testing or in-memory), // use it as a fallback for unconfigured clusters. if let Some(cluster) = &self.miniredis_cluster { log::debug!( "cache: using miniredis fallback for unconfigured cluster {}", name ); cluster.clone() } else { Arc::new(NoopCluster::new(name.clone())) } } } } } /// Trait representing a cache cluster. pub trait Cluster: Send + Sync { /// Returns the name of the cluster. fn name(&self) -> &EncoreName; /// Creates a new cache client for this cluster. fn client(&self) -> anyhow::Result; } /// Implementation of a configured cache cluster. pub struct ClusterImpl { name: EncoreName, client: redis::Client, key_prefix: Option, tracer: Tracer, min_conns: u32, max_conns: u32, } impl ClusterImpl { fn new( name: EncoreName, client: redis::Client, key_prefix: Option, tracer: Tracer, min_conns: u32, max_conns: u32, ) -> Self { Self { name, client, key_prefix, tracer, min_conns, max_conns, } } } impl Cluster for ClusterImpl { fn name(&self) -> &EncoreName { &self.name } fn client(&self) -> anyhow::Result { Client::new( self.client.clone(), self.key_prefix.clone(), self.tracer.clone(), self.min_conns, self.max_conns, ) } } /// Builds cluster configurations from proto config. fn clusters_from_cfg( clusters: Vec, creds: &pb::infrastructure::Credentials, secrets: &secrets::Manager, tracer: Tracer, ) -> anyhow::Result>> { let mut result = HashMap::new(); // Build role lookup let roles: HashMap<&str, &pb::RedisRole> = creds .redis_roles .iter() .map(|r| (r.rid.as_str(), r)) .collect(); for cluster in clusters { // Skip in-memory clusters; they'll use the miniredis fallback. if cluster.in_memory { continue; } // Get the primary server let server = cluster .servers .iter() .find(|s| s.kind() == pb::ServerKind::Primary); let Some(server) = server else { log::warn!( "no primary server found for Redis cluster {}, skipping", cluster.rid ); continue; }; // Process each database in the cluster for db in &cluster.databases { // Get the read-write pool for this db let Some(pool) = db.conn_pools.iter().find(|p| !p.is_readonly) else { log::warn!( "no read-write pool found for Redis database {}, skipping", db.encore_name ); continue; }; // Get the role to authenticate with let role = roles.get(pool.role_rid.as_str()).with_context(|| { format!( "no role found with rid {} for Redis database {}", pool.role_rid, db.encore_name ) })?; // Build connection info and client let client = build_redis_client(server, db, role, secrets)?; let name: EncoreName = db.encore_name.clone().into(); result.insert( name.clone(), Arc::new(ClusterImpl::new( name, client, db.key_prefix.clone(), tracer.clone(), pool.min_connections as u32, pool.max_connections as u32, )), ); } } Ok(result) } /// Builds a Redis client with proper TLS configuration. fn build_redis_client( server: &pb::RedisServer, db: &pb::RedisDatabase, role: &pb::RedisRole, secrets: &secrets::Manager, ) -> anyhow::Result { use pb::redis_role::Auth; // Parse host and port let (host, port) = if server.host.starts_with('/') { // Unix socket - use URL-based connection let url = build_unix_socket_url(&server.host, db.database_idx, role, secrets)?; return redis::Client::open(url).context("failed to create Redis client"); } else if let Some((h, p)) = server.host.split_once(':') { (h.to_string(), p.parse::().context("invalid port")?) } else { (server.host.clone(), 6379) }; let (username, password) = match &role.auth { Some(Auth::AuthString(secret_data)) => { let password = secrets.load(secret_data.clone()); let password = password .get() .context("failed to resolve Redis auth string")?; let password_str = std::str::from_utf8(password).context("invalid auth string")?; // Trim whitespace/newlines that might be in the secret let password_str = password_str.trim().to_string(); (None, Some(password_str)) } Some(Auth::Acl(acl)) => { let password = acl .password .as_ref() .context("ACL auth requires password")?; let password = secrets.load(password.clone()); let password = password.get().context("failed to resolve Redis password")?; let password_str = std::str::from_utf8(password).context("invalid password")?; let password_str = password_str.trim().to_string(); // If username is empty, treat it as password-only auth (like AuthString) let username = if acl.username.is_empty() { None } else { Some(acl.username.clone()) }; (username, Some(password_str)) } None => (None, None), }; // Build connection address based on TLS config let mut addr = if let Some(tls_config) = &server.tls_config { // TLS enabled - check for insecure mode let insecure = tls_config.disable_ca_validation; ConnectionAddr::TcpTls { host, port, insecure, tls_params: None, // TLS params will be set via build_with_tls } } else { // No TLS ConnectionAddr::Tcp(host, port) }; // Handle hostname verification separately from CA validation if let Some(tls_config) = &server.tls_config { if tls_config.disable_tls_hostname_verification { addr.set_danger_accept_invalid_hostnames(true); } } let mut redis_info = RedisConnectionInfo::default().set_db(db.database_idx as i64); if let Some(user) = username { redis_info = redis_info.set_username(user); } if let Some(pass) = password { redis_info = redis_info.set_password(pass); } // Build connection info using builder pattern let conn_info = addr .into_connection_info() .context("failed to create connection info")? .set_redis_settings(redis_info); // Create client with or without TLS certificates if let Some(tls_config) = &server.tls_config { // Build TLS certificates config let root_cert = tls_config .server_ca_cert .as_ref() .map(|cert| cert.as_bytes().to_vec()); let tls_certs = TlsCertificates { client_tls: None, // No client cert support yet root_cert, }; redis::Client::build_with_tls(conn_info, tls_certs) .context("failed to create Redis client with TLS") } else { redis::Client::open(conn_info).context("failed to create Redis client") } } /// Builds a Unix socket connection URL. fn build_unix_socket_url( socket_path: &str, db_idx: i32, role: &pb::RedisRole, secrets: &secrets::Manager, ) -> anyhow::Result { use pb::redis_role::Auth; // Build auth portion for query string let auth_params = match &role.auth { Some(Auth::AuthString(secret_data)) => { let password = secrets.load(secret_data.clone()); let password = password .get() .context("failed to resolve Redis auth string")?; let password_str = std::str::from_utf8(password).context("invalid auth string")?; format!("&password={}", urlencoding::encode(password_str)) } Some(Auth::Acl(acl)) => { let password = acl .password .as_ref() .context("ACL auth requires password")?; let password = secrets.load(password.clone()); let password = password.get().context("failed to resolve Redis password")?; let password_str = std::str::from_utf8(password).context("invalid password")?; format!( "&username={}&password={}", urlencoding::encode(&acl.username), urlencoding::encode(password_str) ) } None => String::new(), }; Ok(format!( "redis+unix://{}?db={db_idx}{auth_params}", socket_path )) } ================================================ FILE: runtimes/core/src/cache/miniredis.rs ================================================ use std::net::SocketAddr; use std::time::Duration; use miniredis_rs::Miniredis; /// An in-process miniredis server with a background cleanup task /// that fast-forwards time and prunes excess keys (matching the /// behavior of the old Go miniredis-encore binary). /// /// The server and cleanup task run as tokio tasks and will shut /// down when the tokio runtime is dropped. pub struct MiniredisServer { server: Miniredis, addr: SocketAddr, } impl MiniredisServer { /// Start a new in-process miniredis server on a random port. /// /// Also spawns a background cleanup task that fast-forwards /// time by 1s every second and prunes keys above 100 every 15s. pub async fn start() -> anyhow::Result { let server = Miniredis::run() .await .map_err(|e| anyhow::anyhow!("failed to start miniredis: {}", e))?; let addr = server.addr(); // Spawn the cleanup task on the current runtime. tokio::spawn(cleanup_task(server.clone())); Ok(Self { server, addr }) } /// Returns the bound address of the miniredis server. pub fn addr(&self) -> SocketAddr { self.addr } /// Returns a reference to the underlying miniredis server. pub fn server(&self) -> &Miniredis { &self.server } } /// Background task that fast-forwards miniredis time and periodically prunes /// excess keys, matching the Go binary's doCleanup behavior. async fn cleanup_task(server: Miniredis) { let mut interval = tokio::time::interval(Duration::from_secs(1)); let mut acc = Duration::ZERO; loop { interval.tick().await; server.fast_forward(Duration::from_secs(1)); acc += Duration::from_secs(1); if acc >= Duration::from_secs(15) { acc = Duration::ZERO; // Prune to 100 keys. let keys = server.keys(); if keys.len() > 100 { let to_delete = keys.len() - 100; for key in keys.iter().take(to_delete) { server.del(key); } } } } } ================================================ FILE: runtimes/core/src/cache/mod.rs ================================================ mod client; mod error; mod manager; pub mod miniredis; mod noop; mod tracer; pub use client::{Client, ListDirection, TtlOp}; pub use error::{Error, OpError, OpResult, Result}; pub use manager::{Cluster, ClusterImpl, Manager, ManagerConfig}; ================================================ FILE: runtimes/core/src/cache/noop.rs ================================================ use crate::cache::client::Client; use crate::cache::manager::Cluster; use crate::names::EncoreName; /// NoopCluster is returned when a cache cluster is not configured. /// All operations on it will return an error immediately. pub struct NoopCluster { name: EncoreName, } impl NoopCluster { pub fn new(name: EncoreName) -> Self { Self { name } } } impl Cluster for NoopCluster { fn name(&self) -> &EncoreName { &self.name } fn client(&self) -> anyhow::Result { anyhow::bail!("cache: this service is not configured to use this cache cluster") } } ================================================ FILE: runtimes/core/src/cache/tracer.rs ================================================ use std::future::Future; use crate::{ cache::{error::Error, OpError, OpResult}, model::Request, trace::{ protocol::{self, CacheCallStartData, CacheOpResult}, Tracer, }, }; pub(crate) struct CacheTracer(Tracer); impl CacheTracer { pub(crate) fn new(inner: Tracer) -> Self { Self(inner) } pub(crate) async fn trace<'a, T, F, Fut>( &self, source: Option<&'a Request>, operation: &'static str, is_write: bool, keys: &'a [&'a str], f: F, ) -> OpResult where F: FnOnce() -> Fut, Fut: Future>, { let traced = if let Some(source) = source { let start_id = self.0.cache_call_start(CacheCallStartData { source, operation, is_write, keys, }); Some((start_id, source)) } else { None }; let result = match f().await { Ok(value) => Ok(value), Err(err) => Err(OpError::new( operation, keys.first().copied().unwrap_or(""), err.clone(), )), }; if let Some((start_id, source)) = traced { let (cache_op_result, error) = match result.as_ref() { Ok(_) => (CacheOpResult::Ok, None), Err(err) => match &err.source { Error::Miss => (CacheOpResult::NoSuchKey, None), Error::KeyExist => (CacheOpResult::Conflict, None), _ => (CacheOpResult::Err, Some(err)), }, }; self.0.cache_call_end(protocol::CacheCallEndData { start_id, source, result: cache_op_result, error, }); } result } } ================================================ FILE: runtimes/core/src/error/conversions.rs ================================================ use crate::error::AppError; impl From<&str> for AppError { fn from(message: &str) -> Self { AppError::new(message) } } impl From for AppError { fn from(message: String) -> Self { AppError::new(message) } } impl From for AppError { fn from(error: anyhow::Error) -> Self { let message = error.to_string(); // Create a chain of causes let mut cause: Option> = None; while let Some(err) = error.chain().next_back() { cause = Some(Box::new(Self { message: err.to_string(), stack: vec![], cause, })); } Self { message, stack: vec![], cause, } } } ================================================ FILE: runtimes/core/src/error/mod.rs ================================================ mod conversions; use backtrace::SymbolName; use colored::Colorize; use serde::{Deserialize, Serialize}; use std::fmt::Display; /// AppError represents an error that occurred in the application langauge /// (i.e. in TypeScript code, not the rust runtime). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AppError { /// The error message pub message: String, /// The stack trace pub stack: StackTrace, /// The cause of the error (if any) pub cause: Option>, } impl AppError { /// Create a new AppError with the given message. /// /// Note: The stack trace will be set to the rust stack trace at the time this function is called. /// If you want to set the stack trace to something else, use `AppError::with_stack`. #[track_caller] pub fn new>(message: S) -> Self { Self { message: message.into(), stack: capture_stack_trace(), cause: None, } } /// Wrap an existing error with a new message. /// /// Note: The stack trace will be set to the rust stack trace at the time this function is called. /// If you want to set the stack trace to something else, use `AppError::with_stack`. #[track_caller] pub fn wrap, S: Into>(error: Err, message: S) -> Self { Self { message: message.into(), stack: capture_stack_trace(), cause: Some(Box::new(error.into())), } } /// Updates the stack trace of the error to the given stack trace pub fn with_stack(self, stack: StackTrace) -> Self { Self { stack, ..self } } /// Updates the cause of the error to the given error pub fn with_cause>(self, cause: Err) -> Self { Self { cause: Some(Box::new(cause.into())), ..self } } /// Trims the stack trace to remove any frames from before /// the given file and line number. /// /// This is useful for removing frames caused by a conversion into an AppError. /// If the given file and line number are not found in the stack trace, then the original /// stack trace is returned. pub fn trim_stack(self, file: &str, line: u32, drop_extra: usize) -> Self { let idx = self .stack .iter() .position(|frame| frame.file == file && frame.line == line) .map(|idx| idx + 1 + drop_extra); // + 1 for the frame we called this on match idx { Some(idx) => Self { stack: self.stack.into_iter().skip(idx).collect(), ..self }, None => self, } } } const MAX_FRAMES_TO_DISPLAY: usize = 6; const STACK_TAB_SIZE: &str = " "; /// Write the stack trace to the given formatter. pub fn write_stack_trace(stack: &StackTrace, f: &mut W) -> std::fmt::Result { // If we have a stack trace add it if !stack.is_empty() { write!(f, "\n{}{}", STACK_TAB_SIZE, "Stack:".magenta())?; // What's the longest function name including module name (for modules not named "main") let mut longest_func = 0; for frame in stack.iter().take(MAX_FRAMES_TO_DISPLAY + 1) { if let Some(func) = &frame.function { longest_func = longest_func.max( func.len() + // Function name length frame.module.as_ref().map(|s| s.len() + 1).unwrap_or(0), // plus "[module]." if module is present ); } } for (index, frame) in stack.iter().enumerate() { let (module_and_function, readable_len) = match (&frame.module, &frame.function) { (Some(module), Some(function)) => ( format!("{}.{}", module.bright_black(), function.magenta()), module.len() + function.len() + 1, ), (None, Some(function)) => (format!("{}", function.magenta()), function.len()), (Some(_), None) => ("".to_string(), 0), (None, None) => ("".to_string(), 0), }; let spacing_after_function = if longest_func >= readable_len { " ".repeat(longest_func - readable_len) } else { "".to_string() }; let line_and_column = frame .column .map(|col| format!("{}:{}", frame.line, col)) .unwrap_or(format!("{}", frame.line)); write!( f, "\n{}{}at {}{} {}:{}", STACK_TAB_SIZE, STACK_TAB_SIZE, module_and_function, spacing_after_function, frame.file, line_and_column )?; // Only print the first 6 frames if index >= MAX_FRAMES_TO_DISPLAY { write!( f, "\n{}{}... remaining {} frames omitted...", STACK_TAB_SIZE, STACK_TAB_SIZE, stack.len() - index )?; break; } } } Ok(()) } /// Display the error in a human readable format. impl Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{} {}", "Error:".red(), self.message.replace('\n', "\n\t") )?; write_stack_trace(&self.stack, f)?; // While there are more causes, print them let mut cause = &self.cause; while cause.is_some() { let error = cause.as_ref().expect("cause should not be None"); write!(f, "\n\n\t{} {}", "Caused by:".red(), error.message)?; write_stack_trace(&error.stack, f)?; cause = &error.cause; } Ok(()) } } /// A stack trace from an error. pub type StackTrace = Vec; /// A stack frame in a backtrace from an error. /// /// Note: The serde field names, match those used in our Go `errinsrc` package. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StackFrame { /// The name of the file on the file system #[serde(rename = "file")] pub file: String, /// The line number in that file #[serde(rename = "line")] pub line: u32, /// The column number in that file (if available) #[serde(skip_serializing)] pub column: Option, /// The name of the module containing the line (if available) #[serde(skip_serializing)] pub module: Option, /// The name of the function or method containing the line (if available) #[serde(rename = "func")] pub function: Option, } /// capture_stack_trace captures a stack trace from the current location in the rust code base #[track_caller] fn capture_stack_trace() -> StackTrace { let caller = std::panic::Location::caller(); let backtrace = backtrace::Backtrace::new(); let stacktrace: StackTrace = convert_backtrace_to_stack_trace(&backtrace) .into_iter() .skip_while(|frame| { // Skip all the frames before the caller !(frame.file.ends_with(caller.file()) && frame.line == caller.line()) }) .collect(); // If the backtrace is empty, then we just return the caller which thanks to the // track_caller macro means we will always have at least one frame to report if stacktrace.is_empty() { vec![StackFrame { file: caller.file().to_string(), line: caller.line(), column: None, module: None, function: None, }] } else { // otherwise we can report the full trace stacktrace } } /// convert_backtrace_to_stack_trace converts a rust backtrace to a stack trace fn convert_backtrace_to_stack_trace(backtrace: &backtrace::Backtrace) -> StackTrace { let mut stack_trace = Vec::new(); for frame in backtrace.frames() { if let Some(symbol) = frame.symbols().first() { match (symbol.filename(), symbol.lineno()) { (Some(filename), Some(line)) => { let (module, function) = split_symbol_into_module_function(symbol.name()); let frame = StackFrame { file: trim_file_path(filename.to_string_lossy().to_string()), line, column: symbol.colno(), module, function, }; if !is_common_rust_frame(&frame) { stack_trace.push(frame); } } _ => continue, } } } stack_trace } fn split_symbol_into_module_function( symbol: Option, ) -> (Option, Option) { match symbol { Some(symbol) => { let symbol_str = symbol.to_string(); let parts: Vec<&str> = symbol_str.split("::").collect(); if parts.len() < 3 { return (None, Some(symbol.to_string())); } let function_idx = parts .iter() .rposition(|s| s.starts_with(|c: char| c.is_uppercase())) .unwrap_or_else(|| { parts .iter() .rposition(|s| (*s).eq("{{closure}}")) .map(|idx| idx - 1) .unwrap_or_else(|| parts.len() - 2) }); let module = parts[..function_idx].join("::"); let function = parts[function_idx..parts.len() - 1].join("::"); (Some(module), Some(function)) } None => (None, None), } } /// trim_file_path trims the file path to be relative to the root of the binary being compiled fn trim_file_path(full_path: String) -> String { let compile_target = env!("ENCORE_BINARY_SRC_PATH"); full_path .strip_prefix(compile_target) .map(|str| str[1..].to_string()) .unwrap_or(full_path) .to_string() } fn is_common_rust_frame(frame: &StackFrame) -> bool { frame.file.starts_with("/rustc/") } ================================================ FILE: runtimes/core/src/infracfg.rs ================================================ use crate::encore::runtime::v1::infrastructure::{Credentials, Resources}; use crate::encore::runtime::v1::{ self as pbruntime, environment, gateway, metrics_provider, pub_sub_cluster, pub_sub_subscription, pub_sub_topic, redis_role, secret_data, service_auth, service_discovery, AppSecret, Deployment, Environment, Infrastructure, MetricsProvider, Observability, PubSubCluster, PubSubSubscription, PubSubTopic, RedisCluster, RedisConnectionPool, RedisDatabase, RedisRole, RedisServer, RuntimeConfig, SqlCluster, SqlConnectionPool, SqlDatabase, SqlRole, SqlServer, TlsConfig, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Serialize, Deserialize)] pub struct InfraConfig { pub metadata: Option, pub graceful_shutdown: Option, pub auth: Option>, pub service_discovery: Option>, pub metrics: Option, pub used_metrics: Option>, pub sql_servers: Option>, pub redis: Option>, pub pubsub: Option>, pub secrets: Option, pub hosted_services: Option>, pub hosted_gateways: Option>, pub cors: Option, pub object_storage: Option>, pub worker_threads: Option, pub log_config: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ObjectStorage { #[serde(rename = "gcs")] GCS(GCS), #[serde(rename = "s3")] S3(S3), } #[derive(Debug, Serialize, Deserialize)] pub struct GCS { pub endpoint: Option, pub buckets: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct S3 { pub region: String, pub endpoint: Option, pub access_key_id: Option, pub secret_access_key: Option, pub buckets: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct Bucket { pub name: String, pub key_prefix: Option, pub public_base_url: Option, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct Metadata { pub app_id: Option, pub env_name: Option, pub env_type: Option, pub cloud: Option, pub base_url: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct CORS { pub debug: Option, pub allow_headers: Option>, pub expose_headers: Option>, pub allow_origins_without_credentials: Option>, pub allow_origins_with_credentials: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct GracefulShutdown { pub total: Option, pub shutdown_hooks: Option, pub handlers: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Auth { #[serde(rename = "key")] Key(KeyAuth), } #[derive(Debug, Serialize, Deserialize)] pub struct KeyAuth { pub id: i32, pub key: EnvString, } #[derive(Debug, Serialize, Deserialize)] pub struct ServiceDiscovery { pub base_url: String, pub auth: Option>, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Metrics { #[serde(rename = "prometheus")] Prometheus(PrometheusMetrics), #[serde(rename = "datadog")] Datadog(DatadogMetrics), #[serde(rename = "gcp_cloud_monitoring")] GCPCloudMonitoring(GCPCloudMonitoringMetrics), #[serde(rename = "aws_cloudwatch")] AWSCloudWatch(AWSCloudWatchMetrics), } #[derive(Debug, Serialize, Deserialize)] pub struct PrometheusMetrics { pub collection_interval: Option, pub remote_write_url: EnvString, } #[derive(Debug, Serialize, Deserialize)] pub struct DatadogMetrics { pub collection_interval: Option, pub site: String, pub api_key: EnvString, } #[derive(Debug, Serialize, Deserialize)] pub struct GCPCloudMonitoringMetrics { pub collection_interval: Option, pub project_id: String, pub monitored_resource_type: String, pub monitored_resource_labels: Option>, pub metric_names: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct AWSCloudWatchMetrics { pub collection_interval: Option, pub namespace: String, } #[derive(Debug, Serialize, Deserialize)] pub struct Metric { name: String, services: Vec, } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum Secrets { Map(HashMap), EnvRef(EnvRef), } #[derive(Debug, Serialize, Deserialize)] pub struct EnvRef { #[serde(rename = "$env")] pub env: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum EnvString { String(String), EnvRef(EnvRef), } #[derive(Debug, Serialize, Deserialize)] pub struct SQLServer { pub host: String, pub tls_config: Option, pub databases: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct TLSConfig { #[serde(default)] pub disabled: bool, pub ca: Option, pub client_cert: Option, #[serde(default)] pub disable_tls_hostname_verification: bool, #[serde(default)] pub disable_ca_validation: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct SQLDatabase { pub name: Option, pub max_connections: Option, pub min_connections: Option, pub username: String, pub password: EnvString, pub client_cert: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct Redis { pub host: String, pub database_index: i32, pub auth: Option, pub key_prefix: Option, pub tls_config: Option, pub max_connections: Option, pub min_connections: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct RedisAuth { pub r#type: String, pub username: Option, pub password: Option, pub auth_string: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct ClientCert { pub cert: String, pub key: EnvString, } // PubSub-related structures #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum PubSub { #[serde(rename = "gcp_pubsub")] GCPPubsub(GCPPubsub), #[serde(rename = "aws_sns_sqs")] AWSSnsSqs(AWSSnsSqs), #[serde(rename = "nsq")] NSQ(NSQPubsub), } #[derive(Debug, Serialize, Deserialize)] pub struct GCPPubsub { pub project_id: String, pub topics: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct GCPTopic { pub name: String, pub project_id: Option, #[serde(skip_serializing_if = "HashMap::is_empty", default)] pub subscriptions: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct GCPSub { pub name: String, pub project_id: Option, pub push_config: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct PushConfig { pub service_account: String, pub jwt_audience: String, pub id: String, } #[derive(Debug, Serialize, Deserialize)] pub struct AWSSnsSqs { pub topics: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct AWSTopic { pub arn: String, #[serde(skip_serializing_if = "HashMap::is_empty", default)] pub subscriptions: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct AWSSub { pub url: String, } #[derive(Debug, Serialize, Deserialize)] pub struct NSQPubsub { pub hosts: String, pub topics: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct NSQTopic { pub name: String, #[serde(skip_serializing_if = "HashMap::is_empty", default)] pub subscriptions: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct NSQSub { pub name: String, } pub fn map_infra_to_runtime(infra: InfraConfig) -> RuntimeConfig { let mut next_rid = 0; let mut get_next_rid = || { let rid = next_rid; next_rid += 1; rid.to_string() }; let metadata = infra.metadata.unwrap_or_default(); // Map the Environment let environment = Some(Environment { app_id: "".to_string(), app_slug: metadata.app_id.unwrap_or_default(), env_id: "".to_string(), env_name: metadata.env_name.unwrap_or_default(), env_type: metadata .env_type .as_ref() .map(|t| match t.as_str() { "development" => environment::Type::Development as i32, "production" => environment::Type::Production as i32, "ephemeral" => environment::Type::Ephemeral as i32, "test" => environment::Type::Test as i32, _ => environment::Type::Unspecified as i32, }) .unwrap_or(environment::Type::Unspecified as i32), cloud: metadata .cloud .as_ref() .map(|c| match c.as_str() { "local" => environment::Cloud::Local as i32, "encore" => environment::Cloud::Encore as i32, "aws" => environment::Cloud::Aws as i32, "gcp" => environment::Cloud::Gcp as i32, "azure" => environment::Cloud::Azure as i32, _ => environment::Cloud::Unspecified as i32, }) .unwrap_or(environment::Cloud::Unspecified as i32), }); // Map GracefulShutdown let graceful_shutdown = infra .graceful_shutdown .as_ref() .map(|gs| pbruntime::GracefulShutdown { total: gs.total.map(|t| prost_types::Duration { seconds: t as i64, nanos: 0, }), shutdown_hooks: gs.shutdown_hooks.map(|t| prost_types::Duration { seconds: t as i64, nanos: 0, }), handlers: gs.handlers.map(|t| prost_types::Duration { seconds: t as i64, nanos: 0, }), }); // Map Auth methods let auth_methods = infra .auth .as_ref() .map(|auths| { auths .iter() .map(|auth| { let auth_method = match auth { Auth::Key(k) => { service_auth::AuthMethod::EncoreAuth(service_auth::EncoreAuth { auth_keys: vec![pbruntime::EncoreAuthKey { id: k.id as u32, data: Some(map_env_string_to_secret_data(&k.key)), }], }) } }; pbruntime::ServiceAuth { auth_method: Some(auth_method), } }) .collect() }) .unwrap_or_else(|| { vec![pbruntime::ServiceAuth { auth_method: Some(service_auth::AuthMethod::Noop(service_auth::NoopAuth {})), }] }); // Map ServiceDiscovery let service_discovery = infra.service_discovery.map(|services| { let services_mapped = services .into_iter() .map(|(name, sd)| { let svc_auth_methods = sd .auth .map(|auths| { auths .into_iter() .map(|auth| match auth { Auth::Key(k) => pbruntime::ServiceAuth { auth_method: Some(service_auth::AuthMethod::EncoreAuth( service_auth::EncoreAuth { auth_keys: vec![pbruntime::EncoreAuthKey { id: k.id as u32, data: Some(map_env_string_to_secret_data(&k.key)), }], }, )), }, }) .collect() }) .unwrap_or(auth_methods.clone()); ( name, service_discovery::Location { base_url: sd.base_url, auth_methods: svc_auth_methods, }, ) }) .collect(); pbruntime::ServiceDiscovery { services: services_mapped, } }); // Map Buckets let buckets = infra.object_storage.map(|object_storages| { object_storages .into_iter() .map(|os| match os { ObjectStorage::GCS(gcs) => pbruntime::BucketCluster { rid: get_next_rid(), provider: Some(pbruntime::bucket_cluster::Provider::Gcs( pbruntime::bucket_cluster::Gcs { endpoint: gcs.endpoint, anonymous: false, local_sign: None, }, )), buckets: gcs .buckets .into_iter() .map(|(name, bucket)| pbruntime::Bucket { encore_name: name, cloud_name: bucket.name, key_prefix: bucket.key_prefix, public_base_url: bucket.public_base_url, rid: get_next_rid(), }) .collect(), }, ObjectStorage::S3(s3) => pbruntime::BucketCluster { rid: get_next_rid(), provider: Some(pbruntime::bucket_cluster::Provider::S3( pbruntime::bucket_cluster::S3 { region: s3.region, endpoint: s3.endpoint, access_key_id: s3.access_key_id, secret_access_key: s3 .secret_access_key .as_ref() .map(map_env_string_to_secret_data), }, )), buckets: s3 .buckets .into_iter() .map(|(name, bucket)| pbruntime::Bucket { encore_name: name, cloud_name: bucket.name, key_prefix: bucket.key_prefix, public_base_url: bucket.public_base_url, rid: get_next_rid(), }) .collect(), }, }) .collect() }); // Map Metrics let metrics = infra.metrics.map(|metrics| { let (provider, interval) = match metrics { Metrics::Prometheus(pm) => ( metrics_provider::Provider::PromRemoteWrite( metrics_provider::PrometheusRemoteWrite { remote_write_url: Some(map_env_string_to_secret_data(&pm.remote_write_url)), }, ), pm.collection_interval, ), Metrics::Datadog(dd) => ( metrics_provider::Provider::Datadog(metrics_provider::Datadog { site: dd.site, api_key: Some(map_env_string_to_secret_data(&dd.api_key)), }), dd.collection_interval, ), Metrics::GCPCloudMonitoring(gcp) => ( metrics_provider::Provider::Gcp(metrics_provider::GcpCloudMonitoring { project_id: gcp.project_id, monitored_resource_type: gcp.monitored_resource_type, monitored_resource_labels: gcp.monitored_resource_labels.unwrap_or_default(), metric_names: gcp.metric_names.unwrap_or_default(), }), gcp.collection_interval, ), Metrics::AWSCloudWatch(aws) => ( metrics_provider::Provider::Aws(metrics_provider::AwsCloudWatch { namespace: aws.namespace, }), aws.collection_interval, ), }; vec![MetricsProvider { rid: get_next_rid(), collection_interval: interval.map(|i| prost_types::Duration { seconds: i as i64, nanos: 0, }), provider: Some(provider), }] }); // Map Observability let observability = Some(Observability { metrics: metrics.unwrap_or_default(), tracing: Vec::new(), logs: Vec::new(), }); let cors = infra.cors.map(|cors| gateway::Cors { debug: cors.debug.unwrap_or(false), disable_credentials: false, allowed_origins_without_credentials: cors .allow_origins_without_credentials .map(|f| gateway::CorsAllowedOrigins { allowed_origins: f }), allowed_origins_with_credentials: cors.allow_origins_with_credentials.map(|f| { gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins( gateway::CorsAllowedOrigins { allowed_origins: f }, ) }), extra_allowed_headers: cors.allow_headers.unwrap_or_default(), extra_exposed_headers: cors.expose_headers.unwrap_or_default(), allow_private_network_access: true, }); let gateways = infra .hosted_gateways .map(|gateways| { gateways .into_iter() .map(|gateway| pbruntime::Gateway { rid: get_next_rid(), encore_name: gateway, base_url: metadata.base_url.clone().unwrap_or_default(), hostnames: vec![], cors: cors.clone(), }) .collect::>() }) .unwrap_or_default(); // Map Deployment let deployment = Some(Deployment { deploy_id: String::new(), deployed_at: None, dynamic_experiments: Vec::new(), hosted_gateways: gateways.iter().map(|g| g.rid.clone()).collect(), hosted_services: infra .hosted_services .map(|services| { services .iter() .map(|service| pbruntime::HostedService { name: service.clone(), worker_threads: infra.worker_threads, log_config: infra.log_config.clone(), }) .collect() }) .unwrap_or_default(), auth_methods, observability, service_discovery, graceful_shutdown, metrics: infra .used_metrics .unwrap_or_default() .into_iter() .map(|m| pbruntime::Metric { encore_name: m.name, services: m.services, }) .collect(), }); let mut credentials = Credentials { client_certs: Vec::new(), sql_roles: Vec::new(), redis_roles: Vec::new(), }; // Map SQL Servers let sql_clusters = infra.sql_servers.map(|servers| { servers .into_iter() .map(|server| { let default_client_cert = server .tls_config .as_ref() .and_then(|tls| tls.client_cert.as_ref()) .map(|f| { let rid = get_next_rid(); let client_cert = pbruntime::ClientCert { rid: rid.clone(), cert: f.cert.clone(), key: Some(map_env_string_to_secret_data(&f.key)), }; credentials.client_certs.push(client_cert); rid }); let databases = server .databases .into_iter() .map(|(name, db)| { let client_cert = db .client_cert .map(|f| { let rid = get_next_rid(); let client_cert = pbruntime::ClientCert { rid: rid.clone(), cert: f.cert, key: Some(map_env_string_to_secret_data(&f.key)), }; credentials.client_certs.push(client_cert); rid }) .or_else(|| default_client_cert.clone()); let role_rid = get_next_rid(); let role = SqlRole { rid: role_rid.clone(), client_cert_rid: client_cert, username: db.username, password: Some(map_env_string_to_secret_data(&db.password)), }; credentials.sql_roles.push(role); SqlDatabase { rid: get_next_rid(), encore_name: name.clone(), cloud_name: db.name.unwrap_or(name), conn_pools: vec![SqlConnectionPool { is_readonly: false, role_rid, min_connections: db.min_connections.unwrap_or(0), max_connections: db.max_connections.unwrap_or(100), }], } }) .collect(); SqlCluster { rid: get_next_rid(), servers: vec![SqlServer { rid: get_next_rid(), host: server.host, kind: pbruntime::ServerKind::Primary as i32, tls_config: server.tls_config.map_or_else( || Some(TlsConfig::default()), |tls| match tls.disabled { true => None, false => Some(TlsConfig { server_ca_cert: tls.ca, disable_tls_hostname_verification: tls .disable_tls_hostname_verification, disable_ca_validation: tls.disable_ca_validation, }), }, ), }], databases, } }) .collect() }); // Map Redis let redis_clusters = infra.redis.map(|redis_map| { redis_map .into_iter() .map(|(name, redis)| { let client_cert = redis .tls_config .as_ref() .and_then(|tls| tls.client_cert.as_ref()) .map(|f| { let rid = get_next_rid(); let client_cert = pbruntime::ClientCert { rid: rid.clone(), cert: f.cert.clone(), key: Some(map_env_string_to_secret_data(&f.key)), }; credentials.client_certs.push(client_cert); rid }); let auth = redis.auth.map(|ra| match ra.r#type.as_str() { "auth_string" => redis_role::Auth::AuthString(map_env_string_to_secret_data( ra.auth_string.as_ref().unwrap(), )), "acl" => redis_role::Auth::Acl(redis_role::AuthAcl { username: ra.username.unwrap(), password: Some(map_env_string_to_secret_data( ra.password.as_ref().unwrap(), )), }), _ => redis_role::Auth::AuthString(map_env_string_to_secret_data( ra.auth_string.as_ref().unwrap(), )), }); let role_rid = get_next_rid(); let role = RedisRole { rid: role_rid.clone(), client_cert_rid: client_cert, auth, }; credentials.redis_roles.push(role); let database = RedisDatabase { rid: get_next_rid(), encore_name: name, // Use the key as the name database_idx: redis.database_index, key_prefix: redis.key_prefix, conn_pools: vec![RedisConnectionPool { is_readonly: false, role_rid, min_connections: redis.min_connections.unwrap_or(0), max_connections: redis.max_connections.unwrap_or(100), }], }; RedisCluster { rid: String::new(), // Assign a unique RID servers: vec![RedisServer { rid: String::new(), // Assign a unique RID host: redis.host, kind: pbruntime::ServerKind::Primary as i32, tls_config: redis.tls_config.map_or_else( || Some(TlsConfig::default()), |tls| match tls.disabled { true => None, false => Some(TlsConfig { server_ca_cert: tls.ca, disable_tls_hostname_verification: tls .disable_tls_hostname_verification, disable_ca_validation: tls.disable_ca_validation, }), }, ), }], databases: vec![database], in_memory: false, } }) .collect() }); // Map PubSub let pubsub_clusters = infra.pubsub.map(|pubsubs| { pubsubs .into_iter() .map(|pubsub| { // Handle different PubSub types let (provider, topics, subscriptions) = match pubsub { PubSub::GCPPubsub(gcp) => { let topics = gcp .topics .iter() .map(|(name, topic)| PubSubTopic { rid: String::new(), encore_name: name.clone(), cloud_name: topic.name.clone(), delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce as i32, ordering_attr: None, provider_config: Some(pub_sub_topic::ProviderConfig::GcpConfig( pub_sub_topic::GcpConfig { project_id: topic .project_id .clone() .unwrap_or_else(|| gcp.project_id.clone()), }, )), }) .collect(); let subscriptions = gcp .topics .iter() .flat_map(|(topic_name, topic)| { topic.subscriptions.iter().map(|(sub_name, sub)| { PubSubSubscription { rid: String::new(), topic_encore_name: topic_name.clone(), subscription_encore_name: sub_name.clone(), topic_cloud_name: topic.name.clone(), subscription_cloud_name: sub.name.clone(), push_only: sub.push_config.is_some(), provider_config: Some( pub_sub_subscription::ProviderConfig::GcpConfig( pub_sub_subscription::GcpConfig { project_id: sub .project_id .clone() .unwrap_or_else(|| gcp.project_id.clone()), push_service_account: sub .push_config .as_ref() .map(|pc| pc.service_account.clone()), push_jwt_audience: sub .push_config .as_ref() .map(|pc| pc.jwt_audience.clone()), }, ), ), } }) }) .collect(); let provider = pub_sub_cluster::Provider::Gcp(pub_sub_cluster::GcpPubSub {}); (Some(provider), topics, subscriptions) } PubSub::AWSSnsSqs(aws) => { let topics = aws .topics .iter() .map(|(name, topic)| PubSubTopic { rid: String::new(), encore_name: name.clone(), cloud_name: topic.arn.clone(), delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce as i32, // AWS typically provides at-least-once delivery ordering_attr: None, // Add ordering if necessary provider_config: None, // AWS doesn't need additional provider config here }) .collect(); let subscriptions = aws .topics .iter() .flat_map(|(topic_name, topic)| { topic.subscriptions.iter().map(|(sub_name, sub)| { PubSubSubscription { rid: String::new(), topic_encore_name: topic_name.clone(), subscription_encore_name: sub_name.clone(), topic_cloud_name: topic.arn.clone(), subscription_cloud_name: sub.url.clone(), push_only: false, // AWS SQS doesn't typically use push config provider_config: None, // AWS doesn't need additional provider config } }) }) .collect(); let provider = pub_sub_cluster::Provider::Aws(pub_sub_cluster::AwsSqsSns {}); (Some(provider), topics, subscriptions) } PubSub::NSQ(nsq) => { let topics = nsq .topics .iter() .map(|(name, topic)| PubSubTopic { rid: String::new(), encore_name: name.clone(), cloud_name: topic.name.clone(), // NSQ doesn't have cloud-specific names, using the topic name delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce as i32, // NSQ typically guarantees at-least-once delivery ordering_attr: None, // NSQ doesn't handle message ordering natively provider_config: None, // No additional provider config for NSQ }) .collect(); let subscriptions = nsq .topics .iter() .flat_map(|(topic_name, topic)| { topic.subscriptions.iter().map(|(sub_name, sub)| { PubSubSubscription { rid: String::new(), topic_encore_name: topic_name.clone(), subscription_encore_name: sub_name.clone(), topic_cloud_name: topic.name.clone(), // Using topic name for simplicity subscription_cloud_name: sub.name.clone(), push_only: false, // NSQ is pull-based, no push config provider_config: None, // No additional provider config for NSQ } }) }) .collect(); let provider = pub_sub_cluster::Provider::Nsq(pub_sub_cluster::Nsq { hosts: vec![nsq.hosts.clone()], // Mapping NSQ hosts }); (Some(provider), topics, subscriptions) } }; PubSubCluster { rid: get_next_rid(), topics, subscriptions, provider, } }) .collect() }); // Map Secrets let app_secrets: Vec = match infra.secrets { Some(Secrets::Map(secrets_map)) => secrets_map .into_iter() .map(|(name, value)| AppSecret { rid: get_next_rid(), encore_name: name.clone(), data: Some(map_env_string_to_secret_data(&value)), }) .collect(), Some(Secrets::EnvRef(env_ref)) => { // Fetch the environment variable match std::env::var(env_ref.env) { Ok(secrets_json) => { // Parse the JSON string into a HashMap match serde_json::from_str::>(&secrets_json) { Ok(secrets_map) => secrets_map .into_iter() .map(|(name, value)| AppSecret { rid: get_next_rid(), encore_name: name, data: Some(pbruntime::SecretData { encoding: secret_data::Encoding::None as i32, source: Some(secret_data::Source::Embedded(value.into_bytes())), sub_path: None, }), }) .collect(), Err(_) => { ::log::error!( "Failed to parse secrets JSON from secret environment variable" ); Vec::new() } } } Err(_) => { ::log::error!("Failed to read secrets from environment variable"); Vec::new() } } } None => Vec::new(), }; // Map Infrastructure Resources let resources = Some(Resources { gateways, sql_clusters: sql_clusters.unwrap_or_default(), pubsub_clusters: pubsub_clusters.unwrap_or_default(), redis_clusters: redis_clusters.unwrap_or_default(), app_secrets, bucket_clusters: buckets.unwrap_or_default(), }); let infra_struct = Some(Infrastructure { resources, credentials: Some(credentials), }); // Construct the final RuntimeConfig RuntimeConfig { environment, infra: infra_struct, deployment, encore_platform: None, } } // Helper function to map EnvString to SecretData fn map_env_string_to_secret_data(env_string: &EnvString) -> pbruntime::SecretData { match env_string { EnvString::String(s) => pbruntime::SecretData { encoding: secret_data::Encoding::None as i32, source: Some(secret_data::Source::Embedded(s.clone().into_bytes())), sub_path: None, }, EnvString::EnvRef(env_ref) => pbruntime::SecretData { encoding: secret_data::Encoding::None as i32, source: Some(secret_data::Source::Env(env_ref.env.clone())), sub_path: None, }, } } #[cfg(test)] mod tests { use super::*; use prost::Message; use serde_json; use std::fs; #[test] fn test_map_infra_to_runtime() { // Load and parse the infra.config.json fixture let infra_json = fs::read_to_string(format!( "{}/resources/test/infra.config.json", env!("CARGO_MANIFEST_DIR") )) .expect("Failed to read infra.config.json"); let infra_config: InfraConfig = serde_json::from_str(&infra_json).expect("Failed to parse infra.config.json"); // Convert InfraConfig to Runtime let runtime: RuntimeConfig = map_infra_to_runtime(infra_config); // Load and parse the runtime.json fixture let runtime_data = fs::read(format!( "{}/resources/test/runtime.pb", env!("CARGO_MANIFEST_DIR") )) .expect("Failed to read runtime.json"); let expected_runtime = RuntimeConfig::decode(runtime_data.as_slice()).expect("Failed to parse runtime.json"); // Compare the converted runtime with the expected runtime assert_eq!( runtime, expected_runtime, "Converted runtime does not match expected runtime" ); } } ================================================ FILE: runtimes/core/src/lib.rs ================================================ use std::borrow::Borrow; use std::collections::HashSet; use std::fmt::Display; use std::hash::Hash; use std::io::Read; use std::path::Path; use std::sync::Arc; use anyhow::Context; use base64::Engine; use duct::cmd; use prost::Message; use crate::api::reqauth::platform; pub use names::{CloudName, EncoreName, EndpointName}; use crate::encore::parser::meta::v1 as metapb; use crate::encore::runtime::v1 as runtimepb; pub mod api; mod base32; pub mod cache; pub mod error; pub mod infracfg; pub mod log; pub mod meta; pub mod metadata; pub mod metrics; pub mod model; mod names; pub mod objects; pub mod proccfg; pub mod pubsub; pub mod runtime_config; pub mod secrets; pub mod sqldb; mod trace; pub mod encore { pub mod runtime { pub mod v1 { include!(concat!(env!("OUT_DIR"), "/encore.runtime.v1.rs")); } } pub mod parser { pub mod meta { pub mod v1 { include!(concat!(env!("OUT_DIR"), "/encore.parser.meta.v1.rs")); } } pub mod schema { pub mod v1 { include!(concat!(env!("OUT_DIR"), "/encore.parser.schema.v1.rs")); } } } } pub struct RuntimeBuilder { cfg: Option, proc_cfg: Option, md: Option, err: Option, test_mode: bool, is_worker: bool, } impl Default for RuntimeBuilder { fn default() -> Self { Self::new() } } impl RuntimeBuilder { pub fn new() -> Self { Self { cfg: None, proc_cfg: None, md: None, err: None, test_mode: false, is_worker: false, } } pub fn with_test_mode(mut self, enabled: bool) -> Self { self.test_mode = enabled; if enabled { enable_test_mode().unwrap(); } self } pub fn with_worker(mut self, enabled: bool) -> Self { self.is_worker = enabled; self } pub fn with_runtime_config(mut self, cfg: runtimepb::RuntimeConfig) -> Self { self.cfg = Some(cfg); self } pub fn with_proc_config(mut self, proc_cfg: proccfg::ProcessConfig) -> Self { self.proc_cfg = Some(proc_cfg); self } pub fn with_runtime_config_from_env(mut self) -> Self { if self.err.is_none() { match infra_config_from_env() { Ok(opt_cfg) => match opt_cfg { Some(cfg) => self.cfg = Some(cfg), None => match runtime_config_from_env() { Ok(cfg) => self.cfg = Some(cfg), Err(e) => { self.err = Some( anyhow::Error::new(e).context("unable to parse runtime config"), ) } }, }, Err(e) => { self.err = Some(anyhow::Error::new(e).context("unable to parse infra config")) } } match proc_config_from_env() { Ok(cfg) => self.proc_cfg = cfg, Err(e) => { self.err = Some(anyhow::Error::new(e).context("unable to parse process config")) } } } self } pub fn with_meta(mut self, md: metapb::Data) -> Self { self.md = Some(md); self } pub fn with_meta_autodetect(mut self) -> Self { fn auto_detect() -> Result { match meta_from_env() { Ok(md) => Ok(md), Err(ParseError::EnvNotPresent) => { let path = Path::new("/encore/meta"); parse_meta(path).context("unable to parse app metadata file") } Err(e) => { Err(anyhow::Error::new(e) .context("unable to parse app metadata from environment")) } } } if self.err.is_none() { match auto_detect() { Ok(md) => self.md = Some(md), Err(e) => self.err = Some(e), } } self } pub fn with_meta_from_env(mut self) -> Self { if self.err.is_none() { match meta_from_env() { Ok(md) => self.md = Some(md), Err(e) => { self.err = Some(anyhow::Error::new(e).context("unable to parse app metadata")) } } } self } pub fn with_meta_from_path(mut self, meta_path: &Path) -> Self { if self.err.is_none() { match parse_meta(meta_path) { Ok(md) => self.md = Some(md), Err(e) => { self.err = Some(anyhow::Error::new(e).context("unable to parse app metadata")) } } } self } pub fn build(self) -> anyhow::Result { if let Some(err) = self.err { return Err(err); } let mut cfg = self.cfg.context("runtime config not provided")?; let md = self.md.context("metadata not provided")?; if let Some(proc_config) = self.proc_cfg { proc_config.apply(&mut cfg)?; } Runtime::new(cfg, md, self.test_mode) } } pub struct Runtime { md: metapb::Data, pubsub: pubsub::Manager, secrets: secrets::Manager, sqldb: sqldb::Manager, cache: cache::Manager, objects: objects::Manager, api: api::Manager, app_meta: meta::AppMeta, compute: ComputeConfig, runtime: tokio::runtime::Runtime, metrics: metrics::Manager, runtime_config: runtime_config::RuntimeConfig, } impl Runtime { pub fn builder() -> RuntimeBuilder { RuntimeBuilder::new() } pub fn new( mut cfg: runtimepb::RuntimeConfig, md: metapb::Data, testing: bool, ) -> anyhow::Result { // Initialize OpenSSL system root certificates, so that libraries can find them. unsafe { openssl_probe::init_openssl_env_vars(); } let tokio_rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .context("failed to build tokio runtime")?; let app_meta = meta::AppMeta::new(&cfg, &md); let runtime_config = runtime_config::RuntimeConfig::new(&cfg); let environment = cfg.environment.take().unwrap_or_default(); let mut infra = cfg.infra.take().unwrap_or_default(); let resources = infra.resources.take().unwrap_or_default(); let creds = infra.credentials.take().unwrap_or_default(); let encore_platform = cfg.encore_platform.take().unwrap_or_default(); let mut deployment = cfg.deployment.take().unwrap_or_default(); let service_discovery = deployment.service_discovery.take().unwrap_or_default(); let observability = deployment.observability.take().unwrap_or_default(); let http_client = reqwest::Client::builder() .build() .context("failed to build http client")?; let secrets = secrets::Manager::new(resources.app_secrets); let platform_validator = platform::RequestValidator::new( &secrets, encore_platform.platform_signing_keys.clone(), ); let platform_validator = Arc::new(platform_validator); // Initialize metrics manager from runtime config let metrics_manager = metrics::Manager::from_runtime_config( &observability, &environment, &secrets, &http_client, tokio_rt.handle().clone(), ); // Set up observability. let disable_tracing = testing || std::env::var("ENCORE_NOTRACE").is_ok_and(|v| !v.is_empty()); let tracer = if !disable_tracing { let trace_cfg = observability .tracing .into_iter() .find_map(|p| match p.provider { Some(runtimepb::tracing_provider::Provider::Encore(encore)) => { #[allow(deprecated)] let sampling_rate = encore.sampling_rate; Some((encore.sampling_config, sampling_rate, encore.trace_endpoint)) } _ => None, }) .and_then(|(cfg, sr, ep)| match reqwest::Url::parse(&ep) { Ok(ep) => Some((cfg, sr, ep)), Err(err) => { ::log::warn!("disabling tracing: invalid trace endpoint {}: {}", ep, err); None } }); match trace_cfg { Some((trace_sampling_config, trace_sampling_rate, trace_endpoint)) => { let config = trace::ReporterConfig { app_id: environment.app_id.clone(), env_id: environment.env_id.clone(), deploy_id: deployment.deploy_id.clone(), app_commit: md.app_revision.clone(), trace_endpoint, trace_sampling_config: trace::TraceSamplingConfig::new( trace_sampling_config, trace_sampling_rate, ), platform_validator: platform_validator.clone(), }; let (tracer, reporter) = trace::streaming_tracer(http_client.clone(), config); tokio_rt.spawn(reporter.start_reporting()); tracer } None => trace::Tracer::noop(), } } else { trace::Tracer::noop() }; log::set_tracer(tracer.clone()); // Find push subscriptions which should be proxied to the subscribing service by the gateway let proxied_push_subs = resources .pubsub_clusters .iter() .flat_map(|c| c.subscriptions.iter()) .filter(|s| s.push_only) .filter_map(|s| { let topic = md .pubsub_topics .iter() .find(|t| t.name == s.topic_encore_name)?; let sub = topic .subscriptions .iter() .find(|ms| ms.name == s.subscription_encore_name)?; match deployment .hosted_services .iter() .any(|s| s.name == sub.service_name) { true => None, false => Some(( s.rid.clone(), api::gateway::ProxiedPushSub { service_name: EncoreName::from(sub.service_name.clone()), topic: EncoreName::from(topic.name.clone()), subscription: EncoreName::from(sub.name.clone()), }, )), } }) .collect(); let pubsub = pubsub::Manager::new(tracer.clone(), resources.pubsub_clusters, &md)?; let objects = objects::Manager::new(&secrets, tracer.clone(), resources.bucket_clusters, &md); let sqldb = sqldb::ManagerConfig { clusters: resources.sql_clusters, creds: &creds, secrets: &secrets, tracer: tracer.clone(), runtime: tokio_rt.handle().clone(), } .build() .context("unable to initialize sqldb proxy")?; let cache = cache::ManagerConfig { clusters: resources.redis_clusters, creds: &creds, secrets: &secrets, tracer: tracer.clone(), testing, runtime: tokio_rt.handle().clone(), } .build() .context("unable to initialize cache manager")?; // Determine the compute configuration. let compute = { let mut cfg = ComputeConfig::default(); for svc in deployment.hosted_services.iter() { if let Some(log_config) = &svc.log_config { cfg.log_level = Some(log_config.clone()); } if let Some(worker_threads) = svc.worker_threads { // If we have worker threads already configured on the compute config, // determine the new value. cfg.worker_threads = Some(match (cfg.worker_threads, worker_threads) { // If either explicitly wants 1 worker threads (disabling it), set it to 1. (Some(1), _) | (_, 1) => 1, // If we have worker threads enabled on both, set it to the minimum. (Some(a), b) if a > 1 && b > 1 => a.min(b), // Otherwise use the existing value, if any. (Some(a), _) => a, // If we don't have an existing value, use the new value. (None, b) => b, }); } } cfg }; let api = api::ManagerConfig { meta: &md, environment: &environment, gateways: resources.gateways, hosted_services: deployment.hosted_services, hosted_gateway_rids: deployment.hosted_gateways, svc_auth_methods: deployment.auth_methods, deploy_id: deployment.deploy_id, platform: &encore_platform, secrets: &secrets, service_discovery, http_client: http_client.clone(), tracer, platform_validator, pubsub_push_registry: pubsub.push_registry(), runtime: tokio_rt.handle().clone(), testing, proxied_push_subs, metrics: &metrics_manager, } .build() .context("unable to initialize api manager")?; let sqldb_handle = sqldb.start_serving(); tokio_rt.spawn(async move { if let Err(err) = sqldb_handle.await { ::log::error!("sqldb proxy failed: {err}"); } }); ::log::debug!("encore runtime successfully initialized"); Ok(Self { md, pubsub, secrets, sqldb, cache, objects, api, app_meta, compute, runtime: tokio_rt, metrics: metrics_manager, runtime_config, }) } #[inline] pub fn pubsub(&self) -> &pubsub::Manager { &self.pubsub } #[inline] pub fn secrets(&self) -> &secrets::Manager { &self.secrets } #[inline] pub fn sqldb(&self) -> &sqldb::Manager { &self.sqldb } #[inline] pub fn cache(&self) -> &cache::Manager { &self.cache } #[inline] pub fn objects(&self) -> &objects::Manager { &self.objects } #[inline] pub fn metadata(&self) -> &metapb::Data { &self.md } #[inline] pub fn api(&self) -> &api::Manager { &self.api } #[inline] pub fn metrics(&self) -> &metrics::Manager { &self.metrics } #[inline] pub fn endpoints(&self) -> &api::EndpointMap { self.api.endpoints() } #[inline] pub fn tokio_handle(&self) -> &tokio::runtime::Handle { self.runtime.handle() } #[inline] pub fn run_blocking(&self) { self.runtime.block_on(async move { let api_handle = self.api().start_serving(); if let Err(err) = api_handle.await { ::log::error!("failed to start serving: {:?}", err); } }); } #[inline] pub fn app_meta(&self) -> &meta::AppMeta { &self.app_meta } #[inline] pub fn runtime_config(&self) -> &runtime_config::RuntimeConfig { &self.runtime_config } #[inline] pub fn compute(&self) -> &ComputeConfig { &self.compute } } #[derive(Debug, Clone, Default)] pub struct ComputeConfig { pub log_level: Option, pub worker_threads: Option, } #[derive(Debug)] enum ParseError { EnvNotPresent, EnvVar(std::env::VarError), Base64(base64::DecodeError), Proto(prost::DecodeError), IO(std::io::Error), } impl Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParseError::EnvNotPresent => write!(f, "environment variable not present"), ParseError::EnvVar(e) => write!(f, "failed to read environment variable: {e}"), ParseError::Base64(e) => write!(f, "failed to decode environment variable: {e}"), ParseError::Proto(e) => write!(f, "failed to parse environment variable: {e}"), ParseError::IO(e) => write!(f, "failed to read file: {e}"), } } } impl std::error::Error for ParseError {} fn infra_config_from_env() -> Result, ParseError> { let cfg_path = match std::env::var("ENCORE_INFRA_CONFIG_PATH") { Ok(cfg) => cfg, Err(std::env::VarError::NotPresent) => return Ok(None), Err(e) => return Err(ParseError::EnvVar(e)), }; let file_content = std::fs::read_to_string(cfg_path).map_err(ParseError::IO)?; let infra_config: infracfg::InfraConfig = serde_json::from_str(&file_content) .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; let runtime_config = infracfg::map_infra_to_runtime(infra_config); Ok(Some(runtime_config)) } fn runtime_config_from_env() -> Result { let cfg = match std::env::var("ENCORE_RUNTIME_CONFIG") { Ok(cfg) => cfg, Err(std::env::VarError::NotPresent) => { // Not present. Check the ENCORE_RUNTIME_CONFIG_PATH environment variable. match std::env::var("ENCORE_RUNTIME_CONFIG_PATH") { Ok(path) => { let path = Path::new(&path); return parse_runtime_config(path); } Err(std::env::VarError::NotPresent) => return Err(ParseError::EnvNotPresent), Err(e) => return Err(ParseError::EnvVar(e)), } } Err(e) => return Err(ParseError::EnvVar(e)), }; if cfg.starts_with("gzip:") { // Parse the remainder as base64-encoded gzip data. let cfg = cfg.as_bytes(); let cfg = &cfg["gzip:".len()..]; let gzip_data = base64::engine::general_purpose::STANDARD .decode(cfg) .map_err(ParseError::Base64)?; let mut decoder = flate2::read::GzDecoder::new(&gzip_data[..]); let mut raw_data = Vec::new(); decoder.read_to_end(&mut raw_data).map_err(ParseError::IO)?; runtimepb::RuntimeConfig::decode(&raw_data[..]).map_err(ParseError::Proto) } else { let decoded = base64::engine::general_purpose::STANDARD .decode(cfg.as_bytes()) .map_err(ParseError::Base64)?; runtimepb::RuntimeConfig::decode(&decoded[..]).map_err(ParseError::Proto) } } fn parse_runtime_config(path: &Path) -> Result { let data = std::fs::read(path).map_err(ParseError::IO)?; runtimepb::RuntimeConfig::decode(&data[..]).map_err(ParseError::Proto) } fn proc_config_from_env() -> Result, ParseError> { let encoded_config = match std::env::var("ENCORE_PROCESS_CONFIG") { Ok(config) => config, Err(std::env::VarError::NotPresent) => return Ok(None), Err(e) => return Err(ParseError::EnvVar(e)), }; let decoded = base64::engine::general_purpose::STANDARD .decode(encoded_config) .map_err(ParseError::Base64)?; let json_str = String::from_utf8(decoded) .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; let config = serde_json::from_str(&json_str) .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; Ok(Some(config)) } fn meta_from_env() -> Result { let cfg = match std::env::var("ENCORE_APP_META") { Ok(cfg) => cfg, Err(std::env::VarError::NotPresent) => { // Not present. Check the ENCORE_APP_META_PATH environment variable. match std::env::var("ENCORE_APP_META_PATH") { Ok(path) => { let path = Path::new(&path); return parse_meta(path); } Err(std::env::VarError::NotPresent) => return Err(ParseError::EnvNotPresent), Err(e) => return Err(ParseError::EnvVar(e)), } } Err(e) => return Err(ParseError::EnvVar(e)), }; if cfg.starts_with("gzip:") { // Parse the remainder as base64-encoded gzip data. let cfg = cfg.as_bytes(); let cfg = &cfg["gzip:".len()..]; let gzip_data = base64::engine::general_purpose::STANDARD .decode(cfg) .map_err(ParseError::Base64)?; let mut decoder = flate2::read::GzDecoder::new(&gzip_data[..]); let mut raw_data = Vec::new(); decoder.read_to_end(&mut raw_data).map_err(ParseError::IO)?; metapb::Data::decode(&raw_data[..]).map_err(ParseError::Proto) } else { let decoded = base64::engine::general_purpose::STANDARD .decode(cfg.as_bytes()) .map_err(ParseError::Base64)?; metapb::Data::decode(&decoded[..]).map_err(ParseError::Proto) } } fn parse_meta(path: &Path) -> Result { let data = std::fs::read(path).map_err(ParseError::IO)?; metapb::Data::decode(&data[..]).map_err(ParseError::Proto) } fn enable_test_mode() -> Result<(), ParseError> { if std::env::var("ENCORE_APP_META").is_ok() || std::env::var("ENCORE_APP_META_PATH").is_ok() { return Ok(()); } let out = cmd!("encore", "test", "--prepare") .stdout_capture() .stderr_capture() .run() .map_err(ParseError::IO)?; if !out.status.success() { return Err(ParseError::IO(std::io::Error::other( String::from_utf8(out.stderr).unwrap(), ))); } let data = String::from_utf8(out.stdout).map_err(|e| ParseError::IO(std::io::Error::other(e)))?; for line in data.split('\n') { let Some((name, value)) = line.split_once('=') else { continue; }; match name { "ENCORE_APP_META" => std::env::set_var("ENCORE_APP_META", value), "ENCORE_RUNTIME_CONFIG" => std::env::set_var("ENCORE_RUNTIME_CONFIG", value), _ => (), } } Ok(()) } /// Describes which services or gateways are hosted by this server. #[derive(Debug, Clone)] pub struct Hosted(pub HashSet); impl Hosted { pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn iter(&self) -> impl Iterator { self.0.iter() } /// Reports whether the given service/entity is hosted by this runtime. pub fn contains(&self, name: &Q) -> bool where String: Borrow, Q: Eq + Hash + ?Sized, { self.0.contains(name) } } impl FromIterator for Hosted { fn from_iter>(iter: I) -> Self { Self(iter.into_iter().collect()) } } /// Returns the version of the Encore runtime. pub fn version() -> &'static str { option_env!("ENCORE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) } /// Returns the git commit used to build the Encore runtime. pub fn build_commit() -> &'static str { env!("ENCORE_BINARY_GIT_HASH") } ================================================ FILE: runtimes/core/src/log/consolewriter.rs ================================================ use crate::error::{write_stack_trace, StackFrame}; use crate::log::fields::FieldConfig; use crate::log::writers::Writer; use anyhow::Context; use chrono::Timelike; use colored::Colorize; use serde_json::Value; use std::cell::RefCell; use std::collections::BTreeMap; use std::io::Write; use std::sync::Mutex; pub struct ConsoleWriter { field_config: &'static FieldConfig, mu: Mutex>>, } impl ConsoleWriter { pub fn new(field_config: &'static FieldConfig, w: W) -> Self { Self { field_config, mu: Mutex::new(RefCell::new(Box::new(w))), } } fn write_fields( &self, buf: &mut Vec, values: &BTreeMap, ) -> anyhow::Result<()> { // error field is always first if let Some(err) = values.get(self.field_config.error_field_name) { if !buf.is_empty() { buf.push(b' '); } write!( buf, "{}{}", format!("{}=", self.field_config.error_field_name).cyan(), format!("{err}").red() ) .context("unable to write error field value")?; } for key in values.keys() { if key == self.field_config.timestamp_field_name || key == self.field_config.level_field_name || key == self.field_config.caller_field_name || key == self.field_config.message_field_name || key == self.field_config.error_field_name || key == self.field_config.stack_trace_field_name { continue; } if !buf.is_empty() { buf.push(b' '); } write!(buf, "{}", format!("{key}=").cyan()) .context(format!("unable to write field key {key}"))?; let value = values.get(key).expect("key not found"); let value_to_print = match value { // Strings have special handling, as if the string does not contain any special characters or whitespace // we just print the string, otherwise we print the string as a JSON string (i.e. wrapped in quotes and escaped) Value::String(s) => { if s.contains(' ') || s.contains('\t') || s.contains('\n') || s.contains('\r') || s.contains('\\') || s.contains('"') { format!("{value}") } else { s.to_string() } } _ => format!("{value}"), }; write!(buf, "{value_to_print}") .context(format!("unable to write field value {key}"))?; } // Finally, write the stack trace to the log if let Some(stack) = values.get(self.field_config.stack_trace_field_name) { let stack: Result, serde_json::Error> = serde_json::from_value(stack.clone()); if let Ok(stack) = stack { let mut writer = VecWriter { buf }; write_stack_trace(&stack, &mut writer).context("unable to write stack trace")?; } } Ok(()) } } struct VecWriter<'a> { buf: &'a mut Vec, } impl std::fmt::Write for VecWriter<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.buf.extend_from_slice(s.as_bytes()); Ok(()) } } impl Writer for ConsoleWriter { fn write(&self, level: log::Level, values: &BTreeMap) -> anyhow::Result<()> { let mut buf = Vec::with_capacity(256); write_part( &mut buf, self.field_config.timestamp_field_name, values, format_timestamp, )?; write_level(&mut buf, level)?; write_part( &mut buf, self.field_config.caller_field_name, values, format_caller, )?; write_part( &mut buf, self.field_config.message_field_name, values, format_message, )?; self.write_fields(&mut buf, values)?; buf.write_all(b"\n").context("new line")?; match self.mu.lock() { Ok(guard) => { let mut w = guard .try_borrow_mut() .context("unable to borrow console output")?; w.write_all(&buf).context("write")?; Ok(()) } Err(poisoned) => Err(anyhow::anyhow!("poisoned mutex: {:?}", poisoned)), } } } fn write_part( buf: &mut Vec, field: &'static str, values: &BTreeMap, mapper: fn(&str) -> anyhow::Result, ) -> anyhow::Result<()> { if let Some(value) = values.get(field) { if let Some(value) = value.as_str() { let value = mapper(value).context(format!("unable to map part {field}"))?; if !buf.is_empty() { buf.push(b' '); } write!(buf, "{value}").context(format!("unable to write part {field}"))?; } } Ok(()) } fn format_timestamp(timestamp: &str) -> anyhow::Result { let timestamp = chrono::DateTime::parse_from_rfc3339(timestamp) .context(format!("unable to parse timestamp: {timestamp}"))?; let datetime: chrono::DateTime = timestamp.into(); let (is_pm, hour) = datetime.hour12(); let minute = datetime.minute(); let mut timestamp = String::with_capacity(32); timestamp.push_str(&format!("{hour:02}:{minute:02}")); if is_pm { timestamp.push_str("PM"); } else { timestamp.push_str("AM"); } Ok(format!("{}", timestamp.bright_black())) } fn write_level(buf: &mut Vec, level: log::Level) -> anyhow::Result<()> { let level_str = match level { log::Level::Trace => "TRC".magenta(), log::Level::Debug => "DBG".yellow(), log::Level::Info => "INF".green(), log::Level::Warn => "WRN".red(), log::Level::Error => "ERR".red().bold(), }; if !buf.is_empty() { buf.push(b' '); } write!(buf, "{level_str}").context("unable to write log level")?; Ok(()) } fn format_caller(caller: &str) -> anyhow::Result { Ok(format!("{} {}", caller.bold(), ">".cyan())) } fn format_message(message: &str) -> anyhow::Result { Ok(message.to_string()) } ================================================ FILE: runtimes/core/src/log/fields.rs ================================================ /// Fields control the names of the fields that are used in the log output. #[derive(Debug)] pub struct FieldConfig { pub timestamp_field_name: &'static str, pub level_field_name: &'static str, pub level_trace_value: &'static str, pub level_debug_value: &'static str, pub level_info_value: &'static str, pub level_warn_value: &'static str, pub level_error_value: &'static str, pub level_fatal_value: &'static str, pub message_field_name: &'static str, pub error_field_name: &'static str, pub caller_field_name: &'static str, pub stack_trace_field_name: &'static str, } pub static DEFAULT_FIELDS: FieldConfig = FieldConfig { timestamp_field_name: "time", level_field_name: "level", level_trace_value: "trace", level_debug_value: "debug", level_info_value: "info", level_warn_value: "warn", level_error_value: "error", level_fatal_value: "fatal", message_field_name: "message", error_field_name: "error", caller_field_name: "caller", stack_trace_field_name: "stack", }; pub static GCP_FIELDS: FieldConfig = FieldConfig { timestamp_field_name: "timestamp", level_field_name: "severity", level_trace_value: "DEBUG", level_debug_value: "DEBUG", level_info_value: "INFO", level_warn_value: "WARNING", level_error_value: "ERROR", level_fatal_value: "CRITICAL", message_field_name: "message", error_field_name: "error", caller_field_name: "caller", stack_trace_field_name: "stacktrace", }; impl FieldConfig { pub fn default() -> &'static FieldConfig { // If we're running in GCP, then we'll use the GCP fields. for var in &[ "GCP_PROJECT", "GOOGLE_CLOUD_PROJECT", "GCP_METADATA_PROJECT", ] { if std::env::var(var).is_ok() { return &GCP_FIELDS; } } &DEFAULT_FIELDS } } ================================================ FILE: runtimes/core/src/log/logger.rs ================================================ use crate::error::AppError; use crate::log::fields::FieldConfig; use crate::log::writers::{default_writer, Writer}; use crate::model::{self, LogField}; use crate::trace::protocol::LogMessageData; use crate::trace::Tracer; use anyhow::Context; use env_logger::filter::Filter; use log::{Log, Metadata, Record}; use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; use std::time::SystemTime; pub type Fields = BTreeMap; /// Logger is a structured JSON logger that can be used to emit structured logs to stderr #[derive(Debug, Clone)] pub struct Logger { filter: Arc, app_level: log::LevelFilter, field_config: &'static FieldConfig, writer: Arc, extra_fields: Fields, tracer: Arc>, } impl Logger { /// New returns a new logger with the given field config. pub fn new( app_level: log::LevelFilter, filter: Filter, field_config: &'static FieldConfig, ) -> Self { Self { filter: Arc::new(filter), app_level, field_config, writer: default_writer(field_config), extra_fields: Fields::new(), tracer: Arc::new(RwLock::new(Tracer::noop())), } } /// Sets the loggers tracer pub fn set_tracer(&self, tracer: Tracer) { let mut t = self.tracer.write().expect("tracer lock poisoned"); *t = tracer; } /// Returns a new logger with the given log level. pub fn with_level(&self, level: log::LevelFilter) -> Self { Self { app_level: level, ..self.clone() } } /// Returns a new logger with the given writer. pub fn with_writer(&self, writer: Arc) -> Self { Self { writer, ..self.clone() } } /// Returns a new logger with the given fields added to the context /// that the logger will use when emitting logs as extra fields pub fn with(&self, fields: Fields) -> Self { let mut replacement = self.clone(); for (key, value) in fields.iter() { replacement .extra_fields .insert(key.to_string(), value.clone()); } replacement } /// Returns the current log level as expected by the `log` crate. fn level_to_value(&self, level: ::log::Level) -> serde_json::Value { serde_json::Value::from(match level { ::log::Level::Trace => self.field_config.level_trace_value, ::log::Level::Debug => self.field_config.level_debug_value, ::log::Level::Info => self.field_config.level_info_value, ::log::Level::Warn => self.field_config.level_warn_value, ::log::Level::Error => self.field_config.level_error_value, }) } /// Takes the given message and attempts to log it to the configured writer. fn try_log( &self, request: Option<&model::Request>, level: log::Level, msg: String, error: Option, caller: Option, fields: Option, ) -> anyhow::Result<()> { self.write_to_trace(request, level, &msg, &fields); let mut values = Fields::new(); // Copy the extra fields into the values map. for (key, value) in self.extra_fields.iter() { values.insert(key.to_string(), value.clone()); } // Copy the fields from the logger into the values map. if let Some(fields) = fields { values.extend(fields); } // If we have a caller field, add it to the values map. if let Some(caller) = caller { values.insert( self.field_config.caller_field_name.to_string(), serde_json::Value::from(caller), ); } // If we have an error field, then let's add it if let Some(error) = error { values.insert( self.field_config.error_field_name.to_string(), serde_json::Value::from(error.message), ); if !error.stack.is_empty() { values.insert( self.field_config.stack_trace_field_name.to_string(), serde_json::to_value(error.stack)?, ); } } // Now add the standard fields. values.insert( self.field_config.level_field_name.to_string(), self.level_to_value(level), ); values.insert( self.field_config.timestamp_field_name.to_string(), iso8601_now(), ); values.insert( self.field_config.message_field_name.to_string(), serde_json::Value::from(msg), ); if let Some(req) = request { match &req.data { model::RequestData::RPC(rpc) => { let ep = &rpc.endpoint.name; values.insert( "service".into(), serde_json::Value::String(ep.service().to_string()), ); values.insert( "endpoint".into(), serde_json::Value::String(ep.endpoint().to_string()), ); if let Some(uid) = &rpc.auth_user_id { values.insert("uid".into(), serde_json::Value::String(uid.clone())); } } model::RequestData::Auth(auth) => { let ep = &auth.auth_handler; values.insert( "service".into(), serde_json::Value::String(ep.service().to_string()), ); values.insert( "endpoint".into(), serde_json::Value::String(ep.endpoint().to_string()), ); } model::RequestData::PubSub(msg) => { values.insert( "service".into(), serde_json::Value::String(msg.service.to_string()), ); values.insert( "topic".into(), serde_json::Value::String(msg.topic.to_string()), ); values.insert( "subscription".into(), serde_json::Value::String(msg.subscription.to_string()), ); } model::RequestData::Stream(data) => { let ep = &data.endpoint.name; values.insert( "service".into(), serde_json::Value::String(ep.service().to_string()), ); values.insert( "endpoint".into(), serde_json::Value::String(ep.endpoint().to_string()), ); } }; values.insert( "trace_id".into(), serde_json::Value::String(req.span.0.serialize_encore()), ); values.insert( "span_id".into(), serde_json::Value::String(req.span.1.serialize_encore()), ); if let Some(corr_id) = &req.ext_correlation_id { values.insert( "x_correlation_id".into(), serde_json::Value::String(corr_id.clone()), ); } else if let Some(parent_trace) = &req.parent_trace { values.insert( "x_correlation_id".into(), serde_json::Value::String(parent_trace.serialize_encore()), ); } } // Now write the log to the configured writer. self.writer .write(level, &values) .context("unable to write")?; Ok(()) } /// Takes a `log::Record` and attempts to log it to the configured writer. fn try_log_record(&self, record: &Record) -> anyhow::Result<()> { let kvs = record.key_values(); let mut visitor = KeyValueVisitor(BTreeMap::new()); let _ = kvs.visit(&mut visitor); let msg = match record.args().as_str() { Some(msg) => msg.to_string(), None => record.args().to_string(), }; let caller = match (record.module_path(), record.file(), record.line()) { (Some(module), _, _) => Some(module.to_string()), (_, Some(file), Some(line)) => Some(format!("{file}:{line}")), _ => None, }; self.try_log(None, record.level(), msg, None, caller, Some(visitor.0)) } /// Writes the log to trace fn write_to_trace( &self, request: Option<&model::Request>, level: log::Level, msg: &str, fields: &Option, ) { let fields = if !self.extra_fields.is_empty() || fields.is_some() { let field_vec: Vec<_> = self .extra_fields .iter() .chain(fields.iter().flat_map(|f| f.iter())) .map(|(key, val)| match val { serde_json::Value::Number(num) => { if num.is_i64() { LogField { key, value: model::LogFieldValue::I64(num.as_i64().unwrap()), } } else if num.is_u64() { LogField { key, value: model::LogFieldValue::U64(num.as_u64().unwrap()), } } else if num.is_f64() { LogField { key, value: model::LogFieldValue::F64(num.as_f64().unwrap()), } } else { // this can't happen as we have handle all the cases above, // but we need to handle this case for the iterator to function LogField { key, value: model::LogFieldValue::Json(&serde_json::Value::Null), } } } serde_json::Value::Bool(b) => LogField { key, value: model::LogFieldValue::Bool(*b), }, serde_json::Value::String(ref str) => LogField { key, value: model::LogFieldValue::String(str), }, serde_json::Value::Array(_) | serde_json::Value::Object(_) | serde_json::Value::Null => LogField { key, value: model::LogFieldValue::Json(val), }, }) .collect(); Some(field_vec.into_iter()) } else { None }; self.tracer .read() .expect("tracer lock poisoned") .log_message(LogMessageData { source: request, msg, level, fields, }); } } /// This trait defines the logging functions that are available on the `Logger` type. /// /// It is used to allow Rust code to emit structured logs via our `Logger` implementation /// as it will automatically capture the caller location. pub trait LogFromRust { fn trace(&self, req: Option<&model::Request>, msg: T, fields: Option); fn debug(&self, req: Option<&model::Request>, msg: T, fields: Option); fn info(&self, req: Option<&model::Request>, msg: T, fields: Option); fn warn>( &self, req: Option<&model::Request>, msg: T, error: Option, fields: Option, ); fn error>( &self, req: Option<&model::Request>, msg: T, error: Option, fields: Option, ); } /// This trait defines the logging functions that are available on the `Logger` type. /// /// It is used to allow other languages to emit structured logs via our `Logger` implementation /// with them passing in their own caller location. pub trait LogFromExternalRuntime { fn log>( &self, request: Option<&model::Request>, level: log::Level, msg: T, error: Option, caller: Option, fields: Option, ) -> anyhow::Result<()>; } impl LogFromExternalRuntime for Logger where T: std::fmt::Display, { /// Logs the given message at the trace level fn log>( &self, request: Option<&model::Request>, level: log::Level, msg: T, error: Option, caller: Option, fields: Option, ) -> anyhow::Result<()> { if level > self.app_level { return Ok(()); } self.try_log( request, level, msg.to_string(), error.map(|e| e.into().trim_stack(file!(), line!(), 1)), caller, fields, ) } } impl LogFromRust for Logger where T: std::fmt::Display, { #[track_caller] fn trace(&self, req: Option<&model::Request>, msg: T, fields: Option) { if log::Level::Trace > self.app_level { return; } self.try_log(req, log::Level::Trace, msg.to_string(), None, None, fields) .expect("failed to log"); } #[track_caller] fn debug(&self, req: Option<&model::Request>, msg: T, fields: Option) { if log::Level::Debug > self.app_level { return; } self.try_log(req, log::Level::Debug, msg.to_string(), None, None, fields) .expect("failed to log"); } #[track_caller] fn info(&self, req: Option<&model::Request>, msg: T, fields: Option) { if log::Level::Info > self.app_level { return; } self.try_log(req, log::Level::Info, msg.to_string(), None, None, fields) .expect("failed to log"); } #[track_caller] fn warn>( &self, req: Option<&model::Request>, msg: T, error: Option, fields: Option, ) { if log::Level::Warn > self.app_level { return; } self.try_log( req, log::Level::Warn, msg.to_string(), error.map(|e| e.into().trim_stack(file!(), line!(), 1)), None, fields, ) .expect("failed to log"); } #[track_caller] fn error>( &self, req: Option<&model::Request>, msg: T, error: Option, fields: Option, ) { if log::Level::Error > self.app_level { return; } self.try_log( req, log::Level::Error, msg.to_string(), error.map(|e| e.into().trim_stack(file!(), line!(), 1)), None, fields, ) .expect("failed to log"); } } /// Returns the current unix timestamp in milliseconds. #[inline] pub fn iso8601_now() -> serde_json::Value { let now = SystemTime::now(); let date = chrono::DateTime::::from(now); serde_json::Value::from(date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)) } /// Implement the `Log` trait for `Logger` which allows other creates which use the `log` facade /// crate to emit structured logs via our `Logger` implementation. impl Log for Logger { fn enabled(&self, metadata: &Metadata) -> bool { self.filter.enabled(metadata) } fn log(&self, record: &Record) { if self.enabled(record.metadata()) { self.try_log_record(record).unwrap_or_else(|e| { eprintln!("failed to log: {e}"); }); } } fn flush(&self) {} } /// A visitor that can be used to visit key-value pairs and insert them into a `BTreeMap`. /// after converting them from the `log::kv::Value` type to `serde_json::Value`. struct KeyValueVisitor(BTreeMap); impl log::kv::Visitor<'_> for KeyValueVisitor { #[inline] fn visit_pair( &mut self, key: log::kv::Key, value: log::kv::Value, ) -> Result<(), log::kv::Error> { match serde_json::to_value(value) { Ok(value) => { self.0.insert(key.to_string(), value); Ok(()) } Err(e) => Err(log::kv::Error::boxed(e)), } } } ================================================ FILE: runtimes/core/src/log/mod.rs ================================================ use once_cell::sync::OnceCell; mod consolewriter; mod fields; mod logger; mod writers; use crate::log::fields::FieldConfig; pub use logger::{Fields, LogFromExternalRuntime, LogFromRust, Logger}; use crate::trace::Tracer; /// The global root logger instance that is used by both the `log` crate /// and all other code in the Encore runtime. static ROOT: OnceCell<&Logger> = OnceCell::new(); /// Initialize the global logger with the `root()` instance /// /// This function is idempotent and will not re-initialize the logger /// if it has already been initialized. pub fn init() { // Initialize the logger first. _ = root(); // Set a custom panic hook to ensure panics are logged at error level. // We write directly to stderr in JSON format to ensure the message // is properly captured by log aggregators like Cloud Run. std::panic::set_hook(Box::new(|info| { use std::io::Write; let msg = info.to_string(); let location = info .location() .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())); // Write JSON directly to stderr to ensure proper log level detection. let json = serde_json::json!({ "level": "error", "severity": "ERROR", "message": msg, "caller": location, "time": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), }); let _ = writeln!(std::io::stderr(), "{}", json); })); } /// Set the tracer on the global logger pub fn set_tracer(tracer: Tracer) { root().set_tracer(tracer); } /// Returns a reference to the global root logger instance. pub fn root() -> &'static Logger { ROOT.get_or_init(|| { let logger = { let fields = FieldConfig::default(); // Construct our rust log filter. let filter = { // If RUST_LOG is set, use that. let level = std::env::var("RUST_LOG").unwrap_or_else(|_| { // Otherwise use ENCORE_RUNTIME_LOG to set the Encore runtime log level, // which defaults let level = std::env::var("ENCORE_RUNTIME_LOG").unwrap_or("debug".to_string()); format!("encore_={level},pingora_core::listeners=warn,pingora_core::services::listening=warn,tokio_postgres::proxy={level},tokio_postgres::connect_proxy={level}") }); env_logger::filter::Builder::new().parse(&level).build() }; // Construct our app log level. let app_level: log::LevelFilter = std::env::var("ENCORE_LOG") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(log::LevelFilter::Trace); Logger::new(app_level, filter, fields) }; // Leak the logger to ensure it has a static lifetime. // We only do this once. let logger = Box::leak(Box::new(logger)); let disable_logging = std::env::var("ENCORE_NOLOG").is_ok_and(|v| !v.is_empty()); let filter = if disable_logging { log::LevelFilter::Off } else { log::LevelFilter::Trace }; #[cfg(feature = "rttrace")] { let filter = tracing_subscriber::EnvFilter::from_env("ENCORE_RUNTIME_TRACE"); tracing_subscriber::fmt() .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ENTER) .with_env_filter(filter) .with_writer(std::io::stderr) .init(); } log::set_max_level(filter); log::set_logger(logger).expect("unable to set global logger instance"); logger }) } ================================================ FILE: runtimes/core/src/log/writers.rs ================================================ use crate::log::consolewriter::ConsoleWriter; use crate::log::fields::FieldConfig; use anyhow::Context; use serde_json::Value; use std::collections::BTreeMap; use std::env; use std::fmt::Debug; use std::io::{IoSlice, Write}; use std::sync::mpsc::{self, Receiver, RecvError, SyncSender, TryRecvError}; use std::sync::Arc; use std::time::Duration; /// A log writer. pub trait Writer: Send + Sync + 'static { /// Write the given key-value pairs to the log. fn write(&self, level: log::Level, values: &BTreeMap) -> anyhow::Result<()>; } impl Debug for dyn Writer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Writer").finish() } } /// default_writer returns the default writer based on the environment. /// /// If the `ENCORE_LOG_FORMAT` environment variable is set to `console` then /// the pretty console writer will be used to write logs to stderr, otherwise /// JSONL logs will be written to stderr. /// /// For JSONL logs, if a tokio runtime is detected then the async writer /// will be used, otherwise a blocking writer will be used, resulting /// in blocking writes to stderr. pub fn default_writer(fields: &'static FieldConfig) -> Arc { // Check if the user has set the `ENCORE_LOG_FORMAT` environment variable to `console`. // if so we'll use the pretty console writer. for var in &["ENCORE_LOG_FORMAT"] { if let Ok(format) = env::var(var) { if format == "console" { return Arc::new(ConsoleWriter::new(fields, std::io::stderr())); } } } Arc::new(ActorWriter::default()) } // ActorWriter creates a bounded channel that sends log data to a separate thread that handles the writing. pub struct ActorWriter { sender: SyncSender>, } impl ActorWriter { pub fn new(mut writer: W) -> Self { let (sender, recv) = mpsc::sync_channel::>(10_000); std::thread::spawn(move || { while let Ok(bytes) = Self::recv_batch(&recv) { Self::write_batch_with_retry(&mut writer, &bytes); } }); Self { sender } } fn recv_batch(recv: &Receiver>) -> Result>, RecvError> { const MAX_BATCH_SIZE: usize = 256; // wait for a log message let mut bufs = vec![recv.recv()?]; // receive logs until channel is empty or max batch size is reached loop { match recv.try_recv() { Ok(log) => { bufs.push(log); if bufs.len() >= MAX_BATCH_SIZE { break; } } // on error, break the loop and return the bufs that we have already collected. Err(TryRecvError::Disconnected) => break, Err(TryRecvError::Empty) => break, } } Ok(bufs) } fn write_batch_with_retry(writer: &mut W, bufs: &[Vec]) { const INITIAL_DELAY_MS: u64 = 1; const MAX_DELAY_MS: u64 = 1000; let mut io_slices = bufs .iter() .map(|buf| IoSlice::new(buf)) .collect::>(); let mut bufs = &mut io_slices[..]; // Guarantee that bufs is empty if it contains no data, // to avoid calling write_vectored if there is no data to be written. IoSlice::advance_slices(&mut bufs, 0); let mut delay_ms = INITIAL_DELAY_MS; while !bufs.is_empty() { match writer.write_vectored(bufs) { Ok(0) | Err(_) => { std::thread::sleep(Duration::from_millis(delay_ms)); delay_ms = u64::min(delay_ms * 2, MAX_DELAY_MS); } Ok(n) => { delay_ms = INITIAL_DELAY_MS; IoSlice::advance_slices(&mut bufs, n) } } } } } impl Writer for ActorWriter { fn write(&self, _: log::Level, values: &BTreeMap) -> anyhow::Result<()> { let mut buf = Vec::with_capacity(256); serde_json::to_writer(&mut buf, values) .map_err(std::io::Error::from) .context("serde_writer")?; buf.extend_from_slice(b"\n"); self.sender.send(buf)?; Ok(()) } } impl Default for ActorWriter { fn default() -> Self { Self::new(std::io::stderr()) } } ================================================ FILE: runtimes/core/src/meta/mod.rs ================================================ use serde::Serialize; use crate::encore::parser::meta::v1 as meta; use crate::encore::runtime::v1 as rt; #[derive(Debug, Clone, Default, Serialize)] pub struct AppMeta { /// The Encore application ID. If the application is not linked to the Encore platform this will be an empty string. /// To link to the Encore platform run `encore app link` from your terminal in the root directory of the Encore app. pub app_id: String, /// The base URL which can be used to call the API of this running application. /// /// For local development it is "http://localhost:", typically "http://localhost:4000". /// /// If a custom domain is used for this environment it is returned here, but note that /// changes only take effect at the time of deployment while custom domains can be updated at any time. pub api_base_url: String, /// Information about the environment the app is running in. pub environment: EnvironmentMeta, /// Information about the build. pub build: BuildMeta, /// Information about the deployment. pub deploy: DeployMeta, } impl AppMeta { pub fn new(rt: &rt::RuntimeConfig, md: &meta::Data) -> AppMeta { let env = rt.environment.as_ref(); let app_id = env.map(|e| e.app_id.clone()).unwrap_or_default(); let api_base_url = rt .infra .as_ref() .and_then(|infra| infra.resources.as_ref()) .and_then(|res| res.gateways.first()) .map(|gw| gw.base_url.clone()) .unwrap_or_default(); AppMeta { app_id, api_base_url, environment: env.map_or_else(EnvironmentMeta::default, EnvironmentMeta::from), build: BuildMeta::from(md), deploy: rt .deployment .as_ref() .map_or_else(DeployMeta::default, DeployMeta::from), } } } #[derive(Debug, Clone, Default, Serialize)] pub struct EnvironmentMeta { /// The name of environment that this application. /// For local development it is "local". pub name: String, /// The type of environment is this application running in. /// For local development it is "development". pub r#type: EnvironmentType, /// The cloud this is running in. /// For local development it is "local". pub cloud: CloudProvider, } impl From<&rt::Environment> for EnvironmentMeta { fn from(env: &rt::Environment) -> Self { let env_type = rt::environment::Type::try_from(env.env_type) .unwrap_or(rt::environment::Type::Unspecified); let cloud = rt::environment::Cloud::try_from(env.cloud) .unwrap_or(rt::environment::Cloud::Unspecified); EnvironmentMeta { name: env.env_name.clone(), r#type: EnvironmentType::from(&env_type), cloud: CloudProvider::from(&cloud), } } } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum EnvironmentType { // A production environment. Production, // A long-lived cloud-hosted, non-production environment, such as test environments, or local development. #[default] Development, // A short-lived cloud-hosted, non-production environments, such as preview environments Ephemeral, // When running automated tests. Test, } impl From<&rt::environment::Type> for EnvironmentType { fn from(env: &rt::environment::Type) -> Self { use rt::environment::Type::*; match env { Production => Self::Production, Ephemeral => Self::Ephemeral, Test => Self::Test, Development | Unspecified => Self::Development, } } } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum CloudProvider { AWS, GCP, Azure, Encore, #[default] Local, } impl From<&rt::environment::Cloud> for CloudProvider { fn from(cloud: &rt::environment::Cloud) -> Self { use rt::environment::Cloud::*; match cloud { Aws => Self::AWS, Gcp => Self::GCP, Azure => Self::Azure, Encore => Self::Encore, Local | Unspecified => Self::Local, } } } #[derive(Debug, Clone, Default, Serialize)] pub struct BuildMeta { // The git commit that formed the base of this build. pub revision: String, // Whether there were uncommitted changes on top of the commit. pub uncommitted_changes: bool, } impl From<&meta::Data> for BuildMeta { fn from(md: &meta::Data) -> Self { BuildMeta { revision: md.app_revision.clone(), uncommitted_changes: md.uncommitted_changes, } } } #[derive(Debug, Clone, Serialize)] pub struct HostedService { // The name of the service pub name: String, } #[derive(Debug, Clone, Serialize)] pub struct DeployMeta { // The unique id of the deployment. Generated by the Encore Platform. pub id: String, // The time the deployment was made. pub deploy_time: chrono::DateTime, // The services hosted by this deployment. pub hosted_services: Vec, } impl From<&rt::Deployment> for DeployMeta { fn from(rt: &rt::Deployment) -> Self { DeployMeta { id: rt.deploy_id.clone(), deploy_time: rt .deployed_at .as_ref() .and_then(|d| chrono::DateTime::from_timestamp(d.seconds, d.nanos as u32)) .unwrap_or_else(chrono::Utc::now), hosted_services: rt .hosted_services .iter() .map(|s| HostedService { name: s.name.clone(), }) .collect(), } } } impl Default for DeployMeta { fn default() -> Self { DeployMeta { id: "".into(), deploy_time: chrono::Utc::now(), hosted_services: vec![], } } } ================================================ FILE: runtimes/core/src/metadata/aws.rs ================================================ use std::time::Duration; use anyhow::Context; #[derive(serde::Deserialize)] pub struct AwsTaskMeta { #[serde(rename = "ServiceName")] pub service_name: String, #[serde(rename = "Revision")] pub revision: String, #[serde(rename = "TaskARN")] pub task_arn: String, } #[derive(Debug)] pub struct AwsMetadataClient { http_client: reqwest::Client, metadata_uri: String, } impl AwsMetadataClient { pub fn new(http_client: reqwest::Client, metadata_uri: String) -> Self { AwsMetadataClient { http_client, metadata_uri, } } pub async fn fetch_task_meta(&self) -> anyhow::Result { let req = self .http_client .get(format!("{}/task", self.metadata_uri)) .timeout(Duration::from_secs(30)) .build() .context("create metadata request")?; let resp = self .http_client .execute(req) .await .context("send metadata request")?; let task_meta = resp .json::() .await .context("deserialize task metadata")?; Ok(task_meta) } } ================================================ FILE: runtimes/core/src/metadata/gce.rs ================================================ //! GCE metadata client implementation based on golangs "cloud.google.com/go/compute/metadata" use std::time::Duration; use tokio::sync::OnceCell; #[derive(Debug, thiserror::Error)] pub enum GceMetadataError { #[error("HTTP request failed: {0}")] HttpRequest(#[from] reqwest::Error), #[error("GCE metadata not defined (404)")] NotDefined, #[error("GCE metadata server temporarily unavailable (503)")] ServiceUnavailable, #[error("GCE metadata server returned error status: {status}")] HttpStatus { status: reqwest::StatusCode }, #[error("Failed to read response body: {0}")] ResponseBody(reqwest::Error), } // Default metadata server IP as documented by Google const METADATA_IP: &str = "169.254.169.254"; // Env to override metadata server host, if not set the METADATA_IP will be used const METADATA_HOST_ENV: &str = "GCE_METADATA_HOST"; const METADATA_FLAVOR_HEADER: &str = "Metadata-Flavor"; const GOOGLE_HEADER_VALUE: &str = "Google"; const USER_AGENT: &str = "encore-runtime/0.1.0"; // Timeout and retry configuration const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); const MAX_RETRIES: usize = 3; // Global cache for instance ID to ensure it's shared across all calls static INSTANCE_ID_CACHE: OnceCell = OnceCell::const_new(); #[derive(Debug)] pub struct GceMetadataClient { http_client: reqwest::Client, } impl GceMetadataClient { pub fn new(http_client: reqwest::Client) -> Self { Self { http_client } } /// Build metadata URL, checking GCE_METADATA_HOST environment variable first fn build_metadata_url(path: &str) -> String { let host = std::env::var(METADATA_HOST_ENV).unwrap_or_else(|_| METADATA_IP.to_string()); let path = path.trim_start_matches('/'); // Remove leading slashes like Go does format!("http://{}/computeMetadata/v1/{}", host, path) } /// Get the instance ID from GCE metadata server, with global caching /// This ensures only one HTTP request is made even with concurrent calls pub async fn instance_id(&self) -> Result { let instance_id = INSTANCE_ID_CACHE .get_or_try_init(|| async { self.fetch_metadata("instance/id").await }) .await?; Ok(instance_id.clone()) } /// Fetch metadata from the GCE metadata server pub async fn fetch_metadata(&self, path: &str) -> Result { let url = Self::build_metadata_url(path); for attempt in 1..=MAX_RETRIES { let result = self.try_fetch(&url).await; match &result { Ok(_) => { return result; } Err(e) if attempt == MAX_RETRIES => { log::error!( "Failed to fetch GCE metadata after {} attempts: {}", MAX_RETRIES, e ); return result; } Err(e) => { log::warn!("Attempt {} failed: {}, retrying...", attempt, e); tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await; } } } unreachable!("unexpected failure fetching metadata") } async fn try_fetch(&self, url: &str) -> Result { let response = self .http_client .get(url) .header(METADATA_FLAVOR_HEADER, GOOGLE_HEADER_VALUE) .header(http::header::USER_AGENT, USER_AGENT) .timeout(REQUEST_TIMEOUT) .send() .await?; let status = response.status(); if status.is_success() { let text = response .text() .await .map_err(GceMetadataError::ResponseBody)?; Ok(text.trim().to_string()) } else if status == reqwest::StatusCode::NOT_FOUND { Err(GceMetadataError::NotDefined) } else if status == reqwest::StatusCode::SERVICE_UNAVAILABLE { Err(GceMetadataError::ServiceUnavailable) } else { Err(GceMetadataError::HttpStatus { status }) } } } ================================================ FILE: runtimes/core/src/metadata/mod.rs ================================================ use std::collections::HashMap; use crate::{ encore::runtime::v1::{environment::Cloud, Environment}, metadata::aws::AwsMetadataClient, }; use anyhow::Context; use tokio::sync::OnceCell; mod aws; mod gce; #[derive(Debug)] pub struct ContainerMetaClient { cell: OnceCell, env: Environment, http_client: reqwest::Client, fallback: ContainerMetadata, } impl ContainerMetaClient { pub fn new(env: Environment, http_client: reqwest::Client) -> Self { Self { cell: OnceCell::new(), fallback: ContainerMetadata { env_name: env.env_name.clone(), ..Default::default() }, env, http_client, } } pub async fn collect(&self) -> anyhow::Result<&ContainerMetadata> { self.cell .get_or_try_init(|| async { ContainerMetadata::collect(&self.env, &self.http_client).await }) .await } pub fn fallback(&self) -> &ContainerMetadata { &self.fallback } } #[derive(Debug, Clone, Default)] pub struct ContainerMetadata { pub service_id: String, pub revision_id: String, pub instance_id: String, pub env_name: String, } impl ContainerMetadata { pub fn labels(&self) -> Vec<(String, String)> { vec![ ("service_id".to_string(), self.service_id.clone()), ("revision_id".to_string(), self.revision_id.clone()), ("instance_id".to_string(), self.instance_id.clone()), ("env_name".to_string(), self.env_name.clone()), ] } pub async fn collect(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { match env.cloud() { Cloud::Gcp | Cloud::Encore => Self::collect_gcp(env, http_client).await, Cloud::Aws => Self::collect_aws(env, http_client).await, Cloud::Azure | Cloud::Unspecified | Cloud::Local => anyhow::bail!( "can't collect container meta in {}", env.cloud().as_str_name() ), } } async fn collect_aws(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { // Encore supports running on both ECS Fargate and EKS. // For Fargate, we can get the metadata from the ECS metadata service. // For EKS there doesn't appear to be a standard way to get the metadata, so skip it in that case. let metadata_uri = std::env::var("ECS_CONTAINER_METADATA_URI_V4") .map_err(|_| anyhow::anyhow!("unable to get ecs container metadata uri"))?; let client = AwsMetadataClient::new(http_client.clone(), metadata_uri); let task_meta = client.fetch_task_meta().await?; let instance_id = task_meta .task_arn .get(task_meta.task_arn.len().saturating_sub(8)..) .unwrap_or(&task_meta.task_arn) .to_string(); Ok(Self { service_id: task_meta.service_name, revision_id: task_meta.revision, instance_id, env_name: env.env_name.clone(), }) } async fn collect_gcp(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { let service = std::env::var("K_SERVICE").map_err(|_| { anyhow::anyhow!("unable to get service ID: env variable 'K_SERVICE' unset") })?; let revision = std::env::var("K_REVISION").map_err(|_| { anyhow::anyhow!("unable to get revision ID: env variable 'K_REVISION' unset") })?; let revision = revision .strip_prefix(&format!("{}-", service)) .unwrap_or(&revision) .to_string(); let instance_id = match std::env::var("K_POD") { Ok(pod_id) => { // If we have a K8s POD name, take the last part of it which is the random pod ID // On GKE, the InstanceID appears to be the Node, so if the multiple replicas are running // on the same InstanceID then we'd have a collision. This is unlikely, but possible - // hence why we use the pod ID instead. pod_id .rsplit('-') .next() .ok_or_else(|| anyhow::anyhow!("invalid instance ID '{}'", pod_id))? .to_string() } Err(_) => { // If we don't have a K8s POD name, we're running on Cloud Run and can get the instance ID from the metadata server let metadata_client = gce::GceMetadataClient::new(http_client.clone()); metadata_client .instance_id() .await .context("failed to get instance ID from GCE metadata server")? } }; Ok(Self { service_id: service, revision_id: revision, instance_id, env_name: env.env_name.clone(), }) } } /// Process environment variable substitution in labels /// Replaces $ENV:VARIABLE_NAME with the actual environment variable value pub fn process_env_substitution(labels: &mut HashMap) { for (_, value) in labels.iter_mut() { if value.starts_with("$ENV:") { let env_var = &value[5..]; if let Ok(env_value) = std::env::var(env_var) { *value = env_value; } } } } ================================================ FILE: runtimes/core/src/metrics/atomic.rs ================================================ use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; use crate::metrics::{CounterOps, GaugeOps}; impl CounterOps for AtomicU64 { fn increment(&self, value: u64) { self.fetch_add(value, Ordering::Release); } fn get(&self) -> crate::metrics::MetricValue { crate::metrics::MetricValue::CounterU64(self.load(Ordering::Acquire)) } } impl CounterOps for AtomicU64 { fn increment(&self, value: i64) { self.fetch_add(value as u64, Ordering::Release); } fn get(&self) -> crate::metrics::MetricValue { crate::metrics::MetricValue::CounterI64(self.load(Ordering::Acquire) as i64) } } impl CounterOps for Arc where AtomicU64: CounterOps, { fn increment(&self, value: T) { CounterOps::::increment(&(**self), value) } fn get(&self) -> crate::metrics::MetricValue { CounterOps::::get(&(**self)) } } impl GaugeOps for AtomicU64 { fn set(&self, value: u64) { self.swap(value, Ordering::AcqRel); } fn get(&self) -> crate::metrics::MetricValue { crate::metrics::MetricValue::GaugeU64(self.load(Ordering::Acquire)) } } impl GaugeOps for AtomicU64 { fn set(&self, value: i64) { self.swap(value as u64, Ordering::AcqRel); } fn get(&self) -> crate::metrics::MetricValue { crate::metrics::MetricValue::GaugeI64(self.load(Ordering::Acquire) as i64) } } impl GaugeOps for AtomicU64 { fn set(&self, value: f64) { self.swap(value.to_bits(), Ordering::AcqRel); } fn get(&self) -> crate::metrics::MetricValue { crate::metrics::MetricValue::GaugeF64(f64::from_bits(self.load(Ordering::Acquire))) } } impl GaugeOps for Arc where AtomicU64: GaugeOps, { fn set(&self, value: T) { GaugeOps::::set(&(**self), value) } fn get(&self) -> crate::metrics::MetricValue { GaugeOps::::get(&(**self)) } } ================================================ FILE: runtimes/core/src/metrics/counter.rs ================================================ use malachite::base::num::basic::traits::One; use crate::metrics; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::atomic::AtomicU64; use std::sync::Arc; pub trait CounterOps { fn increment(&self, value: T); fn get(&self) -> crate::metrics::MetricValue; } /// A typed counter that can be incremented /// T must be compatible with CounterOps for type-safe operations pub struct Counter { atomic: Arc, _phantom: PhantomData, } impl Counter where Arc: CounterOps, T: One, { /// Create a new counter with the given atomic storage /// This is typically called by Registry, not directly by users pub(crate) fn new(atomic: Arc) -> Self { Self { atomic, _phantom: PhantomData, } } /// Increment the counter by 1 pub fn increment(&self) { CounterOps::increment(&self.atomic, T::ONE); } /// Get the current value of the counter pub fn get(&self) -> metrics::MetricValue { CounterOps::get(&self.atomic) } } /// A counter schema that defines static labels and required dynamic label keys /// Validates dynamic labels at increment time and creates separate time series /// for each unique combination of static + dynamic labels #[derive(Clone, Debug)] pub struct Schema { name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, _phantom: PhantomData, } impl Schema where Arc: CounterOps, T: One + Send + Sync + 'static, { /// Create a new counter schema pub(crate) fn new( name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, ) -> Self { Self { name, static_labels, required_dynamic_keys, registry, _phantom: PhantomData, } } pub fn with(&self, dynamic_labels: L) -> Counter where L: IntoIterator, K: Into, V: Into, { // Convert dynamic_labels to HashMap first let dynamic_labels_map: HashMap = dynamic_labels .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(); // Validate required keys are present let missing: Vec = self .required_dynamic_keys .iter() .filter(|key| !dynamic_labels_map.contains_key(*key)) .cloned() .collect(); if !missing.is_empty() { log::warn!( "missing required dynamic labels for metric '{}': {:?}, required keys: {:?}", self.name, missing, self.required_dynamic_keys ); } self.get_or_create_counter(dynamic_labels_map) } /// Increment the counter with the given dynamic labels pub fn increment(&self) where T: One, { if !self.required_dynamic_keys.is_empty() { log::warn!( "incrementing counter '{}' without required dynamic labels, required keys: {:?}", self.name, self.required_dynamic_keys ); } self.get_or_create_counter(HashMap::new()).increment(); } /// Get or create a counter for the given dynamic labels fn get_or_create_counter(&self, dynamic_labels: HashMap) -> Counter { // Create merged labels (static + dynamic) let mut merged_labels = self.static_labels.clone(); for (key, value) in dynamic_labels { merged_labels.push((key, value)); } self.registry.get_or_create_counter( &self.name, merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())), ) } } /// Builder for creating counter schemas with static labels and required dynamic keys pub struct CounterSchemaBuilder { name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, _phantom: std::marker::PhantomData, } impl CounterSchemaBuilder where Arc: CounterOps, T: One + Send + Sync + 'static, { pub(crate) fn new(name: String, registry: Arc) -> Self { Self { name, static_labels: Vec::new(), required_dynamic_keys: HashSet::new(), registry, _phantom: std::marker::PhantomData, } } /// Add static labels that are set once when the schema is created pub fn static_labels(mut self, labels: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { for (key, value) in labels { self.static_labels .push((key.as_ref().to_string(), value.as_ref().to_string())); } self } /// Add a single static label pub fn static_label(mut self, key: &str, value: &str) -> Self { self.static_labels .push((key.to_string(), value.to_string())); self } /// Specify required dynamic label keys that must be provided at increment time pub fn require_dynamic_keys(mut self, keys: I) -> Self where I: IntoIterator, K: AsRef, { for key in keys { self.required_dynamic_keys.insert(key.as_ref().to_string()); } self } /// Add a single required dynamic key pub fn require_dynamic_key(mut self, key: &str) -> Self { self.required_dynamic_keys.insert(key.to_string()); self } /// Build the counter schema pub fn build(self) -> Schema { Schema::new( self.name, self.static_labels, self.required_dynamic_keys, self.registry, ) } } ================================================ FILE: runtimes/core/src/metrics/exporter/aws.rs ================================================ use crate::metadata::ContainerMetaClient; use crate::metrics::exporter::Exporter; use crate::metrics::{CollectedMetric, MetricValue}; use anyhow::Context; use aws_sdk_cloudwatch as cloudwatch; use aws_sdk_cloudwatch::types::{Dimension, MetricDatum}; use std::sync::Arc; use std::time::SystemTime; #[derive(Debug)] pub struct Aws { client: Arc, namespace: String, container_meta_client: ContainerMetaClient, container_dims: tokio::sync::OnceCell>>, } #[derive(Debug)] struct LazyCloudWatchClient { cell: tokio::sync::OnceCell>, } impl LazyCloudWatchClient { fn new() -> Self { Self { cell: tokio::sync::OnceCell::new(), } } async fn get(&self) -> &anyhow::Result { self.cell .get_or_init(|| async { let config = aws_config::defaults(aws_config::BehaviorVersion::v2025_08_07()) .load() .await; Ok(cloudwatch::Client::new(&config)) }) .await } } impl Aws { pub fn new(namespace: String, container_meta_client: ContainerMetaClient) -> Self { Self { client: Arc::new(LazyCloudWatchClient::new()), namespace, container_meta_client, container_dims: tokio::sync::OnceCell::new(), } } async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { if metrics.is_empty() { return Ok(()); } let client = match self.client.get().await { Ok(client) => client, Err(e) => { log::error!("failed to get CloudWatch client: {}", e); return Err(anyhow::anyhow!("failed to get CloudWatch client: {}", e)); } }; log::trace!( "Exporting {} metrics to AWS CloudWatch namespace {}", metrics.len(), self.namespace ); let metric_data = self.get_metric_data(metrics).await; // Send metrics in batches (CloudWatch allows up to 1000 metrics per request) for batch in metric_data.chunks(1000) { if let Err(e) = self.send_metric_batch(client, batch.to_vec()).await { log::error!("failed to export metrics batch: {}", e); } } Ok(()) } async fn container_dimensions(&self) -> Arc> { self.container_dims .get_or_try_init(|| async { let container_metadata = self.container_meta_client.collect().await?; anyhow::Ok(Arc::new( container_metadata .labels() .into_iter() .map(|(key, value)| Dimension::builder().name(key).value(value).build()) .collect(), )) }) .await .map(Arc::clone) .unwrap_or_else(|e| { log::warn!("failed fetching container metadata: {e}, using fallback"); Arc::new( self.container_meta_client .fallback() .labels() .into_iter() .map(|(key, value)| Dimension::builder().name(key).value(value).build()) .collect(), ) }) } async fn get_metric_data(&self, collected: Vec) -> Vec { let now = SystemTime::now(); let mut data: Vec = Vec::with_capacity(collected.len()); let container_dims = self.container_dimensions().await; let container_dims_len = container_dims.len(); for metric in collected { let metric_name = metric.key.name().to_string(); let mut dimensions: Vec = Vec::with_capacity(container_dims_len + metric.key.labels().count()); // Add container metadata dimensions dimensions.extend(container_dims.iter().cloned()); for label in metric.key.labels() { let value = label.value(); if value.is_empty() { log::warn!( "Skipping empty label '{}' for metric '{}' - CloudWatch does not support empty dimension values", label.key(), metric_name ); continue; } dimensions.push(Dimension::builder().name(label.key()).value(value).build()); } let value = match metric.value { MetricValue::CounterU64(val) => val as f64, MetricValue::CounterI64(val) => val as f64, MetricValue::GaugeF64(val) => val, MetricValue::GaugeU64(val) => val as f64, MetricValue::GaugeI64(val) => val as f64, }; let mut datum_builder = MetricDatum::builder() .metric_name(metric_name) .timestamp(aws_smithy_types::DateTime::from(now)) .value(value) .set_dimensions(Some(dimensions)); // For cumulative counters, include the start time if matches!( metric.value, MetricValue::CounterU64(_) | MetricValue::CounterI64(_) ) { // CloudWatch uses storage resolution to determine how data is aggregated // For counters, we use high resolution (1 second) to better track cumulative values datum_builder = datum_builder.storage_resolution(1); } data.push(datum_builder.build()) } data } async fn send_metric_batch( &self, client: &cloudwatch::Client, metric_data: Vec, ) -> Result<(), anyhow::Error> { client .put_metric_data() .namespace(&self.namespace) .set_metric_data(Some(metric_data)) .send() .await .context("send metrics to CloudWatch")?; Ok(()) } } #[async_trait::async_trait] impl Exporter for Aws { async fn export(&self, metrics: Vec) { if let Err(err) = self.export_metrics(metrics).await { log::error!("Failed to export metrics to AWS CloudWatch: {}", err); } } } ================================================ FILE: runtimes/core/src/metrics/exporter/datadog.rs ================================================ use crate::{ encore::runtime::v1 as pb, metadata::ContainerMetaClient, metrics::{exporter::Exporter, CollectedMetric, MetricValue}, secrets, }; use anyhow::Context; use dashmap::DashMap; use datadog_api_client::datadog; use datadog_api_client::datadogV2::api_metrics::{MetricsAPI, SubmitMetricsOptionalParams}; use datadog_api_client::datadogV2::model::{ MetricIntakeType, MetricPayload, MetricPoint, MetricSeries, }; use std::sync::{Arc, Mutex}; use std::time::SystemTime; pub struct Datadog { client: Arc, container_meta_client: ContainerMetaClient, last_export: Arc>, last_value: Arc>, container_tags: tokio::sync::OnceCell>>, } #[derive(Clone)] struct DatadogClient { config: datadog::Configuration, } impl DatadogClient { async fn send_metrics(&self, metric_series: Vec) -> Result<(), anyhow::Error> { let api = MetricsAPI::with_config(self.config.clone()); let payload = MetricPayload::new(metric_series); api.submit_metrics(payload, SubmitMetricsOptionalParams::default()) .await .context("submit metrics to Datadog")?; Ok(()) } } struct LazyDatadogClient { cell: tokio::sync::OnceCell, site: String, api_key: String, } impl LazyDatadogClient { fn new(site: String, api_key: String) -> Self { Self { cell: tokio::sync::OnceCell::new(), site, api_key, } } async fn get(&self) -> &DatadogClient { self.cell .get_or_init(|| async { // Create Datadog API client configuration let mut configuration = datadog::Configuration::new(); configuration .server_variables .insert("site".to_string(), self.site.clone()); configuration.set_auth_key( "apiKeyAuth", datadog::APIKey { key: self.api_key.clone(), prefix: String::new(), }, ); DatadogClient { config: configuration, } }) .await } } impl Datadog { pub fn new( provider_cfg: &pb::metrics_provider::Datadog, secrets: &secrets::Manager, container_meta_client: ContainerMetaClient, ) -> anyhow::Result { let api_key = match &provider_cfg.api_key { Some(data) => { let secret = secrets.load(data.clone()); let api_key_bytes = secret.get().context("failed to resolve Datadog API key")?; std::str::from_utf8(api_key_bytes) .context("Datadog API key is not valid UTF-8")? .to_string() } None => { return Err(anyhow::anyhow!("Datadog API key not provided")); } }; Ok(Self { client: Arc::new(LazyDatadogClient::new(provider_cfg.site.clone(), api_key)), container_meta_client, last_export: Arc::new(Mutex::new( SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("system time before Unix epoch") .as_secs() as i64, )), last_value: Arc::new(DashMap::new()), container_tags: tokio::sync::OnceCell::new(), }) } async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { if metrics.is_empty() { return Ok(()); } let client = self.client.get().await; log::trace!( "Exporting {} metrics to Datadog site {}", metrics.len(), self.client.site ); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("system time before Unix epoch") .as_secs() as i64; let metric_series = self.get_metric_data(metrics, now).await; if !metric_series.is_empty() { client.send_metrics(metric_series).await?; } // Update last export time if let Ok(mut last_export) = self.last_export.lock() { *last_export = now; } Ok(()) } async fn container_tags_vec(&self) -> Arc> { self.container_tags .get_or_try_init(|| async { let container_metadata = self.container_meta_client.collect().await?; anyhow::Ok(Arc::new( container_metadata .labels() .into_iter() .map(|(key, value)| format!("{}:{}", key, value)) .collect(), )) }) .await .map(Arc::clone) .unwrap_or_else(|e| { log::warn!("failed fetching container metadata: {e}, using fallback"); Arc::new( self.container_meta_client .fallback() .labels() .into_iter() .map(|(key, value)| format!("{}:{}", key, value)) .collect(), ) }) } async fn get_metric_data( &self, collected: Vec, now: i64, ) -> Vec { let mut data: Vec = Vec::with_capacity(collected.len()); let container_tags = self.container_tags_vec().await; let container_tags_len = container_tags.len(); let last_export = self.last_export.lock().ok().map(|t| *t).unwrap_or(now); let interval = now - last_export; for metric in collected { let metric_name = metric.key.name().to_string(); // Build tags: container metadata + metric labels let mut tags: Vec = Vec::with_capacity(container_tags_len + metric.key.labels().count()); // Add container metadata tags tags.extend(container_tags.iter().cloned()); for label in metric.key.labels() { tags.push(format!("{}:{}", label.key(), label.value())); } let (metric_type, value) = match metric.value { MetricValue::CounterU64(val) => { let value = val as f64; let key = metric.key.get_hash(); let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0); self.last_value.insert(key, value); let delta = value - last_val; (MetricIntakeType::COUNT, delta) } MetricValue::CounterI64(val) => { let value = val as f64; let key = metric.key.get_hash(); let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0); self.last_value.insert(key, value); let delta = value - last_val; (MetricIntakeType::COUNT, delta) } MetricValue::GaugeF64(val) => (MetricIntakeType::GAUGE, val), MetricValue::GaugeU64(val) => (MetricIntakeType::GAUGE, val as f64), MetricValue::GaugeI64(val) => (MetricIntakeType::GAUGE, val as f64), }; let point = MetricPoint::new().timestamp(now).value(value); let series = MetricSeries::new(metric_name, vec![point]) .type_(metric_type) .interval(interval) .tags(tags); data.push(series); } data } } #[async_trait::async_trait] impl Exporter for Datadog { async fn export(&self, metrics: Vec) { if let Err(err) = self.export_metrics(metrics).await { log::error!("Failed to export metrics to Datadog: {}", err); } } } ================================================ FILE: runtimes/core/src/metrics/exporter/gcp.rs ================================================ use crate::metadata::ContainerMetaClient; use crate::metrics::exporter::Exporter; use crate::metrics::{CollectedMetric, MetricValue}; use anyhow::Context; use google_cloud_api::model::metric_descriptor::{MetricKind, ValueType}; use google_cloud_api::model::{Metric, MonitoredResource}; use google_cloud_monitoring_v3::client::MetricService; use google_cloud_monitoring_v3::model::{Point, TimeInterval, TimeSeries, TypedValue}; use std::collections::HashMap; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::OnceCell; type LabelPairs = Vec<(String, String)>; #[derive(Debug)] pub struct Gcp { client: Arc, project_id: String, monitored_resource_type: String, monitored_resource_labels: HashMap, metric_names: HashMap, container_meta_client: Arc, container_labels: Arc>>, } #[derive(Debug)] struct LazyMonitoringClient { cell: OnceCell>, } impl LazyMonitoringClient { fn new() -> Self { Self { cell: OnceCell::new(), } } async fn get(&self) -> &anyhow::Result { self.cell .get_or_init(|| async { MetricService::builder() .build() .await .inspect_err(|e| log::error!("Failed to create GCP monitoring client: {e:?}")) .context("create monitoring client") }) .await } } impl Gcp { pub fn new( project_id: String, monitored_resource_type: String, monitored_resource_labels: HashMap, metric_names: HashMap, container_meta_client: ContainerMetaClient, ) -> Self { Self { client: Arc::new(LazyMonitoringClient::new()), project_id, monitored_resource_type, monitored_resource_labels, metric_names, container_meta_client: Arc::new(container_meta_client), container_labels: Arc::new(OnceCell::new()), } } async fn export_metrics(&self, metrics: Vec) -> Result<(), anyhow::Error> { if metrics.is_empty() { return Ok(()); } let client = match self.client.get().await { Ok(client) => client, Err(e) => { log::error!("Failed to get monitoring client: {}", e); return Err(anyhow::anyhow!("Failed to get monitoring client: {}", e)); } }; log::trace!( "Exporting {} metrics to GCP project {}", metrics.len(), self.project_id ); let time_series = self.get_metric_data(metrics).await; // Send metrics in batches (Google Cloud allows up to 200 time series per request) for batch in time_series.chunks(200) { if let Err(e) = self.send_time_series_batch(client, batch.to_vec()).await { log::error!("Failed to export metrics batch: {}", e); } } Ok(()) } async fn container_labels(&self) -> Arc> { self.container_labels .get_or_try_init(|| async { let container_metadata = self.container_meta_client.collect().await?; anyhow::Ok(Arc::new(container_metadata.labels())) }) .await .map(Arc::clone) .unwrap_or_else(|e| { log::warn!("failed fetching container metadata: {e}, using fallback"); Arc::new(self.container_meta_client.fallback().labels()) }) } async fn get_metric_data(&self, collected: Vec) -> Vec { let end_time = SystemTime::now(); let ts_end_time: google_cloud_wkt::Timestamp = end_time.try_into().unwrap_or_default(); let mut data: Vec = Vec::with_capacity(collected.len()); let container_labels = self.container_labels().await; let container_labels_len = container_labels.len(); let instance_id = &self .container_meta_client .collect() .await .unwrap_or(self.container_meta_client.fallback()) .instance_id; for metric in collected { let cloud_metric_name = match self.metric_names.get(metric.key.name()) { Some(name) => name, None => { log::warn!( "Skipping metric '{}' - no cloud metric name configured", metric.key.name() ); continue; } }; let mut labels = HashMap::with_capacity(container_labels_len + metric.key.labels().len()); labels.extend(container_labels.iter().cloned()); labels.extend( metric .key .labels() .map(|label| (label.key().to_string(), label.value().to_string())), ); let (kind, value_type, typed_value, interval) = match metric.value { MetricValue::CounterU64(val) => { let start_time: google_cloud_wkt::Timestamp = metric.registered_at.try_into().unwrap_or_default(); ( MetricKind::Cumulative, ValueType::Int64, TypedValue::new().set_int64_value(val as i64), TimeInterval::new() .set_start_time(start_time) .set_end_time(ts_end_time), ) } MetricValue::CounterI64(val) => { let start_time: google_cloud_wkt::Timestamp = metric.registered_at.try_into().unwrap_or_default(); ( MetricKind::Cumulative, ValueType::Int64, TypedValue::new().set_int64_value(val), TimeInterval::new() .set_start_time(start_time) .set_end_time(ts_end_time), ) } MetricValue::GaugeF64(val) => ( MetricKind::Gauge, ValueType::Double, TypedValue::new().set_double_value(val), TimeInterval::new().set_end_time(ts_end_time), ), MetricValue::GaugeU64(val) => ( MetricKind::Gauge, ValueType::Int64, TypedValue::new().set_int64_value(val as i64), TimeInterval::new().set_end_time(ts_end_time), ), MetricValue::GaugeI64(val) => ( MetricKind::Gauge, ValueType::Int64, TypedValue::new().set_int64_value(val), TimeInterval::new().set_end_time(ts_end_time), ), }; // Add container instance ID to node_id if present let mut monitored_resource_labels = self.monitored_resource_labels.clone(); if let Some(node_id) = monitored_resource_labels.get("node_id") { monitored_resource_labels.insert( "node_id".to_string(), format!("{}-{}", node_id, instance_id), ); } let mut mr = MonitoredResource::new().set_type(&self.monitored_resource_type); mr.labels = monitored_resource_labels; data.push( TimeSeries::new() .set_metric_kind(kind) .set_metric( Metric::new() .set_type(format!("custom.googleapis.com/{}", cloud_metric_name)) .set_labels(labels), ) .set_resource(mr) .set_value_type(value_type) .set_points(vec![Point::new() .set_interval(interval) .set_value(typed_value)]), ); } data } async fn send_time_series_batch( &self, client: &MetricService, time_series: Vec, ) -> Result<(), anyhow::Error> { client .create_time_series() .set_name(format!("projects/{}", self.project_id)) .set_time_series(time_series) .send() .await .map_err(anyhow::Error::new)?; Ok(()) } } #[async_trait::async_trait] impl Exporter for Gcp { async fn export(&self, metrics: Vec) { if let Err(err) = self.export_metrics(metrics).await { log::error!("Failed to export metrics to GCP: {}", err); } } } ================================================ FILE: runtimes/core/src/metrics/exporter/mod.rs ================================================ mod aws; mod datadog; mod gcp; mod prometheus; pub use aws::Aws; pub use datadog::Datadog; pub use gcp::Gcp; pub use prometheus::Prometheus; #[async_trait::async_trait] pub trait Exporter: Send + Sync { async fn export(&self, metrics: Vec); } ================================================ FILE: runtimes/core/src/metrics/exporter/prometheus.rs ================================================ use crate::encore::runtime::v1 as pb; use crate::metadata::ContainerMetaClient; use crate::metrics::exporter::Exporter; use crate::metrics::{CollectedMetric, MetricValue}; use crate::secrets; use anyhow::Context; use prost::Message; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::OnceCell; use url::Url; #[derive(Debug)] pub struct Prometheus { client: reqwest::Client, remote_write_url: Url, container_meta_client: ContainerMetaClient, container_labels: OnceCell>>, } impl Prometheus { pub fn new( provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite, secrets: &secrets::Manager, container_meta_client: ContainerMetaClient, ) -> anyhow::Result { let remote_write_url = match &provider_cfg.remote_write_url { Some(data) => { let secret = secrets.load(data.clone()); let url_bytes = secret .get() .context("failed to resolve Prometheus Remote Write Url")?; let s = std::str::from_utf8(url_bytes) .context("Prometheus Remote Write Url is not valid UTF-8")?; Url::parse(s).context("Prometheus Remote Write Url not valid")? } None => { return Err(anyhow::anyhow!("Prometheus Remote Write Url not provided")); } }; Ok(Self { client: reqwest::Client::new(), remote_write_url, container_meta_client, container_labels: OnceCell::new(), }) } async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { if metrics.is_empty() { return Ok(()); } log::trace!( "Exporting {} metrics to Prometheus remote write host {}", metrics.len(), self.remote_write_url.host_str().unwrap_or_default() ); let time_series = self.get_metric_data(metrics).await; // Create WriteRequest with the time series let write_request = prompb::WriteRequest { timeseries: time_series, metadata: vec![], }; // Serialize to protobuf let mut proto_buf = Vec::new(); write_request .encode(&mut proto_buf) .context("marshal metrics into Protobuf")?; // Compress with Snappy let mut encoder = snap::raw::Encoder::new(); let encoded = encoder .compress_vec(&proto_buf) .context("compress metrics with Snappy")?; // Send HTTP request let response = self .client .post(self.remote_write_url.clone()) .header("Content-Type", "application/x-protobuf") .header("Content-Encoding", "snappy") .header("User-Agent", "encore") .header("X-Prometheus-Remote-Write-Version", "0.1.0") .body(encoded) .send() .await .context("send metrics to Prometheus remote write destination")?; if !response.status().is_success() { let status = response.status(); let body = response .text() .await .unwrap_or_else(|_| "".to_string()); anyhow::bail!( "Prometheus remote write returned non-success status {}: {}", status, body ); } Ok(()) } async fn container_labels(&self) -> Arc> { self.container_labels .get_or_try_init(|| async { let container_metadata = self.container_meta_client.collect().await?; anyhow::Ok(Arc::new( container_metadata .labels() .into_iter() .map(|(name, value)| prompb::Label { name, value }) .collect(), )) }) .await .map(Arc::clone) .unwrap_or_else(|e| { log::warn!("failed fetching container metadata: {e}, using fallback"); Arc::new( self.container_meta_client .fallback() .labels() .into_iter() .map(|(name, value)| prompb::Label { name, value }) .collect(), ) }) } async fn get_metric_data(&self, collected: Vec) -> Vec { let now = SystemTime::now(); let timestamp = from_time(now); let mut data: Vec = Vec::with_capacity(collected.len()); let container_labels = self.container_labels().await; let container_labels_len = container_labels.len(); for metric in collected { let metric_name = metric.key.name().to_string(); // Build labels: container metadata + metric labels + __name__ let mut labels: Vec = Vec::with_capacity(container_labels_len + metric.key.labels().len() + 1); // Add container metadata labels labels.extend(container_labels.iter().cloned()); // Add metric-specific labels for label in metric.key.labels() { labels.push(prompb::Label { name: label.key().to_string(), value: label.value().to_string(), }); } // Add __name__ label for the metric name labels.push(prompb::Label { name: "__name__".to_string(), value: metric_name, }); // Sort labels lexicographically by name, as required by some Prometheus implementations. labels.sort_unstable_by(|a, b| a.name.cmp(&b.name)); // Convert metric value to float64 let value = match metric.value { MetricValue::CounterU64(val) => val as f64, MetricValue::CounterI64(val) => val as f64, MetricValue::GaugeF64(val) => val, MetricValue::GaugeU64(val) => val as f64, MetricValue::GaugeI64(val) => val as f64, }; data.push(prompb::TimeSeries { labels, samples: vec![prompb::Sample { value, timestamp }], exemplars: vec![], histograms: vec![], }); } data } } #[async_trait::async_trait] impl Exporter for Prometheus { async fn export(&self, metrics: Vec) { if let Err(err) = self.export_metrics(metrics).await { log::error!("Failed to export metrics to Prometheus: {:#}", err); } } } /// Convert SystemTime to Prometheus timestamp (milliseconds since Unix epoch) fn from_time(t: SystemTime) -> i64 { match t.duration_since(SystemTime::UNIX_EPOCH) { Ok(duration) => { let secs = duration.as_secs() as i64; let nanos = duration.subsec_nanos() as i64; secs * 1000 + nanos / 1_000_000 } Err(_) => 0, // If time is before Unix epoch, return 0 } } #[allow(dead_code)] mod prompb { include!(concat!(env!("OUT_DIR"), "/prometheus.rs")); } #[cfg(test)] mod tests { use super::*; use crate::encore::runtime::v1 as pb; use std::time::SystemTime; /// Test that labels are sorted lexicographically by name. /// Some Prometheus implementations require this or they will reject the request /// with "out of order labels" error. #[tokio::test] async fn test_labels_are_sorted() { let env = pb::Environment::default(); let container_meta_client = ContainerMetaClient::new(env, reqwest::Client::new()); // Create a metric with labels in non-sorted order: "zebra", "apple", "middle" let key = metrics::Key::from_parts( "test_metric", vec![ metrics::Label::new("zebra", "last"), metrics::Label::new("apple", "first"), metrics::Label::new("middle", "mid"), ], ); let collected = vec![CollectedMetric { key, value: MetricValue::CounterU64(42), registered_at: SystemTime::now(), }]; let prometheus = Prometheus { client: reqwest::Client::new(), remote_write_url: Url::parse("http://localhost:9090/api/v1/write").unwrap(), container_meta_client, container_labels: OnceCell::new(), }; let time_series = prometheus.get_metric_data(collected).await; assert_eq!(time_series.len(), 1); let label_names: Vec<&str> = time_series[0] .labels .iter() .map(|l| l.name.as_str()) .collect(); // Labels should be sorted lexicographically assert_eq!( label_names, vec![ "__name__", "apple", "env_name", "instance_id", "middle", "revision_id", "service_id", "zebra" ] ); } } ================================================ FILE: runtimes/core/src/metrics/gauge.rs ================================================ use crate::metrics; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::atomic::AtomicU64; use std::sync::Arc; pub trait GaugeOps { fn set(&self, value: T); fn get(&self) -> crate::metrics::MetricValue; } /// A typed gauge that can be set, incremented, or decremented /// T must be compatible with GaugeOps for type-safe operations pub struct Gauge { atomic: Arc, _phantom: PhantomData, } impl Gauge where Arc: GaugeOps, { /// Create a new gauge with the given atomic storage /// This is typically called by Registry, not directly by users pub(crate) fn new(atomic: Arc) -> Self { Self { atomic, _phantom: PhantomData, } } /// Set the gauge to the specified value pub fn set(&self, value: T) { GaugeOps::set(&self.atomic, value); } /// Get the current value of the gauge pub fn get(&self) -> metrics::MetricValue { GaugeOps::get(&self.atomic) } } /// A gauge schema that defines static labels and required dynamic label keys /// Validates dynamic labels at set/add/sub time and creates separate time series /// for each unique combination of static + dynamic labels #[derive(Clone, Debug)] pub struct Schema { name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, _phantom: PhantomData, } impl Schema where Arc: GaugeOps, T: Send + Sync + 'static, { /// Create a new gauge schema pub(crate) fn new( name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, ) -> Self { Self { name, static_labels, required_dynamic_keys, registry, _phantom: PhantomData, } } /// Set the gauge value directly without dynamic labels pub fn set(&self, value: T) { if !self.required_dynamic_keys.is_empty() { log::warn!( "setting gauge '{}' without required dynamic labels, required keys: {:?}", self.name, self.required_dynamic_keys ); } self.get_or_create_gauge(HashMap::new()).set(value); } // Set the dynamic label values and return a completed Gauge pub fn with(&self, dynamic_labels: L) -> Gauge where L: IntoIterator, K: Into, V: Into, { // Convert dynamic_labels to HashMap first let dynamic_labels_map: HashMap = dynamic_labels .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(); // Validate required keys are present let missing: Vec = self .required_dynamic_keys .iter() .filter(|key| !dynamic_labels_map.contains_key(*key)) .cloned() .collect(); if !missing.is_empty() { log::warn!( "missing required dynamic labels for metric '{}': {:?}, required keys: {:?}", self.name, missing, self.required_dynamic_keys ); } self.get_or_create_gauge(dynamic_labels_map) } /// Get or create a gauge for the given dynamic labels fn get_or_create_gauge(&self, dynamic_labels: HashMap) -> Gauge { // Create merged labels (static + dynamic) let mut merged_labels = self.static_labels.clone(); for (key, value) in dynamic_labels { merged_labels.push((key, value)); } self.registry.get_or_create_gauge( &self.name, merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())), ) } } /// Builder for creating gauge schemas with static labels and required dynamic keys pub struct GaugeSchemaBuilder { name: String, static_labels: Vec<(String, String)>, required_dynamic_keys: HashSet, registry: Arc, _phantom: std::marker::PhantomData, } impl GaugeSchemaBuilder where Arc: GaugeOps, T: Send + Sync + 'static, { pub(crate) fn new(name: String, registry: Arc) -> Self { Self { name, static_labels: Vec::new(), required_dynamic_keys: HashSet::new(), registry, _phantom: std::marker::PhantomData, } } /// Add static labels that are set once when the schema is created pub fn static_labels(mut self, labels: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { for (key, value) in labels { self.static_labels .push((key.as_ref().to_string(), value.as_ref().to_string())); } self } /// Add a single static label pub fn static_label(mut self, key: &str, value: &str) -> Self { self.static_labels .push((key.to_string(), value.to_string())); self } /// Specify required dynamic label keys that must be provided at set/add/sub time pub fn require_dynamic_keys(mut self, keys: I) -> Self where I: IntoIterator, K: AsRef, { for key in keys { self.required_dynamic_keys.insert(key.as_ref().to_string()); } self } /// Add a single required dynamic key pub fn require_dynamic_key(mut self, key: &str) -> Self { self.required_dynamic_keys.insert(key.to_string()); self } /// Build the gauge schema pub fn build(self) -> Schema { Schema::new( self.name, self.static_labels, self.required_dynamic_keys, self.registry, ) } } ================================================ FILE: runtimes/core/src/metrics/manager.rs ================================================ use crate::{ encore::runtime::v1::{self as pb, Environment}, metadata::{process_env_substitution, ContainerMetaClient}, metrics::{ exporter::{self, Exporter}, registry::Registry, }, secrets, }; use std::sync::Arc; use std::time::Duration; #[derive(Debug, Clone)] enum ProviderType { Gcp(pb::metrics_provider::GcpCloudMonitoring), EncoreCloud(pb::metrics_provider::GcpCloudMonitoring), Aws(pb::metrics_provider::AwsCloudWatch), Datadog(pb::metrics_provider::Datadog), Prometheus(pb::metrics_provider::PrometheusRemoteWrite), } impl ProviderType { fn from_config(provider: &pb::MetricsProvider) -> Option { match &provider.provider { Some(pb::metrics_provider::Provider::Gcp(config)) => Some(Self::Gcp(config.clone())), Some(pb::metrics_provider::Provider::EncoreCloud(config)) => { Some(Self::EncoreCloud(config.clone())) } Some(pb::metrics_provider::Provider::Aws(config)) => Some(Self::Aws(config.clone())), Some(pb::metrics_provider::Provider::Datadog(config)) => { Some(Self::Datadog(config.clone())) } Some(pb::metrics_provider::Provider::PromRemoteWrite(config)) => { Some(Self::Prometheus(config.clone())) } None => { log::warn!("no metrics provider configured"); None } } } fn create_exporter( &self, env: &Environment, secrets: &secrets::Manager, http_client: &reqwest::Client, ) -> anyhow::Result> { match self { Self::Gcp(config) | Self::EncoreCloud(config) => { Ok(Self::create_gcp_exporter(config, env, http_client)) } Self::Aws(config) => Ok(Self::create_aws_exporter(config, env, http_client)), Self::Datadog(config) => { Self::create_datadog_exporter(config, secrets, env, http_client) } Self::Prometheus(config) => { Self::create_prometheus_exporter(config, secrets, env, http_client) } } } fn create_prometheus_exporter( provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite, secrets: &secrets::Manager, env: &Environment, http_client: &reqwest::Client, ) -> anyhow::Result> { let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); Ok(Arc::new(exporter::Prometheus::new( provider_cfg, secrets, container_meta_client, )?)) } fn create_datadog_exporter( provider_cfg: &pb::metrics_provider::Datadog, secrets: &secrets::Manager, env: &Environment, http_client: &reqwest::Client, ) -> anyhow::Result> { let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); Ok(Arc::new(exporter::Datadog::new( provider_cfg, secrets, container_meta_client, )?)) } fn create_aws_exporter( provider_cfg: &pb::metrics_provider::AwsCloudWatch, env: &Environment, http_client: &reqwest::Client, ) -> Arc { let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); Arc::new(exporter::Aws::new( provider_cfg.namespace.clone(), container_meta_client, )) } fn create_gcp_exporter( provider_cfg: &pb::metrics_provider::GcpCloudMonitoring, env: &Environment, http_client: &reqwest::Client, ) -> Arc { let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); let mut labels = provider_cfg.monitored_resource_labels.clone(); process_env_substitution(&mut labels); Arc::new(exporter::Gcp::new( provider_cfg.project_id.clone(), provider_cfg.monitored_resource_type.clone(), labels, provider_cfg.metric_names.clone(), container_meta_client, )) } } #[derive(Clone)] pub struct Manager { exporter: Option>, registry: Arc, } impl Manager { pub fn new() -> Self { let registry = Arc::new(Registry::new()); Self { exporter: None, registry, } } pub fn registry(&self) -> &Arc { &self.registry } pub fn from_runtime_config( observability: &pb::Observability, environment: &pb::Environment, secrets: &secrets::Manager, http_client: &reqwest::Client, runtime_handle: tokio::runtime::Handle, ) -> Self { let mut manager = Self::new(); for metrics_provider in &observability.metrics { if let Some(provider_type) = ProviderType::from_config(metrics_provider) { match provider_type.create_exporter(environment, secrets, http_client) { Ok(exporter) => { manager.exporter = Some(exporter); break; // Take the first valid provider } Err(err) => { log::error!("Failed to create metrics exporter: {}", err); } } } } // Start collection if we have an exporter if manager.exporter.is_some() { let collection_interval = observability .metrics .first() .and_then(|p| p.collection_interval.as_ref()) .and_then(|d| Duration::try_from(d.clone()).ok()) .filter(|d| !d.is_zero()) .unwrap_or(Duration::from_secs(60)); // Default to 1 minute manager.start_collection_loop(runtime_handle, collection_interval); } manager } pub fn with_exporter(mut self, exporter: std::sync::Arc) -> Self { self.exporter = Some(exporter); self } pub async fn collect_and_export(&self) { let metrics = self.registry.collect(); if let Some(ref exporter) = self.exporter { exporter.export(metrics).await; } } pub fn collect_metrics(&self) -> Vec { self.registry.collect() } pub fn start_collection_loop( &self, runtime_handle: tokio::runtime::Handle, interval: std::time::Duration, ) { let manager = self.clone(); runtime_handle.spawn(async move { let mut interval_timer = tokio::time::interval(interval); loop { interval_timer.tick().await; manager.collect_and_export().await; } }); } } impl Default for Manager { fn default() -> Self { Self::new() } } ================================================ FILE: runtimes/core/src/metrics/mod.rs ================================================ mod atomic; mod exporter; mod manager; mod registry; mod system; pub mod counter; pub mod gauge; #[cfg(test)] mod test; use std::sync::Arc; pub use counter::{Counter, CounterOps}; pub use gauge::{Gauge, GaugeOps}; pub use manager::Manager; pub use registry::{CollectedMetric, MetricValue, MetricsCollector, Registry}; pub use system::SystemMetricsCollector; /// Create a requests counter schema pub fn requests_total_counter( registry: &Arc, service: &str, endpoint: &str, ) -> counter::Schema { registry .counter_schema::("e_requests_total") .static_labels([("service", service), ("endpoint", endpoint)]) .require_dynamic_key("code") .build() } /// Create a memory usage gauge schema pub fn memory_usage_gauge_schema(registry: &Arc) -> gauge::Schema { registry .gauge_schema::("e_sys_memory_used_bytes") .build() } ================================================ FILE: runtimes/core/src/metrics/registry.rs ================================================ use crate::metrics::counter::{CounterOps, CounterSchemaBuilder}; use crate::metrics::gauge::{GaugeOps, GaugeSchemaBuilder}; use super::system::SystemMetricsCollector; use super::{Counter, Gauge}; use dashmap::DashMap; use malachite::base::num::basic::traits::One; use metrics::{Key, Label}; use std::sync::atomic::AtomicU64; use std::sync::{Arc, RwLock}; use std::time::SystemTime; /// Trait for external metrics collectors (e.g., JS runtime, other language runtimes) pub trait MetricsCollector: Send + Sync { /// Collect all metrics from this collector fn collect(&self) -> Vec; } struct MetricStorage { atomic: Arc, getter: Box MetricValue + Send + Sync>, registered_at: SystemTime, } impl std::fmt::Debug for MetricStorage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MetricStorage") .field("atomic", &self.atomic) .field("registered_at", &self.registered_at) .finish() } } #[derive(Debug, Clone, PartialEq)] pub enum MetricValue { // Counter variants CounterU64(u64), CounterI64(i64), // Gauge variants GaugeU64(u64), GaugeI64(i64), GaugeF64(f64), } #[derive(Debug, Clone)] pub struct CollectedMetric { pub key: Key, pub value: MetricValue, pub registered_at: SystemTime, } pub struct Registry { counters: DashMap, gauges: DashMap, system_metrics: SystemMetricsCollector, external_collectors: RwLock>>, } impl std::fmt::Debug for Registry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Registry") .field("counters", &self.counters) .field("gauges", &self.gauges) .field("system_metrics", &self.system_metrics) .finish() } } impl Registry { pub fn new() -> Self { Self { counters: DashMap::new(), gauges: DashMap::new(), system_metrics: SystemMetricsCollector::new(), external_collectors: RwLock::new(Vec::new()), } } /// Register an external metrics collector (e.g., from JS runtime) pub fn register_collector(&self, collector: Arc) { self.external_collectors .write() .expect("mutex poisoned") .push(collector); } /// Create a counter with the given name and labels pub fn get_or_create_counter<'a, T>( &self, name: &str, labels: impl IntoIterator, ) -> Counter where Arc: CounterOps, T: One + Send + Sync + 'static, { let labels_vec: Vec