From 96585f54552dbabc701584e2937257e9fc507d36 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Thu, 23 Apr 2026 14:50:04 -0400 Subject: [PATCH 1/2] migrate to effect v4 --- package.json | 8 +- packages/amp/package.json | 8 +- packages/amp/src/admin/api.ts | 548 ++++++------ packages/amp/src/admin/domain.ts | 261 +++--- packages/amp/src/admin/error.ts | 797 +++++++++--------- packages/amp/src/admin/service.ts | 131 +-- packages/amp/src/arrow-flight/errors.ts | 14 +- packages/amp/src/arrow-flight/service.ts | 27 +- packages/amp/src/arrow-flight/transport.ts | 9 +- packages/amp/src/arrow-flight/types.ts | 4 +- packages/amp/src/auth/error.ts | 468 ++++++---- packages/amp/src/auth/service.ts | 642 +++++++------- packages/amp/src/cdc-stream/batch-store.ts | 7 +- packages/amp/src/cdc-stream/errors.ts | 4 +- packages/amp/src/cdc-stream/stream.ts | 14 +- packages/amp/src/core/domain.ts | 404 +++++---- .../src/internal/arrow-flight-ipc/Errors.ts | 8 +- packages/amp/src/internal/pkce.ts | 2 +- packages/amp/src/protocol-stream/errors.ts | 20 +- packages/amp/src/protocol-stream/messages.ts | 17 +- packages/amp/src/protocol-stream/service.ts | 10 +- packages/amp/src/registry/api.ts | 737 ++++++++++------ packages/amp/src/registry/domain.ts | 186 ++-- packages/amp/src/registry/error.ts | 458 +++++----- .../amp/src/transactional-stream/errors.ts | 8 +- .../src/transactional-stream/memory-store.ts | 4 +- .../src/transactional-stream/state-actor.ts | 18 +- .../src/transactional-stream/state-store.ts | 4 +- .../amp/src/transactional-stream/stream.ts | 6 +- .../amp/src/transactional-stream/types.ts | 33 +- .../test/arrow-flight-ipc/roundtrip.test.ts | 1 + .../arrow-test-harness/FlightDataGenerator.ts | 13 +- .../test/arrow-test-harness/RandomUtils.ts | 51 +- packages/amp/test/arrow-test-harness/Types.ts | 7 +- .../arrow-test-harness/ValueComparison.ts | 3 +- .../generators/primitives.ts | 8 +- packages/amp/test/cdc-stream/stream.test.ts | 15 +- .../test/protocol-stream/validation.test.ts | 125 ++- packages/cli/package.json | 12 +- packages/cli/src/bin.ts | 3 +- packages/cli/src/cli.ts | 26 +- packages/cli/src/commands/auth.ts | 2 +- packages/cli/src/commands/auth/login.ts | 170 ++-- packages/cli/src/commands/auth/logout.ts | 42 +- packages/cli/src/commands/auth/token.ts | 66 +- packages/cli/src/commands/query.ts | 43 +- packages/cli/src/errors.ts | 3 - pnpm-lock.yaml | 647 +++++--------- pnpm-workspace.yaml | 7 + tsconfig.base.json | 2 +- 50 files changed, 3078 insertions(+), 3025 deletions(-) delete mode 100644 packages/cli/src/errors.ts diff --git a/package.json b/package.json index 9cb0540..4630108 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "type": "module", "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501", "scripts": { + "prepare": "effect-language-service patch", "build": "tspc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", "build:tsgo": "tsgo -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build:tsgo", "check": "tspc -b tsconfig.json", @@ -25,17 +26,14 @@ "@babel/core": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@effect/language-service": "^0.74.0", - "@effect/platform": "^0.94.5", - "@effect/platform-node": "^0.104.1", - "@effect/vitest": "^0.27.0", + "@effect/language-service": "catalog:", + "@effect/vitest": "catalog:", "@types/node": "^25.2.3", "@typescript/native-preview": "7.0.0-dev.20260130.1", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "babel-plugin-annotate-pure-calls": "^0.5.0", "dprint": "^0.51.1", - "effect": "^3.19.18", "glob": "^13.0.0", "globals": "^17.2.0", "madge": "^8.0.0", diff --git a/packages/amp/package.json b/packages/amp/package.json index aee27fc..3b3e292 100644 --- a/packages/amp/package.json +++ b/packages/amp/package.json @@ -45,8 +45,7 @@ "@bufbuild/protobuf": "^2.11.0", "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-node": "^2.1.1", - "@effect/platform": "^0.94.5", - "effect": "^3.19.18" + "effect": "catalog:" }, "devDependencies": { "@bufbuild/buf": "^1.65.0", @@ -54,12 +53,11 @@ "@bufbuild/protoc-gen-es": "^2.11.0", "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-node": "^2.1.1", - "@effect/platform": "^0.94.5", - "effect": "^3.19.18" + "effect": "catalog:" }, "dependencies": { "jiti": "^2.6.1", "jose": "^6.1.3", - "viem": "^2.46.1" + "viem": "catalog:" } } diff --git a/packages/amp/src/admin/api.ts b/packages/amp/src/admin/api.ts index 44d582e..29943d8 100644 --- a/packages/amp/src/admin/api.ts +++ b/packages/amp/src/admin/api.ts @@ -9,12 +9,12 @@ * - Schema analysis * - Manifests (registration) */ -import * as HttpApi from "@effect/platform/HttpApi" -import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint" -import * as HttpApiError from "@effect/platform/HttpApiError" -import * as HttpApiGroup from "@effect/platform/HttpApiGroup" -import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" +import * as HttpApi from "effect/unstable/httpapi/HttpApi" +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint" +import * as HttpApiError from "effect/unstable/httpapi/HttpApiError" +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup" +import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema" import * as Models from "../core/domain.ts" import * as Domain from "./domain.ts" import * as Error from "./error.ts" @@ -26,390 +26,340 @@ import * as Error from "./error.ts" /** * A URL parameter for the dataset namespace. */ -const datasetNamespaceParam = HttpApiSchema.param("namespace", Models.DatasetNamespace) +const DatasetNamespaceParam = Models.DatasetNamespace /** * A URL parameter for the dataset name. */ -const datasetNameParam = HttpApiSchema.param("name", Models.DatasetName) +const DatasetNameParam = Models.DatasetName /** * A URL parameter for the dataset revision. */ -const datasetRevisionParam = HttpApiSchema.param("revision", Models.DatasetRevision) +const DatasetRevisionParam = Models.DatasetRevision /** * A URL parameter for the unique job identifier. */ -const jobIdParam = HttpApiSchema.param( - "jobId", - Schema.NumberFromString.annotations({ - identifier: "JobId", - description: "The unique identifier for a job." - }) -) +const JobIdParam = Schema.NumberFromString.annotate({ + identifier: "JobId", + description: "The unique identifier for a job." +}) /** * Query parameters for listing jobs. */ -const jobsQueryParams = Schema.Struct({ +const JobsQueryParams = Schema.Struct({ limit: Schema.NumberFromString.pipe(Schema.optional), - lastJobId: Schema.NumberFromString.pipe( - Schema.optional, - Schema.fromKey("last_job_id") - ), + lastJobId: Schema.optional(Schema.NumberFromString), status: Schema.String.pipe(Schema.optional) -}) +}).pipe(Schema.encodeKeys({ lastJobId: "last_job_id" })) // ============================================================================= // Dataset Endpoints // ============================================================================= // GET /datasets - List all datasets -const getDatasets = HttpApiEndpoint.get("getDatasets")`/datasets` - .addError(Error.DatasetStoreError) - .addError(Error.MetadataDbError) - .addError(Error.ListAllDatasetsError) - .addSuccess(Domain.GetDatasetsResponse) +const getDatasets = HttpApiEndpoint.get("getDatasets", "/datasets", { + error: [ + Error.DatasetStoreError, + Error.MetadataDbError, + Error.ListAllDatasetsError, + HttpApiError.Forbidden, + HttpApiError.Unauthorized + ], + success: Domain.GetDatasetsResponse +}) -/** - * Error type for the `getDatasets` endpoint. - */ -export type GetDatasetsError = - | Error.DatasetStoreError - | Error.MetadataDbError - | Error.ListAllDatasetsError +export type GetDatasetsError = typeof getDatasets["~Error"] // POST /datasets - Register a dataset -const registerDataset = HttpApiEndpoint.post("registerDataset")`/datasets` - .addError(Error.InvalidPayloadFormatError) - .addError(Error.InvalidManifestError) - .addError(Error.ManifestLinkingError) - .addError(Error.ManifestNotFoundError) - .addError(Error.ManifestRegistrationError) - .addError(Error.ManifestValidationError) - .addError(Error.StoreError) - .addError(Error.UnsupportedDatasetKindError) - .addError(Error.VersionTaggingError) - .addSuccess(Schema.Void, { status: 201 }) - .setPayload(Domain.RegisterDatasetPayload) +const registerDataset = HttpApiEndpoint.post("registerDataset", "/datasets", { + payload: Domain.RegisterDatasetPayload, + error: [ + Error.InvalidPayloadFormatError, + Error.InvalidManifestError, + Error.ManifestLinkingError, + Error.ManifestNotFoundError, + Error.ManifestRegistrationError, + Error.ManifestValidationError, + Error.StoreError, + Error.UnsupportedDatasetKindError, + Error.VersionTaggingError + ], + success: Schema.Void +}) -/** - * Error type for the `registerDataset` endpoint. - */ -export type RegisterDatasetError = - | Error.InvalidPayloadFormatError - | Error.InvalidManifestError - | Error.ManifestLinkingError - | Error.ManifestNotFoundError - | Error.ManifestRegistrationError - | Error.ManifestValidationError - | Error.StoreError - | Error.UnsupportedDatasetKindError - | Error.VersionTaggingError +export type RegisterDatasetError = typeof registerDataset["~Error"] // GET /datasets/{namespace}/{name}/versions - List versions const getDatasetVersions = HttpApiEndpoint.get( - "getDatasetVersions" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions` - .addError(Error.DatasetStoreError) - .addError(Error.InvalidRequestError) - .addError(Error.MetadataDbError) - .addError(Error.ListVersionTagsError) - .addSuccess(Domain.GetDatasetVersionsResponse) + "getDatasetVersions", + "/datasets/:namespace/:name/versions", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + error: [ + Error.DatasetStoreError, + Error.InvalidRequestError, + Error.MetadataDbError, + Error.ListVersionTagsError + ], + success: Domain.GetDatasetVersionsResponse + } +) -/** - * Error type for the `getDatasetVersions` endpoint. - */ -export type GetDatasetVersionsError = - | Error.DatasetStoreError - | Error.InvalidRequestError - | Error.MetadataDbError - | Error.ListVersionTagsError +export type GetDatasetVersionsError = typeof getDatasetVersions["~Error"] // GET /datasets/{namespace}/{name}/versions/{revision} - Get dataset version const getDatasetVersion = HttpApiEndpoint.get( - "getDatasetVersion" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}` - .addError(Error.DatasetNotFoundError) - .addError(Error.DatasetStoreError) - .addError(Error.InvalidPathError) - .addError(Error.MetadataDbError) - .addError(Error.ResolveRevisionError) - .addError(Error.GetManifestPathError) - .addError(Error.ReadManifestError) - .addError(Error.ParseManifestError) - .addSuccess(Domain.GetDatasetVersionResponse) + "getDatasetVersion", + "/datasets/:namespace/:name/versions/:revision", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + error: [ + Error.DatasetNotFoundError, + Error.DatasetStoreError, + Error.InvalidPathError, + Error.MetadataDbError, + Error.ResolveRevisionError, + Error.GetManifestPathError, + Error.ReadManifestError, + Error.ParseManifestError + ], + success: Domain.GetDatasetVersionResponse + } +) -/** - * Error type for the `getDatasetVersion` endpoint. - */ -export type GetDatasetVersionError = - | Error.DatasetNotFoundError - | Error.DatasetStoreError - | Error.InvalidPathError - | Error.MetadataDbError - | Error.ResolveRevisionError - | Error.GetManifestPathError - | Error.ReadManifestError - | Error.ParseManifestError +export type GetDatasetVersionError = typeof getDatasetVersion["~Error"] // POST /datasets/{namespace}/{name}/versions/{revision}/deploy - Deploy dataset const deployDataset = HttpApiEndpoint.post( - "deployDataset" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/deploy` - .addError(Error.DatasetNotFoundError) - .addError(Error.DatasetStoreError) - .addError(Error.InvalidPathError) - .addError(Error.InvalidBodyError) - .addError(Error.MetadataDbError) - .addError(Error.SchedulerError) - .addError(Error.ResolveRevisionError) - .addError(Error.GetDatasetError) - .addError(Error.ListVersionTagsError) - .addError(Error.WorkerNotAvailableError) - .addSuccess(Domain.DeployDatasetResponse, { status: 202 }) - .setPayload(Domain.DeployDatasetPayload) + "deployDataset", + "/datasets/:namespace/:name/versions/:revision/deploy", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + payload: Domain.DeployDatasetPayload, + error: [ + Error.DatasetNotFoundError, + Error.DatasetStoreError, + Error.InvalidPathError, + Error.InvalidBodyError, + Error.MetadataDbError, + Error.SchedulerError, + Error.ResolveRevisionError, + Error.GetDatasetError, + Error.ListVersionTagsError, + Error.WorkerNotAvailableError + ], + success: Domain.DeployDatasetResponse.pipe(HttpApiSchema.status(202)) + } +) -/** - * Error type for the `deployDataset` endpoint. - */ -export type DeployDatasetError = - | Error.DatasetNotFoundError - | Error.DatasetStoreError - | Error.InvalidPathError - | Error.InvalidBodyError - | Error.MetadataDbError - | Error.SchedulerError - | Error.ResolveRevisionError - | Error.GetDatasetError - | Error.ListVersionTagsError - | Error.WorkerNotAvailableError +export type DeployDatasetError = typeof deployDataset["~Error"] // GET /datasets/{namespace}/{name}/versions/{revision}/manifest - Get manifest const getDatasetManifest = HttpApiEndpoint.get( - "getDatasetManifest" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/manifest` - .addError(Error.DatasetNotFoundError) - .addError(Error.DatasetStoreError) - .addError(Error.InvalidPathError) - .addError(Error.MetadataDbError) - .addError(Error.GetManifestPathError) - .addError(Error.ReadManifestError) - .addError(Error.ParseManifestError) - .addSuccess(Models.DatasetManifest) + "getDatasetManifest", + "/datasets/:namespace/:name/versions/:revision/manifest", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + error: [ + Error.DatasetNotFoundError, + Error.DatasetStoreError, + Error.InvalidPathError, + Error.MetadataDbError, + Error.GetManifestPathError, + Error.ReadManifestError, + Error.ParseManifestError + ], + success: Models.DatasetManifest + } +) -/** - * Error type for the `getDatasetManifest` endpoint. - */ -export type GetDatasetManifestError = - | Error.DatasetNotFoundError - | Error.DatasetStoreError - | Error.InvalidPathError - | Error.MetadataDbError - | Error.GetManifestPathError - | Error.ReadManifestError - | Error.ParseManifestError +export type GetDatasetManifestError = typeof getDatasetManifest["~Error"] // GET /datasets/{namespace}/{name}/versions/{revision}/sync-progress - Get sync progress const getDatasetSyncProgress = HttpApiEndpoint.get( - "getDatasetSyncProgress" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/sync-progress` - .addError(Error.DatasetNotFoundError) - .addError(Error.GetDatasetError) - .addError(Error.GetSyncProgressError) - .addError(Error.InvalidPathParamsError) - .addError(Error.ResolveRevisionError) - .addError(Error.PhysicalTableError) - .addSuccess(Domain.GetDatasetSyncProgressResponse) + "getDatasetSyncProgress", + "/datasets/:namespace/:name/versions/:revision/sync-progress", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + error: [ + Error.DatasetNotFoundError, + Error.GetDatasetError, + Error.GetSyncProgressError, + Error.InvalidPathParamsError, + Error.ResolveRevisionError, + Error.PhysicalTableError + ], + success: Domain.GetDatasetSyncProgressResponse + } +) -/** - * Error type for the `getDatasetSyncProgress` endpoint. - */ -export type GetDatasetSyncProgressError = - | Error.DatasetNotFoundError - | Error.GetDatasetError - | Error.GetSyncProgressError - | Error.InvalidPathParamsError - | Error.ResolveRevisionError - | Error.PhysicalTableError +export type GetDatasetSyncProgressError = typeof getDatasetSyncProgress["~Error"] // ============================================================================= // Job Endpoints // ============================================================================= // GET /jobs - List jobs -const getJobs = HttpApiEndpoint.get("getJobs")`/jobs` - .addError(Error.InvalidQueryParametersError) - .addError(Error.LimitTooLargeError) - .addError(Error.LimitInvalidError) - .addError(Error.ListJobsError) - .addSuccess(Domain.GetJobsResponse) - .setUrlParams(jobsQueryParams) +const getJobs = HttpApiEndpoint.get("getJobs", "/jobs", { + query: JobsQueryParams, + error: [ + Error.InvalidQueryParametersError, + Error.LimitTooLargeError, + Error.LimitInvalidError, + Error.ListJobsError + ], + success: Domain.GetJobsResponse +}) -/** - * Error type for the `getJobs` endpoint. - */ -export type GetJobsError = - | Error.InvalidQueryParametersError - | Error.LimitTooLargeError - | Error.LimitInvalidError - | Error.ListJobsError +export type GetJobsError = typeof getJobs["~Error"] // GET /jobs/{jobId} - Get job by ID -const getJobById = HttpApiEndpoint.get("getJobById")`/jobs/${jobIdParam}` - .addError(Error.InvalidJobIdError) - .addError(Error.JobNotFoundError) - .addError(Error.GetJobError) - .addSuccess(Models.JobInfo) +const getJobById = HttpApiEndpoint.get("getJobById", "/jobs/:id", { + params: { + id: JobIdParam + }, + error: [ + Error.InvalidJobIdError, + Error.JobNotFoundError, + Error.GetJobError + ], + success: Models.JobInfo +}) -/** - * Error type for the `getJobById` endpoint. - */ -export type GetJobByIdError = - | Error.InvalidJobIdError - | Error.JobNotFoundError - | Error.GetJobError +export type GetJobByIdError = typeof getJobById["~Error"] // PUT /jobs/{jobId}/stop - Stop job -const stopJob = HttpApiEndpoint.put("stopJob")`/jobs/${jobIdParam}/stop` - .addError(Error.InvalidJobIdError) - .addError(Error.JobNotFoundError) - .addError(Error.StopJobError) - .addError(Error.UnexpectedStateConflictError) - .addSuccess(Schema.Void, { status: 200 }) +const stopJob = HttpApiEndpoint.put("stopJob", "/jobs/:id/stop", { + params: { + id: JobIdParam + }, + error: [ + Error.InvalidJobIdError, + Error.JobNotFoundError, + Error.StopJobError, + Error.UnexpectedStateConflictError + ], + success: Schema.Void.pipe(HttpApiSchema.status(200)) +}) -/** - * Error type for the `stopJob` endpoint. - */ -export type StopJobError = - | Error.InvalidJobIdError - | Error.JobNotFoundError - | Error.StopJobError - | Error.UnexpectedStateConflictError +export type StopJobError = typeof stopJob["~Error"] // DELETE /jobs/{jobId} - Delete job -const deleteJob = HttpApiEndpoint.del("deleteJob")`/jobs/${jobIdParam}` - .addError(Error.InvalidJobIdError) - .addError(Error.JobConflictError) - .addError(Error.GetJobError) - .addError(Error.DeleteJobError) - .addSuccess(Schema.Void, { status: 204 }) +const deleteJob = HttpApiEndpoint.delete("deleteJob", "/jobs/:id", { + params: { + id: JobIdParam + }, + error: [ + Error.InvalidJobIdError, + Error.JobConflictError, + Error.GetJobError, + Error.DeleteJobError + ], + success: Schema.Void.pipe(HttpApiSchema.status(204)) +}) -/** - * Error type for the `deleteJob` endpoint. - */ -export type DeleteJobError = - | Error.InvalidJobIdError - | Error.JobConflictError - | Error.GetJobError - | Error.DeleteJobError +export type DeleteJobError = typeof deleteJob["~Error"] // ============================================================================= // Worker Endpoints // ============================================================================= // GET /workers - List workers -const getWorkers = HttpApiEndpoint.get("getWorkers")`/workers` - .addError(Error.SchedulerListWorkersError) - .addSuccess(Domain.GetWorkersResponse) +const getWorkers = HttpApiEndpoint.get("getWorkers", "/workers", { + error: Error.SchedulerListWorkersError, + success: Domain.GetWorkersResponse +}) -/** - * Error type for the `getWorkers` endpoint. - */ -export type GetWorkersError = Error.SchedulerListWorkersError +export type GetWorkersError = typeof getWorkers["~Error"] // ============================================================================= // Schema Endpoints // ============================================================================= // POST /schema - Analyze schema -const getOutputSchema = HttpApiEndpoint.post("getOutputSchema")`/schema` - .addError(Error.CatalogQualifiedTableError) - .addError(Error.CatalogQualifiedFunctionError) - .addError(Error.DatasetNotFoundError) - .addError(Error.DependencyAliasNotFoundError) - .addError(Error.DependencyNotFoundError) - .addError(Error.DependencyResolutionError) - .addError(Error.EmptyTablesAndFunctionsError) - .addError(Error.EthCallNotAvailableError) - .addError(Error.EthCallUdfCreationError) - .addError(Error.FunctionNotFoundInDatasetError) - .addError(Error.FunctionReferenceResolutionError) - .addError(Error.GetDatasetError) - .addError(Error.InvalidPayloadFormatError) - .addError(Error.InvalidTableNameError) - .addError(Error.InvalidTableSqlError) - .addError(Error.InvalidDependencyAliasForTableRefError) - .addError(Error.InvalidDependencyAliasForFunctionRefError) - .addError(Error.NonIncrementalQueryError) - .addError(Error.SchemaInferenceError) - .addError(Error.TableNotFoundInDatasetError) - .addError(Error.TableReferenceResolutionError) - .addError(Error.UnqualifiedTableError) - .addSuccess(Domain.GetOutputSchemaResponse) - .setPayload(Domain.GetOutputSchemaPayload) +const getOutputSchema = HttpApiEndpoint.post("getOutputSchema", "/schema", { + payload: Domain.GetOutputSchemaPayload, + error: [ + Error.CatalogQualifiedTableError, + Error.CatalogQualifiedFunctionError, + Error.DatasetNotFoundError, + Error.DependencyAliasNotFoundError, + Error.DependencyNotFoundError, + Error.DependencyResolutionError, + Error.EmptyTablesAndFunctionsError, + Error.EthCallNotAvailableError, + Error.EthCallUdfCreationError, + Error.FunctionNotFoundInDatasetError, + Error.FunctionReferenceResolutionError, + Error.GetDatasetError, + Error.InvalidPayloadFormatError, + Error.InvalidTableNameError, + Error.InvalidTableSqlError, + Error.InvalidDependencyAliasForTableRefError, + Error.InvalidDependencyAliasForFunctionRefError, + Error.NonIncrementalQueryError, + Error.SchemaInferenceError, + Error.TableNotFoundInDatasetError, + Error.TableReferenceResolutionError, + Error.UnqualifiedTableError + ], + success: Domain.GetOutputSchemaResponse +}) -/** - * Error type for the `getOutputSchema` endpoint. - */ -export type GetOutputSchemaError = - | Error.CatalogQualifiedTableError - | Error.CatalogQualifiedFunctionError - | Error.DatasetNotFoundError - | Error.DependencyAliasNotFoundError - | Error.DependencyNotFoundError - | Error.DependencyResolutionError - | Error.EmptyTablesAndFunctionsError - | Error.EthCallNotAvailableError - | Error.EthCallUdfCreationError - | Error.FunctionNotFoundInDatasetError - | Error.FunctionReferenceResolutionError - | Error.GetDatasetError - | Error.InvalidPayloadFormatError - | Error.InvalidTableNameError - | Error.InvalidTableSqlError - | Error.InvalidDependencyAliasForTableRefError - | Error.InvalidDependencyAliasForFunctionRefError - | Error.NonIncrementalQueryError - | Error.SchemaInferenceError - | Error.TableNotFoundInDatasetError - | Error.TableReferenceResolutionError - | Error.UnqualifiedTableError +export type GetOutputSchemaError = typeof getOutputSchema["~Error"] // ============================================================================= // Manifest Endpoints // ============================================================================= // POST /manifests - Register manifest -const registerManifest = HttpApiEndpoint.post("registerManifest")`/manifests` - .addError(Error.InvalidPayloadFormatError) - .addError(Error.InvalidManifestError) - .addError(Error.ManifestValidationError) - .addError(Error.ManifestStorageError) - .addError(Error.ManifestRegistrationError) - .addError(Error.UnsupportedDatasetKindError) - .addSuccess(Domain.RegisterManifestResponse, { status: 201 }) - .setPayload(Schema.Any) +const registerManifest = HttpApiEndpoint.post("registerManifest", "/manifests", { + payload: Schema.Any, + error: [ + Error.InvalidPayloadFormatError, + Error.InvalidManifestError, + Error.ManifestValidationError, + Error.ManifestStorageError, + Error.ManifestRegistrationError, + Error.UnsupportedDatasetKindError + ], + success: Domain.RegisterManifestResponse.pipe(HttpApiSchema.status(201)) +}) -/** - * Error type for the `registerManifest` endpoint. - */ -export type RegisterManifestError = - | Error.InvalidPayloadFormatError - | Error.InvalidManifestError - | Error.ManifestValidationError - | Error.ManifestStorageError - | Error.ManifestRegistrationError - | Error.UnsupportedDatasetKindError +export type RegisterManifestError = typeof registerManifest["~Error"] // ============================================================================= // Provider Endpoints // ============================================================================= // GET /providers - List providers -const getProviders = HttpApiEndpoint.get("getProviders")`/providers` - .addSuccess(Domain.GetProvidersResponse) +const getProviders = HttpApiEndpoint.get("getProviders", "/providers", { + success: Domain.GetProvidersResponse +}) + +export type GetProvidersError = typeof getProviders["~Error"] // ============================================================================= // Admin API Groups @@ -480,6 +430,4 @@ export class Api extends HttpApi.make("AmpAdminApi") .add(SchemaGroup) .add(ManifestGroup) .add(ProviderGroup) - .addError(HttpApiError.Forbidden) - .addError(HttpApiError.Unauthorized) {} diff --git a/packages/amp/src/admin/domain.ts b/packages/amp/src/admin/domain.ts index f6c5edb..f1704f5 100644 --- a/packages/amp/src/admin/domain.ts +++ b/packages/amp/src/admin/domain.ts @@ -12,142 +12,115 @@ import * as Models from "../core/domain.ts" /** * Response schema for listing datasets. */ -export class GetDatasetsResponse extends Schema.Class( - "Amp/AdminApi/GetDatasetsResponse" -)({ - datasets: Schema.Array(Schema.Struct({ - namespace: Models.DatasetNamespace, - name: Models.DatasetName, - versions: Schema.Array(Models.DatasetVersion), - latestVersion: Models.DatasetVersion.pipe( - Schema.optional, - Schema.fromKey("latest_version") - ) - })) -}, { identifier: "GetDatasetsResponse" }) {} +export const GetDatasetsResponse = Schema.Struct({ + datasets: Schema.Array( + Schema.Struct({ + namespace: Models.DatasetNamespace, + name: Models.DatasetName, + versions: Schema.Array(Models.DatasetVersion), + latestVersion: Schema.optional(Models.DatasetVersion) + }).pipe(Schema.encodeKeys({ latestVersion: "latest_version" })) + ) +}).annotate({ identifier: "GetDatasetsResponse" }) + +export type GetDatasetsResponse = typeof GetDatasetsResponse.Type /** * Request payload for registering a dataset. */ -export class RegisterDatasetPayload extends Schema.Class( - "Amp/AdminApi/RegisterDatasetPayload" -)({ +export const RegisterDatasetPayload = Schema.Struct({ namespace: Schema.String, name: Schema.String, version: Schema.optional(Schema.String), manifest: Models.DatasetManifest -}, { identifier: "RegisterDatasetPayload" }) {} +}).annotate({ identifier: "RegisterDatasetPayload" }) + +export type RegisterDatasetPayload = typeof RegisterDatasetPayload.Type /** * Response schema for getting a dataset version. */ -export class GetDatasetVersionResponse extends Schema.Class( - "Amp/AdminApi/GetDatasetVersionResponse" -)({ +export const GetDatasetVersionResponse = Schema.Struct({ kind: Models.DatasetKind, namespace: Models.DatasetNamespace, name: Models.DatasetName, revision: Models.DatasetRevision, - manifestHash: Models.DatasetHash.pipe( - Schema.propertySignature, - Schema.fromKey("manifest_hash") - ) -}, { identifier: "GetDatasetVersionResponse" }) {} + manifestHash: Models.DatasetHash +}).pipe(Schema.encodeKeys({ manifestHash: "manifest_hash" })).annotate({ identifier: "GetDatasetVersionResponse" }) + +export type GetDatasetVersionResponse = typeof GetDatasetVersionResponse.Type /** * Response schema for listing dataset versions. */ -export class GetDatasetVersionsResponse extends Schema.Class( - "Amp/AdminApi/GetDatasetVersionsResponse" -)({ +export const GetDatasetVersionsResponse = Schema.Struct({ versions: Schema.Array(Models.DatasetVersion) -}, { identifier: "GetDatasetVersionsResponse" }) {} +}).annotate({ identifier: "GetDatasetVersionsResponse" }) + +export type GetDatasetVersionsResponse = typeof GetDatasetVersionsResponse.Type /** * Request payload for deploying a dataset. */ -export class DeployDatasetPayload extends Schema.Class( - "Amp/AdminApi/DeployDatasetPayload" -)({ - endBlock: Schema.NullOr(Schema.String).pipe( - Schema.optional, - Schema.fromKey("end_block") - ), +export const DeployDatasetPayload = Schema.Struct({ + endBlock: Schema.optional(Schema.NullOr(Schema.String)), parallelism: Schema.optional(Schema.Number), - workerId: Schema.String.pipe( - Schema.optional, - Schema.fromKey("worker_id") - ) -}, { identifier: "DeployDatasetPayload" }) {} + workerId: Schema.optional(Schema.String) +}).pipe(Schema.encodeKeys({ + endBlock: "end_block", + workerId: "worker_id" +})).annotate({ identifier: "DeployDatasetPayload" }) + +export type DeployDatasetPayload = typeof DeployDatasetPayload.Type /** * Response schema for deploying a dataset. */ -export class DeployDatasetResponse extends Schema.Class( - "Amp/AdminApi/DeployDatasetResponse" -)({ - jobId: Models.JobId.pipe( - Schema.propertySignature, - Schema.fromKey("job_id") - ) -}, { identifier: "DeployDatasetResponse" }) {} +export const DeployDatasetResponse = Schema.Struct({ + jobId: Models.JobId +}).pipe(Schema.encodeKeys({ jobId: "job_id" })).annotate({ identifier: "DeployDatasetResponse" }) + +export type DeployDatasetResponse = typeof DeployDatasetResponse.Type /** * Table sync progress information. */ export const TableSyncProgress = Schema.Struct({ - tableName: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("table_name") - ), - currentBlock: Schema.Int.pipe( - Schema.optional, - Schema.fromKey("current_block") - ), - startBlock: Schema.Int.pipe( - Schema.optional, - Schema.fromKey("start_block") - ), - jobId: Models.JobId.pipe( - Schema.optional, - Schema.fromKey("job_id") - ), - jobStatus: Models.JobStatus.pipe( - Schema.optional, - Schema.fromKey("job_status") - ), - filesCount: Schema.Int.pipe( - Schema.propertySignature, - Schema.fromKey("files_count") - ), - totalSizeBytes: Schema.Int.pipe( - Schema.propertySignature, - Schema.fromKey("total_size_bytes") - ) -}).annotations({ identifier: "TableSyncProgress" }) + tableName: Schema.String, + currentBlock: Schema.optional(Schema.Int), + startBlock: Schema.optional(Schema.Int), + jobId: Schema.optional(Models.JobId), + jobStatus: Schema.optional(Models.JobStatus), + filesCount: Schema.Int, + totalSizeBytes: Schema.Int +}).pipe(Schema.encodeKeys({ + tableName: "table_name", + currentBlock: "current_block", + startBlock: "start_block", + jobId: "job_id", + jobStatus: "job_status", + filesCount: "files_count", + totalSizeBytes: "total_size_bytes" +})).annotate({ identifier: "TableSyncProgress" }) + export type TableSyncProgress = typeof TableSyncProgress.Type /** * Response schema for getting dataset sync progress. */ -export class GetDatasetSyncProgressResponse extends Schema.Class( - "Amp/AdminApi/GetDatasetSyncProgressResponse" -)({ - namespace: Models.DatasetNamespace.pipe( - Schema.propertySignature, - Schema.fromKey("dataset_namespace") - ), - name: Models.DatasetName.pipe( - Schema.propertySignature, - Schema.fromKey("dataset_name") - ), +export const GetDatasetSyncProgressResponse = Schema.Struct({ + namespace: Models.DatasetNamespace, + name: Models.DatasetName, revision: Models.DatasetRevision, - manifestHash: Models.DatasetHash.pipe( - Schema.propertySignature, - Schema.fromKey("manifest_hash") - ), + manifestHash: Models.DatasetHash, tables: Schema.Array(TableSyncProgress) -}) {} +}).pipe(Schema.encodeKeys({ + namespace: "dataset_namespace", + name: "dataset_name", + manifestHash: "manifest_hash" +})).annotate({ identifier: "GetDatasetSyncProgressResponse" }) + +export type GetDatasetSyncProgressResponse = typeof GetDatasetSyncProgressResponse.Type // ============================================================================= // Job Request/Response Schemas @@ -156,15 +129,12 @@ export class GetDatasetSyncProgressResponse extends Schema.Class( - "Amp/AdminApi/GetJobsResponse" -)({ +export const GetJobsResponse = Schema.Struct({ jobs: Schema.Array(Models.JobInfo), - nextCursor: Models.JobId.pipe( - Schema.optional, - Schema.fromKey("next_cursor") - ) -}, { identifier: "GetJobsResponse" }) {} + nextCursor: Schema.optional(Models.JobId) +}).pipe(Schema.encodeKeys({ nextCursor: "next_cursor" })).annotate({ identifier: "GetJobsResponse" }) + +export type GetJobsResponse = typeof GetJobsResponse.Type // ============================================================================= // Schema Request/Response Schemas @@ -173,34 +143,31 @@ export class GetJobsResponse extends Schema.Class( /** * Request payload for schema analysis. */ -export class GetOutputSchemaPayload extends Schema.Class( - "Amp/AdminApi/GetOutputSchemaPayload" -)({ - tables: Schema.Record({ - key: Schema.String, - value: Schema.String - }), - dependencies: Schema.Record({ - key: Schema.String, - value: Models.DatasetReferenceFromString - }).pipe(Schema.optional), - functions: Schema.Record({ - key: Schema.String, - value: Models.FunctionDefinition - }).pipe(Schema.optional) -}, { identifier: "GetOutputSchemaPayload" }) {} +export const GetOutputSchemaPayload = Schema.Struct({ + tables: Schema.Record(Schema.String, Schema.String), + dependencies: Schema.optional(Schema.Record( + Schema.String, + Models.DatasetReferenceFromString + )), + functions: Schema.optional(Schema.Record( + Schema.String, + Models.FunctionDefinition + )) +}).annotate({ identifier: "GetOutputSchemaPayload" }) + +export type GetOutputSchemaPayload = typeof GetOutputSchemaPayload.Type /** * Response schema for schema analysis. */ -export class GetOutputSchemaResponse extends Schema.Class( - "Amp/AdminApi/GetOutputSchemaResponse" -)({ - schemas: Schema.Record({ - key: Schema.String, - value: Models.TableSchemaWithNetworks - }) -}, { identifier: "GetOutputSchemaResponse" }) {} +export const GetOutputSchemaResponse = Schema.Struct({ + schemas: Schema.Record( + Schema.String, + Models.TableSchemaWithNetworks + ) +}).annotate({ identifier: "GetOutputSchemaResponse" }) + +export type GetOutputSchemaResponse = typeof GetOutputSchemaResponse.Type // ============================================================================= // Worker Request/Response Schemas @@ -210,25 +177,23 @@ export class GetOutputSchemaResponse extends Schema.Class( - "Amp/AdminApi/GetWorkersResponse" -)({ +export const GetWorkersResponse = Schema.Struct({ workers: Schema.Array(WorkerInfo) -}, { identifier: "GetWorkersResponse" }) {} +}).annotate({ identifier: "GetWorkersResponse" }) + +export type GetWorkersResponse = typeof GetWorkersResponse.Type // ============================================================================= // Manifest Request/Response Schemas @@ -237,11 +202,11 @@ export class GetWorkersResponse extends Schema.Class( /** * Response schema for registering a manifest. */ -export class RegisterManifestResponse extends Schema.Class( - "Amp/AdminApi/RegisterManifestResponse" -)({ +export const RegisterManifestResponse = Schema.Struct({ hash: Models.DatasetHash -}, { identifier: "RegisterManifestResponse" }) {} +}).annotate({ identifier: "RegisterManifestResponse" }) + +export type RegisterManifestResponse = typeof RegisterManifestResponse.Type // ============================================================================= // Provider Request/Response Schemas @@ -254,14 +219,14 @@ export const ProviderInfo = Schema.Struct({ name: Schema.String, network: Models.Network, config: Schema.Any -}).annotations({ identifier: "ProviderInfo" }) +}).annotate({ identifier: "ProviderInfo" }) export type ProviderInfo = typeof ProviderInfo.Type /** * Response schema for listing providers. */ -export class GetProvidersResponse extends Schema.Class( - "Amp/AdminApi/GetProvidersResponse" -)({ +export const GetProvidersResponse = Schema.Struct({ providers: Schema.Array(ProviderInfo) -}, { identifier: "GetProvidersResponse" }) {} +}).annotate({ identifier: "GetProvidersResponse" }) + +export type GetProvidersResponse = typeof GetProvidersResponse.Type diff --git a/packages/amp/src/admin/error.ts b/packages/amp/src/admin/error.ts index 96f4aef..05f4477 100644 --- a/packages/amp/src/admin/error.ts +++ b/packages/amp/src/admin/error.ts @@ -18,36 +18,57 @@ * } * ``` */ -import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" -/** - * Machine-readable error code in SCREAMING_SNAKE_CASE format - * - * Error codes are stable across API versions and should be used - * for programmatic error handling. Examples: `INVALID_SELECTOR`, - * `DATASET_NOT_FOUND`, `METADATA_DB_ERROR` - */ -const ErrorCode = ( - code: Code -): Schema.PropertySignature<":", Code, "error_code", ":", Code> => - Schema.Literal(code).pipe( - Schema.propertySignature, - Schema.fromKey("error_code") - ) - -const BaseErrorFields = { - /** - * Human-readable error message - * - * Messages provide detailed context about the error but may change - * over time. Use `error_code` for programmatic decisions. - */ - message: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("error_message") - ) -} +export const makeError = < + const Code extends string, + const Tag extends string, + const Fields extends Schema.Struct.Fields = {} +>(code: Code, tag: Tag, fields?: Fields): Schema.encodeKeys< + Schema.Struct< + Fields & { + readonly _tag: Schema.withDecodingDefaultKey> + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `METADATA_DB_ERROR` + */ + readonly code: Schema.withConstructorDefault> + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + readonly message: Schema.String + } + >, + { readonly code: "error_code"; readonly message: "error_message" } +> => + Schema.Struct({ + ...fields, + _tag: Schema.tagDefaultOmit(tag), + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `METADATA_DB_ERROR` + */ + code: Schema.Literal(code), + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + message: Schema.String + }).pipe(Schema.encodeKeys({ + code: "error_code", + message: "error_message" + })) as any // ============================================================================= // Dataset Errors @@ -60,12 +81,12 @@ const BaseErrorFields = { * - SQL query contains a catalog-qualified table reference (catalog.schema.table) * - Only dataset-qualified tables are supported (dataset.table) */ -export class CatalogQualifiedTableError extends Schema.Class( - "Amp/AdminApi/CatalogQualifiedTableError" -)({ - ...BaseErrorFields, - code: ErrorCode("CATALOG_QUALIFIED_TABLE") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const CatalogQualifiedTableError = makeError( + "CATALOG_QUALIFIED_TABLE", + "CatalogQualifiedTableError" +).annotate({ httpApiStatus: 400 }) + +export type CatalogQualifiedTableError = typeof CatalogQualifiedTableError.Type /** * CatalogQualifiedFunction - Function reference includes a catalog qualifier. @@ -74,12 +95,12 @@ export class CatalogQualifiedTableError extends Schema.Class( - "Amp/AdminApi/CatalogQualifiedFunctionError" -)({ - ...BaseErrorFields, - code: ErrorCode("CATALOG_QUALIFIED_FUNCTION") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const CatalogQualifiedFunctionError = makeError( + "CATALOG_QUALIFIED_FUNCTION", + "CatalogQualifiedFunctionError" +).annotate({ httpApiStatus: 400 }) + +export type CatalogQualifiedFunctionError = typeof CatalogQualifiedFunctionError.Type /** * DatasetNotFound - The requested dataset does not exist. @@ -89,12 +110,12 @@ export class CatalogQualifiedFunctionError extends Schema.Class( - "Amp/AdminApi/DatasetNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("DATASET_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const DatasetNotFoundError = makeError( + "DATASET_NOT_FOUND", + "DatasetNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type DatasetNotFoundError = typeof DatasetNotFoundError.Type /** * DatasetStoreError - Failure in dataset storage operations. @@ -105,12 +126,12 @@ export class DatasetNotFoundError extends Schema.Class( * - Unsupported dataset kind * - Dataset name validation failures */ -export class DatasetStoreError extends Schema.Class( - "Amp/AdminApi/DatasetStoreError" -)({ - ...BaseErrorFields, - code: ErrorCode("DATASET_STORE_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const DatasetStoreError = makeError( + "DATASET_STORE_ERROR", + "DatasetStoreError" +).annotate({ httpApiStatus: 500 }) + +export type DatasetStoreError = typeof DatasetStoreError.Type /** * DependencyAliasNotFound - Dependency alias not found in dependencies map. @@ -119,12 +140,12 @@ export class DatasetStoreError extends Schema.Class( * - Table reference uses an alias not provided in dependencies * - Function reference uses an alias not provided in dependencies */ -export class DependencyAliasNotFoundError extends Schema.Class( - "Amp/AdminApi/DependencyAliasNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("DEPENDENCY_ALIAS_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const DependencyAliasNotFoundError = makeError( + "DEPENDENCY_ALIAS_NOT_FOUND", + "DependencyAliasNotFoundError" +).annotate({ httpApiStatus: 400 }) + +export type DependencyAliasNotFoundError = typeof DependencyAliasNotFoundError.Type /** * DependencyNotFound - Dependency not found in dataset store. @@ -133,12 +154,12 @@ export class DependencyAliasNotFoundError extends Schema.Class( - "Amp/AdminApi/DependencyNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("DEPENDENCY_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const DependencyNotFoundError = makeError( + "DEPENDENCY_NOT_FOUND", + "DependencyNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type DependencyNotFoundError = typeof DependencyNotFoundError.Type /** * DependencyResolution - Failed to resolve dependency. @@ -146,12 +167,12 @@ export class DependencyNotFoundError extends Schema.Class( - "Amp/AdminApi/DependencyResolutionError" -)({ - ...BaseErrorFields, - code: ErrorCode("DEPENDENCY_RESOLUTION") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const DependencyResolutionError = makeError( + "DEPENDENCY_RESOLUTION", + "DependencyResolutionError" +).annotate({ httpApiStatus: 500 }) + +export type DependencyResolutionError = typeof DependencyResolutionError.Type /** * EmptyTablesAndFunctions - No tables or functions provided. @@ -159,12 +180,12 @@ export class DependencyResolutionError extends Schema.Class( - "Amp/AdminApi/EmptyTablesAndFunctionsError" -)({ - ...BaseErrorFields, - code: ErrorCode("EMPTY_TABLES_AND_FUNCTIONS") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const EmptyTablesAndFunctionsError = makeError( + "EMPTY_TABLES_AND_FUNCTIONS", + "EmptyTablesAndFunctionsError" +).annotate({ httpApiStatus: 400 }) + +export type EmptyTablesAndFunctionsError = typeof EmptyTablesAndFunctionsError.Type /** * EthCallNotAvailable - eth_call function not available for dataset. @@ -173,12 +194,12 @@ export class EmptyTablesAndFunctionsError extends Schema.Class( - "Amp/AdminApi/EthCallNotAvailableError" -)({ - ...BaseErrorFields, - code: ErrorCode("ETH_CALL_NOT_AVAILABLE") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const EthCallNotAvailableError = makeError( + "ETH_CALL_NOT_AVAILABLE", + "EthCallNotAvailableError" +).annotate({ httpApiStatus: 404 }) + +export type EthCallNotAvailableError = typeof EthCallNotAvailableError.Type /** * EthCallUdfCreationError - Failed to create ETH call UDF. @@ -187,12 +208,12 @@ export class EthCallNotAvailableError extends Schema.Class( - "Amp/AdminApi/EthCallUdfCreationError" -)({ - ...BaseErrorFields, - code: ErrorCode("ETH_CALL_UDF_CREATION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const EthCallUdfCreationError = makeError( + "ETH_CALL_UDF_CREATION_ERROR", + "EthCallUdfCreationError" +).annotate({ httpApiStatus: 500 }) + +export type EthCallUdfCreationError = typeof EthCallUdfCreationError.Type /** * FunctionNotFoundInDataset - Function not found in referenced dataset. @@ -201,12 +222,12 @@ export class EthCallUdfCreationError extends Schema.Class( - "Amp/AdminApi/FunctionNotFoundInDatasetError" -)({ - ...BaseErrorFields, - code: ErrorCode("FUNCTION_NOT_FOUND_IN_DATASET") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const FunctionNotFoundInDatasetError = makeError( + "FUNCTION_NOT_FOUND_IN_DATASET", + "FunctionNotFoundInDatasetError" +).annotate({ httpApiStatus: 404 }) + +export type FunctionNotFoundInDatasetError = typeof FunctionNotFoundInDatasetError.Type /** * FunctionReferenceResolution - Failed to resolve function references from SQL. @@ -214,12 +235,12 @@ export class FunctionNotFoundInDatasetError extends Schema.Class( - "Amp/AdminApi/FunctionReferenceResolutionError" -)({ - ...BaseErrorFields, - code: ErrorCode("FUNCTION_REFERENCE_RESOLUTION") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const FunctionReferenceResolutionError = makeError( + "FUNCTION_REFERENCE_RESOLUTION", + "FunctionReferenceResolutionError" +).annotate({ httpApiStatus: 500 }) + +export type FunctionReferenceResolutionError = typeof FunctionReferenceResolutionError.Type /** * GetDatasetError - Failed to retrieve dataset from store. @@ -229,22 +250,22 @@ export class FunctionReferenceResolutionError extends Schema.Class( - "Amp/AdminApi/GetDatasetError" -)({ - ...BaseErrorFields, - code: ErrorCode("GET_DATASET_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const GetDatasetError = makeError( + "GET_DATASET_ERROR", + "GetDatasetError" +).annotate({ httpApiStatus: 500 }) + +export type GetDatasetError = typeof GetDatasetError.Type /** * GetManifestPathError - Failed to query manifest path from metadata database. */ -export class GetManifestPathError extends Schema.Class( - "Amp/AdminApi/GetManifestPathError" -)({ - ...BaseErrorFields, - code: ErrorCode("GET_MANIFEST_PATH_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const GetManifestPathError = makeError( + "GET_MANIFEST_PATH_ERROR", + "GetManifestPathError" +).annotate({ httpApiStatus: 500 }) + +export type GetManifestPathError = typeof GetManifestPathError.Type /** * GetSyncProgressError - Failed to retrieve the dataset sync progress @@ -252,12 +273,12 @@ export class GetManifestPathError extends Schema.Class( * Causes: * - Unable to resolve the dataset synchronization progress server side */ -export class GetSyncProgressError extends Schema.Class( - "Amp/AdminApi/GetSyncProgressError" -)({ - ...BaseErrorFields, - code: ErrorCode("GET_SYNC_PROGRESS_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const GetSyncProgressError = makeError( + "GET_SYNC_PROGRESS_ERROR", + "GetSyncProgressError" +).annotate({ httpApiStatus: 500 }) + +export type GetSyncProgressError = typeof GetSyncProgressError.Type // ============================================================================= // Job Errors @@ -270,12 +291,12 @@ export class GetSyncProgressError extends Schema.Class( * - Job ID contains invalid characters * - Job ID format does not match expected pattern */ -export class InvalidJobIdError extends Schema.Class( - "Amp/AdminApi/InvalidJobIdError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_JOB_ID") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidJobIdError = makeError( + "INVALID_JOB_ID", + "InvalidJobIdError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidJobIdError = typeof InvalidJobIdError.Type /** * JobNotFound - The requested job does not exist. @@ -284,72 +305,72 @@ export class InvalidJobIdError extends Schema.Class( * - Job ID does not exist in the system * - Job has been deleted */ -export class JobNotFoundError extends Schema.Class( - "Amp/AdminApi/JobNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("JOB_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const JobNotFoundError = makeError( + "JOB_NOT_FOUND", + "JobNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type JobNotFoundError = typeof JobNotFoundError.Type /** * JobConflict - Job exists but cannot be deleted (not in terminal state). */ -export class JobConflictError extends Schema.Class( - "Amp/AdminApi/JobConflictError" -)({ - ...BaseErrorFields, - code: ErrorCode("JOB_CONFLICT") -}, HttpApiSchema.annotations({ status: 409 })) {} +export const JobConflictError = makeError( + "JOB_CONFLICT", + "JobConflictError" +).annotate({ httpApiStatus: 409 }) + +export type JobConflictError = typeof JobConflictError.Type /** * GetJobError - Failed to retrieve job from scheduler. */ -export class GetJobError extends Schema.Class( - "Amp/AdminApi/GetJobError" -)({ - ...BaseErrorFields, - code: ErrorCode("GET_JOB_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const GetJobError = makeError( + "GET_JOB_ERROR", + "GetJobError" +).annotate({ httpApiStatus: 500 }) + +export type GetJobError = typeof GetJobError.Type /** * DeleteJobError - Failed to delete job from scheduler. */ -export class DeleteJobError extends Schema.Class( - "Amp/AdminApi/DeleteJobError" -)({ - ...BaseErrorFields, - code: ErrorCode("DELETE_JOB_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const DeleteJobError = makeError( + "DELETE_JOB_ERROR", + "DeleteJobError" +).annotate({ httpApiStatus: 500 }) + +export type DeleteJobError = typeof DeleteJobError.Type /** * StopJobError - Database error during stop operation. */ -export class StopJobError extends Schema.Class( - "Amp/AdminApi/StopJobError" -)({ - ...BaseErrorFields, - code: ErrorCode("STOP_JOB_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const StopJobError = makeError( + "STOP_JOB_ERROR", + "StopJobError" +).annotate({ httpApiStatus: 500 }) + +export type StopJobError = typeof StopJobError.Type /** * ListJobsError - Failed to list jobs from scheduler. */ -export class ListJobsError extends Schema.Class( - "Amp/AdminApi/ListJobsError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIST_JOBS_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ListJobsError = makeError( + "LIST_JOBS_ERROR", + "ListJobsError" +).annotate({ httpApiStatus: 500 }) + +export type ListJobsError = typeof ListJobsError.Type /** * UnexpectedStateConflict - Internal state machine error. */ -export class UnexpectedStateConflictError extends Schema.Class( - "Amp/AdminApi/UnexpectedStateConflictError" -)({ - ...BaseErrorFields, - code: ErrorCode("UNEXPECTED_STATE_CONFLICT") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const UnexpectedStateConflictError = makeError( + "UNEXPECTED_STATE_CONFLICT", + "UnexpectedStateConflictError" +).annotate({ httpApiStatus: 500 }) + +export type UnexpectedStateConflictError = typeof UnexpectedStateConflictError.Type // ============================================================================= // Manifest Errors @@ -363,42 +384,42 @@ export class UnexpectedStateConflictError extends Schema.Class( - "Amp/AdminApi/InvalidManifestError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_MANIFEST") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidManifestError = makeError( + "INVALID_MANIFEST", + "InvalidManifestError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidManifestError = typeof InvalidManifestError.Type /** * ManifestLinkingError - Failed to link manifest to dataset. */ -export class ManifestLinkingError extends Schema.Class( - "Amp/AdminApi/ManifestLinkingError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_LINKING_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ManifestLinkingError = makeError( + "MANIFEST_LINKING_ERROR", + "ManifestLinkingError" +).annotate({ httpApiStatus: 500 }) + +export type ManifestLinkingError = typeof ManifestLinkingError.Type /** * ManifestNotFound - Manifest with the provided hash not found. */ -export class ManifestNotFoundError extends Schema.Class( - "Amp/AdminApi/ManifestNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const ManifestNotFoundError = makeError( + "MANIFEST_NOT_FOUND", + "ManifestNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type ManifestNotFoundError = typeof ManifestNotFoundError.Type /** * ManifestRegistrationError - Failed to register manifest in the system. */ -export class ManifestRegistrationError extends Schema.Class( - "Amp/AdminApi/ManifestRegistrationError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_REGISTRATION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ManifestRegistrationError = makeError( + "MANIFEST_REGISTRATION_ERROR", + "ManifestRegistrationError" +).annotate({ httpApiStatus: 500 }) + +export type ManifestRegistrationError = typeof ManifestRegistrationError.Type /** * ManifestValidationError - Manifest validation error. @@ -408,42 +429,42 @@ export class ManifestRegistrationError extends Schema.Class( - "Amp/AdminApi/ManifestValidationError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_VALIDATION_ERROR") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const ManifestValidationError = makeError( + "MANIFEST_VALIDATION_ERROR", + "ManifestValidationError" +).annotate({ httpApiStatus: 400 }) + +export type ManifestValidationError = typeof ManifestValidationError.Type /** * ManifestStorageError - Failed to write manifest to object store. */ -export class ManifestStorageError extends Schema.Class( - "Amp/AdminApi/ManifestStorageError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_STORAGE_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ManifestStorageError = makeError( + "MANIFEST_STORAGE_ERROR", + "ManifestStorageError" +).annotate({ httpApiStatus: 500 }) + +export type ManifestStorageError = typeof ManifestStorageError.Type /** * ParseManifestError - Failed to parse manifest JSON. */ -export class ParseManifestError extends Schema.Class( - "Amp/AdminApi/ParseManifestError" -)({ - ...BaseErrorFields, - code: ErrorCode("PARSE_MANIFEST_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ParseManifestError = makeError( + "PARSE_MANIFEST_ERROR", + "ParseManifestError" +).annotate({ httpApiStatus: 500 }) + +export type ParseManifestError = typeof ParseManifestError.Type /** * ReadManifestError - Failed to read manifest from object store. */ -export class ReadManifestError extends Schema.Class( - "Amp/AdminApi/ReadManifestError" -)({ - ...BaseErrorFields, - code: ErrorCode("READ_MANIFEST_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ReadManifestError = makeError( + "READ_MANIFEST_ERROR", + "ReadManifestError" +).annotate({ httpApiStatus: 500 }) + +export type ReadManifestError = typeof ReadManifestError.Type // ============================================================================= // Request Validation Errors @@ -452,132 +473,132 @@ export class ReadManifestError extends Schema.Class( /** * InvalidPath - Invalid path parameters. */ -export class InvalidPathError extends Schema.Class( - "Amp/AdminApi/InvalidPathError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_PATH") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidPathError = makeError( + "INVALID_PATH", + "InvalidPathError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidPathError = typeof InvalidPathError.Type /** * InvalidBody - Invalid request body. */ -export class InvalidBodyError extends Schema.Class( - "Amp/AdminApi/InvalidBodyError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_BODY") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidBodyError = makeError( + "INVALID_BODY", + "InvalidBodyError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidBodyError = typeof InvalidBodyError.Type /** * InvalidPathParams - Invalid request path parameters. */ -export class InvalidPathParamsError extends Schema.Class( - "Amp/AdminApi/InvalidPathParamsError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_PATH_PARAMS") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidPathParamsError = makeError( + "INVALID_PATH_PARAMS", + "InvalidPathParamsError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidPathParamsError = typeof InvalidPathParamsError.Type /** * InvalidPayloadFormat - Invalid request payload format. */ -export class InvalidPayloadFormatError extends Schema.Class( - "Amp/AdminApi/InvalidPayloadFormatError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_PAYLOAD_FORMAT") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidPayloadFormatError = makeError( + "INVALID_PAYLOAD_FORMAT", + "InvalidPayloadFormatError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidPayloadFormatError = typeof InvalidPayloadFormatError.Type /** * InvalidQueryParameters - Invalid query parameters. */ -export class InvalidQueryParametersError extends Schema.Class( - "Amp/AdminApi/InvalidQueryParametersError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_QUERY_PARAMETERS") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidQueryParametersError = makeError( + "INVALID_QUERY_PARAMETERS", + "InvalidQueryParametersError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidQueryParametersError = typeof InvalidQueryParametersError.Type /** * InvalidRequest - The request is malformed or contains invalid parameters. */ -export class InvalidRequestError extends Schema.Class( - "Amp/AdminApi/InvalidRequestError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_REQUEST") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidRequestError = makeError( + "INVALID_REQUEST", + "InvalidRequestError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidRequestError = typeof InvalidRequestError.Type /** * InvalidSelector - The provided dataset selector is malformed or invalid. */ -export class InvalidSelectorError extends Schema.Class( - "Amp/AdminApi/InvalidSelectorError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_SELECTOR") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidSelectorError = makeError( + "INVALID_SELECTOR", + "InvalidSelectorError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidSelectorError = typeof InvalidSelectorError.Type /** * InvalidTableName - Table name does not conform to SQL identifier rules. */ -export class InvalidTableNameError extends Schema.Class( - "Amp/AdminApi/InvalidTableNameError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_TABLE_NAME") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidTableNameError = makeError( + "INVALID_TABLE_NAME", + "InvalidTableNameError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidTableNameError = typeof InvalidTableNameError.Type /** * InvalidTableSql - SQL syntax error in table definition. */ -export class InvalidTableSqlError extends Schema.Class( - "Amp/AdminApi/InvalidTableSqlError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_TABLE_SQL") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidTableSqlError = makeError( + "INVALID_TABLE_SQL", + "InvalidTableSqlError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidTableSqlError = typeof InvalidTableSqlError.Type /** * InvalidDependencyAliasForTableRef - Invalid dependency alias in table reference. */ -export class InvalidDependencyAliasForTableRefError extends Schema.Class( - "Amp/AdminApi/InvalidDependencyAliasForTableRefError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_DEPENDENCY_ALIAS_FOR_TABLE_REF") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidDependencyAliasForTableRefError = makeError( + "INVALID_DEPENDENCY_ALIAS_FOR_TABLE_REF", + "InvalidDependencyAliasForTableRefError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidDependencyAliasForTableRefError = typeof InvalidDependencyAliasForTableRefError.Type /** * InvalidDependencyAliasForFunctionRef - Invalid dependency alias in function reference. */ -export class InvalidDependencyAliasForFunctionRefError extends Schema.Class( - "Amp/AdminApi/InvalidDependencyAliasForFunctionRefError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_DEPENDENCY_ALIAS_FOR_FUNCTION_REF") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const InvalidDependencyAliasForFunctionRefError = makeError( + "INVALID_DEPENDENCY_ALIAS_FOR_FUNCTION_REF", + "InvalidDependencyAliasForFunctionRefError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidDependencyAliasForFunctionRefError = typeof InvalidDependencyAliasForFunctionRefError.Type /** * LimitTooLarge - The requested limit exceeds the maximum allowed value. */ -export class LimitTooLargeError extends Schema.Class( - "Amp/AdminApi/LimitTooLargeError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIMIT_TOO_LARGE") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const LimitTooLargeError = makeError( + "LIMIT_TOO_LARGE", + "LimitTooLargeError" +).annotate({ httpApiStatus: 400 }) + +export type LimitTooLargeError = typeof LimitTooLargeError.Type /** * LimitInvalid - The requested limit is invalid (zero). */ -export class LimitInvalidError extends Schema.Class( - "Amp/AdminApi/LimitInvalidError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIMIT_INVALID") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const LimitInvalidError = makeError( + "LIMIT_INVALID", + "LimitInvalidError" +).annotate({ httpApiStatus: 400 }) + +export type LimitInvalidError = typeof LimitInvalidError.Type // ============================================================================= // Database Errors @@ -586,32 +607,32 @@ export class LimitInvalidError extends Schema.Class( /** * MetadataDbError - Database operation failure in the metadata PostgreSQL database. */ -export class MetadataDbError extends Schema.Class( - "Amp/AdminApi/MetadataDbError" -)({ - ...BaseErrorFields, - code: ErrorCode("METADATA_DB_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const MetadataDbError = makeError( + "METADATA_DB_ERROR", + "MetadataDbError" +).annotate({ httpApiStatus: 500 }) + +export type MetadataDbError = typeof MetadataDbError.Type /** * PhysicalTableError - Failed to access the physical table metadata. */ -export class PhysicalTableError extends Schema.Class( - "Amp/AdminApi/PhysicalTableError" -)({ - ...BaseErrorFields, - code: ErrorCode("PHYSICAL_TABLE_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const PhysicalTableError = makeError( + "PHYSICAL_TABLE_ERROR", + "PhysicalTableError" +).annotate({ httpApiStatus: 500 }) + +export type PhysicalTableError = typeof PhysicalTableError.Type /** * ResolveRevisionError - Failed to resolve the dataset revision. */ -export class ResolveRevisionError extends Schema.Class( - "Amp/AdminApi/ResolveRevisionError" -)({ - ...BaseErrorFields, - code: ErrorCode("RESOLVE_REVISION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ResolveRevisionError = makeError( + "RESOLVE_REVISION_ERROR", + "ResolveRevisionError" +).annotate({ httpApiStatus: 500 }) + +export type ResolveRevisionError = typeof ResolveRevisionError.Type // ============================================================================= // Query/Schema Errors @@ -624,52 +645,52 @@ export class ResolveRevisionError extends Schema.Class( * - SQL contains LIMIT, ORDER BY, GROUP BY, DISTINCT, window functions * - SQL uses outer joins */ -export class NonIncrementalQueryError extends Schema.Class( - "Amp/AdminApi/NonIncrementalQueryError" -)({ - ...BaseErrorFields, - code: ErrorCode("NON_INCREMENTAL_QUERY") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const NonIncrementalQueryError = makeError( + "NON_INCREMENTAL_QUERY", + "NonIncrementalQueryError" +).annotate({ httpApiStatus: 400 }) + +export type NonIncrementalQueryError = typeof NonIncrementalQueryError.Type /** * SchemaInference - Failed to infer output schema from query. */ -export class SchemaInferenceError extends Schema.Class( - "Amp/AdminApi/SchemaInferenceError" -)({ - ...BaseErrorFields, - code: ErrorCode("SCHEMA_INFERENCE") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const SchemaInferenceError = makeError( + "SCHEMA_INFERENCE", + "SchemaInferenceError" +).annotate({ httpApiStatus: 500 }) + +export type SchemaInferenceError = typeof SchemaInferenceError.Type /** * TableNotFoundInDataset - Table not found in dataset. */ -export class TableNotFoundInDatasetError extends Schema.Class( - "Amp/AdminApi/TableNotFoundInDatasetError" -)({ - ...BaseErrorFields, - code: ErrorCode("TABLE_NOT_FOUND_IN_DATASET") -}, HttpApiSchema.annotations({ status: 404 })) {} +export const TableNotFoundInDatasetError = makeError( + "TABLE_NOT_FOUND_IN_DATASET", + "TableNotFoundInDatasetError" +).annotate({ httpApiStatus: 404 }) + +export type TableNotFoundInDatasetError = typeof TableNotFoundInDatasetError.Type /** * TableReferenceResolution - Failed to extract table references from SQL. */ -export class TableReferenceResolutionError extends Schema.Class( - "Amp/AdminApi/TableReferenceResolutionError" -)({ - ...BaseErrorFields, - code: ErrorCode("TABLE_REFERENCE_RESOLUTION") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const TableReferenceResolutionError = makeError( + "TABLE_REFERENCE_RESOLUTION", + "TableReferenceResolutionError" +).annotate({ httpApiStatus: 400 }) + +export type TableReferenceResolutionError = typeof TableReferenceResolutionError.Type /** * UnqualifiedTable - Table reference is not qualified with a dataset. */ -export class UnqualifiedTableError extends Schema.Class( - "Amp/AdminApi/UnqualifiedTableError" -)({ - ...BaseErrorFields, - code: ErrorCode("UNQUALIFIED_TABLE") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const UnqualifiedTableError = makeError( + "UNQUALIFIED_TABLE", + "UnqualifiedTableError" +).annotate({ httpApiStatus: 400 }) + +export type UnqualifiedTableError = typeof UnqualifiedTableError.Type // ============================================================================= // Scheduler/Worker Errors @@ -678,32 +699,32 @@ export class UnqualifiedTableError extends Schema.Class( /** * SchedulerError - Indicates a failure in the job scheduling system. */ -export class SchedulerError extends Schema.Class( - "Amp/AdminApi/SchedulerError" -)({ - ...BaseErrorFields, - code: ErrorCode("SCHEDULER_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const SchedulerError = makeError( + "SCHEDULER_ERROR", + "SchedulerError" +).annotate({ httpApiStatus: 500 }) + +export type SchedulerError = typeof SchedulerError.Type /** * WorkerNotAvailable - Specified worker not found or inactive. */ -export class WorkerNotAvailableError extends Schema.Class( - "Amp/AdminApi/WorkerNotAvailableError" -)({ - ...BaseErrorFields, - code: ErrorCode("WORKER_NOT_AVAILABLE") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const WorkerNotAvailableError = makeError( + "WORKER_NOT_AVAILABLE", + "WorkerNotAvailableError" +).annotate({ httpApiStatus: 400 }) + +export type WorkerNotAvailableError = typeof WorkerNotAvailableError.Type /** * SchedulerListWorkersError - Failed to list workers from the scheduler. */ -export class SchedulerListWorkersError extends Schema.Class( - "Amp/AdminApi/SchedulerListWorkersError" -)({ - ...BaseErrorFields, - code: ErrorCode("SCHEDULER_LIST_WORKERS_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const SchedulerListWorkersError = makeError( + "SCHEDULER_LIST_WORKERS_ERROR", + "SchedulerListWorkersError" +).annotate({ httpApiStatus: 500 }) + +export type SchedulerListWorkersError = typeof SchedulerListWorkersError.Type // ============================================================================= // Store Errors @@ -712,49 +733,49 @@ export class SchedulerListWorkersError extends Schema.Class( - "Amp/AdminApi/StoreError" -)({ - ...BaseErrorFields, - code: ErrorCode("STORE_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const StoreError = makeError( + "STORE_ERROR", + "StoreError" +).annotate({ httpApiStatus: 500 }) + +export type StoreError = typeof StoreError.Type /** * UnsupportedDatasetKind - Dataset kind is not supported. */ -export class UnsupportedDatasetKindError extends Schema.Class( - "Amp/AdminApi/UnsupportedDatasetKindError" -)({ - ...BaseErrorFields, - code: ErrorCode("UNSUPPORTED_DATASET_KIND") -}, HttpApiSchema.annotations({ status: 400 })) {} +export const UnsupportedDatasetKindError = makeError( + "UNSUPPORTED_DATASET_KIND", + "UnsupportedDatasetKindError" +).annotate({ httpApiStatus: 400 }) + +export type UnsupportedDatasetKindError = typeof UnsupportedDatasetKindError.Type /** * VersionTaggingError - Failed to tag version for the dataset. */ -export class VersionTaggingError extends Schema.Class( - "Amp/AdminApi/VersionTaggingError" -)({ - ...BaseErrorFields, - code: ErrorCode("VERSION_TAGGING_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const VersionTaggingError = makeError( + "VERSION_TAGGING_ERROR", + "VersionTaggingError" +).annotate({ httpApiStatus: 500 }) + +export type VersionTaggingError = typeof VersionTaggingError.Type /** * ListAllDatasetsError - Failed to list all datasets from dataset store. */ -export class ListAllDatasetsError extends Schema.Class( - "Amp/AdminApi/ListAllDatasetsError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIST_ALL_DATASETS_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ListAllDatasetsError = makeError( + "LIST_ALL_DATASETS_ERROR", + "ListAllDatasetsError" +).annotate({ httpApiStatus: 500 }) + +export type ListAllDatasetsError = typeof ListAllDatasetsError.Type /** * ListVersionTagsError - Failed to list version tags from dataset store. */ -export class ListVersionTagsError extends Schema.Class( - "Amp/AdminApi/ListVersionTagsError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIST_VERSION_TAGS_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} +export const ListVersionTagsError = makeError( + "LIST_VERSION_TAGS_ERROR", + "ListVersionTagsError" +).annotate({ httpApiStatus: 500 }) + +export type ListVersionTagsError = typeof ListVersionTagsError.Type diff --git a/packages/amp/src/admin/service.ts b/packages/amp/src/admin/service.ts index 0fecdcd..b1ab749 100644 --- a/packages/amp/src/admin/service.ts +++ b/packages/amp/src/admin/service.ts @@ -9,21 +9,22 @@ * - Schema analysis * - Manifests (registration) */ -import * as HttpApiClient from "@effect/platform/HttpApiClient" -import type * as HttpApiError from "@effect/platform/HttpApiError" -import * as HttpClient from "@effect/platform/HttpClient" -import type * as HttpClientError from "@effect/platform/HttpClientError" -import * as HttpClientRequest from "@effect/platform/HttpClientRequest" -import type * as KeyValueStore from "@effect/platform/KeyValueStore" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { constUndefined } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" +import * as HttpClient from "effect/unstable/http/HttpClient" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient" +import type * as HttpApiError from "effect/unstable/httpapi/HttpApiError" +import type * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" import * as Auth from "../auth/service.ts" import type * as Models from "../core/domain.ts" import * as Api from "./api.ts" import type * as Domain from "./domain.ts" +import type { DatasetStoreError, ListAllDatasetsError, MetadataDbError, SchedulerListWorkersError } from "./error.ts" // ============================================================================= // Admin API Service Types @@ -44,7 +45,7 @@ export type HttpError = /** * A service which can be used to execute operations against the Amp admin API. */ -export class AdminApi extends Context.Tag("Amp/AdminApi") /** @@ -210,7 +215,7 @@ export class AdminApi extends Context.Tag("Amp/AdminApi") /** @@ -245,7 +250,9 @@ export class AdminApi extends Context.Tag("Amp/AdminApi") -}>() {} +}>()( + "Amp/AdminApi" +) {} export interface MakeOptions { readonly url: string | URL @@ -265,7 +272,7 @@ const make = Effect.fnUntraced(function*(options: MakeOptions) { Effect.fnUntraced(function*(request) { const authInfo = yield* auth.getCachedAuthInfo.pipe( // Treat cache errors as "no auth available" - Effect.catchAll(() => Effect.succeed(Option.none())) + Effect.catch(() => Effect.succeed(Option.none())) ) if (Option.isNone(authInfo)) return request const token = authInfo.value.accessToken @@ -279,127 +286,123 @@ const make = Effect.fnUntraced(function*(options: MakeOptions) { const deployDataset: Service["deployDataset"] = Effect.fn("AdminApi.deployDataset")( function*(namespace, name, revision, options) { - const path = { namespace, name, revision } + const params = { namespace, name, revision } const payload = { endBlock: options?.endBlock, parallelism: options?.parallelism, workerId: options?.workerId } - yield* Effect.annotateCurrentSpan({ ...path, ...payload }) - return yield* client.dataset.deployDataset({ path, payload }) + yield* Effect.annotateCurrentSpan({ params, payload }) + return yield* client.dataset.deployDataset({ params, payload }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const getDatasetManifest: Service["getDatasetManifest"] = Effect.fn("AdminApi.getDatasetManifest")( function*(namespace, name, revision) { - const path = { namespace, name, revision } - yield* Effect.annotateCurrentSpan(path) - return yield* client.dataset.getDatasetManifest({ path }) + const params = { namespace, name, revision } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.dataset.getDatasetManifest({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) - const getDatasets: Service["getDatasets"] = Effect.gen(function*() { - return yield* client.dataset.getDatasets({}) - }).pipe( - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die), + const getDatasets: Service["getDatasets"] = client.dataset.getDatasets({}).pipe( + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die), Effect.withSpan("AdminApi.getDatasets") ) const getDatasetVersion: Service["getDatasetVersion"] = Effect.fn("AdminApi.getDatasetVersion")( function*(namespace, name, revision) { - const path = { namespace, name, revision } - yield* Effect.annotateCurrentSpan(path) - return yield* client.dataset.getDatasetVersion({ path }) + const params = { namespace, name, revision } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.dataset.getDatasetVersion({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const getDatasetVersions: Service["getDatasetVersions"] = Effect.fn("AdminApi.getDatasetVersions")( function*(namespace, name) { - const path = { namespace, name } - yield* Effect.annotateCurrentSpan(path) - return yield* client.dataset.getDatasetVersions({ path }) + const params = { namespace, name } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.dataset.getDatasetVersions({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const registerDataset: Service["registerDataset"] = Effect.fn("AdminApi.registerDataset")( function*(namespace, name, manifest, version) { const payload = { namespace, name, version, manifest } - yield* Effect.annotateCurrentSpan(payload) + yield* Effect.annotateCurrentSpan({ payload }) return yield* client.dataset.registerDataset({ payload }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) - const getDatasetSyncProgress: Service["getDatasetSyncProgress"] = Effect.fn("AdminApi.getDatasetSyncProgress")( + const getDatasetSyncProgress: Service["getDatasetSyncProgress"] = Effect.fn( + "AdminApi.getDatasetSyncProgress" + )( function*(namespace, name, revision) { - const path = { namespace, name, revision } - yield* Effect.annotateCurrentSpan(path) - return yield* client.dataset.getDatasetSyncProgress({ path }) + const params = { namespace, name, revision } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.dataset.getDatasetSyncProgress({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) // Job Operations const getJobs: Service["getJobs"] = Effect.fn("AdminApi.getJobs")( function*(options) { - const urlParams = { + const query = { limit: options?.limit, lastJobId: options?.lastJobId, status: options?.status } - yield* Effect.annotateCurrentSpan(urlParams) - return yield* client.job.getJobs({ urlParams }) + yield* Effect.annotateCurrentSpan({ query }) + return yield* client.job.getJobs({ query }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const getJobById: Service["getJobById"] = Effect.fn("AdminApi.getJobById")( function*(jobId) { - const path = { jobId } - yield* Effect.annotateCurrentSpan(path) - return yield* client.job.getJobById({ path }) + const params = { id: jobId } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.job.getJobById({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const stopJob: Service["stopJob"] = Effect.fn("AdminApi.stopJob")( function*(jobId) { - const path = { jobId } - yield* Effect.annotateCurrentSpan(path) - return yield* client.job.stopJob({ path }) + const params = { id: jobId } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.job.stopJob({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) const deleteJob: Service["deleteJob"] = Effect.fn("AdminApi.deleteJob")( function*(jobId) { - const path = { jobId } - yield* Effect.annotateCurrentSpan(path) - return yield* client.job.deleteJob({ path }) + const params = { id: jobId } + yield* Effect.annotateCurrentSpan({ params }) + return yield* client.job.deleteJob({ params }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) // Worker Operations - const getWorkers: Service["getWorkers"] = Effect.gen(function*() { - return yield* client.worker.getWorkers({}) - }).pipe( - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die), + const getWorkers: Service["getWorkers"] = client.worker.getWorkers({}).pipe( + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die), Effect.withSpan("AdminApi.getWorkers") ) // Provider Operations - const getProviders: Service["getProviders"] = Effect.gen(function*() { - return yield* client.provider.getProviders({}) - }).pipe( - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die), + const getProviders: Service["getProviders"] = client.provider.getProviders({}).pipe( + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die), Effect.withSpan("AdminApi.getProviders") ) @@ -409,7 +412,7 @@ const make = Effect.fnUntraced(function*(options: MakeOptions) { function*(manifest) { return yield* client.manifest.registerManifest({ payload: manifest }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) // Schema Operations @@ -418,7 +421,7 @@ const make = Effect.fnUntraced(function*(options: MakeOptions) { function*(payload) { return yield* client.schema.getOutputSchema({ payload }) }, - Effect.catchTag("HttpApiDecodeError", "ParseError", Effect.die) + Effect.catchTag(["HttpClientError", "SchemaError"], Effect.die) ) return AdminApi.of({ diff --git a/packages/amp/src/arrow-flight/errors.ts b/packages/amp/src/arrow-flight/errors.ts index ad42d6e..ec07313 100644 --- a/packages/amp/src/arrow-flight/errors.ts +++ b/packages/amp/src/arrow-flight/errors.ts @@ -21,7 +21,7 @@ export type ArrowFlightError = /** * Represents an Arrow Flight RPC request that failed. */ -export class RpcError extends Schema.TaggedError( +export class RpcError extends Schema.TaggedErrorClass( "Amp/RpcError" )("RpcError", { method: Schema.String, @@ -35,7 +35,7 @@ export class RpcError extends Schema.TaggedError( * Represents an error that occurred as a result of a `FlightInfo` request * returning an empty list of endpoints from which data can be acquired. */ -export class NoEndpointsError extends Schema.TaggedError( +export class NoEndpointsError extends Schema.TaggedErrorClass( "Amp/NoEndpointsError" )("NoEndpointsError", { /** @@ -52,7 +52,7 @@ export class NoEndpointsError extends Schema.TaggedError( * For Amp queries, there should only ever be **one** authoritative source * of data. */ -export class MultipleEndpointsError extends Schema.TaggedError( +export class MultipleEndpointsError extends Schema.TaggedErrorClass( "Amp/MultipleEndpointsError" )("MultipleEndpointsError", { /** @@ -65,7 +65,7 @@ export class MultipleEndpointsError extends Schema.TaggedError( +export class TicketNotFoundError extends Schema.TaggedErrorClass( "Amp/TicketNotFoundError" )("TicketNotFoundError", { /** @@ -78,7 +78,7 @@ export class TicketNotFoundError extends Schema.TaggedError * Represents an error that occurred as a result of failing to parse an Apache * Arrow RecordBatch. */ -export class ParseRecordBatchError extends Schema.TaggedError( +export class ParseRecordBatchError extends Schema.TaggedErrorClass( "Amp/ParseRecordBatchError" )("ParseRecordBatchError", { /** @@ -91,7 +91,7 @@ export class ParseRecordBatchError extends Schema.TaggedError( +export class ParseDictionaryBatchError extends Schema.TaggedErrorClass( "Amp/ParseDictionaryBatchError" )("ParseDictionaryBatchError", { /** @@ -104,7 +104,7 @@ export class ParseDictionaryBatchError extends Schema.TaggedError( +export class ParseSchemaError extends Schema.TaggedErrorClass( "Amp/ParseSchemaError" )("ParseSchemaError", { /** diff --git a/packages/amp/src/arrow-flight/service.ts b/packages/amp/src/arrow-flight/service.ts index 34124c0..eb59fc0 100644 --- a/packages/amp/src/arrow-flight/service.ts +++ b/packages/amp/src/arrow-flight/service.ts @@ -4,6 +4,7 @@ import { type Client, createClient, createContextValues } from "@connectrpc/conn import * as Cause from "effect/Cause" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Filter from "effect/Filter" import { identity } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" @@ -44,7 +45,7 @@ import type { ExtractQueryResult, QueryOptions, QueryResult } from "./types.ts" /** * A service which can be used to execute queries against an Arrow Flight API. */ -export class ArrowFlight extends Context.Tag("Amp/ArrowFlight") Stream.Stream, ArrowFlightError> -}>() {} +}>()("Amp/ArrowFlight") {} const make = Effect.gen(function*() { const auth = yield* Effect.serviceOption(Auth) const transport = yield* Transport const client = createClient(FlightService, transport) - const decodeRecordBatchMetadata = Schema.decode(RecordBatchMetadataFromUint8Array) + const decodeRecordBatchMetadata = Schema.decodeEffect(RecordBatchMetadataFromUint8Array) /** * Execute a SQL query and return a stream of rows. @@ -122,7 +123,7 @@ const make = Effect.gen(function*() { return yield* new TicketNotFoundError({ query }) } - const flightDataStream = Stream.unwrapScoped(Effect.gen(function*() { + const flightDataStream = Stream.unwrap(Effect.gen(function*() { const controller = yield* Effect.acquireRelease( Effect.sync(() => new AbortController()), (controller) => Effect.sync(() => controller.abort()) @@ -135,18 +136,14 @@ const make = Effect.gen(function*() { let schema: ArrowSchema | undefined const dictionaryRegistry = new DictionaryRegistry() - const dataSchema: Schema.Array$< - Schema.Record$< + const dataSchema: Schema.$Array< + Schema.$Record< typeof Schema.String, typeof Schema.Unknown > - > = Schema.Array( - options?.schema ?? Schema.Record({ - key: Schema.String, - value: Schema.Unknown - }) as any - ) - const decodeRecordBatchData = Schema.decode(dataSchema) + > = Schema.Array(options?.schema ?? Schema.Record(Schema.String, Schema.Unknown) as any) + + const decodeRecordBatchData = Schema.decodeEffect(dataSchema) // Convert FlightData stream to a stream of rows return flightDataStream.pipe( @@ -187,9 +184,9 @@ const make = Effect.gen(function*() { } } - return yield* Effect.die(new Cause.RuntimeException(`Invalid message type received: ${messageType}`)) + return yield* Effect.die(new Cause.IllegalArgumentError(`Invalid message type received: ${messageType}`)) })), - Stream.filterMap(identity) + Stream.filterMap(Filter.fromPredicateOption(identity)) ) }).pipe( Stream.unwrap, diff --git a/packages/amp/src/arrow-flight/transport.ts b/packages/amp/src/arrow-flight/transport.ts index 6763e1d..9d790a2 100644 --- a/packages/amp/src/arrow-flight/transport.ts +++ b/packages/amp/src/arrow-flight/transport.ts @@ -16,10 +16,9 @@ import type { AuthInfo } from "../core/domain.ts" * A transport implements a protocol, such as Connect or gRPC-web, and allows * for the concrete clients to be independent of the protocol. */ -export class Transport extends Context.Tag("@edgeandnode/amp/Transport")< - Transport, - ConnectTransport ->() {} +export class Transport extends Context.Service()( + "@edgeandnode/amp/Transport" +) {} /** * A service which abstracts the set of interceptors that are passed to a given @@ -30,7 +29,7 @@ export class Transport extends Context.Tag("@edgeandnode/amp/Transport")< * mutate the request and response, catch errors and retry/recover, emit * logs, or do nearly everything else. */ -export class Interceptors extends Context.Reference()( +export class Interceptors extends Context.Reference( "Amp/ArrowFlight/ConnectRPC/Interceptors", { defaultValue: () => Arr.empty() } ) {} diff --git a/packages/amp/src/arrow-flight/types.ts b/packages/amp/src/arrow-flight/types.ts index 13c1010..30b12be 100644 --- a/packages/amp/src/arrow-flight/types.ts +++ b/packages/amp/src/arrow-flight/types.ts @@ -35,6 +35,6 @@ export interface QueryOptions { * A utility type to extract the result type for a query. */ export type ExtractQueryResult = Options extends { - readonly schema: Schema.Schema -} ? QueryResult<_A> + readonly schema: Schema.Top +} ? QueryResult> : QueryResult> diff --git a/packages/amp/src/auth/error.ts b/packages/amp/src/auth/error.ts index ced2d74..0980612 100644 --- a/packages/amp/src/auth/error.ts +++ b/packages/amp/src/auth/error.ts @@ -1,23 +1,94 @@ import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Match from "effect/Match" import * as Schema from "effect/Schema" // ============================================================================= // Helpers // ============================================================================= -const AuthErrorCode = (code: Code) => - Schema.Literal(code).pipe( - Schema.propertySignature, - Schema.fromKey("error_code"), - Schema.withConstructorDefault(() => code) - ) +export const makeError = < + const Code extends string, + const Tag extends string, + const Fields extends Schema.Struct.Fields = {} +>( + code: Code, + tag: Tag, + fields?: Fields +): Schema.encodeKeys< + Schema.Struct< + Fields & { + readonly _tag: Schema.withDecodingDefaultKey> + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `METADATA_DB_ERROR` + */ + readonly code: Schema.withConstructorDefault> + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + readonly message: Schema.String + } + >, + { readonly code: "error_code"; readonly message: "error_message" } +> => + Schema.Struct({ + ...fields, + _tag: Schema.tagDefaultOmit(tag), + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `METADATA_DB_ERROR` + */ + code: Schema.Literal(code).pipe( + Schema.withConstructorDefault(Effect.succeed(code)) + ), + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + message: Schema.String + }).pipe(Schema.encodeKeys({ + code: "error_code", + message: "error_message" + })) as any + +// ============================================================================= +// Error Details +// ============================================================================= + +export const DeviceFlowReason = Schema.Literals([ + "expired", + "pending", + "access_denied", + "slow_down" +]) + +export type DeviceFlowReason = Schema.Schema.Type + +export const CacheOperation = Schema.Literals(["read", "write", "clear"]) + +export type CacheOperation = Schema.Schema.Type + +export const VerifyTokenFailureReason = Schema.Literals([ + "expired", + "invalid_signature", + "invalid_claims", + "jwks_error", + "unknown" +]) -const BaseAuthErrorFields = { - message: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("error_message") - ) -} +export type VerifyTokenFailureReason = Schema.Schema.Type // ============================================================================= // Errors @@ -26,195 +97,195 @@ const BaseAuthErrorFields = { /** * Indicates that the user's session has expired and they need to re-authenticate. */ -export class AuthTokenExpiredError extends Schema.TaggedError( - "Amp/Auth/AuthTokenExpiredError" -)("AuthTokenExpiredError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_TOKEN_EXPIRED") -}) { - get userMessage(): string { - return "Your session has expired" - } - get userSuggestion(): string { - return "Run 'amp auth login' to sign in again" - } -} +export const AuthTokenExpiredError = makeError( + "AUTH_TOKEN_EXPIRED", + "AuthTokenExpiredError" +) + +export type AuthTokenExpiredError = typeof AuthTokenExpiredError.Type /** * Indicates that too many authentication requests have been made. */ -export class AuthRateLimitError extends Schema.TaggedError( - "Amp/Auth/AuthRateLimitError" -)("AuthRateLimitError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_RATE_LIMITED"), - retryAfter: Schema.DurationFromMillis -}) { - get userMessage(): string { - return "Too many authentication requests" - } - get userSuggestion(): string { - const duration = Duration.format(this.retryAfter) - return `Please wait about ${duration} before trying again` +export const AuthRateLimitError = makeError( + "AUTH_RATE_LIMITED", + "AuthRateLimitError", + { + retryAfter: Schema.DurationFromMillis } -} +) + +export type AuthRateLimitError = typeof AuthRateLimitError.Type /** * Indicates a general token refresh failure. */ -export class AuthRefreshError extends Schema.TaggedError( - "Amp/Auth/AuthRefreshError" -)("AuthRefreshError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_REFRESH_FAILED"), - status: Schema.optionalWith(Schema.Int, { as: "Option" }), - cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) -}) { - get userMessage(): string { - return "Failed to refresh your authentication token" +export const AuthRefreshError = makeError( + "AUTH_REFRESH_FAILED", + "AuthRefreshError", + { + status: Schema.OptionFromOptional(Schema.Int), + cause: Schema.OptionFromOptional(Schema.Defect) } - get userSuggestion(): string { - return "Try signing out and signing in again with 'amp auth logout' then 'amp auth login'" - } -} +) + +export type AuthRefreshError = typeof AuthRefreshError.Type /** * Indicates that the token belongs to a different user than expected. */ -export class AuthUserMismatchError extends Schema.TaggedError( - "Amp/Auth/AuthUserMismatchError" -)("AuthUserMismatchError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_USER_MISMATCH"), - expectedUserId: Schema.String, - receivedUserId: Schema.String -}) { - get userMessage(): string { - return "Authentication identity mismatch detected" +export const AuthUserMismatchError = makeError( + "AUTH_USER_MISMATCH", + "AuthUserMismatchError", + { + expectedUserId: Schema.String, + receivedUserId: Schema.String } - get userSuggestion(): string { - return "Your cached credentials may be corrupted. Run 'amp auth logout' and 'amp auth login' to re-authenticate" - } -} +) -const DeviceFlowReason = Schema.Literal("expired", "pending", "access_denied", "slow_down") -export type DeviceFlowReason = Schema.Schema.Type +export type AuthUserMismatchError = typeof AuthUserMismatchError.Type /** * Indicates an issue with the device authorization flow. */ -export class AuthDeviceFlowError extends Schema.TaggedError( - "Amp/Auth/AuthDeviceFlowError" -)("AuthDeviceFlowError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_DEVICE_FLOW_ERROR"), - reason: DeviceFlowReason, - verificationUri: Schema.optionalWith(Schema.String, { as: "Option" }) -}) { - get userMessage(): string { - switch (this.reason) { - case "expired": - return "The login code has expired" - case "pending": - return "Waiting for authorization to complete" - case "access_denied": - return "Authorization was denied" - case "slow_down": - return "Too many login attempts" - } +export const AuthDeviceFlowError = makeError( + "AUTH_DEVICE_FLOW_ERROR", + "AuthDeviceFlowError", + { + reason: DeviceFlowReason, + verificationUri: Schema.OptionFromOptional(Schema.String) } - get userSuggestion(): string { - switch (this.reason) { - case "expired": - return "Run 'amp auth login' to start a new login session" - case "pending": - return "Complete the login in your browser" - case "access_denied": - return "Run 'amp auth login' to try again" - case "slow_down": - return "Please wait a moment before trying again" - } - } -} +) -const CacheOperation = Schema.Literal("read", "write", "clear") -export type CacheOperation = Schema.Schema.Type +export type AuthDeviceFlowError = typeof AuthDeviceFlowError.Type /** * Indicates a failure with cache read/write/clear operations. */ -export class AuthCacheError extends Schema.TaggedError( - "Amp/Auth/AuthCacheError" -)("AuthCacheError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_CACHE_ERROR"), - operation: CacheOperation, - cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) -}) { - get userMessage(): string { - switch (this.operation) { - case "read": - return "Could not read saved credentials" - case "write": - return "Could not save your credentials" - case "clear": - return "Could not clear saved credentials" - } - } - get userSuggestion(): string { - return this.operation === "clear" - ? "You may need to manually remove the credentials file" - : "Check file permissions in your configuration directory and try again" +export const AuthCacheError = makeError( + "AUTH_CACHE_ERROR", + "AuthCacheError", + { + operation: CacheOperation, + cause: Schema.OptionFromOptional(Schema.Defect) } -} +) + +export type AuthCacheError = typeof AuthCacheError.Type /** * Indicates network or timeout issues during authentication. */ -export class AuthNetworkError extends Schema.TaggedError( - "Amp/Auth/AuthNetworkError" -)("AuthNetworkError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_NETWORK_ERROR"), - endpoint: Schema.optionalWith(Schema.String, { as: "Option" }), - isTimeout: Schema.Boolean, - cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) -}) { - get userMessage(): string { - return this.isTimeout - ? "Authentication request timed out" - : "Could not connect to the authentication service" +export const AuthNetworkError = makeError( + "AUTH_NETWORK_ERROR", + "AuthNetworkError", + { + endpoint: Schema.OptionFromOptional(Schema.String), + isTimeout: Schema.Boolean, + cause: Schema.OptionFromOptional(Schema.Defect) } - get userSuggestion(): string { - return this.isTimeout - ? "The service may be experiencing high load. Please try again in a few moments" - : "Check your internet connection and try again" +) + +export type AuthNetworkError = typeof AuthNetworkError.Type + +/** + * Indicates that the client failed to construct or encode an authentication request. + */ +export const AuthRequestError = makeError( + "AUTH_REQUEST_ERROR", + "AuthRequestError", + { + endpoint: Schema.OptionFromOptional(Schema.String), + cause: Schema.OptionFromOptional(Schema.Defect) } -} +) -const VerifyTokenFailureReason = Schema.Literal( - "expired", - "invalid_signature", - "invalid_claims", - "jwks_error", - "unknown" +export type AuthRequestError = typeof AuthRequestError.Type + +/** + * Indicates that the authentication service returned a response that violated the expected protocol. + */ +export const AuthProtocolError = makeError( + "AUTH_PROTOCOL_ERROR", + "AuthProtocolError", + { + endpoint: Schema.OptionFromOptional(Schema.String), + status: Schema.OptionFromOptional(Schema.Int), + cause: Schema.OptionFromOptional(Schema.Defect) + } ) -export type VerifyTokenFailureReason = Schema.Schema.Type + +export type AuthProtocolError = typeof AuthProtocolError.Type /** * Indicates a failure when verifying a JWT access token. */ -export class AuthVerifyTokenError extends Schema.TaggedError( - "Amp/Auth/AuthVerifyTokenError" -)("AuthVerifyTokenError", { - ...BaseAuthErrorFields, - code: AuthErrorCode("AUTH_VERIFY_TOKEN_FAILED"), - reason: VerifyTokenFailureReason, - claim: Schema.optionalWith(Schema.String, { as: "Option" }), - cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) -}) { - get userMessage(): string { - switch (this.reason) { +export const AuthVerifyTokenError = makeError( + "AUTH_VERIFY_TOKEN_FAILED", + "AuthVerifyTokenError", + { + reason: VerifyTokenFailureReason, + claim: Schema.OptionFromOptional(Schema.String), + cause: Schema.OptionFromOptional(Schema.Defect) + } +) + +export type AuthVerifyTokenError = typeof AuthVerifyTokenError.Type + +// ============================================================================= +// Union Type +// ============================================================================= + +/** + * A union of all authentication errors for exhaustive handling. + */ +export type AuthError = + | AuthTokenExpiredError + | AuthRateLimitError + | AuthRefreshError + | AuthUserMismatchError + | AuthDeviceFlowError + | AuthCacheError + | AuthNetworkError + | AuthRequestError + | AuthProtocolError + | AuthVerifyTokenError + +export const getUserMessage = Match.type().pipe( + Match.when({ code: "AUTH_TOKEN_EXPIRED" }, () => "Your session has expired"), + Match.when({ code: "AUTH_RATE_LIMITED" }, () => "Too many authentication requests"), + Match.when({ code: "AUTH_REFRESH_FAILED" }, () => "Failed to refresh your authentication token"), + Match.when({ code: "AUTH_USER_MISMATCH" }, () => "Authentication identity mismatch detected"), + Match.when({ code: "AUTH_DEVICE_FLOW_ERROR" }, (error) => { + switch (error.reason) { + case "expired": + return "The login code has expired" + case "pending": + return "Waiting for authorization to complete" + case "access_denied": + return "Authorization was denied" + case "slow_down": + return "Too many login attempts" + } + }), + Match.when({ code: "AUTH_CACHE_ERROR" }, (error) => { + switch (error.operation) { + case "read": + return "Could not read saved credentials" + case "write": + return "Could not save your credentials" + case "clear": + return "Could not clear saved credentials" + } + }), + Match.when({ code: "AUTH_NETWORK_ERROR" }, (error) => + error.isTimeout + ? "Authentication request timed out" + : "Could not connect to the authentication service"), + Match.when({ code: "AUTH_REQUEST_ERROR" }, () => "Failed to prepare the authentication request"), + Match.when({ code: "AUTH_PROTOCOL_ERROR" }, () => "Authentication service returned an invalid response"), + Match.when({ code: "AUTH_VERIFY_TOKEN_FAILED" }, (error) => { + switch (error.reason) { case "expired": return "The access token has expired" case "invalid_signature": @@ -226,9 +297,54 @@ export class AuthVerifyTokenError extends Schema.TaggedError().pipe( + Match.when({ code: "AUTH_TOKEN_EXPIRED" }, () => "Run 'amp auth login' to sign in again"), + Match.when( + { code: "AUTH_RATE_LIMITED" }, + (error) => `Please wait about ${Duration.format(error.retryAfter)} before trying again` + ), + Match.when( + { code: "AUTH_REFRESH_FAILED" }, + () => "Try signing out and signing in again with 'amp auth logout' then 'amp auth login'" + ), + Match.when( + { code: "AUTH_USER_MISMATCH" }, + () => "Your cached credentials may be corrupted. Run 'amp auth logout' and 'amp auth login' to re-authenticate" + ), + Match.when({ code: "AUTH_DEVICE_FLOW_ERROR" }, (error) => { + switch (error.reason) { + case "expired": + return "Run 'amp auth login' to start a new login session" + case "pending": + return "Complete the login in your browser" + case "access_denied": + return "Run 'amp auth login' to try again" + case "slow_down": + return "Please wait a moment before trying again" + } + }), + Match.when({ code: "AUTH_CACHE_ERROR" }, (error) => + error.operation === "clear" + ? "You may need to manually remove the credentials file" + : "Check file permissions in your configuration directory and try again"), + Match.when({ code: "AUTH_NETWORK_ERROR" }, (error) => + error.isTimeout + ? "The service may be experiencing high load. Please try again in a few moments" + : "Check your internet connection and try again"), + Match.when( + { code: "AUTH_REQUEST_ERROR" }, + () => "This is a client-side request construction problem. Please report it if it persists" + ), + Match.when( + { code: "AUTH_PROTOCOL_ERROR" }, + () => "Try again in a moment. If the problem persists, the authentication service may have changed" + ), + Match.when({ code: "AUTH_VERIFY_TOKEN_FAILED" }, (error) => { + switch (error.reason) { case "expired": case "invalid_signature": case "invalid_claims": @@ -238,22 +354,6 @@ export class AuthVerifyTokenError extends Schema.TaggedError -export const CodeVerifier = Schema.NonEmptyTrimmedString.pipe( +export const CodeVerifier = NonEmptyTrimmedString.pipe( Schema.brand("Amp/Auth/CodeVerifier") -).annotations({ identifier: "CodeVerifier" }) +).annotate({ identifier: "CodeVerifier" }) export type CodeVerifier = Schema.Schema.Type -export class PKCEChallenge extends Schema.Class( - "Amp/Auth/PKCEChallenge" -)({ +export const PKCEChallenge = Schema.Struct({ codeChallenge: CodeChallenge, codeVerifier: CodeVerifier -}, { identifier: "PKCEChallenge" }) {} +}).annotate({ identifier: "PKCEChallenge" }) +export type PKCEChallenge = typeof PKCEChallenge.Type -export const DeviceCode = Schema.NonEmptyTrimmedString.pipe( +export const DeviceCode = NonEmptyTrimmedString.pipe( Schema.brand("Amp/Auth/DeviceCode") -).annotations({ identifier: "DeviceCode" }) +).annotate({ identifier: "DeviceCode" }) export type DeviceCode = Schema.Schema.Type -export const UserCode = Schema.NonEmptyTrimmedString.pipe( +export const UserCode = NonEmptyTrimmedString.pipe( Schema.brand("Amp/Auth/UserCode") -).annotations({ identifier: "UserCode" }) +).annotate({ identifier: "UserCode" }) export type UserCode = Schema.Schema.Type -export class DeviceAuthorizationResponse extends Schema.Class( - "Amp/Auth/DeviceAuthorizationResponse" -)({ - deviceCode: DeviceCode.pipe( - Schema.propertySignature, - Schema.fromKey("device_code") - ).annotations({ +export const DeviceAuthorizationResponse = Schema.Struct({ + deviceCode: DeviceCode.annotate({ description: "Device verification code used for polling" }), - userCode: UserCode.pipe( - Schema.propertySignature, - Schema.fromKey("user_code") - ).annotations({ + userCode: UserCode.annotate({ description: "User code to display for manual entry" }), - verificationUri: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("verification_uri") - ).annotations({ + verificationUri: Schema.String.annotate({ description: "URL where user enters the code" }), - expiresIn: Schema.Int.pipe( - Schema.positive(), - Schema.propertySignature, - Schema.fromKey("expires_in") - ).annotations({ + expiresIn: Schema.Int.check(Schema.isGreaterThan(0)).annotate({ description: "Time in seconds until device code expires" }), - interval: Schema.Int.pipe(Schema.positive()).annotations({ + interval: Schema.Int.check(Schema.isGreaterThan(0)).annotate({ description: "Minimum polling interval in seconds" }) -}, { identifier: "DeviceAuthorizationResponse" }) {} +}).pipe(Schema.encodeKeys({ + deviceCode: "device_code", + userCode: "user_code", + verificationUri: "verification_uri", + expiresIn: "expires_in" +})).annotate({ identifier: "DeviceAuthorizationResponse" }) +export type DeviceAuthorizationResponse = typeof DeviceAuthorizationResponse.Type export const DeviceTokenResponse = Schema.Struct({ - accessToken: AccessToken.pipe( - Schema.propertySignature, - Schema.fromKey("access_token") - ).annotations({ description: "The access token for authenticated requests" }), - refreshToken: RefreshToken.pipe( - Schema.propertySignature, - Schema.fromKey("refresh_token") - ).annotations({ description: "The refresh token for renewing access" }), - userId: UserId.pipe( - Schema.propertySignature, - Schema.fromKey("user_id") - ).annotations({ + _tag: Schema.tagDefaultOmit("DeviceTokenResponse"), + accessToken: AccessToken.annotate({ + description: "The access token for authenticated requests" + }), + refreshToken: RefreshToken.annotate({ + description: "The refresh token for renewing access" + }), + userId: UserId.annotate({ description: "The authenticated user's ID" }), - userAccounts: Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Address)).pipe( - Schema.propertySignature, - Schema.fromKey("user_accounts") - ), - expiresIn: Schema.Int.pipe( - Schema.positive(), - Schema.propertySignature, - Schema.fromKey("expires_in") - ).annotations({ description: "Seconds until the token expires from receipt" }) -}).pipe( - Schema.attachPropertySignature("_tag", "DeviceTokenResponse") -).annotations({ identifier: "DeviceTokenResponse" }) + userAccounts: Schema.Array(Schema.Union([NonEmptyTrimmedString, Address])), + expiresIn: Schema.Int.check(Schema.isGreaterThan(0)).annotate({ + description: "Seconds until the token expires from receipt" + }) +}).pipe(Schema.encodeKeys({ + accessToken: "access_token", + refreshToken: "refresh_token", + userId: "user_id", + userAccounts: "user_accounts", + expiresIn: "expires_in" +})).annotate({ identifier: "DeviceTokenResponse" }) export type DeviceTokenResponse = typeof DeviceTokenResponse.Type export const DeviceTokenPendingResponse = Schema.Struct({ + _tag: Schema.tagDefaultOmit("DeviceTokenPendingResponse"), error: Schema.Literal("authorization_pending") -}).pipe( - Schema.attachPropertySignature("_tag", "DeviceTokenPendingResponse") -).annotations({ identifier: "DeviceTokenPendingResponse" }) +}).annotate({ identifier: "DeviceTokenPendingResponse" }) export type DeviceTokenPendingResponse = typeof DeviceTokenPendingResponse.Type export const DeviceTokenExpiredResponse = Schema.Struct({ + _tag: Schema.tagDefaultOmit("DeviceTokenExpiredResponse"), error: Schema.Literal("expired_token") -}).pipe( - Schema.attachPropertySignature("_tag", "DeviceTokenExpiredResponse") -).annotations({ identifier: "DeviceTokenExpiredResponse" }) +}).annotate({ identifier: "DeviceTokenExpiredResponse" }) export type DeviceTokenExpiredResponse = typeof DeviceTokenExpiredResponse.Type -export const DeviceTokenPollingResponse = Schema.Union( +export const DeviceTokenPollingResponse = Schema.Union([ DeviceTokenResponse, DeviceTokenPendingResponse, DeviceTokenExpiredResponse -) +]) export type DeviceTokenPollingResponse = typeof DeviceTokenPollingResponse.Type -export class GenerateTokenRequest extends Schema.Class( - "Amp/Auth/GenerateTokenRequest" -)({ +export const GenerateTokenRequest = Schema.Struct({ audience: Schema.optional(Schema.Array(Schema.String)), duration: Schema.optional(TokenDuration) -}) {} +}) +export type GenerateTokenRequest = typeof GenerateTokenRequest.Type -export class GenerateTokenResponse extends Schema.Class( - "Amp/Auth/GenerateTokenResponse" -)({ +export const GenerateTokenResponse = Schema.Struct({ token: AccessToken, token_type: Schema.Literal("Bearer"), - exp: Schema.Int.pipe(Schema.positive()), - sub: Schema.NonEmptyTrimmedString, + exp: Schema.Int.check(Schema.isGreaterThan(0)), + sub: NonEmptyTrimmedString, iss: Schema.String -}) {} - -export class RefreshTokenRequest extends Schema.Class( - "Amp/Auth/RefreshTokenRequest" -)({ - refreshToken: Schema.Redacted(RefreshToken).pipe( - Schema.propertySignature, - Schema.fromKey("refresh_token") - ), - userId: UserId.pipe( - Schema.propertySignature, - Schema.fromKey("user_id") - ) -}) { - static fromAuthInfo(authInfo: AuthInfo) { - return RefreshTokenRequest.make({ - userId: authInfo.userId, - refreshToken: authInfo.refreshToken - }) - } -} - -export class RefreshTokenResponse extends Schema.Class( - "Amp/models/auth/RefreshTokenResponse" -)({ - token: Schema.NonEmptyTrimmedString, - refreshToken: Schema.NullOr(Schema.String).pipe( - Schema.propertySignature, - Schema.fromKey("refresh_token") - ), - sessionUpdateAction: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("session_update_action") - ), - expiresIn: Schema.Int.pipe( - Schema.positive(), - Schema.propertySignature, - Schema.fromKey("expires_in") - ).annotations({ description: "Seconds from receipt of when the token expires (def is 1hr)" }), +}) +export type GenerateTokenResponse = typeof GenerateTokenResponse.Type + +export const RefreshTokenRequest = Schema.Struct({ + refreshToken: Schema.Redacted(RefreshToken), + userId: UserId +}).pipe(Schema.encodeKeys({ + refreshToken: "refresh_token", + userId: "user_id" +})) +export type RefreshTokenRequest = typeof RefreshTokenRequest.Type + +export const RefreshTokenResponse = Schema.Struct({ + token: NonEmptyTrimmedString, + refreshToken: Schema.NullOr(Schema.String), + sessionUpdateAction: Schema.String, + expiresIn: Schema.Int.check(Schema.isGreaterThan(0)).annotate({ + description: "Seconds from receipt of when the token expires (def is 1hr)" + }), user: Schema.Struct({ id: UserId, - accounts: Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Address)).annotations({ + accounts: Schema.Array(Schema.Union([NonEmptyTrimmedString, Address])).annotate({ description: "List of accounts (connected wallets, etc) belonging to the user", examples: [["cmfd6bf6u006vjx0b7xb2eybx", "0x5c8fA0bDf68C915a88cD68291fC7CF011C126C29"]] }) - }).annotations({ description: "The user the access token belongs to" }) -}) {} + }).annotate({ description: "The user the access token belongs to" }) +}).pipe(Schema.encodeKeys({ + refreshToken: "refresh_token", + sessionUpdateAction: "session_update_action", + expiresIn: "expires_in" +})) +export type RefreshTokenResponse = typeof RefreshTokenResponse.Type // ============================================================================= // Legacy Errors (kept for backwards compatibility) // ============================================================================= -export class VerifySignedAccessTokenError extends Schema.TaggedError( +export class VerifySignedAccessTokenError extends Schema.TaggedErrorClass( "Amp/Auth/VerifySignedAccessTokenError" )("VerifySignedAccessTokenError", { cause: Schema.Defect }) {} @@ -222,22 +196,31 @@ export class VerifySignedAccessTokenError extends Schema.TaggedError readonly requestDeviceAuthorization: (codeChallenge: CodeChallenge) => Effect.Effect< DeviceAuthorizationResponse, - AuthNetworkError | AuthRefreshError + | AuthNetworkError + | AuthProtocolError + | AuthRequestError + | AuthRefreshError > readonly pollDeviceToken: (deviceCode: DeviceCode, codeVerifier: CodeVerifier) => Effect.Effect< AuthInfo, - AuthNetworkError | AuthCacheError | AuthDeviceFlowError + | AuthCacheError + | AuthNetworkError + | AuthProtocolError + | AuthRequestError + | AuthDeviceFlowError > readonly refreshAccessToken: (authInfo: AuthInfo) => Effect.Effect< AuthInfo, | AuthNetworkError + | AuthProtocolError + | AuthRequestError | AuthCacheError | AuthTokenExpiredError | AuthRateLimitError @@ -252,6 +235,8 @@ export class Auth extends Context.Tag("Amp/Auth") Effect.Effect< GenerateTokenResponse, | AuthNetworkError + | AuthProtocolError + | AuthRequestError | AuthTokenExpiredError | AuthRateLimitError | AuthRefreshError @@ -267,7 +252,7 @@ export class Auth extends Context.Tag("Amp/Auth") Effect.Effect readonly clearCachedAuthInfo: Effect.Effect -}>() {} +}>()("Amp/Auth") {} // ============================================================================= // Service Implementation @@ -275,7 +260,7 @@ export class Auth extends Context.Tag("Amp/Auth") + AuthNetworkError.make({ + code: "AUTH_NETWORK_ERROR", + message: `Request to ${endpoint} timed out`, + endpoint: Option.some(endpoint), + isTimeout: true, + cause: Option.some(cause) + }) + + const makeAuthNetworkRequestError = ( + endpoint: string, + cause: { readonly message: string } + ): AuthNetworkError => + AuthNetworkError.make({ + code: "AUTH_NETWORK_ERROR", + message: `Connection failed to ${endpoint}: ${cause.message}`, + endpoint: Option.some(endpoint), + isTimeout: false, + cause: Option.some(cause) + }) + + const makeAuthRefreshSchemaError = ( + endpoint: string, + cause: { readonly message: string } + ): AuthRefreshError => + AuthRefreshError.make({ + code: "AUTH_REFRESH_FAILED", + message: `Failed to parse ${endpoint} response: ${cause.message}`, + status: Option.none(), + cause: Option.some(cause) + }) + + const makeAuthProtocolDecodeError = ( + endpoint: string, + cause: unknown + ): AuthProtocolError => + AuthProtocolError.make({ + code: "AUTH_PROTOCOL_ERROR", + message: `Authentication service returned an unreadable response for ${endpoint}`, + endpoint: Option.some(endpoint), + status: Option.none(), + cause: Option.some(cause) + }) + + const makeAuthProtocolEmptyBodyError = ( + endpoint: string, + cause: unknown + ): AuthProtocolError => + AuthProtocolError.make({ + code: "AUTH_PROTOCOL_ERROR", + message: `Authentication service returned an empty response for ${endpoint}`, + endpoint: Option.some(endpoint), + status: Option.none(), + cause: Option.some(cause) + }) + + const makeAuthProtocolStatusCodeError = ( + endpoint: string, + cause: { readonly response: { readonly status: number } } + ): AuthProtocolError => + AuthProtocolError.make({ + code: "AUTH_PROTOCOL_ERROR", + message: `Authentication service returned an unexpected status for ${endpoint}`, + endpoint: Option.some(endpoint), + status: Option.some(cause.response.status), + cause: Option.some(cause) + }) + + const makeAuthRequestEncodeError = ( + endpoint: string, + cause: unknown + ): AuthRequestError => + AuthRequestError.make({ + code: "AUTH_REQUEST_ERROR", + message: `Failed to encode the authentication request for ${endpoint}`, + endpoint: Option.some(endpoint), + cause: Option.some(cause) + }) + + const makeAuthRequestInvalidUrlError = ( + endpoint: string, + cause: unknown + ): AuthRequestError => + AuthRequestError.make({ + code: "AUTH_REQUEST_ERROR", + message: `Authentication endpoint is invalid for ${endpoint}`, + endpoint: Option.some(endpoint), + cause: Option.some(cause) + }) + + const makeAuthCacheError = ( + operation: "read" | "write" | "clear", + cause: { readonly message: string } + ): AuthCacheError => + AuthCacheError.make({ + code: "AUTH_CACHE_ERROR", + message: `Cache ${operation} failed: ${cause.message}`, + operation, + cause: Option.some(cause) + }) + + const makeAuthDeviceFlowSchemaError = (): AuthDeviceFlowError => + AuthDeviceFlowError.make({ + code: "AUTH_DEVICE_FLOW_ERROR", + message: "Failed to parse device token response", + reason: "expired", + verificationUri: Option.none() + }) + + const makeUnwrappedHttpClientErrorHandlers = ( + endpoint: string, + handlers: { + readonly onSchemaError: (cause: { readonly message: string }) => SchemaError + } + ) => ({ + SchemaError: (cause: { readonly message: string }) => Effect.fail(handlers.onSchemaError(cause)), + TimeoutError: (cause: unknown) => Effect.fail(makeAuthNetworkTimeoutError(endpoint, cause)), + DecodeError: (cause: unknown) => Effect.fail(makeAuthProtocolDecodeError(endpoint, cause)), + EmptyBodyError: (cause: unknown) => Effect.fail(makeAuthProtocolEmptyBodyError(endpoint, cause)), + EncodeError: (cause: unknown) => Effect.fail(makeAuthRequestEncodeError(endpoint, cause)), + InvalidUrlError: (cause: unknown) => Effect.fail(makeAuthRequestInvalidUrlError(endpoint, cause)), + StatusCodeError: (cause: { readonly response: { readonly status: number } }) => + Effect.fail(makeAuthProtocolStatusCodeError(endpoint, cause)), + TransportError: (cause: { readonly message: string }) => Effect.fail(makeAuthNetworkRequestError(endpoint, cause)) + }) + /** * Executes an authenticated HTTP request with standard error handling. * Handles status codes (401, 403, 429) and wraps HTTP errors into SDK errors. @@ -294,7 +408,7 @@ const make = Effect.gen(function*() { endpoint: string, decodeBody: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect< A, - ParseResult.ParseError | HttpClientError.ResponseError + Schema.SchemaError | HttpClientError.HttpClientError > ) => httpClient.execute(request).pipe( @@ -304,19 +418,21 @@ const make = Effect.gen(function*() { "2xx": decodeBody, 401: () => Effect.fail( - new AuthTokenExpiredError({ + AuthTokenExpiredError.make({ + code: "AUTH_TOKEN_EXPIRED", message: "Access token is no longer valid (401 Unauthorized)" }) ), 403: () => Effect.fail( - new AuthTokenExpiredError({ + AuthTokenExpiredError.make({ + code: "AUTH_TOKEN_EXPIRED", message: "Access token lacks required permissions (403 Forbidden)" }) ), 429: Effect.fnUntraced(function*(response) { const message = yield* extractErrorDescription(response) - const retryAfter = Option.fromNullable(response.headers["retry-after"]).pipe( + const retryAfter = Option.fromNullishOr(response.headers["retry-after"]).pipe( Option.flatMap((retryAfter) => { const parsed = Number.parseInt(retryAfter, 10) return Number.isNaN(parsed) @@ -325,52 +441,27 @@ const make = Effect.gen(function*() { }), Option.getOrElse(() => Duration.minutes(1)) ) - return yield* new AuthRateLimitError({ message, retryAfter }) + return yield* Effect.fail(AuthRateLimitError.make({ + code: "AUTH_RATE_LIMITED", + message, + retryAfter + })) }), orElse: Effect.fnUntraced(function*(response) { const message = yield* extractErrorDescription(response) - return yield* new AuthRefreshError({ + return yield* Effect.fail(AuthRefreshError.make({ + code: "AUTH_REFRESH_FAILED", message, status: Option.some(response.status), cause: Option.none() - }) + })) }) }) ), - Effect.catchTag("TimeoutException", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Request to ${endpoint} timed out`, - endpoint: Option.some(endpoint), - isTimeout: true, - cause: Option.some(cause) - }) - )), - Effect.catchTag("RequestError", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Connection failed to ${endpoint}: ${cause.message}`, - endpoint: Option.some(endpoint), - isTimeout: false, - cause: Option.some(cause) - }) - )), - Effect.catchTag("ParseError", (cause) => - Effect.fail( - new AuthRefreshError({ - message: `Failed to parse ${endpoint} response: ${cause.message}`, - status: Option.none(), - cause: Option.some(cause) - }) - )), - Effect.catchTag("ResponseError", (cause) => - Effect.fail( - new AuthRefreshError({ - message: `Failed to read ${endpoint} response: ${cause.message}`, - status: Option.some(cause.response.status), - cause: Option.some(cause) - }) - )) + Effect.unwrapReason("HttpClientError"), + Effect.catchTags(makeUnwrappedHttpClientErrorHandlers(endpoint, { + onSchemaError: (cause) => makeAuthRefreshSchemaError(endpoint, cause) + })) ) // ------------------------------------------------------------------------ @@ -379,7 +470,7 @@ const make = Effect.gen(function*() { const createChallenge = Effect.gen(function*() { const { codeChallenge, codeVerifier } = yield* pkceChallenge() - return new PKCEChallenge({ + return PKCEChallenge.make({ codeChallenge: CodeChallenge.make(codeChallenge), codeVerifier: CodeVerifier.make(codeVerifier) }) @@ -390,47 +481,17 @@ const make = Effect.gen(function*() { const endpoint = "/api/v1/device/authorize" return yield* httpClient.post(endpoint, { acceptJson: true, - body: HttpBody.unsafeJson({ + body: HttpBody.jsonUnsafe({ code_challenge: codeChallenge, code_challenge_method: "S256" }) }).pipe( Effect.timeout("30 seconds"), Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceAuthorizationResponse)), - Effect.catchTag("TimeoutException", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Request to ${endpoint} timed out`, - endpoint: Option.some(endpoint), - isTimeout: true, - cause: Option.some(cause) - }) - )), - Effect.catchTag("RequestError", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Connection failed to ${endpoint}: ${cause.message}`, - endpoint: Option.some(endpoint), - isTimeout: false, - cause: Option.some(cause) - }) - )), - Effect.catchTag("ParseError", (cause) => - Effect.fail( - new AuthRefreshError({ - message: `Failed to parse ${endpoint} response: ${cause.message}`, - status: Option.none(), - cause: Option.some(cause) - }) - )), - Effect.catchTag("ResponseError", (cause) => - Effect.fail( - new AuthRefreshError({ - message: `Failed to read ${endpoint} response: ${cause.message}`, - status: Option.some(cause.response.status), - cause: Option.some(cause) - }) - )) + Effect.unwrapReason("HttpClientError"), + Effect.catchTags(makeUnwrappedHttpClientErrorHandlers(endpoint, { + onSchemaError: (cause) => makeAuthRefreshSchemaError(endpoint, cause) + })) ) } ) @@ -447,57 +508,28 @@ const make = Effect.gen(function*() { }).pipe( Effect.timeout("10 seconds"), Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceTokenPollingResponse)), - Effect.catchTag("TimeoutException", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Request to ${endpoint} timed out`, - endpoint: Option.some(endpoint), - isTimeout: true, - cause: Option.some(cause) - }) - )), - Effect.catchTag("RequestError", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Connection failed to ${endpoint}: ${cause.message}`, - endpoint: Option.some(endpoint), - isTimeout: false, - cause: Option.some(cause) - }) - )), - Effect.catchTag("ResponseError", (cause) => - Effect.fail( - new AuthNetworkError({ - message: `Device token request failed: ${cause.message}`, - endpoint: Option.some(endpoint), - isTimeout: false, - cause: Option.some(cause) - }) - )), - Effect.catchTag("ParseError", () => - Effect.fail( - new AuthDeviceFlowError({ - message: "Failed to parse device token response", - reason: "expired", - verificationUri: Option.none() - }) - )) + Effect.unwrapReason("HttpClientError"), + Effect.catchTags(makeUnwrappedHttpClientErrorHandlers(endpoint, { + onSchemaError: () => makeAuthDeviceFlowSchemaError() + })) ) if (response._tag === "DeviceTokenPendingResponse") { - return yield* new AuthDeviceFlowError({ + return yield* Effect.fail(AuthDeviceFlowError.make({ + code: "AUTH_DEVICE_FLOW_ERROR", message: "Device authorization is still pending", reason: "pending", verificationUri: Option.none() - }) + })) } if (response._tag === "DeviceTokenExpiredResponse") { - return yield* new AuthDeviceFlowError({ + return yield* Effect.fail(AuthDeviceFlowError.make({ + code: "AUTH_DEVICE_FLOW_ERROR", message: "Device authorization code has expired", reason: "expired", verificationUri: Option.none() - }) + })) } const authInfo = yield* makeAuthInfo({ @@ -526,7 +558,7 @@ const make = Effect.gen(function*() { }) { const endpoint = "/api/v1/auth/generate" const request = HttpClientRequest.post(endpoint, { - body: HttpBody.unsafeJson(new GenerateTokenRequest({ audience, duration })), + body: HttpBody.jsonUnsafe(GenerateTokenRequest.make({ audience, duration })), acceptJson: true }).pipe(HttpClientRequest.bearerToken(authInfo.accessToken)) @@ -542,7 +574,10 @@ const make = Effect.gen(function*() { function*(authInfo: AuthInfo) { const endpoint = "/api/v1/auth/refresh" const request = HttpClientRequest.post(endpoint, { - body: HttpBody.unsafeJson(RefreshTokenRequest.fromAuthInfo(authInfo)), + body: HttpBody.jsonUnsafe(RefreshTokenRequest.make({ + userId: authInfo.userId, + refreshToken: authInfo.refreshToken + })), acceptJson: true }).pipe(HttpClientRequest.bearerToken(authInfo.accessToken)) @@ -554,11 +589,12 @@ const make = Effect.gen(function*() { // Validate that the received user ID matches the cached user ID if (response.user.id !== authInfo.userId) { - return yield* new AuthUserMismatchError({ + return yield* Effect.fail(AuthUserMismatchError.make({ + code: "AUTH_USER_MISMATCH", message: `Expected user ID ${authInfo.userId} but received ${response.user.id}`, expectedUserId: authInfo.userId, receivedUserId: response.user.id - }) + })) } const refreshedAuthInfo = yield* makeAuthInfo({ @@ -588,7 +624,8 @@ const make = Effect.gen(function*() { try: () => Jose.jwtVerify(Redacted.value(token), JWKS, { issuer }), catch: (cause) => { if (!(cause instanceof Jose.errors.JOSEError)) { - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `Unknown verification error: ${String(cause)}`, reason: "unknown", claim: Option.none(), @@ -597,38 +634,43 @@ const make = Effect.gen(function*() { } switch (cause.code) { case "ERR_JWT_EXPIRED": - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `Token expired: ${cause.message}`, reason: "expired", - claim: Option.fromNullable((cause as Jose.errors.JWTExpired).claim), + claim: Option.fromNullishOr((cause as Jose.errors.JWTExpired).claim), cause: Option.some(cause) }) case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED": - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `Signature verification failed: ${cause.message}`, reason: "invalid_signature", claim: Option.none(), cause: Option.some(cause) }) case "ERR_JWT_CLAIM_VALIDATION_FAILED": - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `Claim validation failed: ${(cause as Jose.errors.JWTClaimValidationFailed).claim} - ${ (cause as Jose.errors.JWTClaimValidationFailed).reason }`, reason: "invalid_claims", - claim: Option.fromNullable((cause as Jose.errors.JWTClaimValidationFailed).claim), + claim: Option.fromNullishOr((cause as Jose.errors.JWTClaimValidationFailed).claim), cause: Option.some(cause) }) case "ERR_JWKS_NO_MATCHING_KEY": case "ERR_JWKS_TIMEOUT": - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `JWKS error: ${cause.message}`, reason: "jwks_error", claim: Option.none(), cause: Option.some(cause) }) default: - return new AuthVerifyTokenError({ + return AuthVerifyTokenError.make({ + code: "AUTH_VERIFY_TOKEN_FAILED", message: `Verification error: ${cause.message}`, reason: "unknown", claim: Option.none(), @@ -649,33 +691,10 @@ const make = Effect.gen(function*() { const cacheResult = yield* kvs.get(AUTH_INFO_CACHE_KEY).pipe( // Treat "not found" as Option.none() before wrapping other errors Effect.catchIf( - (error) => error._tag === "SystemError" && error.reason === "NotFound", + (error) => error._tag === "KeyValueStoreError", () => Effect.succeed(Option.none()) ), - Effect.catchTag("SystemError", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache read failed: ${cause.message}`, - operation: "read", - cause: Option.some(cause) - }) - )), - Effect.catchTag("ParseError", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache read failed: ${cause.message}`, - operation: "read", - cause: Option.some(cause) - }) - )), - Effect.catchTag("BadArgument", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache read failed: ${cause.message}`, - operation: "read", - cause: Option.some(cause) - }) - )) + Effect.catchTag("SchemaError", (cause) => Effect.fail(makeAuthCacheError("read", cause))) ) if (Option.isNone(cacheResult)) { @@ -688,9 +707,9 @@ const make = Effect.gen(function*() { // Check if we need to refresh the token const needsRefresh = // Missing expiry field - refresh to populate it - Predicate.isNullable(cache.expiry) || + Predicate.isNullish(cache.expiry) || // Missing accounts field - refresh to populate it - Predicate.isNullable(cache.accounts) || + Predicate.isNullish(cache.accounts) || // Token is expired cache.expiry < now || // Token is expiring within 5 minutes @@ -713,55 +732,16 @@ const make = Effect.gen(function*() { const setCachedAuthInfo = Effect.fn("Auth.setCachedAuthInfo")( function*(authInfo: AuthInfo) { yield* kvs.set(AUTH_INFO_CACHE_KEY, authInfo).pipe( - Effect.catchTag("SystemError", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache write failed: ${cause.message}`, - operation: "write", - cause: Option.some(cause) - }) - )), - Effect.catchTag("ParseError", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache write failed: ${cause.message}`, - operation: "write", - cause: Option.some(cause) - }) - )), - Effect.catchTag("BadArgument", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache write failed: ${cause.message}`, - operation: "write", - cause: Option.some(cause) - }) - )) + Effect.catchTags({ + SchemaError: (cause) => Effect.fail(makeAuthCacheError("write", cause)), + KeyValueStoreError: (cause) => Effect.fail(makeAuthCacheError("write", cause)) + }) ) } ) const clearCachedAuthInfo = kvs.remove(AUTH_INFO_CACHE_KEY).pipe( - Effect.catchIf( - (error) => error._tag === "SystemError" && error.reason === "NotFound", - () => Effect.void - ), - Effect.catchTag("SystemError", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache clear failed: ${cause.message}`, - operation: "clear", - cause: Option.some(cause) - }) - )), - Effect.catchTag("BadArgument", (cause) => - Effect.fail( - new AuthCacheError({ - message: `Cache clear failed: ${cause.message}`, - operation: "clear", - cause: Option.some(cause) - }) - )), + Effect.ignore, Effect.withSpan("Auth.clearCachedAuthInfo") ) diff --git a/packages/amp/src/cdc-stream/batch-store.ts b/packages/amp/src/cdc-stream/batch-store.ts index 5f3effe..71195a6 100644 --- a/packages/amp/src/cdc-stream/batch-store.ts +++ b/packages/amp/src/cdc-stream/batch-store.ts @@ -70,7 +70,6 @@ export interface BatchStoreService { // Context.Tag // ============================================================================= -export class BatchStore extends Context.Tag("Amp/CdcStream/BatchStore")< - BatchStore, - BatchStoreService ->() {} +export class BatchStore extends Context.Service()( + "Amp/CdcStream/BatchStore" +) {} diff --git a/packages/amp/src/cdc-stream/errors.ts b/packages/amp/src/cdc-stream/errors.ts index d67d782..12e752b 100644 --- a/packages/amp/src/cdc-stream/errors.ts +++ b/packages/amp/src/cdc-stream/errors.ts @@ -13,11 +13,11 @@ import type { TransactionalStreamError } from "../transactional-stream/errors.ts /** * Error from BatchStore operations. */ -export class BatchStoreError extends Schema.TaggedError( +export class BatchStoreError extends Schema.TaggedErrorClass( "Amp/CdcStream/BatchStoreError" )("BatchStoreError", { reason: Schema.String, - operation: Schema.Literal("append", "seek", "load", "prune"), + operation: Schema.Literals(["append", "seek", "load", "prune"]), cause: Schema.optional(Schema.Defect) }) {} diff --git a/packages/amp/src/cdc-stream/stream.ts b/packages/amp/src/cdc-stream/stream.ts index 627bb24..170b005 100644 --- a/packages/amp/src/cdc-stream/stream.ts +++ b/packages/amp/src/cdc-stream/stream.ts @@ -9,6 +9,8 @@ */ import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Filter from "effect/Filter" +import { identity } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Stream from "effect/Stream" @@ -55,10 +57,9 @@ export interface CdcStreamService { // Context.Tag // ============================================================================= -export class CdcStream extends Context.Tag("Amp/CdcStream")< - CdcStream, - CdcStreamService ->() {} +export class CdcStream extends Context.Service()( + "Amp/CdcStream" +) {} // ============================================================================= // DeleteBatchIterator @@ -80,6 +81,7 @@ const makeDeleteBatchIterator = ( readonly [TransactionId, ReadonlyArray>] | undefined, BatchStoreError > => { + // @effect-diagnostics-next-line effectSucceedWithVoid:off if (cursor >= ids.length) return Effect.succeed(undefined) const id = ids[cursor]! cursor++ @@ -148,7 +150,7 @@ const make = Effect.gen(function*() { if (Option.isSome(event.prune)) { // Best-effort pruning yield* batchStore.prune(event.prune.value).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logWarning("Batch pruning failed (will retry on next watermark)", error) ) ) @@ -161,7 +163,7 @@ const make = Effect.gen(function*() { } }) ), - Stream.filterMap((x) => x), + Stream.filterMap(Filter.fromPredicateOption(identity)), Stream.withSpan("CdcStream.streamCdc") ) } diff --git a/packages/amp/src/core/domain.ts b/packages/amp/src/core/domain.ts index 42f4888..83e5592 100644 --- a/packages/amp/src/core/domain.ts +++ b/packages/amp/src/core/domain.ts @@ -1,32 +1,49 @@ -import * as ParseResult from "effect/ParseResult" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" import * as Schema from "effect/Schema" +import * as SchemaGetter from "effect/SchemaGetter" +import * as SchemaIssue from "effect/SchemaIssue" +import * as SchemaParser from "effect/SchemaParser" +import * as SchemaTransformation from "effect/SchemaTransformation" import { isAddress } from "viem" +/** + * A schema representing a non-empty trimmed string. + */ +export const NonEmptyTrimmedString = Schema.Trimmed.check(Schema.isNonEmpty()) + +export type NonEmptyTrimmedString = typeof NonEmptyTrimmedString.Type + +export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) + +export type NonNegativeInt = typeof NonNegativeInt.Type + /** * A branded type representing a string in Ethereum address format. An Ethereum * address is a unique, 42-character hexadecimal identifier (starting with `0x`) * used to send and receive funds. */ -export const Address = Schema.NonEmptyTrimmedString.pipe( - Schema.filter((val) => isAddress(val)), +export const Address = NonEmptyTrimmedString.check( + Schema.makeFilter((val) => isAddress(val)) +).pipe( Schema.brand("Amp/Models/Address") -).annotations({ identifier: "Address" }) +).annotate({ identifier: "Address" }) export type Address = typeof Address.Type /** * A branded type representing an OAuth2 access token. */ -export const AccessToken = Schema.NonEmptyTrimmedString.pipe( +export const AccessToken = NonEmptyTrimmedString.pipe( Schema.brand("Amp/Models/AccessToken") -).annotations({ identifier: "AccessToken" }) +).annotate({ identifier: "AccessToken" }) export type AccessToken = typeof AccessToken.Type /** * A branded type representing an OAuth2 refresh token. */ -export const RefreshToken = Schema.NonEmptyTrimmedString.pipe( +export const RefreshToken = NonEmptyTrimmedString.pipe( Schema.brand("Amp/Models/RefreshToken") -).annotations({ identifier: "RefreshToken" }) +).annotate({ identifier: "RefreshToken" }) export type RefreshToken = typeof RefreshToken.Type const TOKEN_DURATION_REGEX = @@ -36,10 +53,11 @@ const TOKEN_DURATION_REGEX = * A branded type representing the duration an OAuth2 access token should be * valid for. */ -export const TokenDuration = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(TOKEN_DURATION_REGEX), +export const TokenDuration = NonEmptyTrimmedString.check( + Schema.isPattern(TOKEN_DURATION_REGEX) +).pipe( Schema.brand("TokenDuration") -).annotations({ +).annotate({ identifier: "TokenDuration", examples: [ "7 days" as TokenDuration, @@ -53,10 +71,11 @@ export type TokenDuration = typeof TokenDuration.Type /** * A branded type representing the identifier for an authenticated user. */ -export const UserId = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^(c[a-z0-9]{24}|did:privy:c[a-z0-9]{24})$/), +export const UserId = NonEmptyTrimmedString.check( + Schema.isPattern(/^(c[a-z0-9]{24}|did:privy:c[a-z0-9]{24})$/) +).pipe( Schema.brand("Amp/Models/UserId") -).annotations({ identifier: "UserId" }) +).annotate({ identifier: "UserId" }) export type UserId = typeof UserId.Type /** @@ -66,17 +85,19 @@ export const AuthInfo = Schema.Struct({ accessToken: Schema.Redacted(AccessToken), refreshToken: Schema.Redacted(RefreshToken), userId: UserId, - accounts: Schema.optional(Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Address))), - expiry: Schema.Int.pipe(Schema.positive(), Schema.optional) -}).annotations({ identifier: "AuthInfo" }) + accounts: Schema.optional(Schema.Array( + Schema.Union([NonEmptyTrimmedString, Address]) + )), + expiry: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))) +}).annotate({ identifier: "AuthInfo" }) export type AuthInfo = typeof AuthInfo.Type /** * Represents a block number. */ -export const BlockNumber = Schema.NonNegativeInt.pipe( +export const BlockNumber = NonNegativeInt.pipe( Schema.brand("Amp/Models/BlockNumber") -).annotations({ +).annotate({ identifier: "BlockNumber", description: "A block number" }) @@ -85,18 +106,21 @@ export type BlockNumber = typeof BlockNumber.Type /** * Represents a block hash. */ -export const BlockHash = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^0x[a-z0-9]{64}/), +export const BlockHash = NonEmptyTrimmedString.check( + Schema.isPattern(/^0x[a-z0-9]{64}/) +).pipe( Schema.brand("Amp/Models/BlockHash") -).annotations({ identifier: "BlockHash" }) +).annotate({ identifier: "BlockHash" }) export type BlockHash = typeof BlockHash.Type /** * Represents a blockchain network. */ -export const Network = Schema.Lowercase.pipe( +export const Network = Schema.String.check( + Schema.isLowercased() +).pipe( Schema.brand("Amp/Models/Network") -).annotations({ +).annotate({ title: "Network", description: "a blockchain network", examples: ["mainnet" as Network] @@ -122,10 +146,8 @@ export const BlockRange = Schema.Struct({ /** * The hash associated with the parent of the start block, if present */ - prevHash: Schema.optional(BlockHash).pipe( - Schema.fromKey("prev_hash") - ) -}).annotations({ + prevHash: Schema.optional(BlockHash) +}).pipe(Schema.encodeKeys({ prevHash: "prev_hash" })).annotate({ identifier: "BlockRange", description: "A range of blocks on a given network" }) @@ -143,11 +165,8 @@ export const RecordBatchMetadata = Schema.Struct({ /** * Indicates whether this is the final record batch associated to the ranges. */ - rangesComplete: Schema.Boolean.pipe( - Schema.propertySignature, - Schema.fromKey("ranges_complete") - ) -}).annotations({ + rangesComplete: Schema.Boolean +}).pipe(Schema.encodeKeys({ rangesComplete: "ranges_complete" })).annotate({ identifier: "RecordBatchMetadata", description: "Metadata carrying information about the block ranges covered by this record batch" }) @@ -158,23 +177,25 @@ export type RecordBatchMetadata = typeof RecordBatchMetadata.Type * `FlightData` response into metadata about the associated Arrow Flight * RecordBatch. */ -export const RecordBatchMetadataFromUint8Array = Schema.transformOrFail( - Schema.Uint8ArrayFromSelf, - Schema.parseJson(RecordBatchMetadata), - { - strict: true, - encode: (decoded, _, ast) => - ParseResult.try({ - try: () => new TextEncoder().encode(decoded), - catch: () => new ParseResult.Type(ast, decoded, "Failed to encode record batch metadata") - }), - decode: (encoded, _, ast) => - ParseResult.try({ - try: () => new TextDecoder().decode(encoded), - catch: () => new ParseResult.Type(ast, encoded, "Failed to encode record batch metadata") - }) - } -).pipe(Schema.asSchema) +export const RecordBatchMetadataFromUint8Array = Schema.Uint8Array.pipe( + Schema.decodeTo( + Schema.fromJsonString(RecordBatchMetadata), + SchemaTransformation.transformOrFail({ + encode: (decoded) => + Effect.try({ + try: () => new TextEncoder().encode(decoded), + catch: () => + new SchemaIssue.InvalidValue(Option.some(decoded), { message: "Failed to encode record batch metadata" }) + }), + decode: (encoded) => + Effect.try({ + try: () => new TextDecoder().decode(encoded), + catch: () => + new SchemaIssue.InvalidValue(Option.some(encoded), { message: "Failed to decode record batch metadata" }) + }) + }) + ) +) export type RecordBatchMetadataFromUint8Array = typeof RecordBatchMetadataFromUint8Array.Type /** @@ -182,10 +203,11 @@ export type RecordBatchMetadataFromUint8Array = typeof RecordBatchMetadataFromUi * * If not specified, defaults to `"_"`. */ -export const DatasetNamespace = Schema.NonEmptyString.pipe( - Schema.pattern(/^[a-z0-9_]+$/), +export const DatasetNamespace = Schema.NonEmptyString.check( + Schema.isPattern(/^[a-z0-9_]+$/) +).pipe( Schema.brand("Amp/Models/Address") -).annotations({ +).annotate({ identifier: "DatasetNamespace", description: "The namespace or owner of the dataset. If not specified, defaults to \"_\". " + "Must contain only lowercase letters, digits, and underscores.", @@ -201,10 +223,11 @@ export type DatasetNamespace = typeof DatasetNamespace.Type /** * Represents the name of a dataset. */ -export const DatasetName = Schema.NonEmptyString.pipe( - Schema.pattern(/^[a-z_][a-z0-9_]*$/), +export const DatasetName = Schema.NonEmptyString.check( + Schema.isPattern(/^[a-z_][a-z0-9_]*$/) +).pipe( Schema.brand("Amp/Models/DatasetName") -).annotations({ +).annotate({ identifier: "DatasetName", description: "The name of the dataset. Must start with a lowercase letter or underscore, " + "followed by lowercase letters, digits, or underscores.", @@ -217,9 +240,12 @@ export type DatasetName = typeof DatasetName.Type * * Must be one of `"manifest"`, `"evm-rpc"`, `"eth-beacon"`, or `"firehose"`. */ -export const DatasetKind = Schema.Literal("manifest", "evm-rpc", "eth-beacon", "firehose").pipe( - Schema.brand("Amp/Models/DatasetKind") -).annotations({ +export const DatasetKind = Schema.Literals([ + "manifest", + "evm-rpc", + "eth-beacon", + "firehose" +]).pipe(Schema.brand("Amp/Models/DatasetKind")).annotate({ identifier: "DatasetKind", description: "The kind of the dataset.", examples: [ @@ -234,12 +260,13 @@ export type DatasetKind = typeof DatasetKind.Type /** * Represents the semantic version of the dataset. */ -export const DatasetVersion = Schema.String.pipe( - Schema.pattern( +export const DatasetVersion = Schema.String.check( + Schema.isPattern( /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - ), + ) +).pipe( Schema.brand("Amp/Models/DatasetVersion") -).annotations({ +).annotate({ identifier: "DatasetVersion", description: "The semantic version number for the dataset.", examples: [ @@ -255,10 +282,11 @@ export type DatasetVersion = typeof DatasetVersion.Type /** * Represents the 32-byte SHA-256 hash for the dataset. */ -export const DatasetHash = Schema.String.pipe( - Schema.pattern(/^[0-9a-fA-F]{64}$/), +export const DatasetHash = Schema.String.check( + Schema.isPattern(/^[0-9a-fA-F]{64}$/) +).pipe( Schema.brand("Amp/Models/DatasetHash") -).annotations({ +).annotate({ identifier: "DatasetHash", description: "A 32-byte SHA-256 hash (64 characters) for the dataset.", examples: ["b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" as DatasetHash] @@ -268,9 +296,9 @@ export type DatasetHash = typeof DatasetHash.Type /** * Represents a tag for a dataset version. */ -export const DatasetTag = Schema.Literal("latest", "dev").pipe( +export const DatasetTag = Schema.Literals(["latest", "dev"]).pipe( Schema.brand("Amp/Models/DatasetTag") -).annotations({ +).annotate({ identifier: "DatasetTag", description: "A tag for a dataset version.", examples: ["latest" as DatasetTag, "dev" as DatasetTag] @@ -281,7 +309,11 @@ export type DatasetTag = typeof DatasetTag.Type * Represents a dataset revision reference, which can be either a semver tag, * a 64-character hexadecimal hash, `"latest"`, or `"dev"`. */ -export const DatasetRevision = Schema.Union(DatasetVersion, DatasetHash, DatasetTag).annotations({ +export const DatasetRevision = Schema.Union([ + DatasetVersion, + DatasetHash, + DatasetTag +]).annotate({ identifier: "DatasetRevision", description: "A dataset revision reference (semver tag, 64 character hexadecimal hash, \"latest\", or \"dev\").", examples: [ @@ -301,9 +333,9 @@ export type DatasetRevision = typeof DatasetRevision.Type * The revision can be either a semver version, 64-character hexadecimal hash, * `"latest"`, or `"dev"`. */ -export const DatasetReferenceString = Schema.String.pipe( - Schema.pattern(/^[a-z0-9_]+\/[a-z_][a-z0-9_]*@.+$/) -).annotations({ +export const DatasetReferenceString = Schema.String.check( + Schema.isPattern(/^[a-z0-9_]+\/[a-z_][a-z0-9_]*@.+$/) +).annotate({ identifier: "DatasetReferenceString", description: "A dataset reference as a string in the format `/@`, " + "where revision is a semver version, hash, \"latest\", or \"dev\"", @@ -322,26 +354,23 @@ export const DatasetReference = Schema.Struct({ namespace: DatasetNamespace, name: DatasetName, revision: DatasetRevision -}).annotations({ +}).annotate({ identifier: "DatasetReference", description: "A reference to a specific dataset." }) export type DatasetReference = typeof DatasetReference.Type -const decodeDatasetReference = ParseResult.decode(DatasetReference) +const decodeDatasetReference = SchemaParser.decodeEffect(DatasetReference) /** * Represents a dataset reference parsed from a string in the format: * * `/@` */ -export const DatasetReferenceFromString = Schema.transformOrFail( - Schema.String, - DatasetReference, - { - strict: true, - encode: (ref) => ParseResult.succeed(`${ref.namespace}/${ref.name}@${ref.revision}`), - decode: (str) => { +export const DatasetReferenceFromString = Schema.String.pipe( + Schema.decodeTo(DatasetReference, { + encode: SchemaGetter.transform((ref) => `${ref.namespace}/${ref.name}@${ref.revision}`), + decode: SchemaGetter.transformOrFail((str) => { const at = str.lastIndexOf("@") const slash = str.indexOf("/") @@ -354,9 +383,9 @@ export const DatasetReferenceFromString = Schema.transformOrFail( name, revision }) - } - } -).annotations({ + }) + }) +).annotate({ identifier: "DatasetReferenceFromString", description: "A dataset reference parsed from a string in the format `/@`." }) @@ -365,11 +394,11 @@ export type DatasetReferenceFromString = typeof DatasetReferenceFromString.Type /** * Represents the name and version of the dataset. */ -export const DatasetNameAndVersion = Schema.NonEmptyString.pipe( - Schema.pattern( +export const DatasetNameAndVersion = Schema.NonEmptyString.check( + Schema.isPattern( /^\w+@(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ ) -).annotations({ +).annotate({ identifier: "DatasetNameAndVersion", title: "NameAndVersion", description: "The name and version of the dataset.", @@ -380,7 +409,7 @@ export type DatasetNameAndVersion = typeof DatasetNameAndVersion.Type /** * Represents the address of the dataset repository. */ -export const DatasetRepository = Schema.URL.annotations({ +export const DatasetRepository = Schema.URL.annotate({ identifier: "DatasetRepository", title: "Repository", description: "The address of the dataset repository.", @@ -391,7 +420,7 @@ export type DatasetRepository = typeof DatasetRepository.Type /** * Represents the documentation for the dataset. */ -export const DatasetReadme = Schema.String.annotations({ +export const DatasetReadme = Schema.String.annotate({ identifier: "DatasetReadme", title: "Readme", description: "The documentation for the dataset." @@ -401,9 +430,9 @@ export type DatasetReadme = typeof DatasetReadme.Type /** * Represents additional description and details about the dataset. */ -export const DatasetDescription = Schema.String.pipe( - Schema.maxLength(1024) -).annotations({ +export const DatasetDescription = Schema.String.check( + Schema.isMaxLength(1024) +).annotate({ identifier: "DatasetDescription", title: "Description", description: "Additional description and details about the dataset." @@ -414,7 +443,7 @@ export type DatasetDescription = typeof DatasetDescription.Type * Represents keywords, or traits, about the dataset for discoverability and * searching. */ -export const DatasetKeyword = Schema.String.annotations({ +export const DatasetKeyword = Schema.String.annotate({ identifier: "DatasetKeyword", title: "Keyword", description: "Keywords, or traits, about the dataset for discoverability and searching.", @@ -428,7 +457,7 @@ export type DatasetKeyword = typeof DatasetKeyword.Type * For example, this could be the block or logs table that powers the dataset, * or the 0x address of the smart contract being queried. */ -export const DatasetSource = Schema.String.annotations({ +export const DatasetSource = Schema.String.annotate({ identifier: "DatasetSource", title: "Source", description: "Source of the dataset data. For example, the block or logs table that powers the " + @@ -444,7 +473,7 @@ export type DatasetSource = typeof DatasetSource.Type /** * Represents the license which covers the dataset. */ -export const DatasetLicense = Schema.String.annotations({ +export const DatasetLicense = Schema.String.annotate({ identifier: "DatasetLicense", title: "License", description: "License covering the dataset.", @@ -455,7 +484,7 @@ export type DatasetLicense = typeof DatasetLicense.Type /** * Represents the visibility of a dataset. */ -export const DatasetVisibility = Schema.Literal("public", "private").annotations({ +export const DatasetVisibility = Schema.Literals(["public", "private"]).annotate({ identifier: "DatasetVisibility" }) export type DatasetVisibility = typeof DatasetVisibility.Type @@ -473,7 +502,7 @@ export const DatasetMetadata = Schema.Struct({ sources: Schema.optional(Schema.Array(DatasetSource)), license: Schema.optional(DatasetLicense), visibility: Schema.optional(DatasetVisibility) -}).annotations({ +}).annotate({ identifier: "DatasetMetadata", description: "Metadata associated with a dataset." }) @@ -485,7 +514,7 @@ export type DatasetMetadata = typeof DatasetMetadata.Type export const FunctionSource = Schema.Struct({ source: Schema.String, filename: Schema.String -}).annotations({ +}).annotate({ identifier: "FunctionSource", description: "The source of a function." }) @@ -498,7 +527,7 @@ export const FunctionDefinition = Schema.Struct({ source: FunctionSource, inputTypes: Schema.Array(Schema.String), outputType: Schema.String -}).annotations({ +}).annotate({ identifier: "FunctionDefinition", description: "The data required to define of a function." }) @@ -509,7 +538,7 @@ export type FunctionDefinition = typeof FunctionDefinition.Type */ export const TableDefinition = Schema.Struct({ sql: Schema.String -}).annotations({ +}).annotate({ identifier: "TableDefinition", description: "The data required to define a table." }) @@ -530,19 +559,19 @@ export const DatasetConfig = Schema.Struct({ license: Schema.optional(DatasetLicense), private: Schema.optional(Schema.Boolean), startBlock: Schema.optional(Schema.Number), - dependencies: Schema.Record({ - key: Schema.String, - value: DatasetReferenceFromString - }), - tables: Schema.optional(Schema.Record({ - key: Schema.String, - value: TableDefinition - })), - functions: Schema.optional(Schema.Record({ - key: Schema.String, - value: FunctionDefinition - })) -}).annotations({ + dependencies: Schema.Record( + Schema.String, + DatasetReferenceFromString + ), + tables: Schema.optional(Schema.Record( + Schema.String, + TableDefinition + )), + functions: Schema.optional(Schema.Record( + Schema.String, + FunctionDefinition + )) +}).annotate({ identifier: "DatasetConfig", description: "Configuration associated with a dataset." }) @@ -554,11 +583,8 @@ export type DatasetConfig = typeof DatasetConfig.Type export const TableInfo = Schema.Struct({ name: Schema.String, network: Network, - activeLocation: Schema.String.pipe( - Schema.optional, - Schema.fromKey("active_location") - ) -}).annotations({ + activeLocation: Schema.optional(Schema.String) +}).pipe(Schema.encodeKeys({ activeLocation: "active_location" })).annotate({ identifier: "TableInfo", description: "Information about a table." }) @@ -570,11 +596,8 @@ export type TableInfo = typeof TableInfo.Type export const TableSchemaInfo = Schema.Struct({ name: Schema.String, network: Network, - schema: Schema.Record({ - key: Schema.String, - value: Schema.Any - }) -}).annotations({ + schema: Schema.Record(Schema.String, Schema.Any) +}).annotate({ identifier: "TableSchemaInfo", description: "Information about a table schema." }) @@ -587,7 +610,7 @@ export const DatasetInfo = Schema.Struct({ name: DatasetName, kind: DatasetKind, tables: Schema.Array(TableInfo) -}).annotations({ +}).annotate({ identifier: "DatasetInfo", description: "Information about a dataset." }) @@ -600,7 +623,7 @@ export const ArrowField = Schema.Struct({ name: Schema.String, type: Schema.Any, nullable: Schema.Boolean -}).annotations({ +}).annotate({ identifier: "ArrowField", description: "Information about a field within an Apache Arrow schema." }) @@ -611,7 +634,7 @@ export type ArrowField = typeof ArrowField.Type */ export const ArrowSchema = Schema.Struct({ fields: Schema.Array(ArrowField) -}).annotations({ +}).annotate({ identifier: "ArrowSchema", description: "An Apache Arrow schema." }) @@ -622,7 +645,7 @@ export type ArrowSchema = typeof ArrowSchema.Type */ export const TableSchema = Schema.Struct({ arrow: ArrowSchema -}).annotations({ +}).annotate({ identifier: "TableSchema", description: "A table schema." }) @@ -634,7 +657,7 @@ export type TableSchema = typeof TableSchema.Type export const TableSchemaWithNetworks = Schema.Struct({ schema: TableSchema, networks: Schema.Array(Schema.String) -}).annotations({ +}).annotate({ identifier: "TableSchemaWithNetworks", description: "A table schema with associated networks." }) @@ -645,7 +668,7 @@ export type TableSchemaWithNetworks = typeof TableSchemaWithNetworks.Type */ export const TableInput = Schema.Struct({ sql: Schema.String -}).annotations({ +}).annotate({ identifier: "TableInput", description: "Input SQL for a table." }) @@ -658,7 +681,7 @@ export const Table = Schema.Struct({ input: TableInput, schema: TableSchema, network: Network -}).annotations({ +}).annotate({ identifier: "Table", description: "A table." }) @@ -670,7 +693,7 @@ export type Table = typeof Table.Type export const RawDatasetTable = Schema.Struct({ schema: TableSchema, network: Network -}).annotations({ +}).annotate({ identifier: "RawDatasetTable", description: "A table for a raw dataset." }) @@ -682,7 +705,7 @@ export type RawDatasetTable = typeof RawDatasetTable.Type export const OutputSchema = Schema.Struct({ schema: TableSchema, networks: Schema.Array(Schema.String) -}).annotations({ +}).annotate({ identifier: "OutputSchema", description: "The output schema for a query." }) @@ -696,7 +719,7 @@ export const FunctionManifest = Schema.Struct({ source: FunctionSource, inputTypes: Schema.Array(Schema.String), outputType: Schema.String -}).annotations({ +}).annotate({ identifier: "FunctionManifest", description: "Information associated with a function." }) @@ -707,23 +730,11 @@ export type FunctionManifest = typeof FunctionManifest.Type */ export const DatasetDerived = Schema.Struct({ kind: Schema.Literal("manifest"), - startBlock: Schema.NullOr(Schema.Number).pipe( - Schema.optional, - Schema.fromKey("start_block") - ), - dependencies: Schema.Record({ - key: Schema.String, - value: DatasetReferenceFromString - }), - tables: Schema.Record({ - key: Schema.String, - value: Table - }), - functions: Schema.Record({ - key: Schema.String, - value: FunctionManifest - }) -}).annotations({ + startBlock: Schema.optional(Schema.NullOr(Schema.Number)), + dependencies: Schema.Record(Schema.String, DatasetReferenceFromString), + tables: Schema.Record(Schema.String, Table), + functions: Schema.Record(Schema.String, FunctionManifest) +}).pipe(Schema.encodeKeys({ startBlock: "start_block" })).annotate({ identifier: "DatasetDerived", description: "A SQL-based derived datasets." }) @@ -735,19 +746,13 @@ export type DatasetDerived = typeof DatasetDerived.Type export const DatasetEvmRpc = Schema.Struct({ kind: Schema.Literal("evm-rpc"), network: Network, - startBlock: Schema.Number.pipe( - Schema.optional, - Schema.fromKey("start_block") - ), - finalizedBlocksOnly: Schema.Boolean.pipe( - Schema.optional, - Schema.fromKey("finalized_blocks_only") - ), - tables: Schema.Record({ - key: Schema.String, - value: RawDatasetTable - }) -}).annotations({ + startBlock: Schema.optional(Schema.Number), + finalizedBlocksOnly: Schema.optional(Schema.Boolean), + tables: Schema.Record(Schema.String, RawDatasetTable) +}).pipe(Schema.encodeKeys({ + startBlock: "start_block", + finalizedBlocksOnly: "finalized_blocks_only" +})).annotate({ identifier: "DatasetEvmRpc", description: "An EVM RPC extraction dataset." }) @@ -759,19 +764,13 @@ export type DatasetEvmRpc = typeof DatasetEvmRpc.Type export const DatasetEthBeacon = Schema.Struct({ kind: Schema.Literal("eth-beacon"), network: Network, - startBlock: Schema.Number.pipe( - Schema.optional, - Schema.fromKey("start_block") - ), - finalizedBlocksOnly: Schema.Boolean.pipe( - Schema.optional, - Schema.fromKey("finalized_blocks_only") - ), - tables: Schema.Record({ - key: Schema.String, - value: RawDatasetTable - }) -}).annotations({ + startBlock: Schema.optional(Schema.Number), + finalizedBlocksOnly: Schema.optional(Schema.Boolean), + tables: Schema.Record(Schema.String, RawDatasetTable) +}).pipe(Schema.encodeKeys({ + startBlock: "start_block", + finalizedBlocksOnly: "finalized_blocks_only" +})).annotate({ identifier: "DatasetEthBeacon", description: "An ETH beacon extraction dataset." }) @@ -783,19 +782,13 @@ export type DatasetEthBeacon = typeof DatasetEthBeacon.Type export const DatasetFirehose = Schema.Struct({ kind: Schema.Literal("firehose"), network: Network, - startBlock: Schema.Number.pipe( - Schema.optional, - Schema.fromKey("start_block") - ), - finalizedBlocksOnly: Schema.Boolean.pipe( - Schema.optional, - Schema.fromKey("finalized_blocks_only") - ), - tables: Schema.Record({ - key: Schema.String, - value: RawDatasetTable - }) -}).annotations({ + startBlock: Schema.optional(Schema.Number), + finalizedBlocksOnly: Schema.optional(Schema.Boolean), + tables: Schema.Record(Schema.String, RawDatasetTable) +}).pipe(Schema.encodeKeys({ + startBlock: "start_block", + finalizedBlocksOnly: "finalized_blocks_only" +})).annotate({ identifier: "DatasetFirehose", description: "A Firehose extraction dataset." }) @@ -813,12 +806,12 @@ export type DatasetFirehose = typeof DatasetFirehose.Type * - DatasetEthBeacon (kind: "eth-beacon") - ETH beacon extraction datasets * - DatasetFirehose (kind: "firehose") - Firehose extraction datasets */ -export const DatasetManifest = Schema.Union( +export const DatasetManifest = Schema.Union([ DatasetDerived, DatasetEvmRpc, DatasetEthBeacon, DatasetFirehose -) +]) export type DatasetManifest = typeof DatasetManifest.Type /** @@ -826,7 +819,7 @@ export type DatasetManifest = typeof DatasetManifest.Type */ export const JobId = Schema.Number.pipe( Schema.brand("Amp/Models/JobId") -).annotations({ +).annotate({ identifier: "JobId", description: "The unique identifier for a job." }) @@ -835,7 +828,7 @@ export type JobId = typeof JobId.Type /** * Represents the status of a job. */ -export const JobStatus = Schema.Literal( +export const JobStatus = Schema.Literals([ "SCHEDULED", "RUNNING", "COMPLETED", @@ -844,9 +837,9 @@ export const JobStatus = Schema.Literal( "STOPPING", "FAILED", "UNKNOWN" -).pipe( +]).pipe( Schema.brand("Amp/Models/JobStatus") -).annotations({ +).annotate({ identifier: "JobStatus", description: "The status of a job." }) @@ -858,20 +851,15 @@ export type JobStatus = typeof JobStatus.Type export const JobInfo = Schema.Struct({ id: JobId, status: JobStatus, - createdAt: Schema.DateTimeUtc.pipe( - Schema.propertySignature, - Schema.fromKey("created_at") - ), - updatedAt: Schema.DateTimeUtc.pipe( - Schema.propertySignature, - Schema.fromKey("updated_at") - ), - nodeId: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("node_id") - ), + createdAt: Schema.DateTimeUtc, + updatedAt: Schema.DateTimeUtc, + nodeId: Schema.String, descriptor: Schema.Any -}).annotations({ +}).pipe(Schema.encodeKeys({ + createdAt: "created_at", + updatedAt: "updated_at", + nodeId: "node_id" +})).annotate({ identifier: "JobInfo", description: "Information about a job." }) diff --git a/packages/amp/src/internal/arrow-flight-ipc/Errors.ts b/packages/amp/src/internal/arrow-flight-ipc/Errors.ts index f23124e..6baaeb3 100644 --- a/packages/amp/src/internal/arrow-flight-ipc/Errors.ts +++ b/packages/amp/src/internal/arrow-flight-ipc/Errors.ts @@ -5,7 +5,7 @@ import * as Schema from "effect/Schema" * * @internal */ -export class InvalidArrowDataTypeError extends Schema.TaggedError( +export class InvalidArrowDataTypeError extends Schema.TaggedErrorClass( "Amp/InvalidArrowDataTypeError" )("InvalidArrowDataTypeError", { type: Schema.Number, @@ -18,7 +18,7 @@ export class InvalidArrowDataTypeError extends Schema.TaggedError( +export class InvalidMessageTypeError extends Schema.TaggedErrorClass( "Amp/InvalidMessageTypeError" )("InvalidMessageTypeError", { value: Schema.Number @@ -34,7 +34,7 @@ export class InvalidMessageTypeError extends Schema.TaggedError( +export class MissingFieldError extends Schema.TaggedErrorClass( "Amp/MissingFieldError" )("MissingFieldError", { fieldName: Schema.String, @@ -52,7 +52,7 @@ export class MissingFieldError extends Schema.TaggedError( * * @internal */ -export class UnexpectedMessageTypeError extends Schema.TaggedError( +export class UnexpectedMessageTypeError extends Schema.TaggedErrorClass( "Amp/UnexpectedMessageTypeError" )("UnexpectedMessageTypeError", { expected: Schema.Number, diff --git a/packages/amp/src/internal/pkce.ts b/packages/amp/src/internal/pkce.ts index c977109..b1a5256 100644 --- a/packages/amp/src/internal/pkce.ts +++ b/packages/amp/src/internal/pkce.ts @@ -85,7 +85,7 @@ export const pkceChallenge = Effect.fnUntraced(function*(length?: number) { } if (length < 43 || length > 128) { - return yield* Effect.dieMessage( + return yield* Effect.die( `Expected a length between 43 and 128. Received ${length}.` ) } diff --git a/packages/amp/src/protocol-stream/errors.ts b/packages/amp/src/protocol-stream/errors.ts index 6d0d6db..3d5756d 100644 --- a/packages/amp/src/protocol-stream/errors.ts +++ b/packages/amp/src/protocol-stream/errors.ts @@ -18,7 +18,7 @@ import * as Schema from "effect/Schema" * * Each batch should contain at most one range per network. */ -export class DuplicateNetworkError extends Schema.TaggedError( +export class DuplicateNetworkError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/DuplicateNetworkError" )("DuplicateNetworkError", { /** @@ -36,7 +36,7 @@ export class DuplicateNetworkError extends Schema.TaggedError( +export class NetworkCountChangedError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/NetworkCountChangedError" )("NetworkCountChangedError", { /** @@ -58,7 +58,7 @@ export class NetworkCountChangedError extends Schema.TaggedError( +export class UnexpectedNetworkError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/UnexpectedNetworkError" )("UnexpectedNetworkError", { /** @@ -76,7 +76,7 @@ export class UnexpectedNetworkError extends Schema.TaggedError 0) must have a prevHash to enable hash chain validation. */ -export class MissingPrevHashError extends Schema.TaggedError( +export class MissingPrevHashError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/MissingPrevHashError" )("MissingPrevHashError", { /** @@ -98,7 +98,7 @@ export class MissingPrevHashError extends Schema.TaggedError( +export class InvalidPrevHashError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/InvalidPrevHashError" )("InvalidPrevHashError", { /** @@ -117,7 +117,7 @@ export class InvalidPrevHashError extends Schema.TaggedError( +export class HashMismatchOnConsecutiveBlocksError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/HashMismatchOnConsecutiveBlocksError" )("HashMismatchOnConsecutiveBlocksError", { /** @@ -144,7 +144,7 @@ export class HashMismatchOnConsecutiveBlocksError extends Schema.TaggedError( +export class InvalidReorgError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/InvalidReorgError" )("InvalidReorgError", { /** @@ -162,7 +162,7 @@ export class InvalidReorgError extends Schema.TaggedError( * * Forward gaps (incoming.start > prev.end + 1) are always protocol violations. */ -export class GapError extends Schema.TaggedError( +export class GapError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/GapError" )("GapError", { /** @@ -203,7 +203,7 @@ export type ValidationError = /** * Represents a validation error wrapped for the ProtocolStream service. */ -export class ProtocolValidationError extends Schema.TaggedError( +export class ProtocolValidationError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/ProtocolValidationError" )("ProtocolValidationError", { /** @@ -220,7 +220,7 @@ export class ProtocolValidationError extends Schema.TaggedError( +export class ProtocolArrowFlightError extends Schema.TaggedErrorClass( "Amp/ProtocolStream/ProtocolArrowFlightError" )("ProtocolArrowFlightError", { /** diff --git a/packages/amp/src/protocol-stream/messages.ts b/packages/amp/src/protocol-stream/messages.ts index 0cf94bc..cfbbade 100644 --- a/packages/amp/src/protocol-stream/messages.ts +++ b/packages/amp/src/protocol-stream/messages.ts @@ -35,7 +35,7 @@ export const InvalidationRange = Schema.Struct({ * The end of the invalidation range (inclusive). */ end: BlockNumber -}).annotations({ +}).annotate({ identifier: "InvalidationRange", description: "A range of blocks that must be invalidated due to a reorg" }) @@ -90,15 +90,12 @@ export const ProtocolMessageData = Schema.TaggedStruct("Data", { /** * The decoded record batch data as an array of records. */ - data: Schema.Array(Schema.Record({ - key: Schema.String, - value: Schema.Unknown - })), + data: Schema.Array(Schema.Record(Schema.String, Schema.Unknown)), /** * The block ranges covered by this batch. */ ranges: Schema.Array(BlockRange) -}).annotations({ +}).annotate({ identifier: "ProtocolMessage.Data", description: "New data to process from the protocol stream" }) @@ -125,7 +122,7 @@ export const ProtocolMessageReorg = Schema.TaggedStruct("Reorg", { * The ranges that need to be invalidated due to the reorg. */ invalidation: Schema.Array(InvalidationRange) -}).annotations({ +}).annotate({ identifier: "ProtocolMessage.Reorg", description: "Chain reorganization detected" }) @@ -143,7 +140,7 @@ export const ProtocolMessageWatermark = Schema.TaggedStruct("Watermark", { * The block ranges that are confirmed complete. */ ranges: Schema.Array(BlockRange) -}).annotations({ +}).annotate({ identifier: "ProtocolMessage.Watermark", description: "Watermark indicating ranges are confirmed complete" }) @@ -168,11 +165,11 @@ export type ProtocolMessageWatermark = typeof ProtocolMessageWatermark.Type * } * ``` */ -export const ProtocolMessage = Schema.Union( +export const ProtocolMessage = Schema.Union([ ProtocolMessageData, ProtocolMessageReorg, ProtocolMessageWatermark -).annotations({ +]).annotate({ identifier: "ProtocolMessage", description: "A message from the protocol stream" }) diff --git a/packages/amp/src/protocol-stream/service.ts b/packages/amp/src/protocol-stream/service.ts index c0bc489..421429a 100644 --- a/packages/amp/src/protocol-stream/service.ts +++ b/packages/amp/src/protocol-stream/service.ts @@ -116,10 +116,10 @@ export interface ProtocolStreamService { * Effect.runPromise(program.pipe(Effect.provide(AppLayer))) * ``` */ -export class ProtocolStream extends Context.Tag("Amp/ProtocolStream")< +export class ProtocolStream extends Context.Service< ProtocolStream, ProtocolStreamService ->() {} +>()("Amp/ProtocolStream") {} // ============================================================================= // Implementation @@ -201,13 +201,13 @@ const make = Effect.gen(function*() { Stream.mapError((error: ArrowFlightError) => new ProtocolArrowFlightError({ cause: error })), // Process each batch with state tracking Stream.mapAccumEffect( - initialState, + () => initialState, Effect.fnUntraced( function*( state: ProtocolStreamState, queryResult: QueryResult> ): Effect.fn.Return< - readonly [ProtocolStreamState, ProtocolMessage], + readonly [ProtocolStreamState, ReadonlyArray], ProtocolStreamError > { const batchData = queryResult.data @@ -238,7 +238,7 @@ const make = Effect.gen(function*() { initialized: true } - return [newState, message] as const + return [newState, [message]] as const } ) ), diff --git a/packages/amp/src/registry/api.ts b/packages/amp/src/registry/api.ts index f79e13f..b3ae6ba 100644 --- a/packages/amp/src/registry/api.ts +++ b/packages/amp/src/registry/api.ts @@ -1,9 +1,9 @@ -import * as HttpApi from "@effect/platform/HttpApi" -import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint" -import * as HttpApiError from "@effect/platform/HttpApiError" -import * as HttpApiGroup from "@effect/platform/HttpApiGroup" -import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" +import * as HttpApi from "effect/unstable/httpapi/HttpApi" +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint" +import * as HttpApiError from "effect/unstable/httpapi/HttpApiError" +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup" +import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema" import * as Models from "../core/domain.ts" import * as Domain from "./domain.ts" import * as Errors from "./error.ts" @@ -15,22 +15,22 @@ import * as Errors from "./error.ts" /** * A URL parameter for the dataset namespace. */ -const datasetNamespaceParam = HttpApiSchema.param("namespace", Models.DatasetNamespace) +const DatasetNamespaceParam = Models.DatasetNamespace /** * A URL parameter for the dataset name. */ -const datasetNameParam = HttpApiSchema.param("name", Models.DatasetName) +const DatasetNameParam = Models.DatasetName /** * A URL parameter for the dataset revision. */ -const datasetRevisionParam = HttpApiSchema.param("revision", Models.DatasetRevision) +const DatasetRevisionParam = Models.DatasetRevision /** * A URL parameter for the dataset owners. */ -const datasetOwnerParam = HttpApiSchema.param("owner", Schema.String) +const DatasetOwnerParam = Schema.String // ============================================================================= // Health @@ -40,21 +40,26 @@ const datasetOwnerParam = HttpApiSchema.param("owner", Schema.String) // GET / // ----------------------------------------------------------------------------- -const getHealth = HttpApiEndpoint.get("getHealth")`/`.addSuccess(Domain.HealthcheckResponse) +const getHealth = HttpApiEndpoint.get("getHealth", "/", { + success: Domain.HealthcheckResponse +}) // ----------------------------------------------------------------------------- // GET /health/live // ----------------------------------------------------------------------------- -const getLiveness = HttpApiEndpoint.get("getLiveness")`/health/live`.addSuccess(Domain.LivenessResponse) +const getLiveness = HttpApiEndpoint.get("getLiveness", "/health/live", { + success: Domain.LivenessResponse +}) // ----------------------------------------------------------------------------- // GET /health/ready // ----------------------------------------------------------------------------- -const getReadiness = HttpApiEndpoint.get("getReadiness")`/health/ready` - .addSuccess(Domain.ReadinessResponse) - .addError(Errors.ServiceUnavailableError) +const getReadiness = HttpApiEndpoint.get("getReadiness", "/health/ready", { + success: Domain.ReadinessResponse, + error: Errors.ServiceUnavailableError +}) // ----------------------------------------------------------------------------- // Group Definition @@ -77,179 +82,250 @@ export class HealthApiGroup extends HttpApiGroup.make("health") // GET /api/vX/datasets // ----------------------------------------------------------------------------- -const listDatasets = HttpApiEndpoint.get( - "listDatasets" -)`/datasets` - .setUrlParams(Domain.ListDatasetsParams) - .addSuccess(Domain.DatasetListResponse) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) +const listDatasets = HttpApiEndpoint.get("listDatasets", "/datasets", { + query: Domain.ListDatasetsParams, + success: Domain.DatasetListResponse, + error: [ + Errors.DatasetConversionError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] +}) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/{namespace}/{name} // ----------------------------------------------------------------------------- -const getDatasetByFqdn = HttpApiEndpoint.get( - "getDatasetByFqdn" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}` - .addSuccess(Domain.Dataset) - .addError(Errors.DatasetConversionError) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.InvalidDatasetSelectorError) - .addError(Errors.RegistryDatabaseError) +const getDatasetByFqdn = HttpApiEndpoint.get("getDatasetByFqdn", "/datasets/:namespace/:name", { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + success: Domain.Dataset, + error: [ + Errors.DatasetConversionError, + Errors.DatasetNotFoundError, + Errors.InvalidDatasetSelectorError, + Errors.RegistryDatabaseError + ] +}) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/counts/by-chain // ----------------------------------------------------------------------------- -const getDatasetCountsByChain = HttpApiEndpoint.get( - "getDatasetCountsByChain" -)`/datasets/counts/by-chain` - .addSuccess(Domain.DatasetCountsByChainResponse) - .addError(Errors.RegistryDatabaseError) +const getDatasetCountsByChain = HttpApiEndpoint.get("getDatasetCountsByChain", "/datasets/counts/by-chain", { + success: Domain.DatasetCountsByChainResponse, + error: Errors.RegistryDatabaseError +}) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/counts/by-keyword // ----------------------------------------------------------------------------- -const getDatasetCountsByKeyword = HttpApiEndpoint.get( - "getDatasetCountsByKeyword" -)`/datasets/counts/by-keyword` - .addSuccess(Domain.DatasetCountsByKeywordResponse) - .addError(Errors.RegistryDatabaseError) +const getDatasetCountsByKeyword = HttpApiEndpoint.get("getDatasetCountsByKeyword", "/datasets/counts/by-keyword", { + success: Domain.DatasetCountsByKeywordResponse, + error: Errors.RegistryDatabaseError +}) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/counts/by-last-updated // ----------------------------------------------------------------------------- const getDatasetCountsByLastUpdated = HttpApiEndpoint.get( - "getDatasetCountsByLastUpdated" -)`/datasets/counts/by-last-updated` - .addSuccess(Domain.DatasetCountsByLastUpdatedResponse) - .addError(Errors.RegistryDatabaseError) + "getDatasetCountsByLastUpdated", + "/datasets/counts/by-last-updated", + { + success: Domain.DatasetCountsByLastUpdatedResponse, + error: Errors.RegistryDatabaseError + } +) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/search // ----------------------------------------------------------------------------- -const searchDatasets = HttpApiEndpoint.get( - "searchDatasets" -)`/datasets/search` - .setUrlParams(Domain.SearchDatasetsParams) - .addSuccess(Domain.DatasetSearchResponse) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) +const searchDatasets = HttpApiEndpoint.get("searchDatasets", "/datasets/search", { + query: Domain.SearchDatasetsParams, + success: Domain.DatasetSearchResponse, + error: [ + Errors.DatasetConversionError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] +}) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/search/ai // ----------------------------------------------------------------------------- -const aiSearchDatasets = HttpApiEndpoint.get( - "aiSearchDatasets" -)`/datasets/search/ai` - .setUrlParams(Domain.AiSearchDatasetsParams) - .addSuccess(Domain.DatasetAiSearchResponse) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.RegistryDatabaseError) +const aiSearchDatasets = HttpApiEndpoint.get("aiSearchDatasets", "/datasets/search/ai", { + query: Domain.AiSearchDatasetsParams, + success: Domain.DatasetAiSearchResponse, + error: [ + Errors.DatasetConversionError, + Errors.InvalidQueryParametersError, + Errors.RegistryDatabaseError + ] +}) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/{namespace}/{name}/versions // ----------------------------------------------------------------------------- -const listDatasetVersions = HttpApiEndpoint.get( - "listDatasetVersions" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions` - .addSuccess(Domain.DatasetListVersionsResponse) - .addError(Errors.DatasetVersionConversionError) - .addError(Errors.InvalidSelectorError) - .addError(Errors.RegistryDatabaseError) +const listDatasetVersions = HttpApiEndpoint.get("listDatasetVersions", "/datasets/:namespace/:name/versions", { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + success: Domain.DatasetListVersionsResponse, + error: [ + Errors.DatasetVersionConversionError, + Errors.InvalidSelectorError, + Errors.RegistryDatabaseError + ] +}) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/{namespace}/{name}/versions/latest // ----------------------------------------------------------------------------- const getLatestDatasetVersion = HttpApiEndpoint.get( - "getLatestDatasetVersion" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/latest` - .addSuccess(Domain.DatasetVersion) - .addError(Errors.DatasetVersionConversionError) - .addError(Errors.LatestDatasetVersionNotFoundError) - .addError(Errors.InvalidSelectorError) - .addError(Errors.RegistryDatabaseError) + "getLatestDatasetVersion", + "/datasets/:namespace/:name/versions/latest", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + success: Domain.DatasetVersion, + error: [ + Errors.DatasetVersionConversionError, + Errors.LatestDatasetVersionNotFoundError, + Errors.InvalidSelectorError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/vX/datasets/{namespace}/{name}/versions/{version} // ----------------------------------------------------------------------------- const getDatasetVersionByRevision = HttpApiEndpoint.get( - "getDatasetVersionByRevision" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}` - .addSuccess(Domain.DatasetVersion) - .addError(Errors.DatasetVersionConversionError) - .addError(Errors.DatasetVersionNotFoundError) - .addError(Errors.InvalidSelectorError) - .addError(Errors.RegistryDatabaseError) + "getDatasetVersionByRevision", + "/datasets/:namespace/:name/versions/:revision", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + success: Domain.DatasetVersion, + error: [ + Errors.DatasetVersionConversionError, + Errors.DatasetVersionNotFoundError, + Errors.InvalidSelectorError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/{namespace}/{name}/versions/latest/queries // ----------------------------------------------------------------------------- const listLatestDatasetQueries = HttpApiEndpoint.get( - "listLatestDatasetQueries" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/latest/queries` - .addSuccess(Domain.DatasetListLatestQueriesResponse) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.InvalidDatasetReferenceError) - .addError(Errors.InvalidSelectorError) - .addError(Errors.RegistryDatabaseError) - .addError(Errors.SavedQueryConversionError) + "listLatestDatasetQueries", + "/datasets/:namespace/:name/versions/latest/queries", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + success: Domain.DatasetListLatestQueriesResponse, + error: [ + Errors.DatasetNotFoundError, + Errors.InvalidDatasetReferenceError, + Errors.InvalidSelectorError, + Errors.RegistryDatabaseError, + Errors.SavedQueryConversionError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/{namespace}/{name}/versions/{version}/queries // ----------------------------------------------------------------------------- const listDatasetQueries = HttpApiEndpoint.get( - "listDatasetQueries" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/queries` - .addSuccess(Domain.DatasetListQueriesResponse) - .addError(Errors.InvalidSelectorError) - .addError(Errors.RegistryDatabaseError) - .addError(Errors.SavedQueryConversionError) + "listDatasetQueries", + "/datasets/:namespace/:name/versions/:revision/queries", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + success: Domain.DatasetListQueriesResponse, + error: [ + Errors.InvalidSelectorError, + Errors.RegistryDatabaseError, + Errors.SavedQueryConversionError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/{namespace}/{name}/versions/latest/manifest // ----------------------------------------------------------------------------- const getLatestDatasetManifest = HttpApiEndpoint.get( - "getLatestDatasetManifest" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/latest/manifest` - .addSuccess(Domain.DatasetGetLatestManifestResponse) - .addError(Errors.InvalidSelectorError) - .addError(Errors.InvalidManifestHashError) - .addError(Errors.ManifestNotFoundError) - .addError(Errors.ManifestRetrievalError) - .addError(Errors.ManifestDeserializationError) + "getLatestDatasetManifest", + "/datasets/:namespace/:name/versions/latest/manifest", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + success: Domain.DatasetGetLatestManifestResponse, + error: [ + Errors.InvalidSelectorError, + Errors.InvalidManifestHashError, + Errors.ManifestNotFoundError, + Errors.ManifestRetrievalError, + Errors.ManifestDeserializationError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/datasets/{namespace}/{name}/versions/{version}/manifest // ----------------------------------------------------------------------------- const getDatasetManifest = HttpApiEndpoint.get( - "getDatasetManifest" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/manifest` - .addSuccess(Domain.DatasetGetManifestResponse) - .addError(Errors.InvalidSelectorError) - .addError(Errors.InvalidManifestHashError) - .addError(Errors.ManifestNotFoundError) - .addError(Errors.ManifestRetrievalError) - .addError(Errors.ManifestDeserializationError) + "getDatasetManifest", + "/datasets/:namespace/:name/versions/:revision/manifest", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + success: Domain.DatasetGetManifestResponse, + error: [ + Errors.InvalidSelectorError, + Errors.InvalidManifestHashError, + Errors.ManifestNotFoundError, + Errors.ManifestRetrievalError, + Errors.ManifestDeserializationError + ] + } +) // ----------------------------------------------------------------------------- // Group Definition @@ -286,32 +362,48 @@ export class DatasetsApiGroup extends HttpApiGroup.make("datasets") // ----------------------------------------------------------------------------- const listOwnedDatasets = HttpApiEndpoint.get( - "listOwnedDatasets" -)`/owners/${datasetOwnerParam}/datasets` - .setUrlParams(Domain.ListOwnedDatasetsParams) - .addSuccess(Domain.ListMyDatasetsResponse) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidDatasetOwnerPathError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) + "listOwnedDatasets", + "/owners/:owner/datasets", + { + params: { + owner: DatasetOwnerParam + }, + query: Domain.ListOwnedDatasetsParams, + success: Domain.ListMyDatasetsResponse, + error: [ + Errors.DatasetConversionError, + Errors.InvalidDatasetOwnerPathError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/vX/owners/{owner}/datasets/search // ----------------------------------------------------------------------------- const searchOwnedDatasets = HttpApiEndpoint.get( - "searchOwnedDatasets" -)`/owners/${datasetOwnerParam}/datasets/search` - .setUrlParams(Domain.SearchOwnedDatasetsParams) - .addSuccess(Domain.DatasetSearchResponse) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidDatasetOwnerPathError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) + "searchOwnedDatasets", + "/owners/:owner/datasets/search", + { + params: { + owner: DatasetOwnerParam + }, + query: Domain.SearchOwnedDatasetsParams, + success: Domain.DatasetSearchResponse, + error: [ + Errors.DatasetConversionError, + Errors.InvalidDatasetOwnerPathError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // Group Definition @@ -334,224 +426,329 @@ export class OwnedDatasetsApiGroup extends HttpApiGroup.make("ownedDatasets") // ----------------------------------------------------------------------------- const listMyDatasets = HttpApiEndpoint.get( - "listMyDatasets" -)`/datasets` - .setUrlParams(Domain.ListOwnedDatasetsParams) - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.AuthUserOwnedDatasetListResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) + "listMyDatasets", + "/datasets", + { + query: Domain.ListOwnedDatasetsParams, + headers: Domain.BearerAuthHeader, + success: Domain.AuthUserOwnedDatasetListResponse, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetConversionError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/vX/owners/@me/datasets/{namespace}/{name} // ----------------------------------------------------------------------------- const getMyDatasetByFqdn = HttpApiEndpoint.get( - "getMyDatasetByFqdn" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}` - .setUrlParams(Domain.GetOwnedDatasetsByFqdnParams) - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.Dataset) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetConversionError) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.InvalidDatasetSelectorError) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetByFqdn", + "/datasets/:namespace/:name", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + headers: Domain.BearerAuthHeader, + success: Domain.Dataset, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetConversionError, + Errors.DatasetNotFoundError, + Errors.InvalidDatasetSelectorError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/counts/by-chain // ----------------------------------------------------------------------------- const getMyDatasetCountsByChain = HttpApiEndpoint.get( - "getMyDatasetCountsByChain" -)`/datasets/counts/by-chain` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetCountsByChainResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetCountsByChain", + "/datasets/counts/by-chain", + { + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetCountsByChainResponse, + error: [ + HttpApiError.Unauthorized, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/counts/by-keyword // ----------------------------------------------------------------------------- const getMyDatasetCountsByKeyword = HttpApiEndpoint.get( - "getMyDatasetCountsByKeyword" -)`/datasets/counts/by-keyword` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetCountsByKeywordResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetCountsByKeyword", + "/datasets/counts/by-keyword", + { + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetCountsByKeywordResponse, + error: [ + HttpApiError.Unauthorized, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/counts/by-last-updated // ----------------------------------------------------------------------------- const getMyDatasetCountsByLastUpdated = HttpApiEndpoint.get( - "getMyDatasetCountsByLastUpdated" -)`/datasets/counts/by-last-updated` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetsCountByLastUpdatedResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetCountsByLastUpdated", + "/datasets/counts/by-last-updated", + { + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetsCountByLastUpdatedResponse, + error: [ + HttpApiError.Unauthorized, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/counts/by-status // ----------------------------------------------------------------------------- const getMyDatasetCountsByStatus = HttpApiEndpoint.get( - "getMyDatasetCountsByStatus" -)`/datasets/counts/by-status` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetCountsByStatusResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetCountsByStatus", + "/datasets/counts/by-status", + { + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetCountsByStatusResponse, + error: [ + HttpApiError.Unauthorized, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/counts/by-visibility // ----------------------------------------------------------------------------- const getMyDatasetCountsByVisibility = HttpApiEndpoint.get( - "getMyDatasetCountsByVisibility" -)`/datasets/counts/by-visibility` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetCountsByVisibilityResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.RegistryDatabaseError) + "getMyDatasetCountsByVisibility", + "/datasets/counts/by-visibility", + { + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetCountsByVisibilityResponse, + error: [ + HttpApiError.Unauthorized, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/vX/owners/@me/datasets/search // ----------------------------------------------------------------------------- const searchMyDatasets = HttpApiEndpoint.get( - "searchMyDatasets" -)`/datasets/search` - .setUrlParams(Domain.SearchOwnedDatasetsParams) - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.DatasetSearchResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidQueryParametersError) - .addError(Errors.LimitInvalidError) - .addError(Errors.LimitTooLargeError) - .addError(Errors.RegistryDatabaseError) + "searchMyDatasets", + "/datasets/search", + { + query: Domain.SearchOwnedDatasetsParams, + headers: Domain.BearerAuthHeader, + success: Domain.DatasetSearchResponse, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetConversionError, + Errors.InvalidQueryParametersError, + Errors.LimitInvalidError, + Errors.LimitTooLargeError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // GET /api/v1/owners/@me/datasets/{namespace}/{name}/versions/{version}/queries // ----------------------------------------------------------------------------- const listMyDatasetQueries = HttpApiEndpoint.get( - "listMyDatasetQueries" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}/queries` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.OwnedDatasetListQueriesResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.InvalidSelectorError) - .addError(Errors.ForbiddenError) - .addError(Errors.RegistryDatabaseError) - .addError(Errors.SavedQueryConversionError) + "listMyDatasetQueries", + "/datasets/:namespace/:name/versions/:revision/queries", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + headers: Domain.BearerAuthHeader, + success: Domain.OwnedDatasetListQueriesResponse, + error: [ + HttpApiError.Unauthorized, + Errors.InvalidSelectorError, + Errors.ForbiddenError, + Errors.RegistryDatabaseError, + Errors.SavedQueryConversionError + ] + } +) // ----------------------------------------------------------------------------- // POST /api/v1/owners/@me/datasets/publish // ----------------------------------------------------------------------------- const publishMyDataset = HttpApiEndpoint.post( - "publishMyDataset" -)`/datasets/publish` - .setHeaders(Domain.BearerAuthHeader) - .setPayload(Domain.InsertDatasetPayload) - .addSuccess(HttpApiSchema.Created) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidManifestError) - .addError(Errors.InvalidRequestBodyError) - .addError(Errors.InvalidNamespaceError) - .addError(Errors.NamespaceAccessDeniedError) - .addError(Errors.RegistryDatabaseError) + "publishMyDataset", + "/datasets/publish", + { + headers: Domain.BearerAuthHeader, + payload: Domain.InsertDatasetPayload, + success: Schema.Void.pipe(HttpApiSchema.status(201)), + error: [ + HttpApiError.Unauthorized, + Errors.DatasetConversionError, + Errors.InvalidManifestError, + Errors.InvalidRequestBodyError, + Errors.InvalidNamespaceError, + Errors.NamespaceAccessDeniedError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // POST /api/v1/owners/@me/datasets/{namespace}/{name}/versions/publish // ----------------------------------------------------------------------------- const publishMyDatasetVersion = HttpApiEndpoint.post( - "publishMyDatasetVersion" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/publish` - .setHeaders(Domain.BearerAuthHeader) - .setPayload(Domain.InsertDatasetVersion) - .addSuccess(HttpApiSchema.Created) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetVersionConversionError) - .addError(Errors.InvalidManifestError) - .addError(Errors.InvalidPathParametersError) - .addError(Errors.InvalidRequestBodyError) - .addError(Errors.NamespaceAccessDeniedError) - .addError(Errors.RegistryDatabaseError) + "publishMyDatasetVersion", + "/datasets/:namespace/:name/versions/publish", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + headers: Domain.BearerAuthHeader, + payload: Domain.InsertDatasetVersion, + success: Schema.Void.pipe(HttpApiSchema.status(201)), + error: [ + HttpApiError.Unauthorized, + Errors.DatasetVersionConversionError, + Errors.InvalidManifestError, + Errors.InvalidPathParametersError, + Errors.InvalidRequestBodyError, + Errors.NamespaceAccessDeniedError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // PUT /api/v1/owners/@me/datasets/{namespace}/{name} // ----------------------------------------------------------------------------- const updateMyDatasetMetadata = HttpApiEndpoint.put( - "updateMyDatasetMetadata" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}` - .setHeaders(Domain.BearerAuthHeader) - .setPayload(Domain.UpdateDatasetMetadataPayload) - .addSuccess(Domain.Dataset) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetConversionError) - .addError(Errors.InvalidPathParametersError) - .addError(Errors.InvalidRequestBodyError) - .addError(Errors.RegistryDatabaseError) + "updateMyDatasetMetadata", + "/datasets/:namespace/:name", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + headers: Domain.BearerAuthHeader, + payload: Domain.UpdateDatasetMetadataPayload, + success: Domain.Dataset, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetConversionError, + Errors.InvalidPathParametersError, + Errors.InvalidRequestBodyError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // PATCH /api/v1/owners/@me/datasets/{namespace}/{name}/visibility // ----------------------------------------------------------------------------- const updateMyDatasetVisibility = HttpApiEndpoint.patch( - "updateMyDatasetVisibility" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/visibility` - .setHeaders(Domain.BearerAuthHeader) - .setPayload(Domain.UpdateDatasetVisibilityPayload) - .addSuccess(Domain.Dataset) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.InvalidPathParametersError) - .addError(Errors.InvalidRequestBodyError) - .addError(Errors.RegistryDatabaseError) + "updateMyDatasetVisibility", + "/datasets/:namespace/:name/visibility", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam + }, + headers: Domain.BearerAuthHeader, + payload: Domain.UpdateDatasetVisibilityPayload, + success: Domain.Dataset, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetNotFoundError, + Errors.InvalidPathParametersError, + Errors.InvalidRequestBodyError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // PATCH /api/v1/owners/@me/datasets/{namespace}/{name}/versions/{version} // ----------------------------------------------------------------------------- const updateMyDatasetVersionStatus = HttpApiEndpoint.patch( - "updateMyDatasetVersionStatus" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}` - .setHeaders(Domain.BearerAuthHeader) - .setPayload(Domain.UpdateDatasetVersionStatusPayload) - .addSuccess(Domain.DatasetVersion) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.DatasetVersionConversionError) - .addError(Errors.InvalidPathParametersError) - .addError(Errors.InvalidRequestBodyError) - .addError(Errors.RegistryDatabaseError) + "updateMyDatasetVersionStatus", + "/datasets/:namespace/:name/versions/:revision", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + headers: Domain.BearerAuthHeader, + payload: Domain.UpdateDatasetVersionStatusPayload, + success: Domain.DatasetVersion, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetNotFoundError, + Errors.DatasetVersionConversionError, + Errors.InvalidPathParametersError, + Errors.InvalidRequestBodyError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // DELETE /api/v1/owners/@me/datasets/{namespace}/{name}/versions/{version} // ----------------------------------------------------------------------------- -const archiveMyDatasetVersion = HttpApiEndpoint.del( - "archiveMyDatasetVersion" -)`/datasets/${datasetNamespaceParam}/${datasetNameParam}/versions/${datasetRevisionParam}` - .setHeaders(Domain.BearerAuthHeader) - .addSuccess(Domain.ArchiveDatasetVersionResponse) - .addError(HttpApiError.Unauthorized) - .addError(Errors.DatasetNotFoundError) - .addError(Errors.InvalidPathParametersError) - .addError(Errors.RegistryDatabaseError) +const archiveMyDatasetVersion = HttpApiEndpoint.delete( + "archiveMyDatasetVersion", + "/datasets/:namespace/:name/versions/:revision", + { + params: { + namespace: DatasetNamespaceParam, + name: DatasetNameParam, + revision: DatasetRevisionParam + }, + headers: Domain.BearerAuthHeader, + success: Domain.ArchiveDatasetVersionResponse, + error: [ + HttpApiError.Unauthorized, + Errors.DatasetNotFoundError, + Errors.InvalidPathParametersError, + Errors.RegistryDatabaseError + ] + } +) // ----------------------------------------------------------------------------- // Group Definition diff --git a/packages/amp/src/registry/domain.ts b/packages/amp/src/registry/domain.ts index df0cae2..8d50827 100644 --- a/packages/amp/src/registry/domain.ts +++ b/packages/amp/src/registry/domain.ts @@ -9,45 +9,45 @@ import * as Models from "../core/domain.ts" // General Schemas // ============================================================================= -export const PositiveIntFromString = Schema.NumberFromString.pipe( - Schema.int(), - Schema.positive() -).annotations({ identifier: "PositiveIntFromString" }) +export const PositiveIntFromString = Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThan(0) +).annotate({ identifier: "PositiveIntFromString" }) export type PositiveIntFromString = typeof PositiveIntFromString.Type /** * Represents a service status. */ export const ServiceStatus = Schema.Struct({ - "error": Schema.optionalWith(Schema.String, { nullable: true }), + "error": Schema.optional(Schema.NullOr(Schema.String)), "status": Schema.String -}).annotations({ identifier: "ServiceStatus" }) +}).annotate({ identifier: "ServiceStatus" }) export type ServiceStatus = typeof ServiceStatus.Type /** * Time-based buckets for grouping datasets by last updated time */ -export const LastUpdatedBucket = Schema.Literal( +export const LastUpdatedBucket = Schema.Literals([ "last_day", "last_week", "last_month", "last_year" -).annotations({ identifier: "LastUpdatedBucket" }) +]).annotate({ identifier: "LastUpdatedBucket" }) export type LastUpdatedBucket = typeof LastUpdatedBucket.Type -export const DatasetSortBy = Schema.Literal( +export const DatasetSortBy = Schema.Literals([ "namespace", "name", "owner", "created_at", "updated_at" -).annotations({ identifier: "DatasetSortBy" }) +]).annotate({ identifier: "DatasetSortBy" }) export type DatasetSortBy = typeof DatasetSortBy.Type -export const DatasetSortDirection = Schema.Literal( +export const DatasetSortDirection = Schema.Literals([ "asc", "desc" -).annotations({ identifier: "DatasetSortDirection" }) +]).annotate({ identifier: "DatasetSortDirection" }) export type DatasetSortDirection = typeof DatasetSortDirection.Type /** @@ -60,18 +60,18 @@ export const DatasetVersionAncestry = Schema.Struct({ * dependencies. */ "dataset_reference": Models.DatasetReferenceFromString -}).annotations({ identifier: "DatasetVersionAncestry" }) +}).annotate({ identifier: "DatasetVersionAncestry" }) export type DatasetVersionAncestry = typeof DatasetVersionAncestry.Type /** * Represents the status of a dataset version. */ -export const DatasetVersionStatus = Schema.Literal( +export const DatasetVersionStatus = Schema.Literals([ "draft", "published", "deprecated", "archived" -).annotations({ identifier: "DatasetVersionStatus" }) +]).annotate({ identifier: "DatasetVersionStatus" }) export type DatasetVersionStatus = typeof DatasetVersionStatus.Type /** @@ -82,13 +82,13 @@ export const DatasetVersion = Schema.Struct({ * Array of ancestor DatasetVersion references that this version extends from * (version-pinned dependencies). */ - "ancestors": Schema.optionalWith(Schema.Array(DatasetVersionAncestry), { nullable: true }), + "ancestors": Schema.optional(Schema.NullOr(Schema.Array(DatasetVersionAncestry))), /** * A description of what changed with this version. Allows developers of the * Dataset to communicate to downstream consumers what has changed with this * version from previous versions. Migration guides, etc. */ - "changelog": Schema.optionalWith(Schema.String, { nullable: true }), + "changelog": Schema.optional(Schema.NullOr(Schema.String)), /** * Timestamp when the DatasetVersion record was created (immutable). */ @@ -100,13 +100,13 @@ export const DatasetVersion = Schema.Struct({ /** * Array of descendant DatasetVersion references that extend from this version. */ - "descendants": Schema.optionalWith(Schema.Array(DatasetVersionAncestry), { nullable: true }), + "descendants": Schema.optional(Schema.NullOr(Schema.Array(DatasetVersionAncestry))), "status": DatasetVersionStatus, /** * The published version tag. This is basically the version label. Can be semver, a commit hash, or 'latest'. */ "version_tag": Models.DatasetRevision -}).annotations({ identifier: "DatasetVersion" }) +}).annotate({ identifier: "DatasetVersion" }) export type DatasetVersion = typeof DatasetVersion.Type /** @@ -132,11 +132,11 @@ export const Dataset = Schema.Struct({ /** * Computed link to the latest DatasetVersion reference in PURL format. */ - "dataset_reference": Schema.optionalWith(Models.DatasetReferenceFromString, { nullable: true }), + "dataset_reference": Schema.optional(Schema.NullOr(Models.DatasetReferenceFromString)), /** * Description of the dataset, its intended use, and purpose. */ - "description": Schema.optionalWith(Models.DatasetDescription, { nullable: true }), + "description": Schema.optional(Schema.NullOr(Models.DatasetDescription)), /** * Chains being indexed by the Dataset. Used for discovery by chain. */ @@ -144,12 +144,12 @@ export const Dataset = Schema.Struct({ /** * User-defined or derived keywords defining the usage of the dataset. */ - "keywords": Schema.optionalWith(Schema.Array(Models.DatasetKeyword), { nullable: true }), - "latest_version": Schema.optionalWith(DatasetVersion, { nullable: true }), + "keywords": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetKeyword))), + "latest_version": Schema.optional(Schema.NullOr(DatasetVersion)), /** * Usage license covering the Dataset. */ - "license": Schema.optionalWith(Models.DatasetLicense, { nullable: true }), + "license": Schema.optional(Schema.NullOr(Models.DatasetLicense)), /** * Owner of the Dataset. Can be an organization or user 0x address. */ @@ -157,16 +157,16 @@ export const Dataset = Schema.Struct({ /** * User-defined README for the Dataset providing usage examples and documentation. */ - "readme": Schema.optionalWith(Models.DatasetReadme, { nullable: true }), + "readme": Schema.optional(Schema.NullOr(Models.DatasetReadme)), /** * VCS repository URL containing the Dataset source code. */ - "repository_url": Schema.optionalWith(Models.DatasetRepository, { nullable: true }), + "repository_url": Schema.optional(Schema.NullOr(Models.DatasetRepository)), /** * Source of data being materialized by the Dataset (e.g., contract addresses, * logs, transactions). */ - "source": Schema.optionalWith(Schema.Array(Models.DatasetSource), { nullable: true }), + "source": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetSource))), /** * Timestamp when the Dataset record was last updated. */ @@ -174,9 +174,9 @@ export const Dataset = Schema.Struct({ /** * Link to all DatasetVersion records that this Dataset is a parent of. */ - "versions": Schema.optionalWith(Schema.Array(DatasetVersion), { nullable: true }), + "versions": Schema.optional(Schema.NullOr(Schema.Array(DatasetVersion))), "visibility": Models.DatasetVisibility -}).annotations({ identifier: "Dataset" }) +}).annotate({ identifier: "Dataset" }) export type Dataset = typeof Dataset.Type /** @@ -188,7 +188,7 @@ export const DatasetWithScore = Schema.Struct({ * Weighted relevance score indicating how well this dataset matches the search query. Higher scores indicate better relevance. Score is calculated based on matches in description, keywords, source, and indexing chains fields. */ "score": Schema.Number -}).annotations({ identifier: "DatasetWithScore" }) +}).annotate({ identifier: "DatasetWithScore" }) export type DatasetWithScore = typeof DatasetWithScore.Type /** @@ -205,7 +205,7 @@ export const DatasetCountByChain = Schema.Struct({ * The count of Dataset records indexing this chain */ "count": Schema.Int -}).annotations({ identifier: "DatasetCountByChain" }) +}).annotate({ identifier: "DatasetCountByChain" }) export type DatasetCountByChain = typeof DatasetCountByChain.Type /** @@ -222,7 +222,7 @@ export const DatasetCountByKeyword = Schema.Struct({ * The keyword (e.g., "DeFi", "NFT", "logs") */ "keyword": Schema.String -}).annotations({ identifier: "DatasetCountByKeyword" }) +}).annotate({ identifier: "DatasetCountByKeyword" }) export type DatasetCountByKeyword = typeof DatasetCountByKeyword.Type /** @@ -240,7 +240,7 @@ export const DatasetCountByLastUpdated = Schema.Struct({ * The count of Dataset records updated within this time period */ "count": Schema.Int -}).annotations({ identifier: "DatasetCountByLastUpdatedBucket" }) +}).annotate({ identifier: "DatasetCountByLastUpdatedBucket" }) export type DatasetCountByLastUpdated = typeof DatasetCountByLastUpdated.Type /** @@ -257,7 +257,7 @@ export const DatasetCountByStatus = Schema.Struct({ * The version status (Draft, Published, Deprecated, or Archived) */ "status": DatasetVersionStatus -}).annotations({ identifier: "DatasetCountByStatus" }) +}).annotate({ identifier: "DatasetCountByStatus" }) export type DatasetCountByStatus = typeof DatasetCountByStatus.Type /** @@ -274,7 +274,7 @@ export const DatasetCountByVisibility = Schema.Struct({ * The visibility (Public or Private) */ "visibility": Models.DatasetVisibility -}).annotations({ identifier: "DatasetCountByVisibility" }) +}).annotate({ identifier: "DatasetCountByVisibility" }) export type DatasetCountByVisibility = typeof DatasetCountByVisibility.Type /** @@ -292,12 +292,12 @@ export const SavedQuery = Schema.Struct({ /** * Optional description of what the query does */ - "description": Schema.optionalWith(Schema.String, { nullable: true }), + "description": Schema.optional(Schema.NullOr(Schema.String)), /** * Unique identifier for the saved query (UUID) */ - "id": Schema.String.pipe( - Schema.pattern(new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) + "id": Schema.String.check( + Schema.isPattern(new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) ), /** * Name of the saved query @@ -312,7 +312,7 @@ export const SavedQuery = Schema.Struct({ */ "updated_at": Schema.String, "visibility": Models.DatasetVisibility -}).annotations({ identifier: "SavedQuery" }) +}).annotate({ identifier: "SavedQuery" }) export type SavedQuery = typeof SavedQuery.Type // ============================================================================= @@ -330,7 +330,7 @@ export const ListDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "ListDatasetsParams" }) +}).annotate({ identifier: "ListDatasetsParams" }) export type ListDatasetsParams = typeof ListDatasetsParams.Type /** @@ -343,7 +343,7 @@ export const SearchDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "SearchDatasetsParams" }) +}).annotate({ identifier: "SearchDatasetsParams" }) export type SearchDatasetsParams = typeof SearchDatasetsParams.Type /** @@ -351,7 +351,7 @@ export type SearchDatasetsParams = typeof SearchDatasetsParams.Type */ export const AiSearchDatasetsParams = Schema.Struct({ "search": Schema.String -}).annotations({ identifier: "AiSearchDatasetsParams" }) +}).annotate({ identifier: "AiSearchDatasetsParams" }) export type AiSearchDatasetsParams = typeof AiSearchDatasetsParams.Type /** @@ -365,7 +365,7 @@ export const ListOwnedDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "ListOwnedDatasetsParams" }) +}).annotate({ identifier: "ListOwnedDatasetsParams" }) export type ListOwnedDatasetsParams = typeof ListOwnedDatasetsParams.Type /** @@ -374,7 +374,7 @@ export type ListOwnedDatasetsParams = typeof ListOwnedDatasetsParams.Type export const GetOwnedDatasetsByFqdnParams = Schema.Struct({ "namespace": Models.DatasetNamespace, "name": Models.DatasetName -}).annotations({ identifier: "GetOwnedDatasetsByFqdnParams" }) +}).annotate({ identifier: "GetOwnedDatasetsByFqdnParams" }) export type GetOwnedDatasetsByFqdnParams = typeof GetOwnedDatasetsByFqdnParams.Type /** @@ -387,7 +387,7 @@ export const SearchOwnedDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "SearchOwnedDatasetsParams" }) +}).annotate({ identifier: "SearchOwnedDatasetsParams" }) export type SearchOwnedDatasetsParams = typeof SearchOwnedDatasetsParams.Type /** @@ -401,7 +401,7 @@ export const ListMyDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "ListMyDatasetsParams" }) +}).annotate({ identifier: "ListMyDatasetsParams" }) export type ListMyDatasetsParams = typeof ListMyDatasetsParams.Type /** @@ -414,7 +414,7 @@ export const SearchMyDatasetsParams = Schema.Struct({ "indexing_chains": Schema.optional(Schema.StringFromUriComponent), "keywords": Schema.optional(Schema.StringFromUriComponent), "last_updated": Schema.optional(LastUpdatedBucket) -}).annotations({ identifier: "SearchMyDatasetsParams" }) +}).annotate({ identifier: "SearchMyDatasetsParams" }) export type SearchMyDatasetsParams = typeof SearchMyDatasetsParams.Type // ============================================================================= @@ -425,10 +425,10 @@ export type SearchMyDatasetsParams = typeof SearchMyDatasetsParams.Type * Represents a bearer token header. */ export const BearerAuthHeader = Schema.Struct({ - Authorization: Schema.String.pipe( - Schema.startsWith("Bearer") + Authorization: Schema.String.check( + Schema.isStartsWith("Bearer") ) -}).annotations({ identifier: "BearerAuthHeader" }) +}).annotate({ identifier: "BearerAuthHeader" }) export type BearerAuthHeader = typeof BearerAuthHeader.Type // ============================================================================= @@ -442,18 +442,18 @@ export const InsertDatasetVersion = Schema.Struct({ /** * Optional changelog describing what changed in this version. */ - "changelog": Schema.optionalWith(Schema.String, { nullable: true }), + "changelog": Schema.optional(Schema.NullOr(Schema.String)), "kind": Models.DatasetKind, /** * Manifest JSON content. This should be a valid datasets_derived::Manifest structure. The SHA256 hash will be calculated server-side. */ - "manifest": Schema.Record({ key: Schema.String, value: Schema.Unknown }), + "manifest": Schema.Record(Schema.String, Schema.Unknown), "status": DatasetVersionStatus, /** * Version tag (e.g., '1.0.0', 'latest', '8e0acc0'). Pattern: lowercase, numbers, dots, underscores, hyphens. */ "version_tag": Models.DatasetVersion -}).annotations({ identifier: "InsertDatasetVersion" }) +}).annotate({ identifier: "InsertDatasetVersion" }) export type InsertDatasetVersion = typeof InsertDatasetVersion.Type /** @@ -465,7 +465,7 @@ export const InsertDatasetPayload = Schema.Struct({ /** * Description of the dataset, its intended use, and purpose. */ - "description": Schema.optionalWith(Models.DatasetDescription, { nullable: true }), + "description": Schema.optional(Schema.NullOr(Models.DatasetDescription)), /** * Chains being indexed by the Dataset. Used for discovery by chain. */ @@ -473,11 +473,11 @@ export const InsertDatasetPayload = Schema.Struct({ /** * User-defined keywords defining the usage of the dataset. */ - "keywords": Schema.optionalWith(Schema.Array(Models.DatasetKeyword), { nullable: true }), + "keywords": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetKeyword))), /** * Usage license covering the Dataset. */ - "license": Schema.optionalWith(Models.DatasetLicense, { nullable: true }), + "license": Schema.optional(Schema.NullOr(Models.DatasetLicense)), /** * The dataset name. Pattern: lowercase, alphanumeric with underscores, cannot start with a number. */ @@ -489,18 +489,18 @@ export const InsertDatasetPayload = Schema.Struct({ /** * User-defined README for the Dataset providing usage examples and documentation. */ - "readme": Schema.optionalWith(Models.DatasetReadme, { nullable: true }), + "readme": Schema.optional(Schema.NullOr(Models.DatasetReadme)), /** * VCS repository URL containing the Dataset source code. */ - "repository_url": Schema.optionalWith(Models.DatasetRepository, { nullable: true }), + "repository_url": Schema.optional(Schema.NullOr(Models.DatasetRepository)), /** * Source of data being materialized by the Dataset (e.g., contract addresses). */ - "source": Schema.optionalWith(Schema.Array(Models.DatasetSource), { nullable: true }), + "source": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetSource))), "version": InsertDatasetVersion, "visibility": Models.DatasetVisibility -}).annotations({ identifier: "InsertDataset" }) +}).annotate({ identifier: "InsertDataset" }) export type InsertDatasetPayload = typeof InsertDatasetPayload.Type /** @@ -516,7 +516,7 @@ export const UpdateDatasetMetadataPayload = Schema.Struct({ /** * Dataset description */ - "description": Schema.optionalWith(Models.DatasetDescription, { nullable: true }), + "description": Schema.optional(Schema.NullOr(Models.DatasetDescription)), /** * Chains being indexed by the dataset */ @@ -524,24 +524,24 @@ export const UpdateDatasetMetadataPayload = Schema.Struct({ /** * Keywords for dataset discovery */ - "keywords": Schema.optionalWith(Schema.Array(Models.DatasetKeyword), { nullable: true }), + "keywords": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetKeyword))), /** * License covering the dataset */ - "license": Schema.optionalWith(Models.DatasetLicense, { nullable: true }), + "license": Schema.optional(Schema.NullOr(Models.DatasetLicense)), /** * User-defined README for the dataset */ - "readme": Schema.optionalWith(Models.DatasetReadme, { nullable: true }), + "readme": Schema.optional(Schema.NullOr(Models.DatasetReadme)), /** * VCS repository URL */ - "repository_url": Schema.optionalWith(Models.DatasetRepository, { nullable: true }), + "repository_url": Schema.optional(Schema.NullOr(Models.DatasetRepository)), /** * Source of data being materialized */ - "source": Schema.optionalWith(Schema.Array(Models.DatasetSource), { nullable: true }) -}).annotations({ identifier: "UpdateDatasetMetadataPayload" }) + "source": Schema.optional(Schema.NullOr(Schema.Array(Models.DatasetSource))) +}).annotate({ identifier: "UpdateDatasetMetadataPayload" }) export type UpdateDatasetMetadataPayload = typeof UpdateDatasetMetadataPayload.Type /** @@ -553,7 +553,7 @@ export const UpdateDatasetVersionStatusPayload = Schema.Struct({ * Note: Use the DELETE endpoint to archive a version */ "status": DatasetVersionStatus -}).annotations({ identifier: "UpdateDatasetVersionStatusPayload" }) +}).annotate({ identifier: "UpdateDatasetVersionStatusPayload" }) export type UpdateDatasetVersionStatusPayload = typeof UpdateDatasetVersionStatusPayload.Type /** @@ -564,7 +564,7 @@ export const UpdateDatasetVisibilityPayload = Schema.Struct({ * The new visibility level for the dataset */ "visibility": Models.DatasetVisibility -}).annotations({ identifier: "UpdateDatasetVisibilityPayload" }) +}).annotate({ identifier: "UpdateDatasetVisibilityPayload" }) export type UpdateDatasetVisibilityPayload = typeof UpdateDatasetVisibilityPayload.Type // ============================================================================= @@ -577,7 +577,7 @@ export type UpdateDatasetVisibilityPayload = typeof UpdateDatasetVisibilityPaylo export const HealthcheckResponse = Schema.Struct({ "status": Schema.String, "version": Schema.String -}).annotations({ identifier: "HealthcheckResponse" }) +}).annotate({ identifier: "HealthcheckResponse" }) export type HealthcheckResponse = typeof HealthcheckResponse.Type /** @@ -596,13 +596,13 @@ export const DatasetListResponse = Schema.Struct({ * Total number of datasets matching the query filters */ "total_count": Schema.Int -}).annotations({ identifier: "DatasetListResponse" }) +}).annotate({ identifier: "DatasetListResponse" }) export type DatasetListResponse = typeof DatasetListResponse.Type /** * Response for datasets count by chain. */ -export const DatasetCountsByChainResponse = Schema.Array(DatasetCountByChain).annotations({ +export const DatasetCountsByChainResponse = Schema.Array(DatasetCountByChain).annotate({ identifier: "DatasetCountsByChainResponse" }) export type DatasetCountsByChainResponse = typeof DatasetCountsByChainResponse.Type @@ -610,7 +610,7 @@ export type DatasetCountsByChainResponse = typeof DatasetCountsByChainResponse.T /** * Response for datasets count by keyword. */ -export const DatasetCountsByKeywordResponse = Schema.Array(DatasetCountByKeyword).annotations({ +export const DatasetCountsByKeywordResponse = Schema.Array(DatasetCountByKeyword).annotate({ identifier: "DatasetCountsByKeywordResponse" }) export type DatasetCountsByKeywordResponse = typeof DatasetCountsByKeywordResponse.Type @@ -618,7 +618,7 @@ export type DatasetCountsByKeywordResponse = typeof DatasetCountsByKeywordRespon /** * Response for datasets count by last updated. */ -export const DatasetCountsByLastUpdatedResponse = Schema.Array(DatasetCountByLastUpdated).annotations({ +export const DatasetCountsByLastUpdatedResponse = Schema.Array(DatasetCountByLastUpdated).annotate({ identifier: "DatasetCountsByLastUpdatedResponse" }) export type DatasetCountsByLastUpdatedResponse = typeof DatasetCountsByLastUpdatedResponse.Type @@ -639,13 +639,13 @@ export const DatasetSearchResponse = Schema.Struct({ * Total number of datasets matching the query filters */ "total_count": Schema.Int -}).annotations({ identifier: "DatasetSearchResponse" }) +}).annotate({ identifier: "DatasetSearchResponse" }) export type DatasetSearchResponse = typeof DatasetSearchResponse.Type /** * Response for AI search of datasets. */ -export const DatasetAiSearchResponse = Schema.Array(DatasetWithScore).annotations({ +export const DatasetAiSearchResponse = Schema.Array(DatasetWithScore).annotate({ identifier: "DatasetAiSearchResponse" }) export type DatasetAiSearchResponse = typeof DatasetAiSearchResponse.Type @@ -653,7 +653,7 @@ export type DatasetAiSearchResponse = typeof DatasetAiSearchResponse.Type /** * Response for listing dataset versions. */ -export const DatasetListVersionsResponse = Schema.Array(DatasetVersion).annotations({ +export const DatasetListVersionsResponse = Schema.Array(DatasetVersion).annotate({ identifier: "DatasetListVersionsResponse" }) export type DatasetListVersionsResponse = typeof DatasetListVersionsResponse.Type @@ -661,7 +661,7 @@ export type DatasetListVersionsResponse = typeof DatasetListVersionsResponse.Typ /** * Response for getting latest manifest. */ -export const DatasetGetLatestManifestResponse = Schema.String.annotations({ +export const DatasetGetLatestManifestResponse = Schema.String.annotate({ identifier: "DatasetGetLatestManifestResponse" }) export type DatasetGetLatestManifestResponse = typeof DatasetGetLatestManifestResponse.Type @@ -669,7 +669,7 @@ export type DatasetGetLatestManifestResponse = typeof DatasetGetLatestManifestRe /** * Response for getting a manifest. */ -export const DatasetGetManifestResponse = Schema.String.annotations({ +export const DatasetGetManifestResponse = Schema.String.annotate({ identifier: "DatasetGetManifestResponse" }) export type DatasetGetManifestResponse = typeof DatasetGetManifestResponse.Type @@ -677,7 +677,7 @@ export type DatasetGetManifestResponse = typeof DatasetGetManifestResponse.Type /** * Response for listing latest queries. */ -export const DatasetListLatestQueriesResponse = Schema.Array(SavedQuery).annotations({ +export const DatasetListLatestQueriesResponse = Schema.Array(SavedQuery).annotate({ identifier: "DatasetListLatestQueriesResponse" }) export type DatasetsListLatestQueries = typeof DatasetListLatestQueriesResponse.Type @@ -685,7 +685,7 @@ export type DatasetsListLatestQueries = typeof DatasetListLatestQueriesResponse. /** * Response for listing queries. */ -export const DatasetListQueriesResponse = Schema.Array(SavedQuery).annotations({ +export const DatasetListQueriesResponse = Schema.Array(SavedQuery).annotate({ identifier: "DatasetListQueriesResponse" }) export type DatasetListQueriesResponse = typeof DatasetListQueriesResponse.Type @@ -706,13 +706,13 @@ export const AuthUserOwnedDatasetListResponse = Schema.Struct({ * Total number of datasets matching the query filters */ "total_count": Schema.Int -}).annotations({ identifier: "AuthUserOwnedDatasetListResponse" }) +}).annotate({ identifier: "AuthUserOwnedDatasetListResponse" }) export type AuthUserOwnedDatasetListResponse = typeof AuthUserOwnedDatasetListResponse.Type /** * Response for owned datasets count by chain. */ -export const OwnedDatasetCountsByChainResponse = Schema.Array(DatasetCountByChain).annotations({ +export const OwnedDatasetCountsByChainResponse = Schema.Array(DatasetCountByChain).annotate({ identifier: "OwnedDatasetCountsByChainResponse" }) export type OwnedDatasetCountsByChainResponse = typeof OwnedDatasetCountsByChainResponse.Type @@ -720,7 +720,7 @@ export type OwnedDatasetCountsByChainResponse = typeof OwnedDatasetCountsByChain /** * Response for owned datasets count by keyword. */ -export const OwnedDatasetCountsByKeywordResponse = Schema.Array(DatasetCountByKeyword).annotations({ +export const OwnedDatasetCountsByKeywordResponse = Schema.Array(DatasetCountByKeyword).annotate({ identifier: "OwnedDatasetCountsByKeywordResponse" }) export type OwnedDatasetCountsByKeywordResponse = typeof OwnedDatasetCountsByKeywordResponse.Type @@ -728,7 +728,7 @@ export type OwnedDatasetCountsByKeywordResponse = typeof OwnedDatasetCountsByKey /** * Response for owned datasets count by last updated. */ -export const OwnedDatasetsCountByLastUpdatedResponse = Schema.Array(DatasetCountByLastUpdated).annotations({ +export const OwnedDatasetsCountByLastUpdatedResponse = Schema.Array(DatasetCountByLastUpdated).annotate({ identifier: "OwnedDatasetsCountByLastUpdatedResponse" }) export type OwnedDatasetsCountByLastUpdatedResponse = typeof OwnedDatasetsCountByLastUpdatedResponse.Type @@ -736,7 +736,7 @@ export type OwnedDatasetsCountByLastUpdatedResponse = typeof OwnedDatasetsCountB /** * Response for owned datasets count by status. */ -export const OwnedDatasetCountsByStatusResponse = Schema.Array(DatasetCountByStatus).annotations({ +export const OwnedDatasetCountsByStatusResponse = Schema.Array(DatasetCountByStatus).annotate({ identifier: "OwnedDatasetCountsByStatusResponse" }) export type OwnedDatasetCountsByStatusResponse = typeof OwnedDatasetCountsByStatusResponse.Type @@ -744,7 +744,7 @@ export type OwnedDatasetCountsByStatusResponse = typeof OwnedDatasetCountsByStat /** * Response for owned datasets count by visibility. */ -export const OwnedDatasetCountsByVisibilityResponse = Schema.Array(DatasetCountByVisibility).annotations({ +export const OwnedDatasetCountsByVisibilityResponse = Schema.Array(DatasetCountByVisibility).annotate({ identifier: "wnedDatasetCountsByVisibilityResponse" }) export type OwnedDatasetCountsByVisibilityResponse = typeof OwnedDatasetCountsByVisibilityResponse.Type @@ -757,13 +757,13 @@ export const ArchiveDatasetVersionResponse = Schema.Struct({ * The reference of the archived dataset version */ "reference": Schema.String -}).annotations({ identifier: "ArchiveDatasetVersionResponse" }) +}).annotate({ identifier: "ArchiveDatasetVersionResponse" }) export type ArchiveDatasetVersionResponse = typeof ArchiveDatasetVersionResponse.Type /** * Response for listing owned queries. */ -export const OwnedDatasetListQueriesResponse = Schema.Array(SavedQuery).annotations({ +export const OwnedDatasetListQueriesResponse = Schema.Array(SavedQuery).annotate({ identifier: "OwnedDatasetListQueriesResponse" }) export type OwnedDatasetListQueriesResponse = typeof OwnedDatasetListQueriesResponse.Type @@ -784,7 +784,7 @@ export const ListMyDatasetsResponse = Schema.Struct({ * Total number of datasets matching the query filters */ "total_count": Schema.Int -}).annotations({ identifier: "ListMyDatasetsResponse" }) +}).annotate({ identifier: "ListMyDatasetsResponse" }) export type ListMyDatasetsResponse = typeof ListMyDatasetsResponse.Type /** @@ -792,7 +792,7 @@ export type ListMyDatasetsResponse = typeof ListMyDatasetsResponse.Type */ export const LivenessResponse = Schema.Struct({ "status": Schema.String -}).annotations({ identifier: "LivenessResponse" }) +}).annotate({ identifier: "LivenessResponse" }) export type LivenessResponse = typeof LivenessResponse.Type /** @@ -800,7 +800,7 @@ export type LivenessResponse = typeof LivenessResponse.Type */ export const ReadinessChecks = Schema.Struct({ "database": ServiceStatus -}).annotations({ identifier: "ReadinessChecks" }) +}).annotate({ identifier: "ReadinessChecks" }) export type ReadinessChecks = typeof ReadinessChecks.Type /** @@ -809,5 +809,5 @@ export type ReadinessChecks = typeof ReadinessChecks.Type export const ReadinessResponse = Schema.Struct({ "checks": ReadinessChecks, "status": Schema.String -}).annotations({ identifier: "ReadinessResponse" }) +}).annotate({ identifier: "ReadinessResponse" }) export type ReadinessResponse = typeof ReadinessResponse.Type diff --git a/packages/amp/src/registry/error.ts b/packages/amp/src/registry/error.ts index 42c9b62..b4f6710 100644 --- a/packages/amp/src/registry/error.ts +++ b/packages/amp/src/registry/error.ts @@ -19,218 +19,250 @@ * } * ``` */ -import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" -/** - * Machine-readable error code in SCREAMING_SNAKE_CASE format - * - * Error codes are stable across API versions and should be used - * for programmatic error handling. Examples: `INVALID_SELECTOR`, - * `DATASET_NOT_FOUND`, `REGISTRY_DB_ERROR` - */ -const ErrorCode = ( - code: Code -): Schema.PropertySignature<":", Code, "error_code", ":", Code> => - Schema.Literal(code).pipe( - Schema.propertySignature, - Schema.fromKey("error_code") - ) - -const BaseErrorFields = { - /** - * Human-readable error message - * - * Messages provide detailed context about the error but may change - * over time. Use `error_code` for programmatic decisions. - */ - message: Schema.String.pipe( - Schema.propertySignature, - Schema.fromKey("error_message") - ), - /** - * Request ID for tracing and correlation - * - * This ID can be used to correlate error responses with server logs - * for debugging and support purposes. The ID is generated per-request - * and appears in both logs and error responses. - */ - requestId: Schema.optional(Schema.String).pipe( - Schema.fromKey("request_id") - ) -} - -export class DatasetConversionError extends Schema.Class( - "Amp/RegistryApi/DatasetConversionError" -)({ - ...BaseErrorFields, - code: ErrorCode("DATASET_CONVERSION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class DatasetNotFoundError extends Schema.Class( - "Amp/RegistryApi/DatasetNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("DATASET_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} - -export class DatasetVersionConversionError extends Schema.Class( - "Amp/RegistryApi/DatasetVersionConversionError" -)({ - ...BaseErrorFields, - code: ErrorCode("DATASET_VERSION_CONVERSION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class DatasetVersionNotFoundError extends Schema.Class( - "Amp/RegistryApi/DatasetVersionNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("VERSION_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} - -export class ForbiddenError extends Schema.Class( - "Amp/RegistryApi/ForbiddenError" -)({ - ...BaseErrorFields, - code: ErrorCode("FORBIDDEN") -}, HttpApiSchema.annotations({ status: 403 })) {} - -export class InvalidDatasetOwnerPathError extends Schema.Class( - "Amp/RegistryApi/InvalidDatasetOwnerPathError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_DATASET_OWNER_PATH") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidDatasetReferenceError extends Schema.Class( - "Amp/RegistryApi/InvalidDatasetReferenceError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_REFERENCE") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class InvalidDatasetSelectorError extends Schema.Class( - "Amp/RegistryApi/InvalidDatasetSelectorError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_DATASET_SELECTOR") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidManifestError extends Schema.Class( - "Amp/RegistryApi/InvalidManifestError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_MANIFEST") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidManifestHashError extends Schema.Class( - "Amp/RegistryApi/InvalidManifestHashError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_MANIFEST_HASH") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class InvalidNamespaceError extends Schema.Class( - "Amp/RegistryApi/InvalidNamespaceError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_NAMESPACE") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidQueryParametersError extends Schema.Class( - "Amp/RegistryApi/InvalidQueryParametersError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_QUERY_PARAMETERS") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidPathParametersError extends Schema.Class( - "Amp/RegistryApi/InvalidPathParametersError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_PATH_PARAMETERS") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidRequestBodyError extends Schema.Class( - "Amp/RegistryApi/InvalidRequestBodyError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_REQUEST_BODY") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class InvalidSelectorError extends Schema.Class( - "Amp/RegistryApi/InvalidSelectorError" -)({ - ...BaseErrorFields, - code: ErrorCode("INVALID_SELECTOR") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class LatestDatasetVersionNotFoundError extends Schema.Class( - "Amp/RegistryApi/LatestDatasetVersionNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("LATEST_VERSION_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} - -export class LimitInvalidError extends Schema.Class( - "Amp/RegistryApi/LimitInvalidError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIMIT_INVALID") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class LimitTooLargeError extends Schema.Class( - "Amp/RegistryApi/LimitTooLargeError" -)({ - ...BaseErrorFields, - code: ErrorCode("LIMIT_TOO_LARGE") -}, HttpApiSchema.annotations({ status: 400 })) {} - -export class ManifestDeserializationError extends Schema.Class( - "Amp/RegistryApi/ManifestDeserializationError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_DESERIALIZATION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class ManifestNotFoundError extends Schema.Class( - "Amp/RegistryApi/ManifestNotFoundError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_NOT_FOUND") -}, HttpApiSchema.annotations({ status: 404 })) {} - -export class ManifestRetrievalError extends Schema.Class( - "Amp/RegistryApi/ManifestRetrievalError" -)({ - ...BaseErrorFields, - code: ErrorCode("MANIFEST_RETRIEVAL_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class NamespaceAccessDeniedError extends Schema.Class( - "Amp/RegistryApi/NamespaceAccessDeniedError" -)({ - ...BaseErrorFields, - code: ErrorCode("NAMESPACE_ACCESS_DENIED") -}, HttpApiSchema.annotations({ status: 403 })) {} - -export class RegistryDatabaseError extends Schema.Class( - "Amp/RegistryApi/RegistryDatabaseError" -)({ - ...BaseErrorFields, - code: ErrorCode("AMP_REGISTRY_DB_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class SavedQueryConversionError extends Schema.Class( - "Amp/RegistryApi/SavedQueryConversionError" -)({ - ...BaseErrorFields, - code: ErrorCode("SAVED_QUERY_CONVERSION_ERROR") -}, HttpApiSchema.annotations({ status: 500 })) {} - -export class ServiceUnavailableError extends Schema.Class( - "Amp/RegistryApi/ServiceUnavailableError" -)({ - ...BaseErrorFields, - code: ErrorCode("SERVICE_UNAVAILABLE") -}, HttpApiSchema.annotations({ status: 503 })) {} +export const makeError = < + const Code extends string, + const Tag extends string, + const Fields extends Schema.Struct.Fields = {} +>(code: Code, tag: Tag, fields?: Fields): Schema.encodeKeys< + Schema.Struct< + Fields & { + readonly _tag: Schema.withDecodingDefaultKey> + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `REGISTRY_DB_ERROR` + */ + readonly code: Schema.withConstructorDefault> + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + readonly message: Schema.String + /** + * Request ID for tracing and correlation + * + * This ID can be used to correlate error responses with server logs + * for debugging and support purposes. The ID is generated per-request + * and appears in both logs and error responses. + */ + readonly requestId: Schema.optional + } + >, + { + readonly code: "error_code" + readonly message: "error_message" + readonly requestId: "request_id" + } +> => + Schema.Struct({ + ...fields, + _tag: Schema.tagDefaultOmit(tag), + /** + * Machine-readable error code in SCREAMING_SNAKE_CASE format + * + * Error codes are stable across API versions and should be used + * for programmatic error handling. Examples: `INVALID_SELECTOR`, + * `DATASET_NOT_FOUND`, `REGISTRY_DB_ERROR` + */ + code: Schema.Literal(code), + /** + * Human-readable error message + * + * Messages provide detailed context about the error but may change + * over time. Use `error_code` for programmatic decisions. + */ + message: Schema.String, + /** + * Request ID for tracing and correlation + * + * This ID can be used to correlate error responses with server logs + * for debugging and support purposes. The ID is generated per-request + * and appears in both logs and error responses. + */ + requestId: Schema.optional(Schema.String) + }).pipe(Schema.encodeKeys({ + code: "error_code", + message: "error_message", + requestId: "request_id" + })) as any + +export const DatasetConversionError = makeError( + "DATASET_CONVERSION_ERROR", + "DatasetConversionError" +).annotate({ httpApiStatus: 500 }) + +export type DatasetConversionError = typeof DatasetConversionError.Type + +export const DatasetNotFoundError = makeError( + "DATASET_NOT_FOUND", + "DatasetNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type DatasetNotFoundError = typeof DatasetNotFoundError.Type + +export const DatasetVersionConversionError = makeError( + "DATASET_VERSION_CONVERSION_ERROR", + "DatasetVersionConversionError" +).annotate({ httpApiStatus: 500 }) + +export type DatasetVersionConversionError = typeof DatasetVersionConversionError.Type + +export const DatasetVersionNotFoundError = makeError( + "VERSION_NOT_FOUND", + "DatasetVersionNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type DatasetVersionNotFoundError = typeof DatasetVersionNotFoundError.Type + +export const ForbiddenError = makeError( + "FORBIDDEN", + "ForbiddenError" +).annotate({ httpApiStatus: 403 }) + +export type ForbiddenError = typeof ForbiddenError.Type + +export const InvalidDatasetOwnerPathError = makeError( + "INVALID_DATASET_OWNER_PATH", + "InvalidDatasetOwnerPathError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidDatasetOwnerPathError = typeof InvalidDatasetOwnerPathError.Type + +export const InvalidDatasetReferenceError = makeError( + "INVALID_REFERENCE", + "InvalidDatasetReferenceError" +).annotate({ httpApiStatus: 500 }) + +export type InvalidDatasetReferenceError = typeof InvalidDatasetReferenceError.Type + +export const InvalidDatasetSelectorError = makeError( + "INVALID_DATASET_SELECTOR", + "InvalidDatasetSelectorError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidDatasetSelectorError = typeof InvalidDatasetSelectorError.Type + +export const InvalidManifestError = makeError( + "INVALID_MANIFEST", + "InvalidManifestError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidManifestError = typeof InvalidManifestError.Type + +export const InvalidManifestHashError = makeError( + "INVALID_MANIFEST_HASH", + "InvalidManifestHashError" +).annotate({ httpApiStatus: 500 }) + +export type InvalidManifestHashError = typeof InvalidManifestHashError.Type + +export const InvalidNamespaceError = makeError( + "INVALID_NAMESPACE", + "InvalidNamespaceError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidNamespaceError = typeof InvalidNamespaceError.Type + +export const InvalidQueryParametersError = makeError( + "INVALID_QUERY_PARAMETERS", + "InvalidQueryParametersError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidQueryParametersError = typeof InvalidQueryParametersError.Type + +export const InvalidPathParametersError = makeError( + "INVALID_PATH_PARAMETERS", + "InvalidPathParametersError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidPathParametersError = typeof InvalidPathParametersError.Type + +export const InvalidRequestBodyError = makeError( + "INVALID_REQUEST_BODY", + "InvalidRequestBodyError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidRequestBodyError = typeof InvalidRequestBodyError.Type + +export const InvalidSelectorError = makeError( + "INVALID_SELECTOR", + "InvalidSelectorError" +).annotate({ httpApiStatus: 400 }) + +export type InvalidSelectorError = typeof InvalidSelectorError.Type + +export const LatestDatasetVersionNotFoundError = makeError( + "LATEST_VERSION_NOT_FOUND", + "LatestDatasetVersionNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type LatestDatasetVersionNotFoundError = typeof LatestDatasetVersionNotFoundError.Type + +export const LimitInvalidError = makeError( + "LIMIT_INVALID", + "LimitInvalidError" +).annotate({ httpApiStatus: 400 }) + +export type LimitInvalidError = typeof LimitInvalidError.Type + +export const LimitTooLargeError = makeError( + "LIMIT_TOO_LARGE", + "LimitTooLargeError" +).annotate({ httpApiStatus: 400 }) + +export type LimitTooLargeError = typeof LimitTooLargeError.Type + +export const ManifestDeserializationError = makeError( + "MANIFEST_DESERIALIZATION_ERROR", + "ManifestDeserializationError" +).annotate({ httpApiStatus: 500 }) + +export type ManifestDeserializationError = typeof ManifestDeserializationError.Type + +export const ManifestNotFoundError = makeError( + "MANIFEST_NOT_FOUND", + "ManifestNotFoundError" +).annotate({ httpApiStatus: 404 }) + +export type ManifestNotFoundError = typeof ManifestNotFoundError.Type + +export const ManifestRetrievalError = makeError( + "MANIFEST_RETRIEVAL_ERROR", + "ManifestRetrievalError" +).annotate({ httpApiStatus: 500 }) + +export type ManifestRetrievalError = typeof ManifestRetrievalError.Type + +export const NamespaceAccessDeniedError = makeError( + "NAMESPACE_ACCESS_DENIED", + "NamespaceAccessDeniedError" +).annotate({ httpApiStatus: 403 }) + +export type NamespaceAccessDeniedError = typeof NamespaceAccessDeniedError.Type + +export const RegistryDatabaseError = makeError( + "AMP_REGISTRY_DB_ERROR", + "RegistryDatabaseError" +).annotate({ httpApiStatus: 500 }) + +export type RegistryDatabaseError = typeof RegistryDatabaseError.Type + +export const SavedQueryConversionError = makeError( + "SAVED_QUERY_CONVERSION_ERROR", + "SavedQueryConversionError" +).annotate({ httpApiStatus: 500 }) + +export type SavedQueryConversionError = typeof SavedQueryConversionError.Type + +export const ServiceUnavailableError = makeError( + "SERVICE_UNAVAILABLE", + "ServiceUnavailableError" +).annotate({ httpApiStatus: 503 }) + +export type ServiceUnavailableError = typeof ServiceUnavailableError.Type diff --git a/packages/amp/src/transactional-stream/errors.ts b/packages/amp/src/transactional-stream/errors.ts index a2c5c6b..d38942a 100644 --- a/packages/amp/src/transactional-stream/errors.ts +++ b/packages/amp/src/transactional-stream/errors.ts @@ -13,11 +13,11 @@ import type { ProtocolStreamError } from "../protocol-stream/errors.ts" /** * Error from StateStore operations. */ -export class StateStoreError extends Schema.TaggedError( +export class StateStoreError extends Schema.TaggedErrorClass( "Amp/TransactionalStream/StateStoreError" )("StateStoreError", { reason: Schema.String, - operation: Schema.Literal("advance", "commit", "truncate", "load"), + operation: Schema.Literals(["advance", "commit", "truncate", "load"]), cause: Schema.optional(Schema.Defect) }) {} @@ -30,7 +30,7 @@ export class StateStoreError extends Schema.TaggedError( * This occurs when a reorg is so deep that there's no valid recovery point. * The stream must be restarted with fresh state. */ -export class UnrecoverableReorgError extends Schema.TaggedError( +export class UnrecoverableReorgError extends Schema.TaggedErrorClass( "Amp/TransactionalStream/UnrecoverableReorgError" )("UnrecoverableReorgError", { reason: Schema.String @@ -41,7 +41,7 @@ export class UnrecoverableReorgError extends Schema.TaggedError( +export class PartialReorgError extends Schema.TaggedErrorClass( "Amp/TransactionalStream/PartialReorgError" )("PartialReorgError", { reason: Schema.String, diff --git a/packages/amp/src/transactional-stream/memory-store.ts b/packages/amp/src/transactional-stream/memory-store.ts index b402d75..3e74c71 100644 --- a/packages/amp/src/transactional-stream/memory-store.ts +++ b/packages/amp/src/transactional-stream/memory-store.ts @@ -123,10 +123,10 @@ export const layerWithState = (initial: StateSnapshot): Layer.Layer * expect(snapshot.next).toBe(5) * ``` */ -export class TestState extends Context.Tag("Amp/TransactionalStream/TestState")< +export class TestState extends Context.Service< TestState, { readonly get: Effect.Effect } ->() {} +>()("Amp/TransactionalStream/TestState") {} /** * Test layer providing both `StateStore` and `TestState`. diff --git a/packages/amp/src/transactional-stream/state-actor.ts b/packages/amp/src/transactional-stream/state-actor.ts index c47d63f..1b3a4be 100644 --- a/packages/amp/src/transactional-stream/state-actor.ts +++ b/packages/amp/src/transactional-stream/state-actor.ts @@ -319,11 +319,9 @@ const executeReorg = Effect.fnUntraced(function*( } else { // No recovery point with a non-empty buffer means all buffered watermarks // are affected by the reorg. This is not recoverable. - return yield* Effect.fail( - new UnrecoverableReorgError({ - reason: "All buffered watermarks are affected by the reorg" - }) - ) + return yield* new UnrecoverableReorgError({ + reason: "All buffered watermarks are affected by the reorg" + }) } } else { const [recoveryId, recoveryRanges] = recovery @@ -331,12 +329,10 @@ const executeReorg = Effect.fnUntraced(function*( // 3. Check for partial reorg const partialNetwork = checkPartialReorg(recoveryRanges, invalidation) if (partialNetwork !== undefined) { - return yield* Effect.fail( - new PartialReorgError({ - reason: "Recovery point doesn't align with reorg boundary", - network: partialNetwork - }) - ) + return yield* new PartialReorgError({ + reason: "Recovery point doesn't align with reorg boundary", + network: partialNetwork + }) } anchor = recoveryId diff --git a/packages/amp/src/transactional-stream/state-store.ts b/packages/amp/src/transactional-stream/state-store.ts index d8c309c..29befd9 100644 --- a/packages/amp/src/transactional-stream/state-store.ts +++ b/packages/amp/src/transactional-stream/state-store.ts @@ -123,10 +123,10 @@ export interface StateStoreService { * }) * ``` */ -export class StateStore extends Context.Tag("Amp/TransactionalStream/StateStore")< +export class StateStore extends Context.Service< StateStore, StateStoreService ->() {} +>()("Amp/TransactionalStream/StateStore") {} // ============================================================================= // Helpers diff --git a/packages/amp/src/transactional-stream/stream.ts b/packages/amp/src/transactional-stream/stream.ts index 0f3c590..6237e75 100644 --- a/packages/amp/src/transactional-stream/stream.ts +++ b/packages/amp/src/transactional-stream/stream.ts @@ -148,10 +148,10 @@ export interface TransactionalStreamService { * Effect.runPromise(program.pipe(Effect.provide(AppLayer))) * ``` */ -export class TransactionalStream extends Context.Tag("Amp/TransactionalStream")< +export class TransactionalStream extends Context.Service< TransactionalStream, TransactionalStreamService ->() {} +>()("Amp/TransactionalStream") {} // ============================================================================= // Implementation @@ -195,7 +195,7 @@ const make = Effect.gen(function*() { const retention = options?.retention ?? DEFAULT_RETENTION // Create the stream with proper scoping - return Stream.unwrapScoped( + return Stream.unwrap( Effect.gen(function*() { // 1. Create StateActor const actor: StateActor = yield* makeStateActor(storeService, retention) diff --git a/packages/amp/src/transactional-stream/types.ts b/packages/amp/src/transactional-stream/types.ts index d9a2b0a..1d122d6 100644 --- a/packages/amp/src/transactional-stream/types.ts +++ b/packages/amp/src/transactional-stream/types.ts @@ -5,7 +5,7 @@ */ import * as Option from "effect/Option" import * as Schema from "effect/Schema" -import { BlockRange } from "../core/domain.ts" +import { BlockRange, NonNegativeInt } from "../core/domain.ts" import { InvalidationRange } from "../protocol-stream/messages.ts" // ============================================================================= @@ -16,9 +16,9 @@ import { InvalidationRange } from "../protocol-stream/messages.ts" * Transaction ID - monotonically increasing identifier for each event. * Guaranteed to be unique and never reused, even across crashes. */ -export const TransactionId = Schema.NonNegativeInt.pipe( +export const TransactionId = NonNegativeInt.pipe( Schema.brand("Amp/TransactionalStream/TransactionId") -).annotations({ +).annotate({ identifier: "TransactionId", description: "Monotonically increasing transaction identifier" }) @@ -34,7 +34,7 @@ export type TransactionId = typeof TransactionId.Type export const TransactionIdRange = Schema.Struct({ start: TransactionId, end: TransactionId -}).annotations({ +}).annotate({ identifier: "TransactionIdRange", description: "Inclusive range of transaction IDs" }) @@ -49,19 +49,19 @@ export type TransactionIdRange = typeof TransactionIdRange.Type */ export const UndoCauseReorg = Schema.TaggedStruct("Reorg", { invalidation: Schema.Array(InvalidationRange) -}).annotations({ +}).annotate({ identifier: "UndoCause.Reorg", description: "Undo caused by blockchain reorganization" }) export type UndoCauseReorg = typeof UndoCauseReorg.Type -export const UndoCauseRewind = Schema.TaggedStruct("Rewind", {}).annotations({ +export const UndoCauseRewind = Schema.TaggedStruct("Rewind", {}).annotate({ identifier: "UndoCause.Rewind", description: "Undo caused by rewind on restart (uncommitted transactions)" }) export type UndoCauseRewind = typeof UndoCauseRewind.Type -export const UndoCause = Schema.Union(UndoCauseReorg, UndoCauseRewind).annotations({ +export const UndoCause = Schema.Union([UndoCauseReorg, UndoCauseRewind]).annotate({ identifier: "UndoCause", description: "Cause of an Undo event" }) @@ -78,15 +78,10 @@ export const TransactionEventData = Schema.TaggedStruct("Data", { /** Transaction ID of this event */ id: TransactionId, /** Decoded record batch data */ - data: Schema.Array( - Schema.Record({ - key: Schema.String, - value: Schema.Unknown - }) - ), + data: Schema.Array(Schema.Record(Schema.String, Schema.Unknown)), /** Block ranges covered by this data */ ranges: Schema.Array(BlockRange) -}).annotations({ +}).annotate({ identifier: "TransactionEvent.Data", description: "New data to process" }) @@ -102,7 +97,7 @@ export const TransactionEventUndo = Schema.TaggedStruct("Undo", { cause: UndoCause, /** Range of transaction IDs to invalidate (inclusive) */ invalidate: TransactionIdRange -}).annotations({ +}).annotate({ identifier: "TransactionEvent.Undo", description: "Undo/rollback previously processed data" }) @@ -118,7 +113,7 @@ export const TransactionEventWatermark = Schema.TaggedStruct("Watermark", { ranges: Schema.Array(BlockRange), /** Last transaction ID pruned at this watermark, if any */ prune: Schema.OptionFromNullOr(TransactionId) -}).annotations({ +}).annotate({ identifier: "TransactionEvent.Watermark", description: "Watermark confirming block ranges are complete" }) @@ -127,11 +122,11 @@ export type TransactionEventWatermark = typeof TransactionEventWatermark.Type /** * Union of all transaction event types. */ -export const TransactionEvent = Schema.Union( +export const TransactionEvent = Schema.Union([ TransactionEventData, TransactionEventUndo, TransactionEventWatermark -).annotations({ +]).annotate({ identifier: "TransactionEvent", description: "Event emitted by the transactional stream" }) @@ -180,7 +175,7 @@ export const watermarkEvent = ( _tag: "Watermark", id, ranges: ranges as Array, - prune: Option.fromNullable(prune) + prune: Option.fromNullishOr(prune) }) /** diff --git a/packages/amp/test/arrow-flight-ipc/roundtrip.test.ts b/packages/amp/test/arrow-flight-ipc/roundtrip.test.ts index c22f406..76763c3 100644 --- a/packages/amp/test/arrow-flight-ipc/roundtrip.test.ts +++ b/packages/amp/test/arrow-flight-ipc/roundtrip.test.ts @@ -43,6 +43,7 @@ describe("FlightData roundtrip", () => { // Verify decoded values match expected values const comparison = verifyDecodedValues(testSchema, decoded, generated.expectedValues) + expect(comparison.success, formatComparisonErrors(comparison.errors)).toBe(true) })) diff --git a/packages/amp/test/arrow-test-harness/FlightDataGenerator.ts b/packages/amp/test/arrow-test-harness/FlightDataGenerator.ts index 212da19..4267f64 100644 --- a/packages/amp/test/arrow-test-harness/FlightDataGenerator.ts +++ b/packages/amp/test/arrow-test-harness/FlightDataGenerator.ts @@ -4,7 +4,6 @@ */ import type { ArrowField, ArrowSchema } from "@edgeandnode/amp/internal/arrow-flight-ipc/Schema" import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" import * as Random from "effect/Random" import * as BufferUtils from "./BufferUtils.ts" import * as GeneratorRegistry from "./GeneratorRegistry.ts" @@ -84,10 +83,8 @@ export const generateFlightData = ( schema } }).pipe( - Effect.provide(Layer.mergeAll( - Layer.succeed(Random.Random, Random.make(seed)), - GeneratorRegistry.Live - )) + Effect.provide(GeneratorRegistry.Live), + Random.withSeed(seed) ) } @@ -247,10 +244,8 @@ export const generateMultiBatchFlightData = ( totalRows } }).pipe( - Effect.provide(Layer.mergeAll( - Layer.succeed(Random.Random, Random.make(seed)), - GeneratorRegistry.Live - )) + Effect.provide(GeneratorRegistry.Live), + Random.withSeed(seed) ) } diff --git a/packages/amp/test/arrow-test-harness/RandomUtils.ts b/packages/amp/test/arrow-test-harness/RandomUtils.ts index b64e3d3..f3a86e7 100644 --- a/packages/amp/test/arrow-test-harness/RandomUtils.ts +++ b/packages/amp/test/arrow-test-harness/RandomUtils.ts @@ -16,7 +16,7 @@ import * as Random from "effect/Random" /** * Generate a random bigint in the range [min, max] inclusive. */ -export const nextBigInt = (min: bigint, max: bigint): Effect.Effect => +export const nextBigInt = (min: bigint, max: bigint): Effect.Effect => Effect.gen(function*() { const range = max - min + 1n // For ranges that fit in a safe integer, use simple approach @@ -34,13 +34,13 @@ export const nextBigInt = (min: bigint, max: bigint): Effect.Effect => +export const nextFloat = (min: number, max: number): Effect.Effect => Effect.map(Random.next, (n) => n * (max - min) + min) /** * Returns true with the given probability. */ -export const nextBoolWithProbability = (probability: number): Effect.Effect => +export const nextBoolWithProbability = (probability: number): Effect.Effect => Effect.map(Random.next, (n) => n < probability) // ============================================================================= @@ -52,7 +52,7 @@ const alphanumericChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0 /** * Generate a random alphanumeric string of the given length. */ -export const nextString = (length: number): Effect.Effect => +export const nextString = (length: number): Effect.Effect => Effect.gen(function*() { let result = "" for (let i = 0; i < length; i++) { @@ -65,7 +65,7 @@ export const nextString = (length: number): Effect.Effect => +export const nextStringBetween = (minLength: number, maxLength: number): Effect.Effect => Effect.gen(function*() { const length = yield* Random.nextIntBetween(minLength, maxLength) return yield* nextString(length) @@ -74,7 +74,7 @@ export const nextStringBetween = (minLength: number, maxLength: number): Effect. /** * Generate random bytes of the given length. */ -export const nextBytes = (length: number): Effect.Effect => +export const nextBytes = (length: number): Effect.Effect => Effect.gen(function*() { const bytes = new Uint8Array(length) for (let i = 0; i < length; i++) { @@ -89,7 +89,7 @@ export const nextBytes = (length: number): Effect.Effect => +): Effect.Effect => Effect.gen(function*() { const length = yield* Random.nextIntBetween(minLength, maxLength) return yield* nextBytes(length) @@ -102,7 +102,7 @@ export const nextBytesBetween = ( /** * Generate a random Date within a reasonable range (1970-2100). */ -export const nextDate = (): Effect.Effect => +export const nextDate = (): Effect.Effect => Effect.gen(function*() { // Random timestamp between 1970-01-01 and 2100-01-01 const minMs = 0 @@ -114,13 +114,12 @@ export const nextDate = (): Effect.Effect => /** * Generate a random timestamp in milliseconds since epoch. */ -export const nextTimestampMs = (): Effect.Effect => - Random.nextIntBetween(0, 4102444800000) +export const nextTimestampMs = (): Effect.Effect => Random.nextIntBetween(0, 4102444800000) /** * Generate a random time of day in milliseconds (0 to 86399999). */ -export const nextTimeOfDayMs = (): Effect.Effect => Random.nextIntBetween(0, 86399999) +export const nextTimeOfDayMs = (): Effect.Effect => Random.nextIntBetween(0, 86399999) // ============================================================================= // Array Generators @@ -131,8 +130,8 @@ export const nextTimeOfDayMs = (): Effect.Effect = */ export const nextArray = ( length: number, - generator: Effect.Effect -): Effect.Effect, never, Random.Random> => + generator: Effect.Effect +): Effect.Effect> => Effect.gen(function*() { const result: Array = [] for (let i = 0; i < length; i++) { @@ -146,9 +145,9 @@ export const nextArray = ( */ export const nextNullableArray = ( length: number, - generator: Effect.Effect, + generator: Effect.Effect, nullRate: number -): Effect.Effect, never, Random.Random> => +): Effect.Effect> => Effect.gen(function*() { const result: Array = [] for (let i = 0; i < length; i++) { @@ -169,51 +168,49 @@ export const nextNullableArray = ( /** * Generate a random signed 8-bit integer. */ -export const nextInt8 = (): Effect.Effect => Random.nextIntBetween(-128, 127) +export const nextInt8 = (): Effect.Effect => Random.nextIntBetween(-128, 127) /** * Generate a random unsigned 8-bit integer. */ -export const nextUint8 = (): Effect.Effect => Random.nextIntBetween(0, 255) +export const nextUint8 = (): Effect.Effect => Random.nextIntBetween(0, 255) /** * Generate a random signed 16-bit integer. */ -export const nextInt16 = (): Effect.Effect => Random.nextIntBetween(-32768, 32767) +export const nextInt16 = (): Effect.Effect => Random.nextIntBetween(-32768, 32767) /** * Generate a random unsigned 16-bit integer. */ -export const nextUint16 = (): Effect.Effect => Random.nextIntBetween(0, 65535) +export const nextUint16 = (): Effect.Effect => Random.nextIntBetween(0, 65535) /** * Generate a random signed 32-bit integer. */ -export const nextInt32 = (): Effect.Effect => - Random.nextIntBetween(-2147483648, 2147483647) +export const nextInt32 = (): Effect.Effect => Random.nextIntBetween(-2147483648, 2147483647) /** * Generate a random unsigned 32-bit integer. */ -export const nextUint32 = (): Effect.Effect => Random.nextIntBetween(0, 4294967295) +export const nextUint32 = (): Effect.Effect => Random.nextIntBetween(0, 4294967295) /** * Generate a random signed 64-bit integer. */ -export const nextInt64 = (): Effect.Effect => - nextBigInt(-9223372036854775808n, 9223372036854775807n) +export const nextInt64 = (): Effect.Effect => nextBigInt(-9223372036854775808n, 9223372036854775807n) /** * Generate a random unsigned 64-bit integer. */ -export const nextUint64 = (): Effect.Effect => nextBigInt(0n, 18446744073709551615n) +export const nextUint64 = (): Effect.Effect => nextBigInt(0n, 18446744073709551615n) /** * Generate a random 32-bit float. */ -export const nextFloat32 = (): Effect.Effect => nextFloat(-3.4028235e38, 3.4028235e38) +export const nextFloat32 = (): Effect.Effect => nextFloat(-3.4028235e38, 3.4028235e38) /** * Generate a random 64-bit float. */ -export const nextFloat64 = (): Effect.Effect => nextFloat(-1e100, 1e100) +export const nextFloat64 = (): Effect.Effect => nextFloat(-1e100, 1e100) diff --git a/packages/amp/test/arrow-test-harness/Types.ts b/packages/amp/test/arrow-test-harness/Types.ts index 9a9d440..5ca9527 100644 --- a/packages/amp/test/arrow-test-harness/Types.ts +++ b/packages/amp/test/arrow-test-harness/Types.ts @@ -8,7 +8,6 @@ import type { ArrowField, ArrowSchema, FlightData } from "@edgeandnode/amp/internal/arrow-flight-ipc/Schema" import * as Context from "effect/Context" import type * as Effect from "effect/Effect" -import type * as Random from "effect/Random" // Re-export FlightData for convenience export type { FlightData } @@ -28,7 +27,7 @@ export interface FieldGeneratorConfig { /** Maximum number of items for variable-length types. Default: varies by type */ readonly maxLength?: number /** Custom value generator function (overrides default random generation) */ - readonly valueGenerator?: (index: number) => Effect.Effect + readonly valueGenerator?: (index: number) => Effect.Effect /** Include special float values (NaN, Infinity, -Infinity, -0). Default: false */ readonly includeSpecialFloats?: boolean /** Probability of generating a special float value when includeSpecialFloats is true. Default: 0.1 */ @@ -163,7 +162,7 @@ export interface GeneratorRegistry { readonly getGenerator: (typeId: string) => DataGenerator } -export const GeneratorRegistry = Context.GenericTag("GeneratorRegistry") +export const GeneratorRegistry = Context.Service("GeneratorRegistry") // ============================================================================= // Data Generator Interface @@ -189,5 +188,5 @@ export interface DataGenerator { field: ArrowField, numRows: number, config: FieldGeneratorConfig - ): Effect.Effect + ): Effect.Effect } diff --git a/packages/amp/test/arrow-test-harness/ValueComparison.ts b/packages/amp/test/arrow-test-harness/ValueComparison.ts index 8ec04f1..472e378 100644 --- a/packages/amp/test/arrow-test-harness/ValueComparison.ts +++ b/packages/amp/test/arrow-test-harness/ValueComparison.ts @@ -128,7 +128,7 @@ const valuesEqual = (type: ArrowDataType, expected: unknown, actual: unknown): b return mapsEqual(type, expected as Array, actual as Array) case "union": - return unionsEqual(type, expected, actual) + return unionsEqual(expected, actual) default: return expected === actual @@ -326,7 +326,6 @@ const mapsEqual = ( } const unionsEqual = ( - type: ArrowDataType, expected: unknown, actual: unknown ): boolean => { diff --git a/packages/amp/test/arrow-test-harness/generators/primitives.ts b/packages/amp/test/arrow-test-harness/generators/primitives.ts index f1ad105..b230e57 100644 --- a/packages/amp/test/arrow-test-harness/generators/primitives.ts +++ b/packages/amp/test/arrow-test-harness/generators/primitives.ts @@ -196,8 +196,12 @@ export const floatGenerator: Types.DataGenerator = { values.push(null) } else if (includeSpecial && (yield* Rand.nextBoolWithProbability(specialRate))) { // Generate a precision-appropriate special float value - const idx = yield* Random.nextIntBetween(0, specialFloats.length) - values.push(specialFloats[idx]) + const idx = yield* Random.nextIntBetween(0, specialFloats.length - 1) + const value = specialFloats[idx] + if (value === undefined) { + throw new Error(`Invalid special float index ${idx} for precision ${type.precision}`) + } + values.push(value) } else { // Generate a normal float in the valid range for this precision values.push(yield* Rand.nextFloat(range.min, range.max)) diff --git a/packages/amp/test/cdc-stream/stream.test.ts b/packages/amp/test/cdc-stream/stream.test.ts index 233b249..c128cb0 100644 --- a/packages/amp/test/cdc-stream/stream.test.ts +++ b/packages/amp/test/cdc-stream/stream.test.ts @@ -99,8 +99,7 @@ describe("CdcStream - Insert", () => { const ranges = [makeBlockRange("eth", 0, 10)] const cdc = yield* CdcStream - const chunk = yield* cdc.streamCdc("SELECT * FROM eth.logs").pipe(Stream.runCollect) - const results = Chunk.toReadonlyArray(chunk) + const results = yield* cdc.streamCdc("SELECT * FROM eth.logs").pipe(Stream.runCollect) expect(results.length).toBe(1) const [cdcEvent] = results[0]! @@ -152,8 +151,7 @@ describe("CdcStream - Delete", () => { const data = [{ block: 1 }] const cdc = yield* CdcStream - const chunk = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) - const results = Chunk.toReadonlyArray(chunk) + const results = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) // Should have Insert then Delete expect(results.length).toBe(2) @@ -192,8 +190,7 @@ describe("CdcStream - Delete", () => { it.effect("Undo with no matching batches is skipped", () => Effect.gen(function*() { const cdc = yield* CdcStream - const chunk = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) - const results = Chunk.toReadonlyArray(chunk) + const results = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) // Undo for range with no stored batches should be filtered out expect(results.length).toBe(0) @@ -209,8 +206,7 @@ describe("CdcStream - Delete", () => { it.effect("Delete iterator loads lazily and skips missing", () => Effect.gen(function*() { const cdc = yield* CdcStream - const chunk = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) - const results = Chunk.toReadonlyArray(chunk) + const results = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) // Data events for IDs 1 and 3, then undo covering 1-3 (2 is missing) expect(results.length).toBe(3) // 2 Inserts + 1 Delete @@ -258,8 +254,7 @@ describe("CdcStream - Watermark", () => { it.effect("Watermark events are not exposed to consumer", () => Effect.gen(function*() { const cdc = yield* CdcStream - const chunk = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) - const results = Chunk.toReadonlyArray(chunk) + const results = yield* cdc.streamCdc("SELECT 1").pipe(Stream.runCollect) // Watermark should be filtered out expect(results.length).toBe(0) diff --git a/packages/amp/test/protocol-stream/validation.test.ts b/packages/amp/test/protocol-stream/validation.test.ts index b09a7a8..b374221 100644 --- a/packages/amp/test/protocol-stream/validation.test.ts +++ b/packages/amp/test/protocol-stream/validation.test.ts @@ -21,7 +21,7 @@ import { } from "@edgeandnode/amp/protocol-stream" import { describe, it } from "@effect/vitest" import * as Effect from "effect/Effect" -import * as Either from "effect/Either" +import * as Result from "effect/Result" // ============================================================================= // Test Helpers @@ -56,52 +56,43 @@ describe("validatePrevHash", () => { it.effect("allows genesis block with no prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 0, 10, HASH_A) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validatePrevHash(range) + expect(result).toBeUndefined() })) it.effect("allows genesis block with zero prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 0, 10, HASH_A, ZERO_HASH) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validatePrevHash(range) + expect(result).toBeUndefined() })) it.effect("rejects genesis block with non-zero prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 0, 10, HASH_A, HASH_B) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(InvalidPrevHashError) - } + const result = yield* Effect.flip(validatePrevHash(range)) + expect(result._tag).toBe("InvalidPrevHashError") })) it.effect("allows non-genesis block with valid prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 100, 110, HASH_A, HASH_B) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validatePrevHash(range) + expect(result).toBeUndefined() })) it.effect("rejects non-genesis block with no prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 100, 110, HASH_A) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(MissingPrevHashError) - } + const result = yield* Effect.flip(validatePrevHash(range)) + expect(result._tag).toBe("MissingPrevHashError") })) it.effect("rejects non-genesis block with zero prevHash", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 100, 110, HASH_A, ZERO_HASH) - const result = yield* validatePrevHash(range).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(MissingPrevHashError) - } + const result = yield* Effect.flip(validatePrevHash(range)) + expect(result._tag).toBe("MissingPrevHashError") })) }) @@ -116,8 +107,8 @@ describe("validateNetworks", () => { makeBlockRange("eth", 0, 10, HASH_A), makeBlockRange("polygon", 0, 10, HASH_B) ] - const result = yield* validateNetworks([], incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateNetworks([], incoming) + expect(result).toBeUndefined() })) it.effect("allows consistent networks across batches", ({ expect }) => @@ -130,8 +121,8 @@ describe("validateNetworks", () => { makeBlockRange("eth", 11, 20, HASH_B, HASH_A), makeBlockRange("polygon", 11, 20, HASH_C, HASH_B) ] - const result = yield* validateNetworks(previous, incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateNetworks(previous, incoming) + expect(result).toBeUndefined() })) it.effect("rejects duplicate networks in batch", ({ expect }) => @@ -140,11 +131,8 @@ describe("validateNetworks", () => { makeBlockRange("eth", 0, 10, HASH_A), makeBlockRange("eth", 0, 10, HASH_B) ] - const result = yield* validateNetworks([], incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(DuplicateNetworkError) - } + const result = yield* Effect.flip(validateNetworks([], incoming)) + expect(result._tag).toBe("DuplicateNetworkError") })) it.effect("rejects network count change", ({ expect }) => @@ -156,11 +144,8 @@ describe("validateNetworks", () => { makeBlockRange("eth", 11, 20, HASH_B, HASH_A), makeBlockRange("polygon", 0, 10, HASH_C) ] - const result = yield* validateNetworks(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(NetworkCountChangedError) - } + const result = yield* Effect.flip(validateNetworks(previous, incoming)) + expect(result._tag).toBe("NetworkCountChangedError") })) it.effect("rejects unexpected network", ({ expect }) => @@ -171,11 +156,8 @@ describe("validateNetworks", () => { const incoming = [ makeBlockRange("polygon", 0, 10, HASH_B) ] - const result = yield* validateNetworks(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(UnexpectedNetworkError) - } + const result = yield* Effect.flip(validateNetworks(previous, incoming)) + expect(result._tag).toBe("UnexpectedNetworkError") })) }) @@ -187,23 +169,23 @@ describe("validateConsecutiveness", () => { it.effect("allows first batch without validation", ({ expect }) => Effect.gen(function*() { const incoming = [makeBlockRange("eth", 0, 10, HASH_A)] - const result = yield* validateConsecutiveness([], incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateConsecutiveness([], incoming) + expect(result).toBeUndefined() })) it.effect("allows consecutive blocks with matching hash chain", ({ expect }) => Effect.gen(function*() { const previous = [makeBlockRange("eth", 0, 10, HASH_A)] const incoming = [makeBlockRange("eth", 11, 20, HASH_B, HASH_A)] - const result = yield* validateConsecutiveness(previous, incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateConsecutiveness(previous, incoming) + expect(result).toBeUndefined() })) it.effect("allows identical ranges (watermark repeat)", ({ expect }) => Effect.gen(function*() { const range = makeBlockRange("eth", 0, 10, HASH_A) - const result = yield* validateConsecutiveness([range], [range]).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateConsecutiveness([range], [range]) + expect(result).toBeUndefined() })) it.effect("allows backwards jump with hash mismatch (reorg)", ({ expect }) => @@ -211,8 +193,8 @@ describe("validateConsecutiveness", () => { const previous = [makeBlockRange("eth", 0, 10, HASH_A)] // Backwards jump (start=5 < prev.end+1=11) with different hash chain const incoming = [makeBlockRange("eth", 5, 12, HASH_C, HASH_B)] - const result = yield* validateConsecutiveness(previous, incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateConsecutiveness(previous, incoming) + expect(result).toBeUndefined() })) it.effect("rejects consecutive blocks with hash mismatch", ({ expect }) => @@ -220,11 +202,8 @@ describe("validateConsecutiveness", () => { const previous = [makeBlockRange("eth", 0, 10, HASH_A)] // Consecutive (start=11 == prev.end+1=11) but prevHash doesn't match const incoming = [makeBlockRange("eth", 11, 20, HASH_C, HASH_B)] - const result = yield* validateConsecutiveness(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(HashMismatchOnConsecutiveBlocksError) - } + const result = yield* Effect.flip(validateConsecutiveness(previous, incoming)) + expect(result._tag).toBe("HashMismatchOnConsecutiveBlocksError") })) it.effect("rejects backwards jump with matching hash (invalid reorg)", ({ expect }) => @@ -232,11 +211,8 @@ describe("validateConsecutiveness", () => { const previous = [makeBlockRange("eth", 0, 10, HASH_A)] // Backwards jump but same hash chain - invalid const incoming = [makeBlockRange("eth", 5, 12, HASH_B, HASH_A)] - const result = yield* validateConsecutiveness(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(InvalidReorgError) - } + const result = yield* Effect.flip(validateConsecutiveness(previous, incoming)) + expect(result._tag).toBe("InvalidReorgError") })) it.effect("rejects forward gap", ({ expect }) => @@ -244,13 +220,14 @@ describe("validateConsecutiveness", () => { const previous = [makeBlockRange("eth", 0, 10, HASH_A)] // Gap: start=15 > prev.end+1=11 const incoming = [makeBlockRange("eth", 15, 20, HASH_B, HASH_A)] - const result = yield* validateConsecutiveness(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result)) { - expect(result.left).toBeInstanceOf(GapError) - expect((result.left as GapError).missingStart).toBe(11) - expect((result.left as GapError).missingEnd).toBe(14) - } + const result = yield* Effect.flip(validateConsecutiveness(previous, incoming)) + expect(result).toEqual( + new GapError({ + network: "eth", + missingStart: 11, + missingEnd: 14 + }) + ) })) }) @@ -263,23 +240,23 @@ describe("validateAll", () => { Effect.gen(function*() { const previous = [makeBlockRange("eth", 100, 110, HASH_A, HASH_B)] const incoming = [makeBlockRange("eth", 111, 120, HASH_B, HASH_A)] - const result = yield* validateAll(previous, incoming).pipe(Effect.either) - expect(Either.isRight(result)).toBe(true) + const result = yield* validateAll(previous, incoming) + expect(result).toBeUndefined() })) it.effect("fails on prevHash validation", ({ expect }) => Effect.gen(function*() { const incoming = [makeBlockRange("eth", 100, 110, HASH_A)] // Missing prevHash - const result = yield* validateAll([], incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + const result = yield* Effect.flip(validateAll([], incoming)) + expect(result._tag).toBe("MissingPrevHashError") })) it.effect("fails on network validation", ({ expect }) => Effect.gen(function*() { const previous = [makeBlockRange("eth", 100, 110, HASH_A, HASH_B)] const incoming = [makeBlockRange("polygon", 100, 110, HASH_A, HASH_B)] - const result = yield* validateAll(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + const result = yield* Effect.flip(validateAll(previous, incoming)) + expect(result._tag).toBe("UnexpectedNetworkError") })) it.effect("fails on consecutiveness validation", ({ expect }) => @@ -287,7 +264,7 @@ describe("validateAll", () => { const previous = [makeBlockRange("eth", 100, 110, HASH_A, HASH_B)] // Gap: start=115 > prev.end+1=111 const incoming = [makeBlockRange("eth", 115, 120, HASH_B, HASH_A)] - const result = yield* validateAll(previous, incoming).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + const result = yield* Effect.flip(validateAll(previous, incoming)) + expect(result._tag).toBe("GapError") })) }) diff --git a/packages/cli/package.json b/packages/cli/package.json index e675d00..187bd6f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,17 +51,13 @@ }, "peerDependencies": { "@edgeandnode/amp": "workspace:^", - "@effect/cli": "^0.73.2", - "@effect/platform": "^0.94.5", - "@effect/platform-node": "^0.104.1", - "effect": "^3.19.18" + "@effect/platform-node": "catalog:", + "effect": "catalog:" }, "devDependencies": { "@connectrpc/connect": "^2.1.1", - "@effect/cli": "^0.73.2", - "@effect/platform": "^0.94.5", - "@effect/platform-node": "^0.104.1", - "effect": "^3.19.18" + "@effect/platform-node": "catalog:", + "effect": "catalog:" }, "dependencies": { "open": "^11.0.0" diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index bfb9d21..37d7b9a 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -4,6 +4,5 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime" import { Cli } from "./cli.ts" NodeRuntime.runMain(Cli, { - disableErrorReporting: true, - disablePrettyLogger: true + disableErrorReporting: true }) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index db3b44a..26ab4f2 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,12 +1,11 @@ import * as Auth from "@edgeandnode/amp/auth/service" -import * as CliConfig from "@effect/cli/CliConfig" -import * as Command from "@effect/cli/Command" -import * as NodeContext from "@effect/platform-node/NodeContext" -import * as FetchHttpClient from "@effect/platform/FetchHttpClient" -import * as KeyValueStore from "@effect/platform/KeyValueStore" -import * as Path from "@effect/platform/Path" +import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as Command from "effect/unstable/cli/Command" +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" +import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" import * as NodeOS from "node:os" import PackageJson from "../package.json" with { type: "json" } import { AuthCommand } from "./commands/auth.ts" @@ -17,15 +16,10 @@ const RootCommand = Command.make("amp").pipe( ) const run = Command.run(RootCommand, { - name: "Amp", version: PackageJson["version"] }) -const CliConfigLayer = CliConfig.layer({ - showBuiltIns: false -}) - -const CliCacheLayer = Layer.unwrapEffect( +const CliCacheLayer = Layer.unwrap( Effect.gen(function*() { const path = yield* Path.Path @@ -45,14 +39,10 @@ const AuthLayer = Auth.layer.pipe( const MainLayer = Layer.mergeAll( AuthLayer, - CliConfigLayer, HttpClientLayer ).pipe( - Layer.provideMerge(NodeContext.layer), + Layer.provideMerge(NodeServices.layer), Layer.orDie ) -export const Cli = run(process.argv).pipe( - Effect.provide(MainLayer), - Effect.catchTag("Amp/NonZeroExitCode", () => Effect.sync(() => process.exit(1))) -) +export const Cli = Effect.provide(run, MainLayer) diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index 62e8e52..f17b68e 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -1,4 +1,4 @@ -import * as Command from "@effect/cli/Command" +import * as Command from "effect/unstable/cli/Command" import { LoginCommand } from "./auth/login.ts" import { LogoutCommand } from "./auth/logout.ts" import { TokenCommand } from "./auth/token.ts" diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index c232e05..a26aa52 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -1,99 +1,121 @@ +import type * as AuthError from "@edgeandnode/amp/auth/error" import * as Auth from "@edgeandnode/amp/auth/service" -import * as Command from "@effect/cli/Command" -import * as Prompt from "@effect/cli/Prompt" import * as Console from "effect/Console" +import * as Data from "effect/Data" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Fiber from "effect/Fiber" import * as Option from "effect/Option" +import * as Runtime from "effect/Runtime" import * as Schedule from "effect/Schedule" import * as String from "effect/String" +import * as Command from "effect/unstable/cli/Command" +import * as Prompt from "effect/unstable/cli/Prompt" import Open from "open" -const handleLoginCommand = Effect.fnUntraced(function*() { - const auth = yield* Auth.Auth - - const authInfo = yield* auth.getCachedAuthInfo - - // User already authenticated - if (Option.isSome(authInfo)) { - yield* Console.error("You are already authenticated with Amp.") - return yield* Effect.void - } - - // Perform OAuth2 PKCE flow - const { codeChallenge, codeVerifier } = yield* auth.createChallenge - - const { - expiresIn, - deviceCode, - interval, - userCode, - verificationUri - } = yield* auth.requestDeviceAuthorization(codeChallenge) - - // Show the user the OAuth2 PKCE code - yield* Console.error(String.stripMargin( - `|Copy the following verification code and enter it in your browser: +export class LoginCommandError extends Data.TaggedError("LoginCommandError")<{ + readonly cause: + | AuthError.AuthCacheError + | AuthError.AuthDeviceFlowError + | AuthError.AuthNetworkError + | AuthError.AuthProtocolError + | AuthError.AuthRefreshError + | AuthError.AuthRequestError +}> { + override readonly [Runtime.errorExitCode] = 1 + override readonly [Runtime.errorReported] = false +} + +const handleLoginCommand = Effect.fnUntraced( + function*() { + const auth = yield* Auth.Auth + + const authInfo = yield* auth.getCachedAuthInfo + + // User already authenticated + if (Option.isSome(authInfo)) { + yield* Console.error("You are already authenticated with Amp.") + return yield* Effect.void + } + + // Perform OAuth2 PKCE flow + const { codeChallenge, codeVerifier } = yield* auth.createChallenge + + const { + expiresIn, + deviceCode, + interval, + userCode, + verificationUri + } = yield* auth.requestDeviceAuthorization(codeChallenge) + + // Show the user the OAuth2 PKCE code + yield* Console.error(String.stripMargin( + `|Copy the following verification code and enter it in your browser: | | ${userCode} |` - )) - - // Ask if we should auto-open the user's browser - const autoOpenBrowser = yield* Prompt.confirm({ - message: "Would you like to open your browser automatically?", - initial: true - }).pipe(Effect.zipLeft(Console.error())) - - if (autoOpenBrowser) { - // If so, attempt to open the browser, falling back to a useful message - yield* Effect.try(() => Open(verificationUri, { wait: false })).pipe( - Effect.catchAllCause(() => - Console.error(String.stripMargin( - `|If the browser window does not open automatically, enter the verification code into the following URL: + )) + + // Ask if we should auto-open the user's browser + const autoOpenBrowser = yield* Prompt.confirm({ + message: "Would you like to open your browser automatically?", + initial: true + }) + + yield* Console.error() + + if (autoOpenBrowser) { + // If so, attempt to open the browser, falling back to a useful message + yield* Effect.tryPromise(() => Open(verificationUri, { wait: false })).pipe( + Effect.catchCause(() => + Console.error(String.stripMargin( + `|If the browser window does not open automatically, enter the verification code into the following URL: | | ${verificationUri} |` - )) + )) + ) ) - ) - } else { - // If not, indicate that the user show navigate to the verification URL - yield* Console.error(String.stripMargin( - `|Enter the verification code into the following URL: + } else { + // If not, indicate that the user show navigate to the verification URL + yield* Console.error(String.stripMargin( + `|Enter the verification code into the following URL: | | ${verificationUri} |` - )) - } - - // Initially starts polling with a faster exponential backoff (1s, 1.5s, 2.25s, ...), - // but then caps at the server's requested interval, setting the maximum - // number of polling attempts based on the device code's lifetime - const pollingSchedule = Schedule.exponential("1 second", 1.5).pipe( - Schedule.union(Schedule.spaced(Duration.seconds(interval))), - Schedule.intersect(Schedule.recurs(Math.floor(expiresIn / interval))) - ) + )) + } + + // Initially starts polling with a faster exponential backoff (1s, 1.5s, 2.25s, ...), + // but then caps at the server's requested interval, setting the maximum + // number of polling attempts based on the device code's lifetime + const pollingSchedule = Schedule.exponential("1 second", 1.5).pipe( + Schedule.both(Schedule.spaced(Duration.seconds(interval))), + Schedule.either(Schedule.recurs(Math.floor(expiresIn / interval))) + ) - // Show a spinner while we wait - const spinnerFiber = yield* Effect.fork(showSpinner("Waiting for the user to authenticate...")) - - // Poll for the auth info response - const response = yield* auth.pollDeviceToken(deviceCode, codeVerifier).pipe( - Effect.retry({ - schedule: pollingSchedule, - while: (error) => error._tag === "AuthDeviceFlowError" && error.reason === "pending" - }), - Effect.tapErrorCause(() => Console.error("Authentication timed out or failed. Please try again.")), - Effect.ensuring(Fiber.interrupt(spinnerFiber)) - ) + // Show a spinner while we wait + const spinnerFiber = yield* Effect.forkChild(showSpinner("Waiting for the user to authenticate...")) + + // Poll for the auth info response + const response = yield* auth.pollDeviceToken(deviceCode, codeVerifier).pipe( + Effect.retry({ + schedule: pollingSchedule, + while: (error) => error._tag === "AuthDeviceFlowError" && error.reason === "pending" + }), + Effect.tapCause(() => Console.error("Authentication timed out or failed. Please try again.")), + Effect.ensuring(Fiber.interrupt(spinnerFiber)) + ) - // Cache the auth information so it can be used by other commands - yield* auth.setCachedAuthInfo(response) + // Cache the auth information so it can be used by other commands + yield* auth.setCachedAuthInfo(response) - yield* Console.error("Authenticated successfully!") -}) + yield* Console.error("Authenticated successfully!") + }, + Effect.catchTag("QuitError", () => Effect.void), + Effect.mapError((cause) => new LoginCommandError({ cause })) +) export const LoginCommand = Command.make("login").pipe( Command.withDescription("Login to the Amp CLI"), @@ -106,7 +128,7 @@ export const LoginCommand = Command.make("login").pipe( const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] -const showSpinner = Effect.fnUntraced(function*(message) { +const showSpinner = Effect.fnUntraced(function*(message: string) { let index = 0 return yield* Effect.sync(() => { const frame = SPINNER_FRAMES[index] diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index 9f8aaa5..3cf91a0 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -1,25 +1,39 @@ +import type * as AuthError from "@edgeandnode/amp/auth/error" import * as Auth from "@edgeandnode/amp/auth/service" -import * as Command from "@effect/cli/Command" -import * as Prompt from "@effect/cli/Prompt" import * as Console from "effect/Console" +import * as Data from "effect/Data" import * as Effect from "effect/Effect" +import * as Runtime from "effect/Runtime" +import * as Command from "effect/unstable/cli/Command" +import * as Prompt from "effect/unstable/cli/Prompt" -const handleLogoutCommand = Effect.fnUntraced(function*() { - const auth = yield* Auth.Auth +export class LogoutCommandError extends Data.TaggedError("LogoutCommandError")<{ + readonly cause: AuthError.AuthCacheError +}> { + override readonly [Runtime.errorExitCode] = 1 + override readonly [Runtime.errorReported] = false +} - const shouldLogout = yield* Prompt.confirm({ - message: "Are you sure you want to logout of Amp?", - initial: false - }) +const handleLogoutCommand = Effect.fnUntraced( + function*() { + const auth = yield* Auth.Auth - if (!shouldLogout) { - return yield* Console.error("Logout cancelled, exiting...") - } + const shouldLogout = yield* Prompt.confirm({ + message: "Are you sure you want to logout of Amp?", + initial: false + }) - yield* auth.clearCachedAuthInfo + if (!shouldLogout) { + return yield* Console.error("Logout cancelled, exiting...") + } - yield* Console.error("You have successfully logged out!") -}) + yield* auth.clearCachedAuthInfo + + yield* Console.error("You have successfully logged out!") + }, + Effect.catchTag("QuitError", () => Effect.void), + Effect.mapError((cause) => new LogoutCommandError({ cause })) +) export const LogoutCommand = Command.make("logout").pipe( Command.withDescription("Logout of the Amp CLI"), diff --git a/packages/cli/src/commands/auth/token.ts b/packages/cli/src/commands/auth/token.ts index e1d65a9..88bf9b3 100644 --- a/packages/cli/src/commands/auth/token.ts +++ b/packages/cli/src/commands/auth/token.ts @@ -1,30 +1,46 @@ +import * as AuthError from "@edgeandnode/amp/auth/error" import * as Auth from "@edgeandnode/amp/auth/service" import * as Models from "@edgeandnode/amp/core" -import * as Args from "@effect/cli/Args" -import * as Command from "@effect/cli/Command" -import * as Options from "@effect/cli/Options" import * as Console from "effect/Console" +import * as Data from "effect/Data" import * as DateTime from "effect/DateTime" import * as Effect from "effect/Effect" import * as Redacted from "effect/Redacted" +import * as Runtime from "effect/Runtime" import * as String from "effect/String" -import * as Errors from "../../errors.ts" +import * as Argument from "effect/unstable/cli/Argument" +import * as Command from "effect/unstable/cli/Command" +import * as Flag from "effect/unstable/cli/Flag" -const audience = Options.text("audience").pipe( - Options.withAlias("a"), - Options.withDescription( +export class TokenCommandError extends Data.TaggedError("TokenCommandError")<{ + readonly cause?: + | AuthError.AuthCacheError + | AuthError.AuthDeviceFlowError + | AuthError.AuthNetworkError + | AuthError.AuthProtocolError + | AuthError.AuthRefreshError + | AuthError.AuthRequestError + | undefined +}> { + override readonly [Runtime.errorExitCode] = 1 + override readonly [Runtime.errorReported] = false +} + +const audience = Flag.string("audience").pipe( + Flag.withAlias("a"), + Flag.withDescription( "URLs that are valid to use the generated access token. " + "Becomes the JWT aud value" ), - Options.repeated + Flag.atLeast(0) ) -const duration = Args.text({ name: "duration" }).pipe( - Args.withDescription( +const duration = Argument.string("duration").pipe( + Argument.withDescription( "Duration of the generated access token before it expires. " + "Ex: \"7 days\", \"30 days\", \"1 hour\"" ), - Args.withSchema(Models.TokenDuration) + Argument.withSchema(Models.TokenDuration) ) const handleTokenCommand = Effect.fnUntraced(function*({ audience, duration }: { @@ -34,41 +50,45 @@ const handleTokenCommand = Effect.fnUntraced(function*({ audience, duration }: { const auth = yield* Auth.Auth const authInfo = yield* auth.getCachedAuthInfo.pipe( - Effect.flatten, + Effect.flatMap(Effect.fromOption), Effect.catchTag( - "NoSuchElementException", + "NoSuchElementError", Effect.fnUntraced(function*() { const errorMessage = [ "You must be authenticated with Amp to generate an access token.", "Run \"amp auth login\" to authenticate." ].join(" ") yield* Console.error(errorMessage) - return yield* new Errors.NonZeroExitCode() + return yield* new TokenCommandError({}) }) ) ) const response = yield* auth.generateAccessToken({ authInfo, audience, duration }).pipe( - Effect.catchAll(Effect.fnUntraced(function*(error) { - const errorMessage = `${error.userMessage}. ${error.userSuggestion}` + Effect.catch(Effect.fnUntraced(function*(error) { + const userMessage = AuthError.getUserMessage(error) + const userSuggestion = AuthError.getUserSuggestion(error) + const errorMessage = `${userMessage}. ${userSuggestion}` yield* Console.error(errorMessage) - return yield* new Errors.NonZeroExitCode() + return yield* new TokenCommandError({}) })) ) yield* auth.verifyAccessToken(Redacted.make(response.token), response.iss).pipe( - Effect.catchAll(Effect.fnUntraced(function*(error) { + Effect.catch(Effect.fnUntraced(function*(error) { + const userMessage = AuthError.getUserMessage(error) + const userSuggestion = AuthError.getUserSuggestion(error) const errorMessage = [ "Failed to verify the signed token.", - error.userMessage, - error.userSuggestion + userMessage, + userSuggestion ].join("\n") yield* Console.error(errorMessage) - return yield* new Errors.NonZeroExitCode() + return yield* new TokenCommandError({}) })) ) - const expiresAt = DateTime.unsafeMake(response.exp * 1000) + const expiresAt = DateTime.makeUnsafe(response.exp * 1000) const formatDateTime = DateTime.formatLocal({ timeStyle: "full", dateStyle: "medium" @@ -84,7 +104,7 @@ const handleTokenCommand = Effect.fnUntraced(function*({ audience, duration }: { ].join("\n\n") yield* Console.error(message) -}) +}, Effect.catchTag("AuthCacheError", (cause) => Effect.fail(new TokenCommandError({ cause })))) export const TokenCommand = Command.make("token", { audience, duration }).pipe( Command.withDescription( diff --git a/packages/cli/src/commands/query.ts b/packages/cli/src/commands/query.ts index 6b4a0f6..adf07ab 100644 --- a/packages/cli/src/commands/query.ts +++ b/packages/cli/src/commands/query.ts @@ -1,37 +1,46 @@ import * as ArrowFlight from "@edgeandnode/amp/arrow-flight" import * as NodeArrowFlight from "@edgeandnode/amp/arrow-flight/node" -import * as Args from "@effect/cli/Args" -import * as Command from "@effect/cli/Command" -import * as Options from "@effect/cli/Options" import * as Console from "effect/Console" +import * as Data from "effect/Data" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import type * as Redacted from "effect/Redacted" +import * as Runtime from "effect/Runtime" +import * as Argument from "effect/unstable/cli/Argument" +import * as Command from "effect/unstable/cli/Command" +import * as Flag from "effect/unstable/cli/Flag" type ResultFormat = "json" | "jsonl" | "pretty" | "table" const ResultFormats: ReadonlyArray = ["json", "jsonl", "pretty", "table"] +export class QueryCommandError extends Data.TaggedError("QueryCommandError")<{ + readonly cause: ArrowFlight.ArrowFlightError +}> { + override readonly [Runtime.errorExitCode] = 1 + override readonly [Runtime.errorReported] = false +} + // TODO(Chris): we should re-evaluate this format option -const format = Options.choice("format", ResultFormats).pipe( - Options.withAlias("f"), - Options.withDescription("The format to output the results in."), - Options.withDefault("table") +const format = Flag.choice("format", ResultFormats).pipe( + Flag.withAlias("f"), + Flag.withDescription("The format to output the results in."), + Flag.withDefault("table") ) -const limit = Options.integer("limit").pipe( - Options.withDescription("The number of rows to return from the query."), - Options.optional +const limit = Flag.integer("limit").pipe( + Flag.withDescription("The number of rows to return from the query."), + Flag.optional ) -const query = Args.text({ name: "query" }).pipe( - Args.withDescription("The SQL query to execute.") +const query = Argument.string("query").pipe( + Argument.withDescription("The SQL query to execute.") ) -const token = Options.redacted("token").pipe( - Options.withAlias("t"), - Options.withDescription("The bearer token to use for authentication."), - Options.optional +const token = Flag.redacted("token").pipe( + Flag.withAlias("t"), + Flag.withDescription("The bearer token to use for authentication."), + Flag.optional ) const queryCommandHandler = Effect.fnUntraced(function*(params: { @@ -67,7 +76,7 @@ const queryCommandHandler = Effect.fnUntraced(function*(params: { return yield* Console.table(data) } } -}) +}, Effect.mapError((cause) => new QueryCommandError({ cause }))) export const QueryCommand = Command.make("query", { format, limit, query, token }).pipe( Command.withDescription("Execute a SQL query with Amp"), diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts deleted file mode 100644 index f2bed6a..0000000 --- a/packages/cli/src/errors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Data from "effect/Data" - -export class NonZeroExitCode extends Data.TaggedError("Amp/NonZeroExitCode") {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b139e2a..9cb1afe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,24 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@effect/language-service': + specifier: ^0.85.1 + version: 0.85.1 + '@effect/platform-node': + specifier: ^4.0.0-beta.56 + version: 4.0.0-beta.56 + '@effect/vitest': + specifier: ^4.0.0-beta.56 + version: 4.0.0-beta.56 + effect: + specifier: ^4.0.0-beta.56 + version: 4.0.0-beta.56 + viem: + specifier: ^2.46.1 + version: 2.48.4 + overrides: typescript: ^5.9.3 @@ -27,17 +45,11 @@ importers: specifier: ^7.28.6 version: 7.28.6(@babel/core@7.28.6) '@effect/language-service': - specifier: ^0.74.0 - version: 0.74.0 - '@effect/platform': - specifier: ^0.94.5 - version: 0.94.5(effect@3.19.18) - '@effect/platform-node': - specifier: ^0.104.1 - version: 0.104.1(@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) + specifier: 'catalog:' + version: 0.85.1 '@effect/vitest': - specifier: ^0.27.0 - version: 0.27.0(effect@3.19.18)(vitest@4.0.18) + specifier: 'catalog:' + version: 4.0.0-beta.56(effect@4.0.0-beta.56)(vitest@4.0.18) '@types/node': specifier: ^25.2.3 version: 25.2.3 @@ -56,9 +68,6 @@ importers: dprint: specifier: ^0.51.1 version: 0.51.1 - effect: - specifier: ^3.19.18 - version: 3.19.18 glob: specifier: ^13.0.0 version: 13.0.0 @@ -82,10 +91,10 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: ^6.0.5 - version: 6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2)) + version: 6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3) vitest-mock-express: specifier: ^2.2.0 version: 2.2.0 @@ -99,8 +108,8 @@ importers: specifier: ^6.1.3 version: 6.1.3 viem: - specifier: ^2.46.1 - version: 2.46.1(typescript@5.9.3)(zod@4.3.6) + specifier: 'catalog:' + version: 2.48.4(typescript@5.9.3)(zod@4.3.6) devDependencies: '@bufbuild/buf': specifier: ^1.65.0 @@ -117,12 +126,9 @@ importers: '@connectrpc/connect-node': specifier: ^2.1.1 version: 2.1.1(@bufbuild/protobuf@2.11.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.11.0)) - '@effect/platform': - specifier: ^0.94.5 - version: 0.94.5(effect@3.19.18) effect: - specifier: ^3.19.18 - version: 3.19.18 + specifier: 'catalog:' + version: 4.0.0-beta.56 packages/cli: dependencies: @@ -136,18 +142,12 @@ importers: '@connectrpc/connect': specifier: ^2.1.1 version: 2.1.1(@bufbuild/protobuf@2.11.0) - '@effect/cli': - specifier: ^0.73.2 - version: 0.73.2(@effect/platform@0.94.5(effect@3.19.18))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) - '@effect/platform': - specifier: ^0.94.5 - version: 0.94.5(effect@3.19.18) '@effect/platform-node': - specifier: ^0.104.1 - version: 0.104.1(@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) + specifier: 'catalog:' + version: 4.0.0-beta.56(effect@4.0.0-beta.56)(ioredis@5.10.1) effect: - specifier: ^3.19.18 - version: 3.19.18 + specifier: 'catalog:' + version: 4.0.0-beta.56 packages/tools/oxc: devDependencies: @@ -156,7 +156,7 @@ importers: version: 25.1.0 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3) scratchpad: dependencies: @@ -400,106 +400,28 @@ packages: cpu: [x64] os: [win32] - '@effect/cli@0.73.2': - resolution: {integrity: sha512-K8IJo81+qa1LU8dhxcDU4QO/bIjL/dPd3zUOSCpLiuUNz8Y3/T+WNs3GqIXEhMfCFMSlRZERN0YgmtRlEZUREA==} - peerDependencies: - '@effect/platform': ^0.94.3 - '@effect/printer': ^0.47.0 - '@effect/printer-ansi': ^0.47.0 - effect: ^3.19.16 - - '@effect/cluster@0.56.0': - resolution: {integrity: sha512-ovhsC8jQkgoHkelpGL/EQtoQsPXG9hj7DHVc7A9Vyzptd/DaCP+W1aQKlJrJoe2W+KI/CjrYN8H89M5xiL6FBg==} - peerDependencies: - '@effect/platform': ^0.94.0 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - '@effect/workflow': ^0.16.0 - effect: ^3.19.13 - - '@effect/experimental@0.58.0': - resolution: {integrity: sha512-IEP9sapjF6rFy5TkoqDPc86st/fnqUfjT7Xa3pWJrFGr1hzaMXHo+mWsYOZS9LAOVKnpHuVziDK97EP5qsCHVA==} - peerDependencies: - '@effect/platform': ^0.94.0 - effect: ^3.19.13 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/language-service@0.74.0': - resolution: {integrity: sha512-ZqOvxCUo70WtnV8Kf4ORcEW6euepPn3Yu9btB0vpDhN5ydf1RC3/kl2M2b22tzB0MOITeJzNn5HK7w7NJhQFaw==} + '@effect/language-service@0.85.1': + resolution: {integrity: sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw==} hasBin: true - '@effect/platform-node-shared@0.57.1': - resolution: {integrity: sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform-node@0.104.1': - resolution: {integrity: sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform@0.94.5': - resolution: {integrity: sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A==} - peerDependencies: - effect: ^3.19.17 - - '@effect/printer-ansi@0.47.0': - resolution: {integrity: sha512-tDEQ9XJpXDNYoWMQJHFRMxKGmEOu6z32x3Kb8YLOV5nkauEKnKmWNs7NBp8iio/pqoJbaSwqDwUg9jXVquxfWQ==} - peerDependencies: - '@effect/typeclass': ^0.38.0 - effect: ^3.19.0 - - '@effect/printer@0.47.0': - resolution: {integrity: sha512-VgR8e+YWWhMEAh9qFOjwiZ3OXluAbcVLIOtvp2S5di1nSrPOZxj78g8LE77JSvyfp5y5bS2gmFW+G7xD5uU+2Q==} - peerDependencies: - '@effect/typeclass': ^0.38.0 - effect: ^3.19.0 - - '@effect/rpc@0.73.0': - resolution: {integrity: sha512-iMPf6tTriz8sK0l5x4koFId8Hz5nFptHYg8WqyjHGIIVLTpZxuiSqhmXZG7FnAs5N2n6uCEws4wWGcIgXNUrFg==} - peerDependencies: - '@effect/platform': ^0.94.0 - effect: ^3.19.13 - - '@effect/sql@0.49.0': - resolution: {integrity: sha512-9UEKR+z+MrI/qMAmSvb/RiD9KlgIazjZUCDSpwNgm0lEK9/Q6ExEyfziiYFVCPiptp52cBw8uBHRic8hHnwqXA==} - peerDependencies: - '@effect/experimental': ^0.58.0 - '@effect/platform': ^0.94.0 - effect: ^3.19.13 - - '@effect/typeclass@0.38.0': - resolution: {integrity: sha512-lMUcJTRtG8KXhXoczapZDxbLK5os7M6rn0zkvOgncJW++A0UyelZfMVMKdT5R+fgpZcsAU/1diaqw3uqLJwGxA==} + '@effect/platform-node-shared@4.0.0-beta.56': + resolution: {integrity: sha512-bwIGYjdOMhrQZdBqgE5/cJW+k3+/xVmutYFzSZTAlFU0FJRW/ymrMp8hQ+Cj2J+rjuLal0QOh3FRC/YfvmK0jw==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.19.0 + effect: ^4.0.0-beta.56 - '@effect/vitest@0.27.0': - resolution: {integrity: sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ==} + '@effect/platform-node@4.0.0-beta.56': + resolution: {integrity: sha512-rTi9EZGdcvJ3O/M7jqCxvSFlWFt0FpB+NJ1ZAySUU9uxKKQgBFuOY6u1HdbYfYDI0YuVbLxfL5QN+thUggY8ow==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.19.0 - vitest: ^3.2.0 + effect: ^4.0.0-beta.56 + ioredis: ^5.7.0 - '@effect/workflow@0.16.0': - resolution: {integrity: sha512-MiAdlxx3TixkgHdbw+Yf1Z3tHAAE0rOQga12kIydJqj05Fnod+W/I+kQGRMY/XWRg+QUsVxhmh1qTr7Ype6lrw==} + '@effect/vitest@4.0.0-beta.56': + resolution: {integrity: sha512-4G5yVBWFMnctS4qagAM2cT/hIuqejzFu9JrBcE48jahe3cV0sLRP6mSQF4OMTSakRO2AdCW9szCanthN6HkWWw==} peerDependencies: - '@effect/experimental': ^0.58.0 - '@effect/platform': ^0.94.0 - '@effect/rpc': ^0.73.0 - effect: ^3.19.13 + effect: ^4.0.0-beta.56 + vitest: ^3.0.0 || ^4.0.0 '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} @@ -663,6 +585,9 @@ packages: peerDependencies: hono: ^4 + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -846,88 +771,6 @@ packages: cpu: [x64] os: [win32] - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1134,6 +977,9 @@ packages: '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1149,6 +995,9 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/project-service@8.50.0': resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1456,6 +1305,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1566,6 +1419,10 @@ packages: delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -1644,8 +1501,8 @@ packages: effect@3.19.16: resolution: {integrity: sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ==} - effect@3.19.18: - resolution: {integrity: sha512-KlbNuYzzwpOpnpshIhjCaqweQkthAT1oVG61Z2wIHqo6Sb6n/+pgzFXyTvsLyxcx5Cg3aWaQXa0XQHMuzdVW4A==} + effect@4.0.0-beta.56: + resolution: {integrity: sha512-VToZmpoEkH4hHFHeHDMPRwsPQY7BOCEY7uy8yI1LaFEtjZ5w3T3S4yDbKFA7L63OhPo+kZdSvL3tBLOLLcmrQA==} electron-to-chromium@1.5.278: resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} @@ -1771,6 +1628,10 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-check@4.7.0: + resolution: {integrity: sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==} + engines: {node: '>=12.17.0'} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -2019,6 +1880,14 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -2201,6 +2070,12 @@ packages: kubernetes-types@1.30.0: resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -2276,9 +2151,9 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} hasBin: true mimic-fn@2.1.0: @@ -2324,8 +2199,8 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true - msgpackr@1.11.8: - resolution: {integrity: sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==} + msgpackr@1.11.10: + resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} multipasta@0.2.7: resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} @@ -2343,9 +2218,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -2395,8 +2267,8 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - ox@0.12.1: - resolution: {integrity: sha512-uU0llpthaaw4UJoXlseCyBHmQ3bLrQmz9rRLIAUHqv46uHuae9SE+ukYBRIPVCnlEnHKuWjDUcDFHWx9gbGNoA==} + ox@0.14.20: + resolution: {integrity: sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==} peerDependencies: typescript: ^5.9.3 peerDependenciesMeta: @@ -2522,6 +2394,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@8.4.0: + resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -2552,6 +2427,14 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2706,6 +2589,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -2818,8 +2704,9 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + toml@4.1.1: + resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} + engines: {node: '>=20'} totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} @@ -2873,10 +2760,17 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.19.0: - resolution: {integrity: sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} + undici@8.1.0: + resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==} + engines: {node: '>=22.19.0'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -2904,8 +2798,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true valibot@1.2.0: @@ -2920,8 +2814,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - viem@2.46.1: - resolution: {integrity: sha512-c5YPQR/VueqoPG09Tp1JBw2iItKVRGVI0YkWekquRDZw0ciNBhO3muu2QjO9xFelOXh18q3d/kLbW83B2Oxf0g==} + viem@2.48.4: + resolution: {integrity: sha512-mReP/rgY2P+WeeRSG4sUvccCLKfyAW1C73Y3KkobAqgzYmVna9qyUMNE44xIUkDtfvRuC33r24UhF4baBYovsg==} peerDependencies: typescript: ^5.9.3 peerDependenciesMeta: @@ -3051,8 +2945,8 @@ packages: utf-8-validate: optional: true - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3097,8 +2991,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -3355,108 +3249,32 @@ snapshots: '@dprint/win32-x64@0.51.1': optional: true - '@effect/cli@0.73.2(@effect/platform@0.94.5(effect@3.19.18))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/platform': 0.94.5(effect@3.19.18) - '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18) - '@effect/printer-ansi': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18) - effect: 3.19.18 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.2 - - '@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/platform': 0.94.5(effect@3.19.18) - '@effect/rpc': 0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/workflow': 0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) - effect: 3.19.18 - kubernetes-types: 1.30.0 + '@effect/language-service@0.85.1': {} - '@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18)': + '@effect/platform-node-shared@4.0.0-beta.56(effect@4.0.0-beta.56)': dependencies: - '@effect/platform': 0.94.5(effect@3.19.18) - effect: 3.19.18 - uuid: 11.1.0 - - '@effect/language-service@0.74.0': {} - - '@effect/platform-node-shared@0.57.1(@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/cluster': 0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) - '@effect/platform': 0.94.5(effect@3.19.18) - '@effect/rpc': 0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@parcel/watcher': 2.5.6 - effect: 3.19.18 - multipasta: 0.2.7 - ws: 8.19.0 + '@types/ws': 8.18.1 + effect: 4.0.0-beta.56 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.104.1(@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/cluster': 0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) - '@effect/platform': 0.94.5(effect@3.19.18) - '@effect/platform-node-shared': 0.57.1(@effect/cluster@0.56.0(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18) - '@effect/rpc': 0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/sql': 0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - effect: 3.19.18 - mime: 3.0.0 - undici: 7.19.0 - ws: 8.19.0 + '@effect/platform-node@4.0.0-beta.56(effect@4.0.0-beta.56)(ioredis@5.10.1)': + dependencies: + '@effect/platform-node-shared': 4.0.0-beta.56(effect@4.0.0-beta.56) + effect: 4.0.0-beta.56 + ioredis: 5.10.1 + mime: 4.1.0 + undici: 8.1.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform@0.94.5(effect@3.19.18)': - dependencies: - effect: 3.19.18 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.8 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18) - '@effect/typeclass': 0.38.0(effect@3.19.18) - effect: 3.19.18 - - '@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/typeclass': 0.38.0(effect@3.19.18) - effect: 3.19.18 - - '@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/platform': 0.94.5(effect@3.19.18) - effect: 3.19.18 - msgpackr: 1.11.8 - - '@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18)': - dependencies: - '@effect/experimental': 0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/platform': 0.94.5(effect@3.19.18) - effect: 3.19.18 - uuid: 11.1.0 - - '@effect/typeclass@0.38.0(effect@3.19.18)': - dependencies: - effect: 3.19.18 - - '@effect/vitest@0.27.0(effect@3.19.18)(vitest@4.0.18)': - dependencies: - effect: 3.19.18 - vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) - - '@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(@effect/platform@0.94.5(effect@3.19.18))(@effect/rpc@0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18))(effect@3.19.18)': + '@effect/vitest@4.0.0-beta.56(effect@4.0.0-beta.56)(vitest@4.0.18)': dependencies: - '@effect/experimental': 0.58.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - '@effect/platform': 0.94.5(effect@3.19.18) - '@effect/rpc': 0.73.0(@effect/platform@0.94.5(effect@3.19.18))(effect@3.19.18) - effect: 3.19.18 + effect: 4.0.0-beta.56 + vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3) '@esbuild/aix-ppc64@0.27.2': optional: true @@ -3540,6 +3358,8 @@ snapshots: dependencies: hono: 4.11.8 + '@ioredis/commands@1.5.1': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -3714,66 +3534,6 @@ snapshots: '@oxlint/win32-x64@1.42.0': optional: true - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - '@polka/url@1.0.0-next.29': {} '@rollup/rollup-android-arm-eabi@4.56.0': @@ -3940,6 +3700,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -3959,6 +3723,10 @@ snapshots: '@types/node': 25.2.3 '@types/send': 0.17.6 + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.6.0 + '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) @@ -4048,7 +3816,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3) '@vitest/expect@4.0.18': dependencies: @@ -4059,13 +3827,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3) '@vitest/pretty-format@4.0.18': dependencies: @@ -4093,7 +3861,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3) '@vitest/utils@4.0.18': dependencies: @@ -4317,6 +4085,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4396,6 +4166,8 @@ snapshots: delegates@1.0.0: {} + denque@2.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -4411,7 +4183,8 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true detective-amd@6.0.1: dependencies: @@ -4496,10 +4269,18 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - effect@3.19.18: + effect@4.0.0-beta.56: dependencies: '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 + fast-check: 4.7.0 + find-my-way-ts: 0.1.6 + ini: 6.0.0 + kubernetes-types: 1.30.0 + msgpackr: 1.11.10 + multipasta: 0.2.7 + toml: 4.1.1 + uuid: 13.0.0 + yaml: 2.8.3 electron-to-chromium@1.5.278: {} @@ -4665,6 +4446,10 @@ snapshots: dependencies: pure-rand: 6.1.0 + fast-check@4.7.0: + dependencies: + pure-rand: 8.4.0 + fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -4689,7 +4474,7 @@ snapshots: hono: 4.11.8 mcp-proxy: 6.4.0 strict-event-emitter-types: 2.0.0 - undici: 7.19.0 + undici: 7.25.0 uri-templates: 0.2.0 xsschema: 0.4.0-beta.5(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(effect@3.19.16)(sury@11.0.0-alpha.4)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6) yargs: 18.0.0 @@ -4939,6 +4724,22 @@ snapshots: ini@4.1.3: {} + ini@6.0.0: {} + + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.0.1: {} ipaddr.js@1.9.1: {} @@ -5086,6 +4887,10 @@ snapshots: kubernetes-types@1.30.0: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -5166,7 +4971,7 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@3.0.0: {} + mime@4.1.0: {} mimic-fn@2.1.0: {} @@ -5214,7 +5019,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true - msgpackr@1.11.8: + msgpackr@1.11.10: optionalDependencies: msgpackr-extract: 3.0.3 @@ -5226,8 +5031,6 @@ snapshots: negotiator@1.0.0: {} - node-addon-api@7.1.1: {} - node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -5286,7 +5089,7 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - ox@0.12.1(typescript@5.9.3)(zod@4.3.6): + ox@0.14.20(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -5419,6 +5222,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@8.4.0: {} + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -5454,6 +5259,12 @@ snapshots: picomatch: 2.3.1 optional: true + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + require-from-string@2.0.2: {} requirejs-config-file@4.0.0: @@ -5660,6 +5471,8 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@1.5.0: {} statuses@2.0.2: {} @@ -5749,7 +5562,7 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - toml@3.0.0: {} + toml@4.1.1: {} totalist@3.0.1: {} @@ -5797,7 +5610,11 @@ snapshots: undici-types@7.16.0: {} - undici@7.19.0: {} + undici-types@7.19.2: {} + + undici@7.25.0: {} + + undici@8.1.0: {} unicorn-magic@0.3.0: {} @@ -5817,7 +5634,7 @@ snapshots: util-deprecate@1.0.2: {} - uuid@11.1.0: {} + uuid@13.0.0: {} valibot@1.2.0(typescript@5.9.3): optionalDependencies: @@ -5825,7 +5642,7 @@ snapshots: vary@1.1.2: {} - viem@2.46.1(typescript@5.9.3)(zod@4.3.6): + viem@2.48.4(typescript@5.9.3)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -5833,7 +5650,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) isows: 1.0.7(ws@8.18.3) - ox: 0.12.1(typescript@5.9.3)(zod@4.3.6) + ox: 0.14.20(typescript@5.9.3)(zod@4.3.6) ws: 8.18.3 optionalDependencies: typescript: 5.9.3 @@ -5842,17 +5659,17 @@ snapshots: - utf-8-validate - zod - vite-tsconfig-paths@6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2): + vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.3): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -5864,9 +5681,9 @@ snapshots: '@types/node': 25.1.0 fsevents: 2.3.3 jiti: 2.6.1 - yaml: 2.8.2 + yaml: 2.8.3 - vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2): + vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -5878,16 +5695,16 @@ snapshots: '@types/node': 25.2.3 fsevents: 2.3.3 jiti: 2.6.1 - yaml: 2.8.2 + yaml: 2.8.3 vitest-mock-express@2.2.0: dependencies: '@types/express': 4.17.25 - vitest@4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -5904,7 +5721,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.1.0 @@ -5922,10 +5739,10 @@ snapshots: - tsx - yaml - vitest@4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -5942,7 +5759,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.2.3 @@ -5989,7 +5806,7 @@ snapshots: ws@8.18.3: {} - ws@8.19.0: {} + ws@8.20.0: {} wsl-utils@0.3.1: dependencies: @@ -6008,7 +5825,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yargs-parser@22.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 28ba6c2..2a3e358 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,10 @@ packages: - scripts - packages/* - packages/tools/* + +catalog: + "@effect/language-service": ^0.85.1 + "@effect/platform-node": ^4.0.0-beta.56 + "@effect/vitest": ^4.0.0-beta.56 + "effect": ^4.0.0-beta.56 + "viem": ^2.46.1 diff --git a/tsconfig.base.json b/tsconfig.base.json index b30d874..3e62341 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json", "include": [], "compilerOptions": { "outDir": "${configDir}/dist", From 3bf430cfc5d596b7ca71ee4e98fd7d8f5b4e1938 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Thu, 23 Apr 2026 14:52:57 -0400 Subject: [PATCH 2/2] fix type errors --- packages/amp/test/cdc-stream/stream.test.ts | 1 - packages/amp/test/protocol-stream/validation.test.ts | 8 -------- 2 files changed, 9 deletions(-) diff --git a/packages/amp/test/cdc-stream/stream.test.ts b/packages/amp/test/cdc-stream/stream.test.ts index c128cb0..d77ee05 100644 --- a/packages/amp/test/cdc-stream/stream.test.ts +++ b/packages/amp/test/cdc-stream/stream.test.ts @@ -17,7 +17,6 @@ import { watermarkEvent } from "@edgeandnode/amp/transactional-stream" import { describe, expect, it } from "@effect/vitest" -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Ref from "effect/Ref" diff --git a/packages/amp/test/protocol-stream/validation.test.ts b/packages/amp/test/protocol-stream/validation.test.ts index b374221..31ec6b6 100644 --- a/packages/amp/test/protocol-stream/validation.test.ts +++ b/packages/amp/test/protocol-stream/validation.test.ts @@ -6,14 +6,7 @@ */ import type { BlockHash, BlockNumber, BlockRange, Network } from "@edgeandnode/amp/core" import { - DuplicateNetworkError, GapError, - HashMismatchOnConsecutiveBlocksError, - InvalidPrevHashError, - InvalidReorgError, - MissingPrevHashError, - NetworkCountChangedError, - UnexpectedNetworkError, validateAll, validateConsecutiveness, validateNetworks, @@ -21,7 +14,6 @@ import { } from "@edgeandnode/amp/protocol-stream" import { describe, it } from "@effect/vitest" import * as Effect from "effect/Effect" -import * as Result from "effect/Result" // ============================================================================= // Test Helpers