From 6d2b2f58ca51908f5b41b99aa9923735b9496c88 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 05:30:31 +0200 Subject: [PATCH 01/15] Move Core units into their owning namespaces --- Documentation/reference/capabilities.md | 4 +- Samples/Library/e2e.test.mjs | 8 +-- Source/Core/ArcApplicationBuilder.ts | 4 +- Source/Core/ArcServer.ts | 6 +- .../Core/authorization/AuthorizationPolicy.ts | 2 +- Source/Core/authorization/authorized.ts | 2 +- Source/Core/{results => commands}/ArcTuple.ts | 0 Source/Core/commands/CommandDefinition.ts | 4 +- .../Core/commands/CommandFailureSnapshot.ts | 4 +- ...tionDeclaration.ts => CommandOperation.ts} | 0 .../commands/CommandOperationExecution.ts | 2 +- Source/Core/commands/CommandOperations.ts | 2 +- .../commands/CommandResponseValueHandler.ts | 2 +- Source/Core/{results => commands}/Outcome.ts | 0 .../createCommandResult.ts} | 2 +- .../when_identifying_branded_outcomes.ts | 0 .../when_identifying_unbranded_values.ts | 0 .../for_tuple/when_grouping_values.ts | 0 Source/Core/commands/index.ts | 8 +-- .../Core/commands/modelBound/PreparedValue.ts | 2 +- .../commands/modelBound/commandArgument.ts | 69 +++---------------- .../modelBound/commandContextArgument.ts | 69 ++++++++++++++++--- .../{readModel.ts => commandReadModel.ts} | 0 .../commands/modelBound/compileCommand.ts | 4 +- .../modelBound/encodeCommandResponse.ts | 8 +-- .../Core/commands/prepareCommandResponse.ts | 2 +- .../Core/commands/processCommandResponse.ts | 8 +-- ...andOperation.ts => runCommandOperation.ts} | 10 +-- Source/Core/{results => commands}/tuple.ts | 0 .../ArcApplicationServices.ts | 10 +-- .../with_a_failing_disposer.ts | 2 +- .../optionalService.ts | 4 +- .../{results => execution}/failureTracking.ts | 0 Source/Core/{ => execution}/observability.ts | 0 Source/Core/fetch.ts | 6 +- .../given/an_operation_command.ts | 4 +- .../with_ignored_failure.ts | 2 +- ...ith_a_committed_or_failing_compensation.ts | 4 +- .../with_a_later_scope_failure.ts | 2 +- .../with_cancellation_or_budget_expiry.ts | 2 +- .../with_a_child_or_parent_declaration.ts | 2 +- .../with_invalid_configuration.ts | 2 +- .../with_a_missing_later_dependency.ts | 2 +- ...invalid_values_or_controlled_operations.ts | 2 +- .../with_multiple_handlers_and_control.ts | 2 +- .../with_scoped_handlers.ts | 2 +- .../with_a_rejection.ts | 2 +- .../with_an_empty_rejection.ts | 2 +- .../with_filtered_warnings_and_denial.ts | 2 +- Source/Core/{ => http}/DescriptorBase.ts | 6 +- Source/Core/http/Operation.ts | 2 +- Source/Core/http/createRouteTable.ts | 2 +- Source/Core/http/handleRequest.ts | 4 +- Source/Core/{results => http}/malformed.ts | 0 Source/Core/{results => http}/status.ts | 0 Source/Core/index.ts | 6 +- Source/Core/queries/QueryDefinition.ts | 2 +- .../createQueryResult.ts} | 2 +- .../Core/{results => queries}/emptyPaging.ts | 2 +- .../queries/observable/ObservableOperation.ts | 2 +- .../observable/ObservableQuerySession.ts | 4 +- .../{ => observable}/ObservableSessions.ts | 22 +++--- .../with_repeated_session.ts | 4 +- .../given/a_transfer_result.ts | 2 +- .../observable/observableCleanupFailures.ts | 2 +- Source/Core/queries/queryOperation.ts | 8 +-- Source/Core/queries/queryRendering.ts | 2 +- Source/Core/queries/renderQuery.ts | 2 +- Source/Core/reflection/resolveAll.ts | 9 --- Source/Core/reflection/wireSchema.ts | 2 +- Source/Core/results/index.ts | 20 +++--- .../Core/validation/readModelForValidation.ts | 4 +- 72 files changed, 188 insertions(+), 197 deletions(-) rename Source/Core/{results => commands}/ArcTuple.ts (100%) rename Source/Core/commands/{CommandOperationDeclaration.ts => CommandOperation.ts} (100%) rename Source/Core/{results => commands}/Outcome.ts (100%) rename Source/Core/{results/commandResult.ts => commands/createCommandResult.ts} (94%) rename Source/Core/{results => commands}/for_Outcome/when_identifying_branded_outcomes.ts (100%) rename Source/Core/{results => commands}/for_Outcome/when_identifying_unbranded_values.ts (100%) rename Source/Core/{results => commands}/for_tuple/when_grouping_values.ts (100%) rename Source/Core/commands/modelBound/{readModel.ts => commandReadModel.ts} (100%) rename Source/Core/commands/{commandOperation.ts => runCommandOperation.ts} (97%) rename Source/Core/{results => commands}/tuple.ts (100%) rename Source/Core/{ => dependencyInjection}/ArcApplicationServices.ts (87%) rename Source/Core/{reflection => dependencyInjection}/optionalService.ts (81%) rename Source/Core/{results => execution}/failureTracking.ts (100%) rename Source/Core/{ => execution}/observability.ts (100%) rename Source/Core/{ => http}/DescriptorBase.ts (77%) rename Source/Core/{results => http}/malformed.ts (100%) rename Source/Core/{results => http}/status.ts (100%) rename Source/Core/{results/queryResult.ts => queries/createQueryResult.ts} (94%) rename Source/Core/{results => queries}/emptyPaging.ts (82%) rename Source/Core/queries/{ => observable}/ObservableSessions.ts (88%) rename Source/Core/queries/{ => observable}/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts (89%) delete mode 100644 Source/Core/reflection/resolveAll.ts diff --git a/Documentation/reference/capabilities.md b/Documentation/reference/capabilities.md index 8e2c3095..2efc4cd2 100644 --- a/Documentation/reference/capabilities.md +++ b/Documentation/reference/capabilities.md @@ -28,8 +28,8 @@ Evidence paths are relative to the repository root. Spec folders follow `for_/validate` runs authorization and validation only; a command named `Validate` executes on its own route. | `Source/Core/for_ArcServer/when_validating_a_command`, `.../when_handling_a_validate_named_command` | -| `provide()` and outcomes | Supported | `provide()` runs after validation and may short-circuit with `rejected(...)` or `denied(...)`. Only helper-created values are outcomes; `isOutcome` recognizes them. See [Command outcomes](../commands/command-outcomes.md). | `Source/Core/for_ArcServer/when_providing_a_command`, `Source/Core/for_ArcApplicationBuilder/when_providing_a_model_bound_command`, `Source/Core/results/for_Outcome` | -| Several return values and response value handlers | Bounded | `tuple(...)` flattens branded groups; at most one unhandled value becomes the response, and scoped `CommandResponseValueHandler`s process every other value in deterministic name order. Ordinary arrays are not flattened. At generation time, Chronicle event types/arrays and integration wrappers plus Arc operations are omitted from command responses; branded tuple results select the sole unhandled value, and unions with a single visible type are supported. No general OneOf or Result union classification. | `Source/Core/results/for_tuple`, `Source/Core/for_ArcServer/when_processing_command_response`, `Source/Core/for_ArcApplicationBuilder/when_registering_a_response_handler` | +| `provide()` and outcomes | Supported | `provide()` runs after validation and may short-circuit with `rejected(...)` or `denied(...)`. Only helper-created values are outcomes; `isOutcome` recognizes them. See [Command outcomes](../commands/command-outcomes.md). | `Source/Core/for_ArcServer/when_providing_a_command`, `Source/Core/for_ArcApplicationBuilder/when_providing_a_model_bound_command`, `Source/Core/commands/for_Outcome` | +| Several return values and response value handlers | Bounded | `tuple(...)` flattens branded groups; at most one unhandled value becomes the response, and scoped `CommandResponseValueHandler`s process every other value in deterministic name order. Ordinary arrays are not flattened. At generation time, Chronicle event types/arrays and integration wrappers plus Arc operations are omitted from command responses; branded tuple results select the sole unhandled value, and unions with a single visible type are supported. No general OneOf or Result union classification. | `Source/Core/commands/for_tuple`, `Source/Core/for_ArcServer/when_processing_command_response`, `Source/Core/for_ArcApplicationBuilder/when_registering_a_response_handler` | | Execution scopes | Supported | Low-level `scopes` complete once in reverse order, including a scope whose `begin` threw; a failed completion removes the response. Model-bound commands use command execution runners instead. | `Source/Core/for_ArcServer/when_beginning_a_command_scope`, `.../when_completing_a_command_scope`, `Source/Core/for_ArcApplicationBuilder/when_executing_command_runners` | | Command operations | Bounded | `CommandOperation` declarations are preflighted, executed in order, and compensated in reverse after a known uncommitted failure; `Unknown` and `Mixed` fail closed. No distributed transaction, durable recovery, or crash guarantee. See [Command operations](../commands/operations/index.md). | `Source/Core/for_ArcServer/when_executing_an_operation`, `.../when_an_operation_fails`, `.../when_a_commit_is_unknown`, `.../when_declaring_nested_operations` | | Command context and keys | Bounded | `CommandContext` carries the command, a key resolved once, case-insensitive values, and request identity. `@key()`, `getKey()`, and scoped `CommandKeyResolver`s; `abortSignal()`, `commandContext()`, and `provided(Type)` markers. See [Command context](../commands/command-context.md). | `Source/Core/for_ArcServer/when_resolving_command_keys`, `Source/Core/for_ArcApplicationBuilder/when_resolving_command_keys`, `.../when_resolving_command_arguments` | diff --git a/Samples/Library/e2e.test.mjs b/Samples/Library/e2e.test.mjs index ccd4b10e..b2b491ad 100644 --- a/Samples/Library/e2e.test.mjs +++ b/Samples/Library/e2e.test.mjs @@ -9,10 +9,10 @@ import { setTimeout } from 'node:timers/promises'; import { once } from 'node:events'; import { after, test } from 'node:test'; import { Guid } from '@cratis/fundamentals'; -import { RegisterAuthor } from './Web/src/generated/Authors/Registration/RegisterAuthor.proxy.ts'; -import { AuthorsPage } from './Web/src/generated/Authors/Listing/AuthorsPage.proxy.ts'; -import { AllAuthors } from './Web/src/generated/Authors/Listing/AllAuthors.proxy.ts'; -import { AddBook } from './Web/src/generated/Books/Registration/AddBook.proxy.ts'; +import { RegisterAuthor } from './Web/src/generated/Authors/Registration/RegisterAuthor.proxy.js'; +import { AuthorsPage } from './Web/src/generated/Authors/Listing/AuthorsPage.proxy.js'; +import { AllAuthors } from './Web/src/generated/Authors/Listing/AllAuthors.proxy.js'; +import { AddBook } from './Web/src/generated/Books/Registration/AddBook.proxy.js'; const socket = createServer(); socket.listen(0, '127.0.0.1'); diff --git a/Source/Core/ArcApplicationBuilder.ts b/Source/Core/ArcApplicationBuilder.ts index 1561aa86..29f90d37 100644 --- a/Source/Core/ArcApplicationBuilder.ts +++ b/Source/Core/ArcApplicationBuilder.ts @@ -3,7 +3,7 @@ import type { z } from 'zod'; import type { ArcOptions } from './ArcOptions.js'; import type { CratisConfiguration } from './configuration/loadConfiguration.js'; -import { ArcApplicationServices } from './ArcApplicationServices.js'; +import { ArcApplicationServices } from './dependencyInjection/ArcApplicationServices.js'; import { FetchArcApplication } from './FetchArcApplication.js'; import { ArcServer } from './ArcServer.js'; import type { CommandDefinition } from './commands/CommandDefinition.js'; @@ -28,7 +28,7 @@ import type { CommandKeyResolver } from './commands/CommandKeyResolver.js'; import type { QueryRenderer } from './queries/QueryRenderer.js'; import type { ReadModelInterceptor } from './queries/ReadModelInterceptor.js'; import type { ReadModelForCommandResolver } from './commands/ReadModelForCommandResolver.js'; -import { readModelArgument } from './commands/modelBound/readModel.js'; +import { readModelArgument } from './commands/modelBound/commandReadModel.js'; import type { CommandContext } from './commands/CommandContext.js'; import type { CommandResult } from './commands/CommandResult.js'; import type { CommandExecutionScope } from './commands/CommandExecutionScope.js'; diff --git a/Source/Core/ArcServer.ts b/Source/Core/ArcServer.ts index 449afe4a..aed5f5e3 100644 --- a/Source/Core/ArcServer.ts +++ b/Source/Core/ArcServer.ts @@ -18,7 +18,7 @@ import { createRouteTable } from './http/createRouteTable.js'; import { renderOpenApi } from './openApi/renderOpenApi.js'; import type { Operation } from './http/Operation.js'; import { commandResult, queryResult } from './results/index.js'; -import { recordFailure } from './results/failureTracking.js'; +import { recordFailure } from './execution/failureTracking.js'; import { Severity } from './validation/Severity.js'; import { ServiceRegistry } from './dependencyInjection/ServiceRegistry.js'; import { withServices } from './dependencyInjection/ServiceScope.js'; @@ -26,13 +26,13 @@ import { requestContext } from './execution/RequestContextStore.js'; import { isObservableOperation } from './queries/observable/ObservableOperation.js'; import { CommandOperationBoundary } from './commands/CommandOperationBoundary.js'; import type { ObservableQuerySession } from './queries/observable/ObservableQuerySession.js'; -import { ObservableSessions } from './queries/ObservableSessions.js'; +import { ObservableSessions } from './queries/observable/ObservableSessions.js'; import { ObservableLimits } from './queries/observable/ObservableLimits.js'; import { ObservableQueryHub } from './queries/observable/ObservableQueryHub.js'; import type { ObservableSocket } from './queries/observable/ObservableSocket.js'; import type { ResolvedConnectionContext } from './queries/observable/ResolvedConnectionContext.js'; import { registerObservableCleanup } from './queries/observable/observableCleanupFailures.js'; -import { observe } from './observability.js'; +import { observe } from './execution/observability.js'; export function currentContext(): ExecutionContext | undefined { return requestContext.getStore(); } export class ArcServer { readonly commands: readonly Operation[]; diff --git a/Source/Core/authorization/AuthorizationPolicy.ts b/Source/Core/authorization/AuthorizationPolicy.ts index d5891a6b..3082a2fc 100644 --- a/Source/Core/authorization/AuthorizationPolicy.ts +++ b/Source/Core/authorization/AuthorizationPolicy.ts @@ -3,7 +3,7 @@ import type { ExecutionContext } from '../execution/ExecutionContext.js'; import type { Principal } from '../identity/Principal.js'; import type { ClassType } from '../reflection/ClassType.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; /** The selected caller, operation and request data supplied before validation. */ export interface AuthorizationPolicyContext { diff --git a/Source/Core/authorization/authorized.ts b/Source/Core/authorization/authorized.ts index a8b526db..66d2c5c2 100644 --- a/Source/Core/authorization/authorized.ts +++ b/Source/Core/authorization/authorized.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { Authorization } from './Authorization.js'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; import { currentServices } from '../dependencyInjection/ServiceScope.js'; import type { AuthorizationPolicy, AuthorizationPolicyFunction, AuthorizationPolicyRegistration } from './AuthorizationPolicy.js'; import { authorizationRequirements } from './authorizationRequirements.js'; diff --git a/Source/Core/results/ArcTuple.ts b/Source/Core/commands/ArcTuple.ts similarity index 100% rename from Source/Core/results/ArcTuple.ts rename to Source/Core/commands/ArcTuple.ts diff --git a/Source/Core/commands/CommandDefinition.ts b/Source/Core/commands/CommandDefinition.ts index 41394e31..033f71f0 100644 --- a/Source/Core/commands/CommandDefinition.ts +++ b/Source/Core/commands/CommandDefinition.ts @@ -2,10 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { z } from 'zod'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; -import type { Outcome } from '../results/Outcome.js'; +import type { Outcome } from './Outcome.js'; import type { CommandExecutionScope } from './CommandExecutionScope.js'; import type { CommandFilter } from './CommandFilter.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; import type { ServiceIdentifier } from '../dependencyInjection/ServiceIdentifier.js'; export interface CommandDefinition extends DescriptorBase { schema: S; diff --git a/Source/Core/commands/CommandFailureSnapshot.ts b/Source/Core/commands/CommandFailureSnapshot.ts index c5dccef2..5fc0c73f 100644 --- a/Source/Core/commands/CommandFailureSnapshot.ts +++ b/Source/Core/commands/CommandFailureSnapshot.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { CommandContext } from './CommandContext.js'; import type { CommandResult } from './CommandResult.js'; -import { commandResult } from '../results/commandResult.js'; -import { hasFailure, originalFailure, recordFailure } from '../results/failureTracking.js'; +import { commandResult } from './createCommandResult.js'; +import { hasFailure, originalFailure, recordFailure } from '../execution/failureTracking.js'; /** Preserve the first failure even if a completion scope mutates the result. */ export class CommandFailureSnapshot { original?: CommandResult; diff --git a/Source/Core/commands/CommandOperationDeclaration.ts b/Source/Core/commands/CommandOperation.ts similarity index 100% rename from Source/Core/commands/CommandOperationDeclaration.ts rename to Source/Core/commands/CommandOperation.ts diff --git a/Source/Core/commands/CommandOperationExecution.ts b/Source/Core/commands/CommandOperationExecution.ts index a177e472..d942a01e 100644 --- a/Source/Core/commands/CommandOperationExecution.ts +++ b/Source/Core/commands/CommandOperationExecution.ts @@ -3,7 +3,7 @@ import { currentServices } from '../dependencyInjection/ServiceScope.js'; import type { CommandContext } from './CommandContext.js'; import type { CommandCommitDisposition } from './CommandCommitDisposition.js'; -import { isCommandOperation, type CommandOperation } from './CommandOperationDeclaration.js'; +import { isCommandOperation, type CommandOperation } from './CommandOperation.js'; import { CommandOperationBoundary } from './CommandOperationBoundary.js'; import type { CommandOperationFailure } from './CommandOperationFailure.js'; import type { CommandOperationOutcome } from './CommandOperationOutcome.js'; diff --git a/Source/Core/commands/CommandOperations.ts b/Source/Core/commands/CommandOperations.ts index 323ba49b..fe1ab6f6 100644 --- a/Source/Core/commands/CommandOperations.ts +++ b/Source/Core/commands/CommandOperations.ts @@ -1,6 +1,6 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { CommandOperation } from './CommandOperationDeclaration.js'; +import type { CommandOperation } from './CommandOperation.js'; const brand = Symbol.for('@cratis/arc.core/command-operations'); /** Immutable, explicit batch of server-only operation declarations. */ export class CommandOperations { diff --git a/Source/Core/commands/CommandResponseValueHandler.ts b/Source/Core/commands/CommandResponseValueHandler.ts index 8a1388fe..66bb9e63 100644 --- a/Source/Core/commands/CommandResponseValueHandler.ts +++ b/Source/Core/commands/CommandResponseValueHandler.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { CommandContext } from './CommandContext.js'; -import type { Outcome } from '../results/Outcome.js'; +import type { Outcome } from './Outcome.js'; /** Consume a server-only command return value. Every matching handler processes the value. */ export interface CommandResponseValueHandler { /** Reject an operation journal before any response handlers stage irreversible effects. */ diff --git a/Source/Core/results/Outcome.ts b/Source/Core/commands/Outcome.ts similarity index 100% rename from Source/Core/results/Outcome.ts rename to Source/Core/commands/Outcome.ts diff --git a/Source/Core/results/commandResult.ts b/Source/Core/commands/createCommandResult.ts similarity index 94% rename from Source/Core/results/commandResult.ts rename to Source/Core/commands/createCommandResult.ts index 8c6d9b55..106ea36a 100644 --- a/Source/Core/results/commandResult.ts +++ b/Source/Core/commands/createCommandResult.ts @@ -1,6 +1,6 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { CommandResult } from '../commands/CommandResult.js'; +import type { CommandResult } from './CommandResult.js'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; export function commandResult(context: ExecutionContext, values: Partial> = {}): CommandResult { diff --git a/Source/Core/results/for_Outcome/when_identifying_branded_outcomes.ts b/Source/Core/commands/for_Outcome/when_identifying_branded_outcomes.ts similarity index 100% rename from Source/Core/results/for_Outcome/when_identifying_branded_outcomes.ts rename to Source/Core/commands/for_Outcome/when_identifying_branded_outcomes.ts diff --git a/Source/Core/results/for_Outcome/when_identifying_unbranded_values.ts b/Source/Core/commands/for_Outcome/when_identifying_unbranded_values.ts similarity index 100% rename from Source/Core/results/for_Outcome/when_identifying_unbranded_values.ts rename to Source/Core/commands/for_Outcome/when_identifying_unbranded_values.ts diff --git a/Source/Core/results/for_tuple/when_grouping_values.ts b/Source/Core/commands/for_tuple/when_grouping_values.ts similarity index 100% rename from Source/Core/results/for_tuple/when_grouping_values.ts rename to Source/Core/commands/for_tuple/when_grouping_values.ts diff --git a/Source/Core/commands/index.ts b/Source/Core/commands/index.ts index a309ce65..427814b4 100644 --- a/Source/Core/commands/index.ts +++ b/Source/Core/commands/index.ts @@ -13,16 +13,16 @@ export type { CommandKeyResolver, CanProvideKeyForCommand } from './CommandKeyRe export { DefaultKeyForCommandResolver } from './DefaultKeyForCommandResolver.js'; export type { CommandResponseValueHandler } from './CommandResponseValueHandler.js'; export { commandResponseValueHandler } from './responseValueHandler.js'; -export { CommandOperation } from './CommandOperationDeclaration.js'; +export { CommandOperation } from './CommandOperation.js'; export { CommandOperations, operations } from './CommandOperations.js'; export type { CommandCommitDisposition } from './CommandCommitDisposition.js'; export type { CommandOperationExecutionScope } from './CommandExecutionScope.js'; export type { CommandOperationFailure } from './CommandOperationFailure.js'; export type { CommandOperationOutcome } from './CommandOperationOutcome.js'; export type { CommandRecoverySummary } from './CommandRecoverySummary.js'; -export { abortSignal, commandContext } from './modelBound/commandArgument.js'; +export { abortSignal, commandContext } from './modelBound/commandContextArgument.js'; export { provided } from './modelBound/provided.js'; -export { commandReadModel } from './modelBound/readModel.js'; +export { commandReadModel } from './modelBound/commandReadModel.js'; export { readModelForValidation } from '../validation/readModelForValidation.js'; -export { commandArgument } from './modelBound/commandContextArgument.js'; +export { commandArgument } from './modelBound/commandArgument.js'; export type { ReadModelForCommandResolver } from './ReadModelForCommandResolver.js'; diff --git a/Source/Core/commands/modelBound/PreparedValue.ts b/Source/Core/commands/modelBound/PreparedValue.ts index 2d795415..cd8cfa53 100644 --- a/Source/Core/commands/modelBound/PreparedValue.ts +++ b/Source/Core/commands/modelBound/PreparedValue.ts @@ -1,6 +1,6 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { Outcome } from '../../results/Outcome.js'; +import type { Outcome } from '../Outcome.js'; /** Exclude short-circuit outcomes from the preparation value passed into handle. */ export type PreparedValue = T extends Outcome ? T extends { kind: 'response'; value: infer Value } ? Value : never : T; diff --git a/Source/Core/commands/modelBound/commandArgument.ts b/Source/Core/commands/modelBound/commandArgument.ts index bccaec80..87af0834 100644 --- a/Source/Core/commands/modelBound/commandArgument.ts +++ b/Source/Core/commands/modelBound/commandArgument.ts @@ -1,65 +1,16 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; -import type { ServiceIdentifier } from '../../dependencyInjection/ServiceIdentifier.js'; -import { currentServices } from '../../dependencyInjection/ServiceScope.js'; +import type { ServiceToken } from '../../dependencyInjection/ServiceToken.js'; import type { CommandContext } from '../CommandContext.js'; -import { flattenCommandResponse } from '../processCommandResponse.js'; -import { providedType } from './provided.js'; -import { optionalServiceType } from '../../reflection/optionalService.js'; -import { readModelArgument } from './readModel.js'; -import { ReadModelForCommandError } from '../ReadModelForCommandError.js'; -import { contextArgumentResolver } from './commandContextArgument.js'; -const signalToken = serviceToken('Arc command signal'); -const contextToken = serviceToken('Arc command context'); -const readModels = new WeakMap>(); -/** Explicit AbortSignal parameter marker for @inject on handle() or provide(). */ -export function abortSignal(): typeof signalToken { return signalToken; } -/** Explicit CommandContext parameter marker for @inject on handle() or provide(). */ -export function commandContext(): typeof contextToken { return contextToken; } -/** Resolve service tokens alongside built-in command arguments. */ -export async function resolveCommandArguments(tokens: readonly ServiceIdentifier[], command: CommandContext, - provided: unknown = undefined): Promise { - const candidates = flattenCommandResponse(provided); - const values: unknown[] = []; - for (const token of tokens) { - if (token === signalToken) { values.push(command.signal); continue; } - if (token === contextToken) { values.push(command); continue; } - const contextResolver = contextArgumentResolver(token); - if (contextResolver) { values.push(await contextResolver(command)); continue; } - const model = readModelArgument(token); - if (model) { - const resolvers = await Promise.all((command.readModelResolvers ?? []).map(item => currentServices().resolve(item))); - const matching = resolvers.filter(resolver => resolver.supports(model.type)); - if (matching.length !== 1) throw new Error(`Expected one read-model resolver for ${model.type.name}, found ${matching.length}`); - if (!command.key?.trim()) throw new ReadModelForCommandError(`A command key is required for ${model.type.name}`); - let cached = readModels.get(command); - if (!cached) { cached = new Map(); readModels.set(command, cached); } - if (!cached.has(model.type)) cached.set(model.type, await matching[0]!.find(model.type, command.key, command)); - const found = cached.get(model.type); - if (found === undefined) throw new Error(`Read-model resolver returned no outcome for ${model.type.name}`); - if (found === null && !model.optional) throw new ReadModelForCommandError(`${model.type.name} was not found for the command key`); - values.push(found); - continue; - } - const optional = optionalServiceType(token); - if (optional) { - const scope = currentServices(); - values.push(scope.registry.hasRegistration(optional) ? await scope.resolve(optional) : null); - continue; - } - const type = providedType(token); - if (!type) { values.push(await currentServices().resolve(token)); continue; } - const index = candidates.findIndex(candidate => candidate instanceof type || - type === String && typeof candidate === 'string' || type === Number && typeof candidate === 'number' || - type === Boolean && typeof candidate === 'boolean'); - if (index < 0) throw new Error(`No provided value matches ${type.name}`); - values.push(candidates.splice(index, 1)[0]); - } - return values; + +const resolvers = new WeakMap unknown | Promise>(); +/** Bind an integration-owned handle/provide parameter to the current command context. */ +export function commandArgument(name: string, resolve: (context: CommandContext) => T | Promise): ServiceToken { + const token = serviceToken(name); + resolvers.set(token, resolve); + return token; } -/** Service tokens alone participate in DI preflight. */ -export function commandServiceTokens(tokens: readonly ServiceIdentifier[]): ServiceIdentifier[] { - return tokens.filter(token => token !== signalToken && token !== contextToken && !providedType(token) && - !readModelArgument(token) && !contextArgumentResolver(token) && !optionalServiceType(token)); +export function contextArgumentResolver(token: object): ((context: CommandContext) => unknown | Promise) | undefined { + return resolvers.get(token); } diff --git a/Source/Core/commands/modelBound/commandContextArgument.ts b/Source/Core/commands/modelBound/commandContextArgument.ts index 87af0834..a288f591 100644 --- a/Source/Core/commands/modelBound/commandContextArgument.ts +++ b/Source/Core/commands/modelBound/commandContextArgument.ts @@ -1,16 +1,65 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; -import type { ServiceToken } from '../../dependencyInjection/ServiceToken.js'; +import type { ServiceIdentifier } from '../../dependencyInjection/ServiceIdentifier.js'; +import { currentServices } from '../../dependencyInjection/ServiceScope.js'; import type { CommandContext } from '../CommandContext.js'; - -const resolvers = new WeakMap unknown | Promise>(); -/** Bind an integration-owned handle/provide parameter to the current command context. */ -export function commandArgument(name: string, resolve: (context: CommandContext) => T | Promise): ServiceToken { - const token = serviceToken(name); - resolvers.set(token, resolve); - return token; +import { flattenCommandResponse } from '../processCommandResponse.js'; +import { providedType } from './provided.js'; +import { optionalServiceType } from '../../dependencyInjection/optionalService.js'; +import { readModelArgument } from './commandReadModel.js'; +import { ReadModelForCommandError } from '../ReadModelForCommandError.js'; +import { contextArgumentResolver } from './commandArgument.js'; +const signalToken = serviceToken('Arc command signal'); +const contextToken = serviceToken('Arc command context'); +const readModels = new WeakMap>(); +/** Explicit AbortSignal parameter marker for @inject on handle() or provide(). */ +export function abortSignal(): typeof signalToken { return signalToken; } +/** Explicit CommandContext parameter marker for @inject on handle() or provide(). */ +export function commandContext(): typeof contextToken { return contextToken; } +/** Resolve service tokens alongside built-in command arguments. */ +export async function resolveCommandArguments(tokens: readonly ServiceIdentifier[], command: CommandContext, + provided: unknown = undefined): Promise { + const candidates = flattenCommandResponse(provided); + const values: unknown[] = []; + for (const token of tokens) { + if (token === signalToken) { values.push(command.signal); continue; } + if (token === contextToken) { values.push(command); continue; } + const contextResolver = contextArgumentResolver(token); + if (contextResolver) { values.push(await contextResolver(command)); continue; } + const model = readModelArgument(token); + if (model) { + const resolvers = await Promise.all((command.readModelResolvers ?? []).map(item => currentServices().resolve(item))); + const matching = resolvers.filter(resolver => resolver.supports(model.type)); + if (matching.length !== 1) throw new Error(`Expected one read-model resolver for ${model.type.name}, found ${matching.length}`); + if (!command.key?.trim()) throw new ReadModelForCommandError(`A command key is required for ${model.type.name}`); + let cached = readModels.get(command); + if (!cached) { cached = new Map(); readModels.set(command, cached); } + if (!cached.has(model.type)) cached.set(model.type, await matching[0]!.find(model.type, command.key, command)); + const found = cached.get(model.type); + if (found === undefined) throw new Error(`Read-model resolver returned no outcome for ${model.type.name}`); + if (found === null && !model.optional) throw new ReadModelForCommandError(`${model.type.name} was not found for the command key`); + values.push(found); + continue; + } + const optional = optionalServiceType(token); + if (optional) { + const scope = currentServices(); + values.push(scope.registry.hasRegistration(optional) ? await scope.resolve(optional) : null); + continue; + } + const type = providedType(token); + if (!type) { values.push(await currentServices().resolve(token)); continue; } + const index = candidates.findIndex(candidate => candidate instanceof type || + type === String && typeof candidate === 'string' || type === Number && typeof candidate === 'number' || + type === Boolean && typeof candidate === 'boolean'); + if (index < 0) throw new Error(`No provided value matches ${type.name}`); + values.push(candidates.splice(index, 1)[0]); + } + return values; } -export function contextArgumentResolver(token: object): ((context: CommandContext) => unknown | Promise) | undefined { - return resolvers.get(token); +/** Service tokens alone participate in DI preflight. */ +export function commandServiceTokens(tokens: readonly ServiceIdentifier[]): ServiceIdentifier[] { + return tokens.filter(token => token !== signalToken && token !== contextToken && !providedType(token) && + !readModelArgument(token) && !contextArgumentResolver(token) && !optionalServiceType(token)); } diff --git a/Source/Core/commands/modelBound/readModel.ts b/Source/Core/commands/modelBound/commandReadModel.ts similarity index 100% rename from Source/Core/commands/modelBound/readModel.ts rename to Source/Core/commands/modelBound/commandReadModel.ts diff --git a/Source/Core/commands/modelBound/compileCommand.ts b/Source/Core/commands/modelBound/compileCommand.ts index 0200eb48..98914728 100644 --- a/Source/Core/commands/modelBound/compileCommand.ts +++ b/Source/Core/commands/modelBound/compileCommand.ts @@ -5,9 +5,9 @@ import { Fields } from '@cratis/fundamentals'; import type { CommandContext } from '../CommandContext.js'; import { keyFieldFor } from '../../reflection/key.js'; import type { CommandDefinition } from '../CommandDefinition.js'; -import { isOutcome } from '../../results/Outcome.js'; +import { isOutcome } from '../Outcome.js'; import { reflectedParameters } from '../../reflection/reflectedParameters.js'; -import { commandServiceTokens, resolveCommandArguments } from './commandArgument.js'; +import { commandServiceTokens, resolveCommandArguments } from './commandContextArgument.js'; import { encodeCommandResponse } from './encodeCommandResponse.js'; import { providedType } from './provided.js'; import { ownMetadata } from '../../reflection/ownMetadata.js'; diff --git a/Source/Core/commands/modelBound/encodeCommandResponse.ts b/Source/Core/commands/modelBound/encodeCommandResponse.ts index d19dd6af..f8e30d51 100644 --- a/Source/Core/commands/modelBound/encodeCommandResponse.ts +++ b/Source/Core/commands/modelBound/encodeCommandResponse.ts @@ -1,10 +1,10 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { isArcTuple } from '../../results/ArcTuple.js'; -import { tuple } from '../../results/tuple.js'; -import { isOutcome, response } from '../../results/Outcome.js'; +import { isArcTuple } from '../ArcTuple.js'; +import { tuple } from '../tuple.js'; +import { isOutcome, response } from '../Outcome.js'; import { encode } from '../../reflection/wireSchema.js'; -import { CommandOperation } from '../CommandOperationDeclaration.js'; +import { CommandOperation } from '../CommandOperation.js'; import { isCommandOperations } from '../CommandOperations.js'; /** Preserve server-only declarations and branded return branches until the pipeline classifies them. */ export function encodeCommandResponse(value: unknown): unknown { diff --git a/Source/Core/commands/prepareCommandResponse.ts b/Source/Core/commands/prepareCommandResponse.ts index 394e67cc..81f474fd 100644 --- a/Source/Core/commands/prepareCommandResponse.ts +++ b/Source/Core/commands/prepareCommandResponse.ts @@ -4,7 +4,7 @@ import type { ArcOptions } from '../ArcOptions.js'; import { currentServices } from '../dependencyInjection/ServiceScope.js'; import type { CommandContext } from './CommandContext.js'; import type { CommandExecutionScope } from './CommandExecutionScope.js'; -import { isCommandOperation } from './CommandOperationDeclaration.js'; +import { isCommandOperation } from './CommandOperation.js'; import { isCommandOperations } from './CommandOperations.js'; import { CommandOperationBoundary } from './CommandOperationBoundary.js'; import { CommandOperationExecution } from './CommandOperationExecution.js'; diff --git a/Source/Core/commands/processCommandResponse.ts b/Source/Core/commands/processCommandResponse.ts index 57f1f0f2..27c78ce6 100644 --- a/Source/Core/commands/processCommandResponse.ts +++ b/Source/Core/commands/processCommandResponse.ts @@ -1,11 +1,11 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { isArcTuple } from '../results/ArcTuple.js'; -import { isOutcome, type Outcome } from '../results/Outcome.js'; -import { commandResult } from '../results/commandResult.js'; +import { isArcTuple } from './ArcTuple.js'; +import { isOutcome, type Outcome } from './Outcome.js'; +import { commandResult } from './createCommandResult.js'; import type { CommandResult } from './CommandResult.js'; import type { CommandContext } from './CommandContext.js'; -import { isCommandOperation } from './CommandOperationDeclaration.js'; +import { isCommandOperation } from './CommandOperation.js'; import { isCommandOperations } from './CommandOperations.js'; import type { CommandResponseValueHandler } from './CommandResponseValueHandler.js'; diff --git a/Source/Core/commands/commandOperation.ts b/Source/Core/commands/runCommandOperation.ts similarity index 97% rename from Source/Core/commands/commandOperation.ts rename to Source/Core/commands/runCommandOperation.ts index 823cfe60..aeefd995 100644 --- a/Source/Core/commands/commandOperation.ts +++ b/Source/Core/commands/runCommandOperation.ts @@ -5,13 +5,13 @@ import type { ArcOptions } from '../ArcOptions.js'; import type { CommandDefinition } from './CommandDefinition.js'; import type { CommandResult } from './CommandResult.js'; import type { ValidationResult } from '../validation/ValidationResult.js'; -import { isOutcome } from '../results/Outcome.js'; +import { isOutcome } from './Outcome.js'; import { authorized } from '../authorization/authorized.js'; -import { commandResult } from '../results/commandResult.js'; -import { malformed } from '../results/malformed.js'; +import { commandResult } from './createCommandResult.js'; +import { malformed } from '../http/malformed.js'; import type { Operation } from '../http/Operation.js'; import { fullyQualifiedName } from '../http/fullyQualifiedName.js'; -import { recordFailure } from '../results/failureTracking.js'; +import { recordFailure } from '../execution/failureTracking.js'; import { CommandFailureSnapshot } from './CommandFailureSnapshot.js'; import { ServiceDependencyError } from '../dependencyInjection/ServiceDependencyError.js'; import { ReadModelForCommandError } from './ReadModelForCommandError.js'; @@ -25,7 +25,7 @@ import { setCommandRecovery } from './commandRecovery.js'; import type { CommandExecutionScope, CommandOperationExecutionScope } from './CommandExecutionScope.js'; import type { CommandCommitDisposition } from './CommandCommitDisposition.js'; import type { CommandContext } from './CommandContext.js'; -import { observe } from '../observability.js'; +import { observe } from '../execution/observability.js'; function disposition(scopes: readonly CommandExecutionScope[], context: CommandContext): CommandCommitDisposition { const participants = scopes.filter((scope): scope is CommandOperationExecutionScope => diff --git a/Source/Core/results/tuple.ts b/Source/Core/commands/tuple.ts similarity index 100% rename from Source/Core/results/tuple.ts rename to Source/Core/commands/tuple.ts diff --git a/Source/Core/ArcApplicationServices.ts b/Source/Core/dependencyInjection/ArcApplicationServices.ts similarity index 87% rename from Source/Core/ArcApplicationServices.ts rename to Source/Core/dependencyInjection/ArcApplicationServices.ts index 77546930..3768faf6 100644 --- a/Source/Core/ArcApplicationServices.ts +++ b/Source/Core/dependencyInjection/ArcApplicationServices.ts @@ -1,10 +1,10 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { ServiceRegistration } from './dependencyInjection/ServiceRegistration.js'; -import type { ServiceIdentifier, ServiceClass } from './dependencyInjection/ServiceIdentifier.js'; -import { reflectedParameters } from './reflection/reflectedParameters.js'; -import { ownMetadata } from './reflection/ownMetadata.js'; -import type { ServiceScope } from './dependencyInjection/ServiceScope.js'; +import type { ServiceRegistration } from './ServiceRegistration.js'; +import type { ServiceIdentifier, ServiceClass } from './ServiceIdentifier.js'; +import { reflectedParameters } from '../reflection/reflectedParameters.js'; +import { ownMetadata } from '../reflection/ownMetadata.js'; +import type { ServiceScope } from './ServiceScope.js'; /** Collect class and factory registrations for a built application. */ export class ArcApplicationServices { diff --git a/Source/Core/dependencyInjection/for_ServiceRegistry/when_publishing_a_result_during_singleton_failure/with_a_failing_disposer.ts b/Source/Core/dependencyInjection/for_ServiceRegistry/when_publishing_a_result_during_singleton_failure/with_a_failing_disposer.ts index f8732805..97d6729c 100644 --- a/Source/Core/dependencyInjection/for_ServiceRegistry/when_publishing_a_result_during_singleton_failure/with_a_failing_disposer.ts +++ b/Source/Core/dependencyInjection/for_ServiceRegistry/when_publishing_a_result_during_singleton_failure/with_a_failing_disposer.ts @@ -6,7 +6,7 @@ import { ArcServer } from '../../../ArcServer.js'; import { currentServices } from '../../ServiceScope.js'; import { serviceToken } from '../../ServiceToken.js'; import { defineQuery } from '../../../queries/defineQuery.js'; -import { originalFailure } from '../../../results/failureTracking.js'; +import { originalFailure } from '../../../execution/failureTracking.js'; import { beforeDeadline, captureFailure, gate, serviceContext } from '../given/a_service_lifecycle.js'; should(); diff --git a/Source/Core/reflection/optionalService.ts b/Source/Core/dependencyInjection/optionalService.ts similarity index 81% rename from Source/Core/reflection/optionalService.ts rename to Source/Core/dependencyInjection/optionalService.ts index c8967743..8cc71281 100644 --- a/Source/Core/reflection/optionalService.ts +++ b/Source/Core/dependencyInjection/optionalService.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { serviceToken, type ServiceToken } from '../dependencyInjection/ServiceToken.js'; -import type { ServiceIdentifier } from '../dependencyInjection/ServiceIdentifier.js'; +import { serviceToken, type ServiceToken } from './ServiceToken.js'; +import type { ServiceIdentifier } from './ServiceIdentifier.js'; const optional = new WeakMap>(); /** Bind a nullable service to null when it is not registered. */ diff --git a/Source/Core/results/failureTracking.ts b/Source/Core/execution/failureTracking.ts similarity index 100% rename from Source/Core/results/failureTracking.ts rename to Source/Core/execution/failureTracking.ts diff --git a/Source/Core/observability.ts b/Source/Core/execution/observability.ts similarity index 100% rename from Source/Core/observability.ts rename to Source/Core/execution/observability.ts diff --git a/Source/Core/fetch.ts b/Source/Core/fetch.ts index 3b26a8f7..3f4c70ce 100644 --- a/Source/Core/fetch.ts +++ b/Source/Core/fetch.ts @@ -6,11 +6,11 @@ export { FetchArcApplication as ArcApplication } from './FetchArcApplication.js' export { ArcApplicationBuilder } from './ArcApplicationBuilder.js'; export type { ArcBuilderIntegrationOptions } from './ArcBuilderIntegrationOptions.js'; export { canonicalMetadataSignature } from './reflection/generatedMetadataSignature.js'; -export { optionalService } from './reflection/optionalService.js'; +export { optionalService } from './dependencyInjection/optionalService.js'; export type { GeneratedMetadata, GeneratedArtifactMetadata } from './reflection/GeneratedArtifactMetadata.js'; /** Convert model-bound values to the same JSON-ready shape as Arc's HTTP pipeline. */ export { encode as encodeWireValue } from './reflection/wireSchema.js'; -export { ArcApplicationServices } from './ArcApplicationServices.js'; +export { ArcApplicationServices } from './dependencyInjection/ArcApplicationServices.js'; export { key } from './reflection/key.js'; export { fieldsFor, wireName } from './reflection/wireSchema.js'; export type { ClassType } from './reflection/ClassType.js'; @@ -20,7 +20,7 @@ export { nullable } from './reflection/nullable.js'; export { defaultValue } from './reflection/defaultValue.js'; export { enumeration } from './reflection/enumeration.js'; export type { ArcOptions } from './ArcOptions.js'; -export type { DescriptorBase } from './DescriptorBase.js'; +export type { DescriptorBase } from './http/DescriptorBase.js'; export * from './commands/index.js'; export * from './queries/index.js'; export * from './validation/index.js'; diff --git a/Source/Core/for_ArcServer/given/an_operation_command.ts b/Source/Core/for_ArcServer/given/an_operation_command.ts index d9cd257e..e8e4cd7e 100644 --- a/Source/Core/for_ArcServer/given/an_operation_command.ts +++ b/Source/Core/for_ArcServer/given/an_operation_command.ts @@ -3,11 +3,11 @@ import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandCommitDisposition } from '../../commands/CommandCommitDisposition.js'; import type { CommandContext } from '../../commands/CommandContext.js'; import type { CommandResult } from '../../commands/CommandResult.js'; -import { tuple } from '../../results/tuple.js'; +import { tuple } from '../../commands/tuple.js'; import type { ExecutionContext } from '../../execution/ExecutionContext.js'; export class ProbeOperation extends CommandOperation { diff --git a/Source/Core/for_ArcServer/when_an_operation_calls_a_nested_command/with_ignored_failure.ts b/Source/Core/for_ArcServer/when_an_operation_calls_a_nested_command/with_ignored_failure.ts index 696ada74..46718ab6 100644 --- a/Source/Core/for_ArcServer/when_an_operation_calls_a_nested_command/with_ignored_failure.ts +++ b/Source/Core/for_ArcServer/when_an_operation_calls_a_nested_command/with_ignored_failure.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command } from '../given/an_operation_command.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); describe('when an operation calls a nested command with ignored failure', given(an_operation_command, context => { diff --git a/Source/Core/for_ArcServer/when_an_operation_fails/with_a_committed_or_failing_compensation.ts b/Source/Core/for_ArcServer/when_an_operation_fails/with_a_committed_or_failing_compensation.ts index 8afda80c..79624620 100644 --- a/Source/Core/for_ArcServer/when_an_operation_fails/with_a_committed_or_failing_compensation.ts +++ b/Source/Core/for_ArcServer/when_an_operation_fails/with_a_committed_or_failing_compensation.ts @@ -3,8 +3,8 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command, ProbeOperation } from '../given/an_operation_command.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; -import { tuple } from '../../results/tuple.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; +import { tuple } from '../../commands/tuple.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); describe('when a scope commits before recovery', given(an_operation_command, context => { diff --git a/Source/Core/for_ArcServer/when_an_operation_fails/with_a_later_scope_failure.ts b/Source/Core/for_ArcServer/when_an_operation_fails/with_a_later_scope_failure.ts index 7c1173eb..aafeb272 100644 --- a/Source/Core/for_ArcServer/when_an_operation_fails/with_a_later_scope_failure.ts +++ b/Source/Core/for_ArcServer/when_an_operation_fails/with_a_later_scope_failure.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command } from '../given/an_operation_command.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandOperationFailure } from '../../commands/CommandOperationFailure.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); diff --git a/Source/Core/for_ArcServer/when_an_operation_fails/with_cancellation_or_budget_expiry.ts b/Source/Core/for_ArcServer/when_an_operation_fails/with_cancellation_or_budget_expiry.ts index 5959ae16..40c34fa1 100644 --- a/Source/Core/for_ArcServer/when_an_operation_fails/with_cancellation_or_budget_expiry.ts +++ b/Source/Core/for_ArcServer/when_an_operation_fails/with_cancellation_or_budget_expiry.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandOperationFailure } from '../../commands/CommandOperationFailure.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); diff --git a/Source/Core/for_ArcServer/when_declaring_nested_operations/with_a_child_or_parent_declaration.ts b/Source/Core/for_ArcServer/when_declaring_nested_operations/with_a_child_or_parent_declaration.ts index c8aef710..2b76403c 100644 --- a/Source/Core/for_ArcServer/when_declaring_nested_operations/with_a_child_or_parent_declaration.ts +++ b/Source/Core/for_ArcServer/when_declaring_nested_operations/with_a_child_or_parent_declaration.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { given } from '../../given.js'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); class Effect extends CommandOperation { diff --git a/Source/Core/for_ArcServer/when_declaring_operations/with_invalid_configuration.ts b/Source/Core/for_ArcServer/when_declaring_operations/with_invalid_configuration.ts index b656bf33..c10f62c9 100644 --- a/Source/Core/for_ArcServer/when_declaring_operations/with_invalid_configuration.ts +++ b/Source/Core/for_ArcServer/when_declaring_operations/with_invalid_configuration.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { CommandOperation } from '../../commands/CommandOperationDeclaration.js'; +import { CommandOperation } from '../../commands/CommandOperation.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); class Effect extends CommandOperation { execute(signal: AbortSignal): void { void signal; } } diff --git a/Source/Core/for_ArcServer/when_preflighting_operations/with_a_missing_later_dependency.ts b/Source/Core/for_ArcServer/when_preflighting_operations/with_a_missing_later_dependency.ts index 6b13147c..7bc3803d 100644 --- a/Source/Core/for_ArcServer/when_preflighting_operations/with_a_missing_later_dependency.ts +++ b/Source/Core/for_ArcServer/when_preflighting_operations/with_a_missing_later_dependency.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command, ProbeOperation } from '../given/an_operation_command.js'; import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; -import { tuple } from '../../results/tuple.js'; +import { tuple } from '../../commands/tuple.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); describe('when preflighting operations with a missing later dependency', given(an_operation_command, context => { diff --git a/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts b/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts index 28673daf..664704b7 100644 --- a/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts +++ b/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command, ProbeOperation } from '../given/an_operation_command.js'; import { denied, rejected } from '../../results/index.js'; -import { tuple } from '../../results/tuple.js'; +import { tuple } from '../../commands/tuple.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); for (const [name, value] of [ diff --git a/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts b/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts index 62c563ce..7cc07c23 100644 --- a/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts +++ b/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; -import { tuple } from '../../results/tuple.js'; +import { tuple } from '../../commands/tuple.js'; import { denied, rejected, response } from '../../results/index.js'; import type { CommandContext } from '../../commands/CommandContext.js'; import type { CommandResponseValueHandler } from '../../commands/CommandResponseValueHandler.js'; diff --git a/Source/Core/for_ArcServer/when_processing_command_response/with_scoped_handlers.ts b/Source/Core/for_ArcServer/when_processing_command_response/with_scoped_handlers.ts index a987de1e..a8107569 100644 --- a/Source/Core/for_ArcServer/when_processing_command_response/with_scoped_handlers.ts +++ b/Source/Core/for_ArcServer/when_processing_command_response/with_scoped_handlers.ts @@ -6,7 +6,7 @@ import { given } from '../../given.js'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; -import { tuple } from '../../results/tuple.js'; +import { tuple } from '../../commands/tuple.js'; import type { CommandResult } from '../../commands/CommandResult.js'; import type { CommandContext } from '../../commands/CommandContext.js'; import type { CommandResponseValueHandler } from '../../commands/CommandResponseValueHandler.js'; diff --git a/Source/Core/for_ArcServer/when_providing_a_command/with_a_rejection.ts b/Source/Core/for_ArcServer/when_providing_a_command/with_a_rejection.ts index 3b8191f9..a7615b8c 100644 --- a/Source/Core/for_ArcServer/when_providing_a_command/with_a_rejection.ts +++ b/Source/Core/for_ArcServer/when_providing_a_command/with_a_rejection.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { rejected } from '../../results/Outcome.js'; +import { rejected } from '../../commands/Outcome.js'; import { Severity } from '../../validation/Severity.js'; import { validation } from '../../validation/ValidationResult.js'; import { runtimePost } from '../given/a_runtime_request.js'; diff --git a/Source/Core/for_ArcServer/when_providing_a_command/with_an_empty_rejection.ts b/Source/Core/for_ArcServer/when_providing_a_command/with_an_empty_rejection.ts index dd6d9a77..43c60b66 100644 --- a/Source/Core/for_ArcServer/when_providing_a_command/with_an_empty_rejection.ts +++ b/Source/Core/for_ArcServer/when_providing_a_command/with_an_empty_rejection.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { rejected } from '../../results/Outcome.js'; +import { rejected } from '../../commands/Outcome.js'; import { runtimePost } from '../given/a_runtime_request.js'; should(); diff --git a/Source/Core/for_ArcServer/when_providing_a_command/with_filtered_warnings_and_denial.ts b/Source/Core/for_ArcServer/when_providing_a_command/with_filtered_warnings_and_denial.ts index effd5051..8375efd7 100644 --- a/Source/Core/for_ArcServer/when_providing_a_command/with_filtered_warnings_and_denial.ts +++ b/Source/Core/for_ArcServer/when_providing_a_command/with_filtered_warnings_and_denial.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { z } from 'zod'; import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; -import { denied, rejected } from '../../results/Outcome.js'; +import { denied, rejected } from '../../commands/Outcome.js'; import { Severity } from '../../validation/Severity.js'; import { validation } from '../../validation/ValidationResult.js'; import { runtimePost } from '../given/a_runtime_request.js'; diff --git a/Source/Core/DescriptorBase.ts b/Source/Core/http/DescriptorBase.ts similarity index 77% rename from Source/Core/DescriptorBase.ts rename to Source/Core/http/DescriptorBase.ts index 460cf6f0..8b3a2b60 100644 --- a/Source/Core/DescriptorBase.ts +++ b/Source/Core/http/DescriptorBase.ts @@ -1,8 +1,8 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { Authorization } from './authorization/Authorization.js'; -import type { ClientContract } from './introspection/ClientContract.js'; -import type { GeneratedReturn } from './reflection/GeneratedReturn.js'; +import type { Authorization } from '../authorization/Authorization.js'; +import type { ClientContract } from '../introspection/ClientContract.js'; +import type { GeneratedReturn } from '../reflection/GeneratedReturn.js'; /** Shared operation description for direct and model-bound definitions. */ export interface DescriptorBase { name: string; diff --git a/Source/Core/http/Operation.ts b/Source/Core/http/Operation.ts index e00a2688..14f5d3e7 100644 --- a/Source/Core/http/Operation.ts +++ b/Source/Core/http/Operation.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { z } from 'zod'; import type { CommandResult } from '../commands/CommandResult.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from './DescriptorBase.js'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; import type { QueryOptions } from '../queries/QueryOptions.js'; import type { QueryResult } from '../queries/QueryResult.js'; diff --git a/Source/Core/http/createRouteTable.ts b/Source/Core/http/createRouteTable.ts index 2e54446b..a1ce102c 100644 --- a/Source/Core/http/createRouteTable.ts +++ b/Source/Core/http/createRouteTable.ts @@ -8,7 +8,7 @@ import type { ExecutionContext } from '../execution/ExecutionContext.js'; import type { ObservableSource } from '../queries/observable/ObservableSource.js'; import type { QueryHealthSnapshot } from '../queries/observable/QueryHealthSnapshot.js'; import { inspectClientInput, inspectClientQueryInput } from '../introspection/ClientManifest.js'; -import { commandOperation } from '../commands/commandOperation.js'; +import { commandOperation } from '../commands/runCommandOperation.js'; import { queryOperation } from '../queries/queryOperation.js'; import { observableOperation } from '../queries/observable/ObservableOperation.js'; import { authorizationRequirements, validateAuthorization } from '../authorization/authorizationRequirements.js'; diff --git a/Source/Core/http/handleRequest.ts b/Source/Core/http/handleRequest.ts index 4fe1ff78..be8f971d 100644 --- a/Source/Core/http/handleRequest.ts +++ b/Source/Core/http/handleRequest.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { z } from 'zod'; -import { observe } from '../observability.js'; +import { observe } from '../execution/observability.js'; import { stringifyWire } from '../reflection/stringifyWire.js'; import type { ArcServer } from '../ArcServer.js'; import type { NativeRequestContext } from './NativeRequestContext.js'; @@ -22,7 +22,7 @@ import { allowedSeverity } from '../validation/allowedSeverity.js'; import { authenticate, verifiedPrincipal } from '../authentication/authenticate.js'; import { correlation } from '../execution/correlation.js'; import { exposeExceptionDetails } from '../execution/exposeExceptionDetails.js'; -import { hasFailure, originalFailure } from '../results/failureTracking.js'; +import { hasFailure, originalFailure } from '../execution/failureTracking.js'; import { Severity } from '../validation/Severity.js'; import { requestContext } from '../execution/RequestContextStore.js'; import { isObservableOperation } from '../queries/observable/ObservableOperation.js'; diff --git a/Source/Core/results/malformed.ts b/Source/Core/http/malformed.ts similarity index 100% rename from Source/Core/results/malformed.ts rename to Source/Core/http/malformed.ts diff --git a/Source/Core/results/status.ts b/Source/Core/http/status.ts similarity index 100% rename from Source/Core/results/status.ts rename to Source/Core/http/status.ts diff --git a/Source/Core/index.ts b/Source/Core/index.ts index c61cfc0c..aa2a779b 100644 --- a/Source/Core/index.ts +++ b/Source/Core/index.ts @@ -8,11 +8,11 @@ export type { ArcBuilderIntegrationOptions } from './ArcBuilderIntegrationOption export type { ArcBuilderOptions } from './configuration/ArcBuilderOptions.js'; export { discoveryFiles } from './reflection/discoveryFiles.js'; export { canonicalMetadataSignature } from './reflection/generatedMetadataSignature.js'; -export { optionalService } from './reflection/optionalService.js'; +export { optionalService } from './dependencyInjection/optionalService.js'; export type { GeneratedMetadata, GeneratedArtifactMetadata } from './reflection/GeneratedArtifactMetadata.js'; /** Convert model-bound values to the same JSON-ready shape as Arc's HTTP pipeline. */ export { encode as encodeWireValue } from './reflection/wireSchema.js'; -export { ArcApplicationServices } from './ArcApplicationServices.js'; +export { ArcApplicationServices } from './dependencyInjection/ArcApplicationServices.js'; export { key } from './reflection/key.js'; export { fieldsFor, wireName } from './reflection/wireSchema.js'; export type { ClassType } from './reflection/ClassType.js'; @@ -22,7 +22,7 @@ export { nullable } from './reflection/nullable.js'; export { defaultValue } from './reflection/defaultValue.js'; export { enumeration } from './reflection/enumeration.js'; export type { ArcOptions } from './ArcOptions.js'; -export type { DescriptorBase } from './DescriptorBase.js'; +export type { DescriptorBase } from './http/DescriptorBase.js'; export * from './commands/index.js'; export * from './queries/index.js'; export * from './validation/index.js'; diff --git a/Source/Core/queries/QueryDefinition.ts b/Source/Core/queries/QueryDefinition.ts index 5e921b5b..5e8ebe6c 100644 --- a/Source/Core/queries/QueryDefinition.ts +++ b/Source/Core/queries/QueryDefinition.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { z } from 'zod'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; import type { QueryFilter } from './QueryFilter.js'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; import type { QueryOptions } from './QueryOptions.js'; diff --git a/Source/Core/results/queryResult.ts b/Source/Core/queries/createQueryResult.ts similarity index 94% rename from Source/Core/results/queryResult.ts rename to Source/Core/queries/createQueryResult.ts index 9c3adbb3..60b94ec4 100644 --- a/Source/Core/results/queryResult.ts +++ b/Source/Core/queries/createQueryResult.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { ExecutionContext } from '../execution/ExecutionContext.js'; -import type { QueryResult } from '../queries/QueryResult.js'; +import type { QueryResult } from './QueryResult.js'; import { emptyPaging } from './emptyPaging.js'; export function queryResult(context: ExecutionContext, values: Partial> = {}): QueryResult { diff --git a/Source/Core/results/emptyPaging.ts b/Source/Core/queries/emptyPaging.ts similarity index 82% rename from Source/Core/results/emptyPaging.ts rename to Source/Core/queries/emptyPaging.ts index 0060434d..9da57679 100644 --- a/Source/Core/results/emptyPaging.ts +++ b/Source/Core/queries/emptyPaging.ts @@ -1,5 +1,5 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { Paging } from '../queries/Paging.js'; +import type { Paging } from './Paging.js'; export const emptyPaging = (): Paging => ({ page: 0, size: 0, totalItems: 0, totalPages: 0 }); diff --git a/Source/Core/queries/observable/ObservableOperation.ts b/Source/Core/queries/observable/ObservableOperation.ts index 65bc0272..07ce0a5f 100644 --- a/Source/Core/queries/observable/ObservableOperation.ts +++ b/Source/Core/queries/observable/ObservableOperation.ts @@ -9,7 +9,7 @@ import type { Operation } from '../../http/Operation.js'; import { queryOperation } from '../queryOperation.js'; import { renderQuery } from '../renderQuery.js'; import { queryResult } from '../../results/index.js'; -import { recordFailure } from '../../results/failureTracking.js'; +import { recordFailure } from '../../execution/failureTracking.js'; import type { ObservableQueryDefinition } from './ObservableQueryDefinition.js'; import type { ObservableSource } from './ObservableSource.js'; diff --git a/Source/Core/queries/observable/ObservableQuerySession.ts b/Source/Core/queries/observable/ObservableQuerySession.ts index 5ec4f19e..bcb4f041 100644 --- a/Source/Core/queries/observable/ObservableQuerySession.ts +++ b/Source/Core/queries/observable/ObservableQuerySession.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { ExecutionContext } from '../../execution/ExecutionContext.js'; import type { QueryResult } from '../QueryResult.js'; -import { hasFailure, originalFailure } from '../../results/failureTracking.js'; +import { hasFailure, originalFailure } from '../../execution/failureTracking.js'; import { queryResult } from '../../results/index.js'; import { requestContext } from '../../execution/RequestContextStore.js'; import { withServices } from '../../dependencyInjection/ServiceScope.js'; @@ -12,7 +12,7 @@ import { ObservableEmissionDecision } from './ObservableEmissionDecision.js'; import type { ObservableEmissionContext } from './ObservableEmissionContext.js'; import type { ObservableSessionConfig } from './ObservableSessionConfig.js'; import { clonePrincipal } from './clonePrincipal.js'; -import { beginSubscription, observe } from '../../observability.js'; +import { beginSubscription, observe } from '../../execution/observability.js'; /** An opened pipeline and scope owned by one live subscription (or snapshot request). */ export class ObservableQuerySession { diff --git a/Source/Core/queries/ObservableSessions.ts b/Source/Core/queries/observable/ObservableSessions.ts similarity index 88% rename from Source/Core/queries/ObservableSessions.ts rename to Source/Core/queries/observable/ObservableSessions.ts index 3c85e3c4..3e4cf51d 100644 --- a/Source/Core/queries/ObservableSessions.ts +++ b/Source/Core/queries/observable/ObservableSessions.ts @@ -1,16 +1,16 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { ArcOptions } from '../ArcOptions.js'; -import type { ExecutionContext } from '../execution/ExecutionContext.js'; -import type { QueryOptions } from './QueryOptions.js'; -import type { Operation } from '../http/Operation.js'; -import type { ServiceRegistry } from '../dependencyInjection/ServiceRegistry.js'; -import { ObservableQuerySession } from './observable/ObservableQuerySession.js'; -import { ObservableSubscriptionLimitError } from './observable/ObservableSubscriptionLimitError.js'; -import { isObservableOperation } from './observable/ObservableOperation.js'; -import { observableCallerKey } from './observable/observableCallerKey.js'; -import type { ObservableLimits } from './observable/ObservableLimits.js'; -import { exposeExceptionDetails } from '../execution/exposeExceptionDetails.js'; +import type { ArcOptions } from '../../ArcOptions.js'; +import type { ExecutionContext } from '../../execution/ExecutionContext.js'; +import type { QueryOptions } from '../QueryOptions.js'; +import type { Operation } from '../../http/Operation.js'; +import type { ServiceRegistry } from '../../dependencyInjection/ServiceRegistry.js'; +import { ObservableQuerySession } from './ObservableQuerySession.js'; +import { ObservableSubscriptionLimitError } from './ObservableSubscriptionLimitError.js'; +import { isObservableOperation } from './ObservableOperation.js'; +import { observableCallerKey } from './observableCallerKey.js'; +import type { ObservableLimits } from './ObservableLimits.js'; +import { exposeExceptionDetails } from '../../execution/exposeExceptionDetails.js'; export class ObservableSessions { readonly #observableSessions = new Set(); diff --git a/Source/Core/queries/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts b/Source/Core/queries/observable/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts similarity index 89% rename from Source/Core/queries/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts rename to Source/Core/queries/observable/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts index b8ef915f..3afad9c5 100644 --- a/Source/Core/queries/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts +++ b/Source/Core/queries/observable/for_ObservableSessions/when_recording_cleanup_failure/with_repeated_session.ts @@ -1,8 +1,8 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { should } from 'vitest'; -import { ArcServer } from '../../../ArcServer.js'; -import { ObservableLimits } from '../../observable/ObservableLimits.js'; +import { ArcServer } from '../../../../ArcServer.js'; +import { ObservableLimits } from '../../ObservableLimits.js'; import { ObservableSessions } from '../../ObservableSessions.js'; should(); diff --git a/Source/Core/queries/observable/for_ObservableTransfer/given/a_transfer_result.ts b/Source/Core/queries/observable/for_ObservableTransfer/given/a_transfer_result.ts index d697e741..5ecfb215 100644 --- a/Source/Core/queries/observable/for_ObservableTransfer/given/a_transfer_result.ts +++ b/Source/Core/queries/observable/for_ObservableTransfer/given/a_transfer_result.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { Severity } from '../../../../validation/Severity.js'; -import { queryResult } from '../../../../results/queryResult.js'; +import { queryResult } from '../../../createQueryResult.js'; import type { ExecutionContext } from '../../../../execution/ExecutionContext.js'; const context: ExecutionContext = { diff --git a/Source/Core/queries/observable/observableCleanupFailures.ts b/Source/Core/queries/observable/observableCleanupFailures.ts index 37728c02..f8aed8b3 100644 --- a/Source/Core/queries/observable/observableCleanupFailures.ts +++ b/Source/Core/queries/observable/observableCleanupFailures.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { ArcServer } from '../../ArcServer.js'; -import type { ObservableSessions } from '../ObservableSessions.js'; +import type { ObservableSessions } from './ObservableSessions.js'; const owners = new WeakMap(); diff --git a/Source/Core/queries/queryOperation.ts b/Source/Core/queries/queryOperation.ts index 89b23999..f43bdd1e 100644 --- a/Source/Core/queries/queryOperation.ts +++ b/Source/Core/queries/queryOperation.ts @@ -6,13 +6,13 @@ import type { QueryDefinition } from './QueryDefinition.js'; import type { QueryResult } from './QueryResult.js'; import type { ValidationResult } from '../validation/ValidationResult.js'; import { authorized } from '../authorization/authorized.js'; -import { queryResult } from '../results/queryResult.js'; -import { malformed } from '../results/malformed.js'; +import { queryResult } from './createQueryResult.js'; +import { malformed } from '../http/malformed.js'; import { renderQuery } from './renderQuery.js'; -import { observe } from '../observability.js'; +import { observe } from '../execution/observability.js'; import type { Operation } from '../http/Operation.js'; import { fullyQualifiedName } from '../http/fullyQualifiedName.js'; -import { recordFailure } from '../results/failureTracking.js'; +import { recordFailure } from '../execution/failureTracking.js'; import { ServiceDependencyError } from '../dependencyInjection/ServiceDependencyError.js'; import { prepareDependencies, dependencyFailure, validate, validatorFailure } from '../commands/OperationValidation.js'; import { InvalidQuerySort } from './InvalidQuerySort.js'; diff --git a/Source/Core/queries/queryRendering.ts b/Source/Core/queries/queryRendering.ts index 05b9fe95..e21299e1 100644 --- a/Source/Core/queries/queryRendering.ts +++ b/Source/Core/queries/queryRendering.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { ExecutionContext } from '../execution/ExecutionContext.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; import type { QueryOptions } from './QueryOptions.js'; import type { QueryResult } from './QueryResult.js'; import { isQueryPage } from './QueryPage.js'; diff --git a/Source/Core/queries/renderQuery.ts b/Source/Core/queries/renderQuery.ts index 4c085904..4f8a48cb 100644 --- a/Source/Core/queries/renderQuery.ts +++ b/Source/Core/queries/renderQuery.ts @@ -1,7 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { ArcOptions } from '../ArcOptions.js'; -import type { DescriptorBase } from '../DescriptorBase.js'; +import type { DescriptorBase } from '../http/DescriptorBase.js'; import type { ExecutionContext } from '../execution/ExecutionContext.js'; import { currentServices } from '../dependencyInjection/ServiceScope.js'; import { encode, fieldsFor, wireName } from '../reflection/wireSchema.js'; diff --git a/Source/Core/reflection/resolveAll.ts b/Source/Core/reflection/resolveAll.ts deleted file mode 100644 index 673df8a5..00000000 --- a/Source/Core/reflection/resolveAll.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -import type { ServiceIdentifier } from '../dependencyInjection/ServiceIdentifier.js'; -import { currentServices } from '../dependencyInjection/ServiceScope.js'; - -/** Resolve ordered service tokens in the current execution scope. */ -export async function resolveAll(tokens: readonly ServiceIdentifier[]): Promise { - return Promise.all(tokens.map(token => currentServices().resolve(token))); -} diff --git a/Source/Core/reflection/wireSchema.ts b/Source/Core/reflection/wireSchema.ts index eb510636..8996db3f 100644 --- a/Source/Core/reflection/wireSchema.ts +++ b/Source/Core/reflection/wireSchema.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ConceptAs, DateOnly, DerivedType, Fields, Guid, TimeOnly, TimeSpan, type Field } from '@cratis/fundamentals'; import { z } from 'zod'; -import { isArcTuple } from '../results/ArcTuple.js'; +import { isArcTuple } from '../commands/ArcTuple.js'; import { isQueryPage, queryPage } from '../queries/QueryPage.js'; import { readFieldOptions } from './readFieldOptions.js'; import type { FieldOptions } from './FieldOptions.js'; diff --git a/Source/Core/results/index.ts b/Source/Core/results/index.ts index 47ded970..7ffcfe03 100644 --- a/Source/Core/results/index.ts +++ b/Source/Core/results/index.ts @@ -1,13 +1,13 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -export { commandResult } from './commandResult.js'; +export { commandResult } from '../commands/createCommandResult.js'; /** Preserve the original failure for HTTP logging when an integration fails after command execution. */ -export { recordFailure } from './failureTracking.js'; -export { queryResult } from './queryResult.js'; -export { malformed } from './malformed.js'; -export { status } from './status.js'; -export { emptyPaging } from './emptyPaging.js'; -export { response, rejected, denied, isOutcome } from './Outcome.js'; -export { tuple } from './tuple.js'; -export type { ArcTuple } from './ArcTuple.js'; -export type { Outcome } from './Outcome.js'; +export { recordFailure } from '../execution/failureTracking.js'; +export { queryResult } from '../queries/createQueryResult.js'; +export { malformed } from '../http/malformed.js'; +export { status } from '../http/status.js'; +export { emptyPaging } from '../queries/emptyPaging.js'; +export { response, rejected, denied, isOutcome } from '../commands/Outcome.js'; +export { tuple } from '../commands/tuple.js'; +export type { ArcTuple } from '../commands/ArcTuple.js'; +export type { Outcome } from '../commands/Outcome.js'; diff --git a/Source/Core/validation/readModelForValidation.ts b/Source/Core/validation/readModelForValidation.ts index 7679a5b2..fa1f82f3 100644 --- a/Source/Core/validation/readModelForValidation.ts +++ b/Source/Core/validation/readModelForValidation.ts @@ -2,9 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { AsyncLocalStorage } from 'node:async_hooks'; import type { CommandContext } from '../commands/CommandContext.js'; -import { commandReadModel } from '../commands/modelBound/readModel.js'; +import { commandReadModel } from '../commands/modelBound/commandReadModel.js'; import { ReadModelForCommandError } from '../commands/ReadModelForCommandError.js'; -import { resolveCommandArguments } from '../commands/modelBound/commandArgument.js'; +import { resolveCommandArguments } from '../commands/modelBound/commandContextArgument.js'; import type { ClassType } from '../reflection/ClassType.js'; const current = new AsyncLocalStorage(); From 87973953c6b97951b38c224e0c5263e6f1e0bd2c Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 05:33:28 +0200 Subject: [PATCH 02/15] Keep result helpers internal and expose failure tracking to hosts --- Source/Chronicle/ChronicleUnitOfWork.ts | 3 ++- Source/Core/ArcServer.ts | 3 ++- Source/Core/commands/index.ts | 5 ++++- .../dependencyInjection/ServiceRegistry.ts | 3 +++ Source/Core/fetch.ts | 1 - ...invalid_values_or_controlled_operations.ts | 2 +- .../with_multiple_handlers_and_control.ts | 2 +- .../when_loading_the_public_barrels.ts | 22 +++++++++++++++++++ Source/Core/hosting.ts | 2 ++ Source/Core/http/handleRequest.ts | 5 ++++- Source/Core/index.ts | 1 - .../queries/observable/ObservableOperation.ts | 2 +- .../observable/ObservableQuerySession.ts | 2 +- .../queries/observable/directWebSocket.ts | 3 ++- Source/Core/queries/observable/snapshot.ts | 3 ++- Source/Core/queries/queryRendering.ts | 4 +++- Source/Core/results/index.ts | 13 ----------- Source/Core/validation/index.ts | 1 + 18 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 Source/Core/for_exports/when_loading_the_public_barrels.ts delete mode 100644 Source/Core/results/index.ts diff --git a/Source/Chronicle/ChronicleUnitOfWork.ts b/Source/Chronicle/ChronicleUnitOfWork.ts index b670bf61..5c2dfe5c 100644 --- a/Source/Chronicle/ChronicleUnitOfWork.ts +++ b/Source/Chronicle/ChronicleUnitOfWork.ts @@ -3,7 +3,8 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import type { IEventStore } from '@cratis/chronicle'; import type { AppendOptions, ConcurrencyScope, EventForEventSourceId } from '@cratis/chronicle/eventSequences'; -import { recordFailure, type CommandContext, type CommandResult } from '@cratis/arc.core'; +import type { CommandContext, CommandResult } from '@cratis/arc.core'; +import { recordFailure } from '@cratis/arc.core/hosting'; import type { CommandCommitDisposition } from '@cratis/arc.core'; import { checkResults } from './ChronicleCommand.js'; import type { AggregateRoot } from './AggregateRoot.js'; diff --git a/Source/Core/ArcServer.ts b/Source/Core/ArcServer.ts index aed5f5e3..705a26ff 100644 --- a/Source/Core/ArcServer.ts +++ b/Source/Core/ArcServer.ts @@ -17,7 +17,8 @@ import { handleRequest } from './http/handleRequest.js'; import { createRouteTable } from './http/createRouteTable.js'; import { renderOpenApi } from './openApi/renderOpenApi.js'; import type { Operation } from './http/Operation.js'; -import { commandResult, queryResult } from './results/index.js'; +import { commandResult } from './commands/createCommandResult.js'; +import { queryResult } from './queries/createQueryResult.js'; import { recordFailure } from './execution/failureTracking.js'; import { Severity } from './validation/Severity.js'; import { ServiceRegistry } from './dependencyInjection/ServiceRegistry.js'; diff --git a/Source/Core/commands/index.ts b/Source/Core/commands/index.ts index 427814b4..0e65a911 100644 --- a/Source/Core/commands/index.ts +++ b/Source/Core/commands/index.ts @@ -23,6 +23,9 @@ export type { CommandRecoverySummary } from './CommandRecoverySummary.js'; export { abortSignal, commandContext } from './modelBound/commandContextArgument.js'; export { provided } from './modelBound/provided.js'; export { commandReadModel } from './modelBound/commandReadModel.js'; -export { readModelForValidation } from '../validation/readModelForValidation.js'; export { commandArgument } from './modelBound/commandArgument.js'; +export { response, rejected, denied, isOutcome } from './Outcome.js'; +export { tuple } from './tuple.js'; +export type { ArcTuple } from './ArcTuple.js'; +export type { Outcome } from './Outcome.js'; export type { ReadModelForCommandResolver } from './ReadModelForCommandResolver.js'; diff --git a/Source/Core/dependencyInjection/ServiceRegistry.ts b/Source/Core/dependencyInjection/ServiceRegistry.ts index 737809b7..03a55851 100644 --- a/Source/Core/dependencyInjection/ServiceRegistry.ts +++ b/Source/Core/dependencyInjection/ServiceRegistry.ts @@ -50,7 +50,9 @@ export class ServiceRegistry { } get disposed(): boolean { return this.#state !== ServiceRegistryState.Running; } get singletonFailed(): boolean { return this.#singletonFailed; } + /** @internal Registry-lifetime context for singleton factories. */ get singletonContext(): SingletonServiceContext { return this.#singletonContext; } + /** @internal Poison the registry after a singleton factory failure. */ markSingletonFailure(): void { if (this.#singletonFailed) return; this.#singletonFailed = true; @@ -119,6 +121,7 @@ export class ServiceRegistry { hasRegistration(identifier: ServiceIdentifier): boolean { return this.#registrations.has(normalizeServiceToken(identifier).key); } + /** @internal Retrieve a declaration without constructing its service. */ registration(identifier: ServiceIdentifier): ServiceRegistration { const token = normalizeServiceToken(identifier); if (!token || typeof token.key !== 'symbol' || typeof token.name !== 'string') diff --git a/Source/Core/fetch.ts b/Source/Core/fetch.ts index 3f4c70ce..91975639 100644 --- a/Source/Core/fetch.ts +++ b/Source/Core/fetch.ts @@ -34,4 +34,3 @@ export { routeFor, includeRouteName } from './http/createRouteTable.js'; export type { NativeRequestContext } from './http/NativeRequestContext.js'; export type { Operation } from './http/Operation.js'; export * from './introspection/index.js'; -export * from './results/index.js'; diff --git a/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts b/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts index 664704b7..b1cefe81 100644 --- a/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts +++ b/Source/Core/for_ArcServer/when_processing_command_response/with_invalid_values_or_controlled_operations.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, it, should } from 'vitest'; import { given } from '../../given.js'; import { an_operation_command, ProbeOperation } from '../given/an_operation_command.js'; -import { denied, rejected } from '../../results/index.js'; +import { denied, rejected } from '../../commands/Outcome.js'; import { tuple } from '../../commands/tuple.js'; import type { CommandResult } from '../../commands/CommandResult.js'; should(); diff --git a/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts b/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts index 7cc07c23..e7d430f7 100644 --- a/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts +++ b/Source/Core/for_ArcServer/when_processing_command_response/with_multiple_handlers_and_control.ts @@ -6,7 +6,7 @@ import { ArcServer } from '../../ArcServer.js'; import { defineCommand } from '../../commands/defineCommand.js'; import { serviceToken } from '../../dependencyInjection/ServiceToken.js'; import { tuple } from '../../commands/tuple.js'; -import { denied, rejected, response } from '../../results/index.js'; +import { denied, rejected, response } from '../../commands/Outcome.js'; import type { CommandContext } from '../../commands/CommandContext.js'; import type { CommandResponseValueHandler } from '../../commands/CommandResponseValueHandler.js'; import type { CommandResult } from '../../commands/CommandResult.js'; diff --git a/Source/Core/for_exports/when_loading_the_public_barrels.ts b/Source/Core/for_exports/when_loading_the_public_barrels.ts new file mode 100644 index 00000000..22270273 --- /dev/null +++ b/Source/Core/for_exports/when_loading_the_public_barrels.ts @@ -0,0 +1,22 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import * as node from '../index.js'; +import * as fetch from '../fetch.js'; +import * as hosting from '../hosting.js'; + +describe('when loading the public Arc barrels', () => { + const internal = ['commandResult', 'malformed', 'status', 'emptyPaging', 'recordFailure']; + it('should keep result helpers off the Node root', () => { + internal.filter(name => Object.hasOwn(node, name)).should.deep.equal([]); + }); + it('should keep result helpers off the Fetch root', () => { + internal.filter(name => Object.hasOwn(fetch, name)).should.deep.equal([]); + }); + it('should expose failure tracking to hosting integrations', () => { + hosting.recordFailure.should.be.a('function'); + }); + it('should retain branded command outcomes on the root', () => { + node.tuple.should.be.a('function'); + node.rejected.should.be.a('function'); + }); +}); diff --git a/Source/Core/hosting.ts b/Source/Core/hosting.ts index 979407b2..bd397528 100644 --- a/Source/Core/hosting.ts +++ b/Source/Core/hosting.ts @@ -13,4 +13,6 @@ export { serveUpgradedSocket } from './queries/observable/serveUpgradedSocket.js export { observableLimits } from './queries/observable/observableHosting.js'; export type { NodeWebSocketLike } from './queries/observable/NodeWebSocketLike.js'; export { prepareObservableUpgrade } from './queries/observable/prepareObservableUpgrade.js'; +/** Preserve the original integration failure for host-side diagnostics and cleanup. */ +export { recordFailure } from './execution/failureTracking.js'; export { ObservableHandshakeTimeoutError, withObservableHandshakeTimeout } from './queries/observable/withObservableHandshakeTimeout.js'; diff --git a/Source/Core/http/handleRequest.ts b/Source/Core/http/handleRequest.ts index be8f971d..93ccad46 100644 --- a/Source/Core/http/handleRequest.ts +++ b/Source/Core/http/handleRequest.ts @@ -17,7 +17,10 @@ import { BadRequest } from './BadRequest.js'; import { body } from './body.js'; import { utf8Bytes } from './utf8Bytes.js'; import { getQuery, structuredQuery } from './queryBinding.js'; -import { commandResult, malformed, queryResult, status } from '../results/index.js'; +import { commandResult } from '../commands/createCommandResult.js'; +import { queryResult } from '../queries/createQueryResult.js'; +import { malformed } from './malformed.js'; +import { status } from './status.js'; import { allowedSeverity } from '../validation/allowedSeverity.js'; import { authenticate, verifiedPrincipal } from '../authentication/authenticate.js'; import { correlation } from '../execution/correlation.js'; diff --git a/Source/Core/index.ts b/Source/Core/index.ts index aa2a779b..82fa519c 100644 --- a/Source/Core/index.ts +++ b/Source/Core/index.ts @@ -34,4 +34,3 @@ export * from './execution/index.js'; export * from './dependencyInjection/index.js'; export * from './http/index.js'; export * from './introspection/index.js'; -export * from './results/index.js'; diff --git a/Source/Core/queries/observable/ObservableOperation.ts b/Source/Core/queries/observable/ObservableOperation.ts index 07ce0a5f..6e61858e 100644 --- a/Source/Core/queries/observable/ObservableOperation.ts +++ b/Source/Core/queries/observable/ObservableOperation.ts @@ -8,7 +8,7 @@ import type { QueryResult } from '../QueryResult.js'; import type { Operation } from '../../http/Operation.js'; import { queryOperation } from '../queryOperation.js'; import { renderQuery } from '../renderQuery.js'; -import { queryResult } from '../../results/index.js'; +import { queryResult } from '../createQueryResult.js'; import { recordFailure } from '../../execution/failureTracking.js'; import type { ObservableQueryDefinition } from './ObservableQueryDefinition.js'; import type { ObservableSource } from './ObservableSource.js'; diff --git a/Source/Core/queries/observable/ObservableQuerySession.ts b/Source/Core/queries/observable/ObservableQuerySession.ts index bcb4f041..5bb502be 100644 --- a/Source/Core/queries/observable/ObservableQuerySession.ts +++ b/Source/Core/queries/observable/ObservableQuerySession.ts @@ -3,7 +3,7 @@ import type { ExecutionContext } from '../../execution/ExecutionContext.js'; import type { QueryResult } from '../QueryResult.js'; import { hasFailure, originalFailure } from '../../execution/failureTracking.js'; -import { queryResult } from '../../results/index.js'; +import { queryResult } from '../createQueryResult.js'; import { requestContext } from '../../execution/RequestContextStore.js'; import { withServices } from '../../dependencyInjection/ServiceScope.js'; import type { ObservableSource } from './ObservableSource.js'; diff --git a/Source/Core/queries/observable/directWebSocket.ts b/Source/Core/queries/observable/directWebSocket.ts index bc1c4db2..248f82fe 100644 --- a/Source/Core/queries/observable/directWebSocket.ts +++ b/Source/Core/queries/observable/directWebSocket.ts @@ -5,7 +5,8 @@ import { BadRequest } from '../../http/BadRequest.js'; import { getQuery } from '../../http/queryBinding.js'; import type { ExecutionContext } from '../../execution/ExecutionContext.js'; import type { NativeRequestContext } from '../../http/NativeRequestContext.js'; -import { malformed, queryResult } from '../../results/index.js'; +import { malformed } from '../../http/malformed.js'; +import { queryResult } from '../createQueryResult.js'; import { correlation } from '../../execution/correlation.js'; import { Severity } from '../../validation/Severity.js'; import { isObservableOperation } from './ObservableOperation.js'; diff --git a/Source/Core/queries/observable/snapshot.ts b/Source/Core/queries/observable/snapshot.ts index 1c7d98bb..e16ab9f5 100644 --- a/Source/Core/queries/observable/snapshot.ts +++ b/Source/Core/queries/observable/snapshot.ts @@ -3,7 +3,8 @@ import { BadRequest } from '../../http/BadRequest.js'; import type { ExecutionContext } from '../../execution/ExecutionContext.js'; import type { QueryResult } from '../QueryResult.js'; -import { queryResult, status } from '../../results/index.js'; +import { queryResult } from '../createQueryResult.js'; +import { status } from '../../http/status.js'; import type { ObservableQuerySession } from './ObservableQuerySession.js'; /** Only query routes consume these reserved parameters. Maximum wait bounds retained subscriptions. */ diff --git a/Source/Core/queries/queryRendering.ts b/Source/Core/queries/queryRendering.ts index e21299e1..84d4fa5d 100644 --- a/Source/Core/queries/queryRendering.ts +++ b/Source/Core/queries/queryRendering.ts @@ -6,7 +6,9 @@ import type { QueryOptions } from './QueryOptions.js'; import type { QueryResult } from './QueryResult.js'; import { isQueryPage } from './QueryPage.js'; import { assertClientOutput } from '../introspection/ClientManifest.js'; -import { emptyPaging, malformed, queryResult } from '../results/index.js'; +import { emptyPaging } from './emptyPaging.js'; +import { malformed } from '../http/malformed.js'; +import { queryResult } from './createQueryResult.js'; function compareValues(left: unknown, right: unknown): number { if (left instanceof Date && right instanceof Date) return left.getTime() - right.getTime(); diff --git a/Source/Core/results/index.ts b/Source/Core/results/index.ts deleted file mode 100644 index 7ffcfe03..00000000 --- a/Source/Core/results/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -export { commandResult } from '../commands/createCommandResult.js'; -/** Preserve the original failure for HTTP logging when an integration fails after command execution. */ -export { recordFailure } from '../execution/failureTracking.js'; -export { queryResult } from '../queries/createQueryResult.js'; -export { malformed } from '../http/malformed.js'; -export { status } from '../http/status.js'; -export { emptyPaging } from '../queries/emptyPaging.js'; -export { response, rejected, denied, isOutcome } from '../commands/Outcome.js'; -export { tuple } from '../commands/tuple.js'; -export type { ArcTuple } from '../commands/ArcTuple.js'; -export type { Outcome } from '../commands/Outcome.js'; diff --git a/Source/Core/validation/index.ts b/Source/Core/validation/index.ts index 9b8b64ee..eac0685b 100644 --- a/Source/Core/validation/index.ts +++ b/Source/Core/validation/index.ts @@ -11,3 +11,4 @@ export { CommandValidator } from './CommandValidator.js'; export { QueryValidator } from './QueryValidator.js'; export { ConceptValidator } from './ConceptValidator.js'; export { ModelValidator } from './ModelValidator.js'; +export { readModelForValidation } from './readModelForValidation.js'; From 7f0f65ee976d3f6377af6a966a4cfbc4c4b84b99 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 05:39:26 +0200 Subject: [PATCH 03/15] Add explicit command validation entry points --- .../commands/calling-commands-from-code.md | 12 +++--- Documentation/dependency-injection.md | 2 +- .../testing/low-level-definitions.md | 3 +- Source/Core/ArcServer.ts | 42 ++++++++++++++----- Source/Core/commands/runCommandOperation.ts | 19 +++++---- .../when_validating_an_instance.ts | 32 ++++++++++++++ ...ith_denied_and_validation_only_requests.ts | 4 +- .../with_handler_dependencies.ts | 2 +- ..._missing_cyclic_or_captive_dependencies.ts | 2 +- Source/Core/http/Operation.ts | 5 ++- Source/Core/http/handleRequest.ts | 6 ++- Source/Testing/ArcScenario.ts | 9 +++- Source/Testing/CommandScenario.ts | 10 +++-- ...when_validating_without_running_handler.ts | 21 ++++++++++ 14 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 Source/Core/for_ArcApplicationBuilder/when_validating_an_instance.ts create mode 100644 Source/Testing/for_ArcScenario/when_validating_without_running_handler.ts diff --git a/Documentation/commands/calling-commands-from-code.md b/Documentation/commands/calling-commands-from-code.md index f518662e..13dbbe44 100644 --- a/Documentation/commands/calling-commands-from-code.md +++ b/Documentation/commands/calling-commands-from-code.md @@ -8,8 +8,10 @@ Sometimes a command has to run without an HTTP client: an import job, a message | Entry point | Runs | Trusts the caller with | | --- | --- | --- | | `handle(request, native?)` | The full HTTP pipeline | Nothing beyond an HTTP client | -| `executeCommand(name, input, context, validateOnly?)` | Authorization, binding, validators, and the command | The whole execution context, including allowed severity | -| `execute(command, context, validateOnly?)` | The same, for a decorated command instance | The same | +| `executeCommand(name, input, context)` | Authorization, binding, validators, and the command | The whole execution context, including allowed severity | +| `execute(command, context)` | The same, for a decorated command instance | The same | +| `validateCommand(name, input, context)` | Authorization, binding, and validators, without `provide` or `handle` | The whole execution context | +| `validate(command, context)` | The same, for a decorated command instance | The same | | `performQuery(name, input, context, options?)` | Authorization, binding, validators, and the query | The whole execution context, except allowed severity | ## Run a command and a query directly @@ -20,8 +22,8 @@ This example uses the Tasks sample's classes, added explicitly: import { randomUUID } from 'node:crypto'; import { ArcApplication, Severity } from '@cratis/arc.core'; import { Tasks } from './Features/Tasks/Tasks.js'; -import { RegisterTask } from './Features/Tasks/Registration/RegisterTask.js'; -import { TaskItem } from './Features/Tasks/Listing/TaskItem.js'; +import { RegisterTask } from './Features/Tasks/Registration/Registration.js'; +import { TaskItem } from './Features/Tasks/Listing/Listing.js'; const builder = ArcApplication.createBuilder(); builder.services.addSingleton(Tasks); @@ -56,7 +58,7 @@ The first log line is `true 1a638f8e-4444-4444-8888-a0b10cdd9977`; the query ret | Model-bound query | Namespace, read-model name, and method: `Tasks.Listing.TaskItem.allTasks` when discovered | | Low-level definition | Namespace and name joined with a dot, such as `Tasks.Create`, or the bare name without a namespace | -If you already hold a decorated command instance, `app.server.execute(command, context)` serializes its decorated fields and runs the same pipeline; the registered command name must be unambiguous. Pass `true` as the last argument of `executeCommand` or `execute` to validate without running the command, like the `/validate` route. `performQuery` takes paging and sorting as its fourth argument, for example `{ paging: { page: 0, pageSize: 10 }, sorting: { field: 'title', direction: 'asc' } }`. +If you already hold a decorated command instance, `app.server.execute(command, context)` serializes its decorated fields and runs the same pipeline; the registered command name must be unambiguous. Call `validateCommand(name, input, context)` or `validate(command, context)` to check authorization and validation without running the command, like the `/validate` route. `performQuery` takes paging and sorting as its fourth argument, for example `{ paging: { page: 0, pageSize: 10 }, sorting: { field: 'title', direction: 'asc' } }`. ## What a direct call does differently diff --git a/Documentation/dependency-injection.md b/Documentation/dependency-injection.md index 3ae8a3f8..5aa060de 100644 --- a/Documentation/dependency-injection.md +++ b/Documentation/dependency-injection.md @@ -80,7 +80,7 @@ const context = { correlationId: crypto.randomUUID(), principal: undefined, tenantId: 'acme', signal: new AbortController().signal, allowedSeverity: Severity.Warning }; -const validation = await server.executeCommand('Write', { text: 'hello' }, context, true); +const validation = await server.validateCommand('Write', { text: 'hello' }, context); const result = await server.executeCommand('Write', { text: 'hello' }, context); console.log(validation.isSuccess, result.response, created); // true ['hello'] 1 await server.dispose(); diff --git a/Documentation/testing/low-level-definitions.md b/Documentation/testing/low-level-definitions.md index 7804587f..1b1af25c 100644 --- a/Documentation/testing/low-level-definitions.md +++ b/Documentation/testing/low-level-definitions.md @@ -28,7 +28,8 @@ try { | Method | Runs | | --- | --- | -| `executeCommand(name, input, context?, validateOnly?)` | The direct command pipeline; `context` overrides the scenario's default context fields | +| `executeCommand(name, input, context?)` | The direct command pipeline; `context` overrides the scenario's default context fields | +| `validateCommand(name, input, context?)` | Authorization and validation without running the command handler | | `performQuery(name, input, context?, options?)` | The direct query pipeline, with optional paging and sorting | | `handle(request)` | The full HTTP pipeline, authenticating with the configured handlers | diff --git a/Source/Core/ArcServer.ts b/Source/Core/ArcServer.ts index 705a26ff..e64ceba5 100644 --- a/Source/Core/ArcServer.ts +++ b/Source/Core/ArcServer.ts @@ -35,6 +35,7 @@ import type { ResolvedConnectionContext } from './queries/observable/ResolvedCon import { registerObservableCleanup } from './queries/observable/observableCleanupFailures.js'; import { observe } from './execution/observability.js'; export function currentContext(): ExecutionContext | undefined { return requestContext.getStore(); } +enum OperationMode { Execute, Validate } export class ArcServer { readonly commands: readonly Operation[]; readonly queries: readonly Operation[]; @@ -150,11 +151,15 @@ export class ArcServer { return outcome.value; } - private runScoped(operation: Operation, input: unknown, context: ExecutionContext, options?: QueryOptions, validateOnly = false): Promise { + private runScoped(operation: Operation, input: unknown, context: ExecutionContext, options?: QueryOptions, + mode = OperationMode.Execute): Promise { if (operation.kind === 'command' && CommandOperationBoundary.attempt(this)) return Promise.resolve(commandResult(context, { exceptionMessages: ['Nested commands are unsupported in command operations'] })); const run = () => this.runOwned(context, async () => { - try { return await operation.run(input, context, options, validateOnly); } + try { + if (mode === OperationMode.Validate) return await operation.validateCommand!(input, context); + return await operation.run(input, context, options); + } catch (error) { const result = operation.kind === 'command' ? commandResult(context, { exceptionMessages: [String(error)] }) @@ -169,7 +174,8 @@ export class ArcServer { recordFailure(result, error, previous); return result; }); - const name = operation.kind === 'command' ? validateOnly ? 'cratis.arc.command.validate' : 'cratis.arc.command.execute' : 'cratis.arc.query.perform'; + const name = operation.kind === 'query' ? 'cratis.arc.query.perform' : + mode === OperationMode.Validate ? 'cratis.arc.command.validate' : 'cratis.arc.command.execute'; const qualified = operation.fullyQualifiedName; const attributes = operation.kind === 'command' ? { command_type: qualified } : { query_name: qualified }; const traced = () => observe(name, context.correlationId, attributes, run, undefined, result => result.hasExceptions); @@ -222,19 +228,33 @@ export class ArcServer { return this.#hub.webSocket(request, transport, native, resolved); } - /** Execute a decorated command instance through the ordinary direct-call pipeline. */ - async execute(command: object, context: ExecutionContext, validateOnly = false): Promise { + private operationFor(command: object): Operation { const type = command.constructor; if (!ownMetadata(type as never).command) throw new Error(`Not an Arc command: ${type.name}`); const matches = this.commands.filter(item => item.name === type.name); if (matches.length !== 1) throw new Error(`Ambiguous or unregistered Arc command: ${type.name}`); - const operation = matches[0]!; - return this.executeCommand(operation.fullyQualifiedName, encode(command), context, validateOnly); + return matches[0]!; + } + + /** Execute a decorated command instance through the ordinary direct-call pipeline. */ + async execute(command: object, context: ExecutionContext): Promise { + return this.executeCommand(this.operationFor(command).fullyQualifiedName, encode(command), context); + } + /** Validate a decorated command without running provide, handle, or execution scopes. */ + async validate(command: object, context: ExecutionContext): Promise { + return this.validateCommand(this.operationFor(command).fullyQualifiedName, encode(command), context); + } + /** Execute a registered command by its fully qualified name. */ + async executeCommand(name: string, input: unknown, context: ExecutionContext): Promise { + const operation = this.#commandsByName.get(name); + if (!operation) throw new Error(`Unknown command: ${name}`); + return this.runScoped(operation, input, Object.freeze({ ...context })) as Promise; } - async executeCommand(name: string, input: unknown, context: ExecutionContext, validateOnly = false): Promise { + /** Validate a registered command by its fully qualified name, without running its handler. */ + async validateCommand(name: string, input: unknown, context: ExecutionContext): Promise { const operation = this.#commandsByName.get(name); if (!operation) throw new Error(`Unknown command: ${name}`); - return this.runScoped(operation, input, Object.freeze({ ...context }), undefined, validateOnly) as Promise; + return this.runScoped(operation, input, Object.freeze({ ...context }), undefined, OperationMode.Validate) as Promise; } async performQuery(name: string, input: unknown, context: ExecutionContext, options?: QueryOptions): Promise { const operation = this.#queriesByName.get(name); @@ -251,7 +271,9 @@ export class ArcServer { identitySchema: this.#identitySchema, hubHttp: (incoming, context) => this.#hub.http(incoming, context), runProvider: (context, callback) => this.runProvider(context, callback), - runScoped: (operation, input, context, options, validateOnly) => this.runScoped(operation, input, context, options, validateOnly), + runScoped: (operation, input, context, options) => this.runScoped(operation, input, context, options), + validateCommand: (operation, input, context) => this.runScoped(operation, input, context, undefined, + OperationMode.Validate) as Promise, openSession: (name, input, context, options, admission) => this.#sessions.openSession(name, input, context, options, admission), reserveSession: (session, context) => this.#sessions.reserveSession(session, context) }, request, native); diff --git a/Source/Core/commands/runCommandOperation.ts b/Source/Core/commands/runCommandOperation.ts index aeefd995..5f7f6aa8 100644 --- a/Source/Core/commands/runCommandOperation.ts +++ b/Source/Core/commands/runCommandOperation.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { z } from 'zod'; import type { ArcOptions } from '../ArcOptions.js'; +import type { ExecutionContext } from '../execution/ExecutionContext.js'; import type { CommandDefinition } from './CommandDefinition.js'; import type { CommandResult } from './CommandResult.js'; import type { ValidationResult } from '../validation/ValidationResult.js'; @@ -46,13 +47,11 @@ function failure(context: CommandContext, error: unknown, previous?: CommandResu recordFailure(result, error, previous); return result; } +enum CommandOperationMode { Execute, Validate } + /** Compile a command into the shared direct and HTTP execution pipeline. */ export function commandOperation(definition: CommandDefinition, route: string, options: ArcOptions = {}): Operation { - return { - ...definition, kind: 'command', route, fullyQualifiedName: fullyQualifiedName(definition), - dynamicAuthorization: typeof definition.authorize === 'function', - inputSchema: definition.wireInputSchema ?? z.toJSONSchema(definition.schema), - async run(input, execution, _options, validateOnly): Promise { + const invoke = async (input: unknown, execution: ExecutionContext, mode: CommandOperationMode): Promise => { if (!await authorized(definition.authorization, execution, options.authorizationPolicies ?? {}, definition, input)) return commandResult(execution, { isAuthorized: false }); const parsed = definition.schema.safeParse(input); if (!parsed.success) return commandResult(execution, { validationResults: malformed(execution) }); @@ -78,7 +77,7 @@ export function commandOperation(definition: CommandDefi return result; } if (issues.length) return commandResult(context, { validationResults: issues }); - if (validateOnly) return commandResult(context); + if (mode === CommandOperationMode.Validate) return commandResult(context); const scopes: CommandExecutionScope[] = []; let result: CommandResult = commandResult(context); let journal: CommandOperationExecution | undefined; @@ -157,6 +156,12 @@ export function commandOperation(definition: CommandDefi } catch (error) { return failure(context, error); } }; return options.commandExecutionRunner ? options.commandExecutionRunner(context, execute) : execute(); - } + }; + return { + ...definition, kind: 'command', route, fullyQualifiedName: fullyQualifiedName(definition), + dynamicAuthorization: typeof definition.authorize === 'function', + inputSchema: definition.wireInputSchema ?? z.toJSONSchema(definition.schema), + run: (input, execution) => invoke(input, execution, CommandOperationMode.Execute), + validateCommand: (input, execution) => invoke(input, execution, CommandOperationMode.Validate) }; } diff --git a/Source/Core/for_ArcApplicationBuilder/when_validating_an_instance.ts b/Source/Core/for_ArcApplicationBuilder/when_validating_an_instance.ts new file mode 100644 index 00000000..9a4eada2 --- /dev/null +++ b/Source/Core/for_ArcApplicationBuilder/when_validating_an_instance.ts @@ -0,0 +1,32 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { randomUUID } from 'node:crypto'; +import { Severity } from '../validation/Severity.js'; +import { RegisterItem } from './given/RegisterItem.js'; +import { ItemName } from './given/ItemName.js'; +import { Items } from './given/Items.js'; +import type { CommandResult } from '../commands/CommandResult.js'; +import { ArcApplication } from '../ArcApplication.js'; + +describe('when validating a decorated command instance directly', () => { + let result: CommandResult; + let items: Items; + beforeEach(async () => { + items = new Items(); + const builder = ArcApplication.createBuilder({ configuration: false }); + builder.services.addSingleton(Items, () => items); + builder.add(RegisterItem); + const application = await builder.build(); + try { + result = await application.server.validate(Object.assign(new RegisterItem(), { name: new ItemName('direct') }), { + correlationId: randomUUID(), signal: new AbortController().signal, allowedSeverity: Severity.Warning, + principal: undefined, tenantId: undefined + }); + } finally { await application.dispose(); } + }); + it('should return a successful validation result without a response', () => { + result.isSuccess.should.equal(true); + (result.response === undefined).should.equal(true); + }); + it('should not invoke the handler', () => { items.values.should.have.lengthOf(0); }); +}); diff --git a/Source/Core/for_ArcServer/when_handling_a_validate_named_command/with_denied_and_validation_only_requests.ts b/Source/Core/for_ArcServer/when_handling_a_validate_named_command/with_denied_and_validation_only_requests.ts index 7a18a273..078d4242 100644 --- a/Source/Core/for_ArcServer/when_handling_a_validate_named_command/with_denied_and_validation_only_requests.ts +++ b/Source/Core/for_ArcServer/when_handling_a_validate_named_command/with_denied_and_validation_only_requests.ts @@ -23,8 +23,8 @@ describe('when handling a validate named command with denied and validation only const url = 'http://localhost/api/submit/validate'; deniedStatus = (await server.handle(new Request(url, { method: 'POST', body: '{}' })))?.status; afterDenial = [...events]; - validationSuccess = (await server.executeCommand('Submit', {}, { ...serviceContext('alpha'), - principal: { id: 'a', isAuthenticated: true, roles: [] } }, true)).isSuccess; + validationSuccess = (await server.validateCommand('Submit', {}, { ...serviceContext('alpha'), + principal: { id: 'a', isAuthenticated: true, roles: [] } })).isSuccess; afterValidation = [...events]; const http = new ArcServer({ services: [{ token: handler, lifetime: 'scoped', factory: () => { events.push('http factory'); return {}; } }], commands: [defineCommand({ name: 'Submit', schema: z.object({}), handlerDependencies: [handler], handle: () => { events.push('http handle'); return 1; } })] }); diff --git a/Source/Core/for_ArcServer/when_preflighting_a_command/with_handler_dependencies.ts b/Source/Core/for_ArcServer/when_preflighting_a_command/with_handler_dependencies.ts index 02d56778..4de66d8e 100644 --- a/Source/Core/for_ArcServer/when_preflighting_a_command/with_handler_dependencies.ts +++ b/Source/Core/for_ArcServer/when_preflighting_a_command/with_handler_dependencies.ts @@ -22,7 +22,7 @@ describe('when preflighting a command with handler dependencies', () => { scopes: [() => ({ begin: () => { calls.push('begin'); }, complete: () => { calls.push('complete'); } })], provide: () => { calls.push('provide'); return 1; }, handle: () => { calls.push('handle'); return 2; } })] }); try { - validationSuccess = (await server.executeCommand('Save', {}, serviceContext('alpha'), true)).isSuccess; + validationSuccess = (await server.validateCommand('Save', {}, serviceContext('alpha'))).isSuccess; validationCalls = [...calls]; response = (await server.executeCommand('Save', {}, serviceContext('alpha'))).response; executionCalls = [...calls]; diff --git a/Source/Core/for_ArcServer/when_preflighting_a_command/with_missing_cyclic_or_captive_dependencies.ts b/Source/Core/for_ArcServer/when_preflighting_a_command/with_missing_cyclic_or_captive_dependencies.ts index 13773dd3..59e0e200 100644 --- a/Source/Core/for_ArcServer/when_preflighting_a_command/with_missing_cyclic_or_captive_dependencies.ts +++ b/Source/Core/for_ArcServer/when_preflighting_a_command/with_missing_cyclic_or_captive_dependencies.ts @@ -22,7 +22,7 @@ describe('when preflighting a command with missing cyclic or captive dependencie ], commands: [missing, cycleA, singleton].map((token, index) => defineCommand({ name: `Action${index}`, schema: z.object({}), handlerDependencies: [token], validate: () => { calls.push('validate'); return []; }, provide: () => { calls.push('provide'); }, handle: () => { calls.push('handle'); } })) }); try { - const outcomes = await Promise.all([0, 1, 2].map(index => server.executeCommand(`Action${index}`, {}, serviceContext('alpha'), true))); + const outcomes = await Promise.all([0, 1, 2].map(index => server.validateCommand(`Action${index}`, {}, serviceContext('alpha')))); reasons = outcomes.map(result => result.validationResults[0]?.reason); successes = outcomes.map(result => result.isSuccess); } finally { await server.dispose(); } diff --git a/Source/Core/http/Operation.ts b/Source/Core/http/Operation.ts index 14f5d3e7..a0c0efe6 100644 --- a/Source/Core/http/Operation.ts +++ b/Source/Core/http/Operation.ts @@ -17,5 +17,8 @@ export interface Operation extends DescriptorBase { /** Built-in endpoints are served and documented, but not emitted as application client proxies. */ readonly internal?: boolean; readonly schema: z.ZodType; - run(input: unknown, context: ExecutionContext, options?: QueryOptions, validateOnly?: boolean): Promise; + /** Execute the command or query through its registered pipeline. */ + run(input: unknown, context: ExecutionContext, options?: QueryOptions): Promise; + /** Validate a command without running provide, handle, or execution scopes. */ + validateCommand?(input: unknown, context: ExecutionContext): Promise; } diff --git a/Source/Core/http/handleRequest.ts b/Source/Core/http/handleRequest.ts index 93ccad46..7eed0a31 100644 --- a/Source/Core/http/handleRequest.ts +++ b/Source/Core/http/handleRequest.ts @@ -43,7 +43,8 @@ export interface RequestBindings { readonly identitySchema: Record | undefined; hubHttp(request: Request, native?: NativeRequestContext): Promise; runProvider(context: ExecutionContext, callback: () => T | Promise): Promise; - runScoped(operation: Operation, input: unknown, context: ExecutionContext, options?: QueryOptions, validateOnly?: boolean): Promise; + runScoped(operation: Operation, input: unknown, context: ExecutionContext, options?: QueryOptions): Promise; + validateCommand(operation: Operation, input: unknown, context: ExecutionContext): Promise; openSession(name: string, input: unknown, context: ExecutionContext, options: QueryOptions | undefined, admission: 'subscription' | 'snapshot'): Promise; reserveSession(session: ObservableQuerySession, context: ExecutionContext): void; } @@ -195,7 +196,8 @@ export async function handleRequest(server: ArcServer, bindings: RequestBindings return send(outcome.result, outcome.code); } finally { await session.close(); } } - const result = await bindings.runScoped(operation, input, context, options, isValidation); + const result = isValidation ? await bindings.validateCommand(operation, input, context) : + await bindings.runScoped(operation, input, context, options); if (hasFailure(result) && !await logFailure(originalFailure(result))) return serverFailure(); if (result.exceptionMessages.length) { if (!exposeExceptionDetails(server.options)) { diff --git a/Source/Testing/ArcScenario.ts b/Source/Testing/ArcScenario.ts index a791ce3d..47146d9f 100644 --- a/Source/Testing/ArcScenario.ts +++ b/Source/Testing/ArcScenario.ts @@ -14,8 +14,13 @@ export class ArcScenario { return { correlationId: randomUUID(), principal: undefined, tenantId: undefined, signal: new AbortController().signal, allowedSeverity: Severity.Warning, ...this.context, ...overrides }; } - executeCommand(name: string, input: unknown, context: Partial = {}, validateOnly = false): Promise { - return this.server.executeCommand(name, input, this.execution(context), validateOnly); + /** Execute a registered command using the scenario's trusted context. */ + executeCommand(name: string, input: unknown, context: Partial = {}): Promise { + return this.server.executeCommand(name, input, this.execution(context)); + } + /** Validate a registered command without executing its handler. */ + validateCommand(name: string, input: unknown, context: Partial = {}): Promise { + return this.server.validateCommand(name, input, this.execution(context)); } performQuery(name: string, input: unknown, context: Partial = {}, options?: QueryOptions): Promise { return this.server.performQuery(name, input, this.execution(context), options); diff --git a/Source/Testing/CommandScenario.ts b/Source/Testing/CommandScenario.ts index 01845dca..082674cb 100644 --- a/Source/Testing/CommandScenario.ts +++ b/Source/Testing/CommandScenario.ts @@ -33,13 +33,13 @@ export class CommandScenario { withAllowedValidationSeverity(severity: Severity): this { this.#host.withAllowedValidationSeverity(severity); return this; } /** Execute values or an instance; return the actual pipeline result with focused assertions. */ - async execute(command: T | Partial): Promise { return this.run(command, false); } + async execute(command: T | Partial): Promise { return this.run(command, 'execute'); } /** Run validation without invoking provide or handle. */ - async validate(command: T | Partial): Promise { return this.run(command, true); } + async validate(command: T | Partial): Promise { return this.run(command, 'validate'); } /** Dispose application-owned services. Safe to call more than once. */ dispose(): Promise { return this.#host.dispose(); } - private async run(command: T | Partial, validateOnly: boolean): Promise { + private async run(command: T | Partial, action: 'execute' | 'validate'): Promise { const application = await this.#host.application(); const instance = command instanceof this.#type ? command : Object.assign(Reflect.construct(this.#type, []) as T, command); const matches = application.server.commands.filter(item => item.name === this.#type.name); @@ -47,7 +47,9 @@ export class CommandScenario { const operation = matches[0]!; const input = this.#host.serializationRoundTrip ? wireRoundTrip(instance) : encodeWireValue(instance); const name = [operation.namespace, operation.name].filter(Boolean).join('.'); - const result = await application.server.executeCommand(name, input, this.#host.execution(), validateOnly); + const context = this.#host.execution(); + const result = action === 'validate' ? await application.server.validateCommand(name, input, context) : + await application.server.executeCommand(name, input, context); if (this.#host.serializationRoundTrip && result.response !== undefined) result.response = wireRoundTrip(result.response); return withCommandAssertions(result); } diff --git a/Source/Testing/for_ArcScenario/when_validating_without_running_handler.ts b/Source/Testing/for_ArcScenario/when_validating_without_running_handler.ts new file mode 100644 index 00000000..dbf7bb8b --- /dev/null +++ b/Source/Testing/for_ArcScenario/when_validating_without_running_handler.ts @@ -0,0 +1,21 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { z } from 'zod'; +import { defineCommand } from '@cratis/arc.core'; +import { ArcScenario } from '../ArcScenario.js'; + +describe('when validating a command through a low-level scenario', () => { + let valid: boolean; + let executions: number; + beforeEach(async () => { + executions = 0; + const scenario = new ArcScenario({ commands: [defineCommand({ name: 'Submit', schema: z.object({}), + handle: () => { executions++; return 1; } })] }); + try { valid = (await scenario.validateCommand('Submit', {})).isSuccess; } + finally { await scenario.dispose(); } + }); + it('should validate successfully without running the handler', () => { + valid.should.equal(true); + executions.should.equal(0); + }); +}); From b4ae15954412e1032b8a5738cfcfe1e55334cb5d Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 05:41:22 +0200 Subject: [PATCH 04/15] Extract ArcServer option validation from its constructor --- Source/Core/ArcServer.ts | 50 ++++-------------------- Source/Core/validateOptions.ts | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 42 deletions(-) create mode 100644 Source/Core/validateOptions.ts diff --git a/Source/Core/ArcServer.ts b/Source/Core/ArcServer.ts index e64ceba5..1c022fe1 100644 --- a/Source/Core/ArcServer.ts +++ b/Source/Core/ArcServer.ts @@ -1,6 +1,5 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -import { z } from 'zod'; import type { CommandResult } from './commands/CommandResult.js'; import type { ExecutionContext } from './execution/ExecutionContext.js'; import type { QueryOptions } from './queries/QueryOptions.js'; @@ -10,9 +9,9 @@ import { ownMetadata } from './reflection/ownMetadata.js'; import { withGeneratedMetadata } from './reflection/registerGeneratedMetadata.js'; import type { ArtifactMetadata } from './reflection/ArtifactMetadata.js'; import type { ClassType } from './reflection/ClassType.js'; -import { encode, objectSchema } from './reflection/wireSchema.js'; +import { encode } from './reflection/wireSchema.js'; import type { NativeRequestContext } from './http/NativeRequestContext.js'; -import { validateTenancy } from './tenancy/validateTenancy.js'; +import { validateOptions, validateRegistryOptions, validateTransportOptions } from './validateOptions.js'; import { handleRequest } from './http/handleRequest.js'; import { createRouteTable } from './http/createRouteTable.js'; import { renderOpenApi } from './openApi/renderOpenApi.js'; @@ -58,47 +57,14 @@ export class ArcServer { constructor(options: ArcOptions, generatedMetadata?: ReadonlyMap) { this.#generatedMetadata = generatedMetadata; - const detailsSchema = options.identityDetails?.schema ?? (options.identityDetails?.detailsType - ? objectSchema(options.identityDetails.detailsType) : undefined); - this.options = detailsSchema && options.identityDetails ? { - ...options, identityDetails: { ...options.identityDetails, schema: detailsSchema } - } : options; - if (options.correlationId?.httpHeader !== undefined && !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(options.correlationId?.httpHeader)) - throw new Error('Invalid correlation header'); - if (options.commandCompensationTimeoutMs !== undefined && - (!Number.isSafeInteger(options.commandCompensationTimeoutMs) || options.commandCompensationTimeoutMs < 1 || - options.commandCompensationTimeoutMs > 4_294_967_294)) - throw new Error('Compensation timeout must be positive and at most 4294967294 milliseconds'); - validateTenancy(options.tenancy); - if (options.nativePrincipal && options.authentication?.length) throw new Error('Native principal and Arc authentication handlers cannot be combined'); - if (options.identityDetails && (!(detailsSchema instanceof z.ZodType) || typeof options.identityDetails.provide !== 'function')) - throw new Error('Identity details require a provider schema'); - if ((options.developmentUsers || options.developmentTenants) && !options.development) throw new Error('Discovery providers require development mode'); - this.#identitySchema = detailsSchema ? z.toJSONSchema(detailsSchema) : undefined; - this.observableLimits = new ObservableLimits(options); - if (options.query?.allowedOrigins !== undefined && !Array.isArray(options.query?.allowedOrigins) && - typeof options.query?.allowedOrigins !== 'function') throw new Error('Invalid allowed Origins'); - if (Array.isArray(options.query?.allowedOrigins) && options.query?.allowedOrigins.some(origin => { - if (typeof origin !== 'string') return true; - try { - const parsed = new URL(origin); - return parsed.origin !== origin || !['http:', 'https:'].includes(parsed.protocol); - } catch { return true; } - })) throw new Error('Invalid allowed Origin'); + const validated = validateOptions(options); + this.options = validated.options; + this.#identitySchema = validated.identitySchema; + this.observableLimits = validated.observableLimits; this.#ownsServices = !(options.services instanceof ServiceRegistry); this.services = options.services instanceof ServiceRegistry ? options.services : new ServiceRegistry(options.services); - for (const token of [...options.queryRenderers ?? [], ...options.readModelInterceptors ?? []]) { - if (this.services.registration(token).lifetime === 'singleton') - throw new Error(`Query renderer or read-model interceptor ${this.services.registration(token).token.name} must not be singleton`); - } - if (options.hosting?.maxBodyBytes !== undefined && - (!Number.isSafeInteger(options.hosting.maxBodyBytes) || options.hosting.maxBodyBytes <= 0)) - throw new Error('Invalid maximum body size'); - if (options.query?.keepAliveIntervalMs !== undefined && - (!Number.isSafeInteger(options.query?.keepAliveIntervalMs) || options.query?.keepAliveIntervalMs < 0 || - options.query?.keepAliveIntervalMs > 120_000)) throw new Error('Invalid observable keep-alive interval'); - if (options.query?.enableObservableHealth !== undefined && typeof options.query?.enableObservableHealth !== 'boolean') - throw new Error('Invalid observable health option'); + validateRegistryOptions(options, this.services); + validateTransportOptions(options); const table = createRouteTable(options, context => this.#hub.observeHealth(context)); this.commands = table.commands; this.queries = table.queries; diff --git a/Source/Core/validateOptions.ts b/Source/Core/validateOptions.ts new file mode 100644 index 00000000..18e4bb98 --- /dev/null +++ b/Source/Core/validateOptions.ts @@ -0,0 +1,69 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { z } from 'zod'; +import type { ArcOptions } from './ArcOptions.js'; +import { ServiceRegistry } from './dependencyInjection/ServiceRegistry.js'; +import { objectSchema } from './reflection/wireSchema.js'; +import { ObservableLimits } from './queries/observable/ObservableLimits.js'; +import type { ObservableQueryOptions } from './queries/ObservableQueryOptions.js'; +import { validateTenancy } from './tenancy/validateTenancy.js'; + +function validateOrigins(origins: ObservableQueryOptions['allowedOrigins']): void { + if (origins !== undefined && !Array.isArray(origins) && typeof origins !== 'function') + throw new Error('Invalid allowed Origins'); + if (Array.isArray(origins) && origins.some(origin => { + if (typeof origin !== 'string') return true; + try { + const parsed = new URL(origin); + return parsed.origin !== origin || !['http:', 'https:'].includes(parsed.protocol); + } catch { return true; } + })) throw new Error('Invalid allowed Origin'); +} + +/** Validate input-independent server settings and normalize an identity provider schema. */ +export function validateOptions(options: ArcOptions): { + options: ArcOptions; identitySchema: Record | undefined; observableLimits: ObservableLimits +} { + const detailsSchema = options.identityDetails?.schema ?? (options.identityDetails?.detailsType + ? objectSchema(options.identityDetails.detailsType) : undefined); + const normalized = detailsSchema && options.identityDetails ? { + ...options, identityDetails: { ...options.identityDetails, schema: detailsSchema } + } : options; + if (options.correlationId?.httpHeader !== undefined && !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(options.correlationId.httpHeader)) + throw new Error('Invalid correlation header'); + if (options.commandCompensationTimeoutMs !== undefined && + (!Number.isSafeInteger(options.commandCompensationTimeoutMs) || options.commandCompensationTimeoutMs < 1 || + options.commandCompensationTimeoutMs > 4_294_967_294)) + throw new Error('Compensation timeout must be positive and at most 4294967294 milliseconds'); + validateTenancy(options.tenancy); + if (options.nativePrincipal && options.authentication?.length) + throw new Error('Native principal and Arc authentication handlers cannot be combined'); + if (options.identityDetails && (!(detailsSchema instanceof z.ZodType) || typeof options.identityDetails.provide !== 'function')) + throw new Error('Identity details require a provider schema'); + if ((options.developmentUsers || options.developmentTenants) && !options.development) + throw new Error('Discovery providers require development mode'); + const identitySchema = detailsSchema ? z.toJSONSchema(detailsSchema) : undefined; + const observableLimits = new ObservableLimits(options); + validateOrigins(options.query?.allowedOrigins); + return { options: normalized, identitySchema, observableLimits }; +} + +/** Enforce scoped read-side extensions after the service registry has been created. */ +export function validateRegistryOptions(options: ArcOptions, services: ServiceRegistry): void { + for (const token of [...options.queryRenderers ?? [], ...options.readModelInterceptors ?? []]) { + if (services.registration(token).lifetime === 'singleton') + throw new Error(`Query renderer or read-model interceptor ${services.registration(token).token.name} must not be singleton`); + } +} + +/** Validate transport limits after service registration, before routes are published. */ +export function validateTransportOptions(options: ArcOptions): void { + if (options.hosting?.maxBodyBytes !== undefined && + (!Number.isSafeInteger(options.hosting.maxBodyBytes) || options.hosting.maxBodyBytes <= 0)) + throw new Error('Invalid maximum body size'); + if (options.query?.keepAliveIntervalMs !== undefined && + (!Number.isSafeInteger(options.query.keepAliveIntervalMs) || options.query.keepAliveIntervalMs < 0 || + options.query.keepAliveIntervalMs > 120_000)) throw new Error('Invalid observable keep-alive interval'); + if (options.query?.enableObservableHealth !== undefined && typeof options.query.enableObservableHealth !== 'boolean') + throw new Error('Invalid observable health option'); +} From a6b1d68fe45e13c4911e3066ef3343dbb2649549 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 05:49:39 +0200 Subject: [PATCH 05/15] Prepare the v0.24 source preview --- ContractTests/Client/package.json | 2 +- Documentation/index.md | 2 +- Documentation/reference/packages.md | 2 +- README.md | 2 +- Source/Chronicle/package.json | 6 +++--- Source/CodeAnalysis/package.json | 2 +- Source/Core/package.json | 2 +- Source/Cratis/package.json | 2 +- Source/Drizzle/package.json | 4 ++-- Source/Express/package.json | 2 +- Source/Fastify/package.json | 2 +- Source/Hono/package.json | 2 +- Source/MongoDB/package.json | 4 ++-- Source/Testing/package.json | 2 +- Source/Tools/ProxyGenerator/package.json | 2 +- yarn.lock | 8 ++++---- 16 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ContractTests/Client/package.json b/ContractTests/Client/package.json index 5f702397..cf4f27ff 100644 --- a/ContractTests/Client/package.json +++ b/ContractTests/Client/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.core-client-contract", - "version": "0.23.0", + "version": "0.24.0", "private": true, "type": "module", "dependencies": { diff --git a/Documentation/index.md b/Documentation/index.md index 7434e945..9800f999 100644 --- a/Documentation/index.md +++ b/Documentation/index.md @@ -8,7 +8,7 @@ Arc for TypeScript is a Node.js server implementation of [Arc](/arc/), the Crati Without it, a Node.js backend for an Arc frontend means writing every route, request parser, validation response, and status code by hand, then keeping all of it in step with the frontend. With it, commands and queries run through one pipeline that owns those concerns, the wire behavior follows Arc on .NET, and the proxy generator writes the typed frontend client from your source. :::caution[Source preview, no full parity] -No package is published to npm; the manifests are at version 0.23.0 for a source preview. Arc for TypeScript does **not** have full parity with Arc on .NET, and package names and APIs can still change. The [capability reference](reference/capabilities.md) is the single place for status and evidence. +No package is published to npm; the manifests are at version 0.24.0 for a source preview. Arc for TypeScript does **not** have full parity with Arc on .NET, and package names and APIs can still change. The [capability reference](reference/capabilities.md) is the single place for status and evidence. ::: ## What it looks like diff --git a/Documentation/reference/packages.md b/Documentation/reference/packages.md index 7734ec13..62fbeec3 100644 --- a/Documentation/reference/packages.md +++ b/Documentation/reference/packages.md @@ -3,7 +3,7 @@ title: Packages description: The packages this repository builds, what each exports, their peer dependencies and Node.js requirements, and how they relate to the published @cratis/arc client. --- -Every package in this repository is at version 0.23.0, the version of the source preview. **None is published to npm**; reference them from a clone with the `workspace:^` protocol. They ship ES modules only. +Every package in this repository is at version 0.24.0, the version of the source preview. **None is published to npm**; reference them from a clone with the `workspace:^` protocol. They ship ES modules only. ## Server packages diff --git a/README.md b/README.md index 4e8a7410..94e703d2 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ export class TaskItem { | `@cratis/arc.chronicle` | [`Source/Chronicle`](Source/Chronicle) | **Experimental.** `builder.withChronicle` appends returned events and resolves registered read models by command key; nested command returns join one event-log batch. In-memory command assertions are available under `@cratis/arc.chronicle/testing`. SDK 6.7.0 imports natively and infers read models from projections/reducers; an opt-in kernel suite covers aggregate replay and reactor commands. Full .NET transaction parity remains unverified. | | `@cratis/cratis` | [`Source/Cratis`](Source/Cratis) | **Experimental source preview.** `CratisApplication.createBuilder()` and `builder.addCratis()` compose Arc and a Chronicle client without installing authentication; not yet published to npm. | -Every package manifest is at version 0.23.0. That is the version of this source preview, not an npm release, and the Chronicle package is experimental. The packages ship ES modules only, and schemas use Zod 4. The default core entry, host adapters, MongoDB, and Drizzle packages need Node.js 22 or later. The Fetch entry has a neutral bundle with `node:async_hooks` as its only Node import; its command, query, and SSE paths ran in Deno 2.9.7, while Bun, Cloudflare Workers, and Next.js deployments remain unverified. The root workspace needs Node.js 22.19 or later, because it installs the Chronicle SDK; Node.js 24 LTS is recommended. +Every package manifest is at version 0.24.0. That is the version of this source preview, not an npm release, and the Chronicle package is experimental. The packages ship ES modules only, and schemas use Zod 4. The default core entry, host adapters, MongoDB, and Drizzle packages need Node.js 22 or later. The Fetch entry has a neutral bundle with `node:async_hooks` as its only Node import; its command, query, and SSE paths ran in Deno 2.9.7, while Bun, Cloudflare Workers, and Next.js deployments remain unverified. The root workspace needs Node.js 22.19 or later, because it installs the Chronicle SDK; Node.js 24 LTS is recommended. ## Try it diff --git a/Source/Chronicle/package.json b/Source/Chronicle/package.json index 5ba8d06a..bb83547e 100644 --- a/Source/Chronicle/package.json +++ b/Source/Chronicle/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.chronicle", - "version": "0.23.0", + "version": "0.24.0", "publishConfig": { "access": "public" }, @@ -34,8 +34,8 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.23.0", - "@cratis/arc.testing": "^0.23.0", + "@cratis/arc.core": "^0.24.0", + "@cratis/arc.testing": "^0.24.0", "@cratis/chronicle": "^6.7.0", "@cratis/fundamentals": "^7.19.6", "rxjs": "^7.8.2", diff --git a/Source/CodeAnalysis/package.json b/Source/CodeAnalysis/package.json index a662b033..7fc98e60 100644 --- a/Source/CodeAnalysis/package.json +++ b/Source/CodeAnalysis/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/eslint-plugin-arc-core", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "description": "ESLint diagnostics for Arc for TypeScript server artifacts", diff --git a/Source/Core/package.json b/Source/Core/package.json index 59c81db4..502ae0ec 100644 --- a/Source/Core/package.json +++ b/Source/Core/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.core", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Cratis/package.json b/Source/Cratis/package.json index c86b1718..1d4cced0 100644 --- a/Source/Cratis/package.json +++ b/Source/Cratis/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/cratis", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "description": "Arc and experimental Chronicle composition for Node.js", diff --git a/Source/Drizzle/package.json b/Source/Drizzle/package.json index 23120cde..99966999 100644 --- a/Source/Drizzle/package.json +++ b/Source/Drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.drizzle", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { @@ -29,7 +29,7 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.23.0", + "@cratis/arc.core": "^0.24.0", "@cratis/fundamentals": "^7.19.6", "drizzle-orm": "^0.45.0" }, diff --git a/Source/Express/package.json b/Source/Express/package.json index a02475e6..2dc8ff40 100644 --- a/Source/Express/package.json +++ b/Source/Express/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.express", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Fastify/package.json b/Source/Fastify/package.json index 1bac2ac7..d67da6e9 100644 --- a/Source/Fastify/package.json +++ b/Source/Fastify/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.fastify", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Hono/package.json b/Source/Hono/package.json index a69e7dac..50a19be5 100644 --- a/Source/Hono/package.json +++ b/Source/Hono/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.hono", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/MongoDB/package.json b/Source/MongoDB/package.json index a8046167..5e1700d2 100644 --- a/Source/MongoDB/package.json +++ b/Source/MongoDB/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.mongodb", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { @@ -29,7 +29,7 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.23.0", + "@cratis/arc.core": "^0.24.0", "@cratis/fundamentals": "^7.19.6", "mongodb": "^6.21.0", "rxjs": "^7.8.2" diff --git a/Source/Testing/package.json b/Source/Testing/package.json index 43a6f082..2b201324 100644 --- a/Source/Testing/package.json +++ b/Source/Testing/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.testing", - "version": "0.23.0", + "version": "0.24.0", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Tools/ProxyGenerator/package.json b/Source/Tools/ProxyGenerator/package.json index d4959045..109fa498 100644 --- a/Source/Tools/ProxyGenerator/package.json +++ b/Source/Tools/ProxyGenerator/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.proxygenerator", - "version": "0.23.0", + "version": "0.24.0", "description": "TypeScript source analyzer and deterministic Arc client proxy generator", "repository": { "type": "git", diff --git a/yarn.lock b/yarn.lock index ef153ed7..db5dea7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,8 +45,8 @@ __metadata: rxjs: "npm:^7.8.2" zod: "npm:^4.1.0" peerDependencies: - "@cratis/arc.core": ^0.23.0 - "@cratis/arc.testing": ^0.23.0 + "@cratis/arc.core": ^0.24.0 + "@cratis/arc.testing": ^0.24.0 "@cratis/chronicle": ^6.7.0 "@cratis/fundamentals": ^7.19.6 rxjs: ^7.8.2 @@ -138,7 +138,7 @@ __metadata: postgres: "npm:^3.4.9" sql.js: "npm:^1.14.2" peerDependencies: - "@cratis/arc.core": ^0.23.0 + "@cratis/arc.core": ^0.24.0 "@cratis/fundamentals": ^7.19.6 drizzle-orm: ^0.45.0 languageName: unknown @@ -202,7 +202,7 @@ __metadata: mongodb: "npm:^6.21.0" rxjs: "npm:^7.8.2" peerDependencies: - "@cratis/arc.core": ^0.23.0 + "@cratis/arc.core": ^0.24.0 "@cratis/fundamentals": ^7.19.6 mongodb: ^6.21.0 rxjs: ^7.8.2 From 8c3c01295636e6d2875686806a40abb716861d62 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 06:08:46 +0200 Subject: [PATCH 06/15] Fix clean builds and optional builder extension typing --- .github/workflows/ci.yml | 72 +++++++++++++++++-- CONTRIBUTING.md | 12 ++-- Documentation/core/getting-started.md | 2 +- .../testing/ChronicleCommandScenario.ts | 2 +- Source/Chronicle/withChronicle.ts | 8 ++- Source/Core/ArcApplicationBuilder.ts | 5 ++ Source/Core/fetch.ts | 3 + Source/Cratis/index.ts | 8 ++- Source/Drizzle/withDrizzle.ts | 8 ++- Source/MongoDB/withMongoDB.ts | 8 ++- package.json | 6 +- scripts/check-consumers.mjs | 6 +- scripts/clean.mjs | 40 +++++++++++ 13 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 scripts/clean.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e23af247..f90a570d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,59 @@ # Copyright (c) Cratis. All rights reserved. # Licensed under the MIT license. See LICENSE file in the project root for full license information. -name: Manual CI +name: CI on: + pull_request: + push: + branches: [main] workflow_dispatch: permissions: contents: read concurrency: - group: manual-ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - verify: + changes: + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + code: ${{ steps.classify.outputs.code }} + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Check for code changes + id: classify + env: + BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + run: | + if [[ "${{ github.event_name }}" == workflow_dispatch || "$BASE" =~ ^0+$ ]]; then + echo 'code=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + files=$(git diff --name-only "$BASE" HEAD) + if [[ -z "$files" ]]; then + echo 'No changed files to classify' >&2 + exit 2 + fi + code=false + while IFS= read -r file; do + case "$file" in + Documentation/*|*.md) ;; + *) code=true; break ;; + esac + done <<< "$files" + echo "code=$code" >> "$GITHUB_OUTPUT" + + docs: + needs: changes + if: needs.changes.outputs.code == 'false' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -25,12 +64,31 @@ jobs: - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: '22' + node-version: '22.19.0' + - name: Enable Yarn from the repository package manager + run: corepack enable + - name: Install locked dependencies + run: yarn install --immutable + - name: Check documentation + run: yarn build && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets + + verify: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22.19.0' - name: Enable Yarn from the repository package manager run: corepack enable - name: Install locked dependencies run: yarn install --immutable - - name: Test release guards - run: node --test scripts/for_release/*.test.mjs - name: Run the complete local gate run: yarn ci diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57a6f6e4..b9d16417 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,18 +44,20 @@ The workspaces link to each other, so the sample and the adapters use the local ## Verify your change -Run the complete local gate from the repository root before you open a pull request: +Run the clean release gate from the repository root before you open a pull request: ```bash -yarn ci +yarn ci:clean ``` -It runs, in order: +`yarn clean` lists and removes workspace `dist` directories and `*.tsbuildinfo` files (never tracked files), then `yarn ci` rebuilds and checks the repository. The hosted CI also runs `yarn ci` on fresh Linux checkouts for pull requests and pushes to main. Use `yarn ci` alone for faster verification while working. + +The gate runs, in order: 1. ESLint (`yarn lint`). 2. The type check (`yarn typecheck`): `tsc -b` for every package, then `tsc -p tsconfig.specs.json` for the specs. 3. The build (`yarn build`). -4. The installed-package check (`yarn check:consumers`): packs all ten non-private workspaces, installs them with lockfile-pinned peers outside the workspace, checks tarball contents and dependencies, type-checks NodeNext and Bundler consumers, and runs native ESM HTTP and CLI probes. Core is also checked without optional RxJS. Chronicle and Drizzle use a separate type-check with `skipLibCheck: true` for documented upstream declaration errors; all other consumer files use `skipLibCheck: false`. Run `yarn check:consumers --self-test` to confirm it rejects a planted forbidden file. No package is published. +4. The installed-package check (`yarn check:consumers`): packs all eleven non-private workspaces, installs them with lockfile-pinned peers outside the workspace, checks tarball contents and dependencies, type-checks NodeNext and Bundler consumers, and runs native ESM HTTP and CLI probes. Core is also checked without optional RxJS. Chronicle and Drizzle use a separate type-check with `skipLibCheck: true` for documented upstream declaration errors; all other consumer files use `skipLibCheck: false`. Run `yarn check:consumers --self-test` to confirm it rejects a planted forbidden file. No package is published. 5. The client generation checks (`yarn test:client-generation:verify`): a strict `Bundler` compile of the proxy fixtures with `skipLibCheck: false`, then the generation tests with Node.js. Run `yarn test:client-generation` on its own to build first. 6. The Vitest specs (`yarn test`), including the MongoDB unit specs and the Chronicle specs, which use typed substitutes and never start a Chronicle kernel. 7. The legacy-decorator and decorator-type contract checks (`yarn test:legacy-decorators`, `yarn test:decorator-types`). @@ -70,7 +72,7 @@ Two checks need more than Node.js and are not part of `yarn ci`. Run them when y - `yarn test:conformance` restores and builds the .NET reference host from its lock file, builds the workspace, and runs the 57 paired HTTP checks against `Cratis.Arc` 22.23.0. It needs the .NET 10 SDK and the .NET and ASP.NET Core 10.0.11 runtimes. - `bash Source/MongoDB/run-integration.sh` runs the live MongoDB spec in a disposable Docker container. It exits with 2 when Docker is not available, which means the check did not run. -A hosted run does not replace local verification. The hosted CI workflow is started manually. +A hosted run does not replace local verification. The hosted CI workflow also supports manual runs. ## Conventions diff --git a/Documentation/core/getting-started.md b/Documentation/core/getting-started.md index cd86b1b8..5c71f185 100644 --- a/Documentation/core/getting-started.md +++ b/Documentation/core/getting-started.md @@ -62,7 +62,7 @@ The builder also registers services that change pipeline behavior: | `addQueryRenderer(token)` | A renderer for provider-owned query results; see [Query renderers](../queries/renderers.md) | | `addReadModelInterceptor(token)` | A read-model transform; see [Read-model interception](../queries/read-model-interception.md) | -After importing their packages, call `withMongoDB`, `withDrizzle`, or `withChronicle`. The old `add*` methods and standalone functions remain as deprecated aliases. The builder uses a `Symbol.for`-keyed extension registry rather than changing its prototype; importing an integration registers its install function even if the core is loaded twice. A missing integration fails at the call site. Configuration from `appsettings.json` is available to Chronicle and MongoDB, but model classes, clients, and authentication must be provided explicitly. +After importing their packages, call `withMongoDB`, `withDrizzle`, or `withChronicle`. The old `add*` methods and standalone functions remain as deprecated aliases. Each integration adds its typed method to the portable builder prototype and registers its installer in a `Symbol.for`-keyed registry. The shared registry lets a builder loaded from another copy of core find the installer. A missing integration fails at the call site. Configuration from `appsettings.json` is available to Chronicle and MongoDB, but model classes, clients, and authentication must be provided explicitly. ## Run it, or mount it diff --git a/Source/Chronicle/testing/ChronicleCommandScenario.ts b/Source/Chronicle/testing/ChronicleCommandScenario.ts index 2996560c..d7b41af8 100644 --- a/Source/Chronicle/testing/ChronicleCommandScenario.ts +++ b/Source/Chronicle/testing/ChronicleCommandScenario.ts @@ -4,7 +4,7 @@ import { CommandScenario, type ScenarioCommandResult } from '@cratis/arc.testing import type { IChronicleClient, IEventStore } from '@cratis/chronicle'; import { EventSequenceNumber, type AppendOptions, type AppendResult, type EventForEventSourceId } from '@cratis/chronicle/eventSequences'; type ClassType = new () => T; -import '../index.js'; +import '../withChronicle.js'; interface Appended { readonly tenant: string; readonly source: string; readonly event: object; readonly eventSourceType?: string; readonly eventStreamType?: string; readonly eventStreamId?: string; diff --git a/Source/Chronicle/withChronicle.ts b/Source/Chronicle/withChronicle.ts index 4d6bf6c2..56b146d6 100644 --- a/Source/Chronicle/withChronicle.ts +++ b/Source/Chronicle/withChronicle.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ArcApplicationBuilder } from '@cratis/arc.core'; import type { ArcServer } from '@cratis/arc.core'; +import { ArcApplicationBuilder as FetchArcApplicationBuilder } from '@cratis/arc.core/fetch'; import type { Constructor } from '@cratis/fundamentals'; import { ChronicleArtifacts } from './ChronicleArtifacts.js'; import { ChronicleReadModels } from './ChronicleReadModels.js'; @@ -43,8 +44,8 @@ export function withChronicle(builder: ArcApplicationBuilder, options: Partial): this; } @@ -54,3 +55,6 @@ ArcApplicationBuilder.registerExtension('chronicle', withChronicle); ArcApplicationBuilder.prototype.withChronicle = function (options: Partial) { return this.extend('chronicle', options); }; +FetchArcApplicationBuilder.prototype.withChronicle = function (options: Partial) { + return this.extend('chronicle', options); +}; diff --git a/Source/Core/ArcApplicationBuilder.ts b/Source/Core/ArcApplicationBuilder.ts index 29f90d37..4787297d 100644 --- a/Source/Core/ArcApplicationBuilder.ts +++ b/Source/Core/ArcApplicationBuilder.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import type { z } from 'zod'; import type { ArcOptions } from './ArcOptions.js'; +import type { ArcBuilderExtensions } from './fetch.js'; import type { CratisConfiguration } from './configuration/loadConfiguration.js'; import { ArcApplicationServices } from './dependencyInjection/ArcApplicationServices.js'; import { FetchArcApplication } from './FetchArcApplication.js'; @@ -37,6 +38,10 @@ import { isIdentityDetailsProvider } from './identity/discoverIdentityDetails.js import type { IdentityDetailsProvider } from './identity/IdentityDetailsProvider.js'; /** Collect decorated artifacts and their services into one executable application. */ +// Interface merging exposes integration-owned methods without depending on optional packages in core. +// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging +export interface ArcApplicationBuilder extends ArcBuilderExtensions {} +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class ArcApplicationBuilder { readonly services = new ArcApplicationServices(); readonly #artifacts: Artifact[] = []; diff --git a/Source/Core/fetch.ts b/Source/Core/fetch.ts index 91975639..c3ca5e14 100644 --- a/Source/Core/fetch.ts +++ b/Source/Core/fetch.ts @@ -4,6 +4,9 @@ export { ArcServer, currentContext } from './ArcServer.js'; export { FetchArcApplication as ArcApplication } from './FetchArcApplication.js'; export { ArcApplicationBuilder } from './ArcApplicationBuilder.js'; +/** Integration packages augment this interface on the portable builder. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface ArcBuilderExtensions {} export type { ArcBuilderIntegrationOptions } from './ArcBuilderIntegrationOptions.js'; export { canonicalMetadataSignature } from './reflection/generatedMetadataSignature.js'; export { optionalService } from './dependencyInjection/optionalService.js'; diff --git a/Source/Cratis/index.ts b/Source/Cratis/index.ts index 81976917..27a0d516 100644 --- a/Source/Cratis/index.ts +++ b/Source/Cratis/index.ts @@ -2,14 +2,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ArcApplication, ArcApplicationBuilder } from '@cratis/arc.core'; import type { ArcBuilderOptions } from '@cratis/arc.core'; +import { ArcApplicationBuilder as FetchArcApplicationBuilder } from '@cratis/arc.core/fetch'; import '@cratis/arc.chronicle'; import type { ChronicleRegistration } from '@cratis/arc.chronicle'; export * from '@cratis/arc.core'; export * from '@cratis/arc.chronicle'; -declare module '@cratis/arc.core' { - interface ArcApplicationBuilder { +declare module '@cratis/arc.core/fetch' { + interface ArcBuilderExtensions { /** Attach Chronicle as part of the Cratis composition. */ addCratis(options?: Partial): this; } @@ -18,6 +19,9 @@ declare module '@cratis/arc.core' { ArcApplicationBuilder.prototype.addCratis = function (options: Partial = {}) { return this.extend('chronicle', options); }; +FetchArcApplicationBuilder.prototype.addCratis = function (options: Partial = {}) { + return this.extend('chronicle', options); +}; /** Node setup analog of AddCratis; Chronicle connection settings can come from appsettings.json. */ export class CratisApplication { static createBuilder(options: ArcBuilderOptions = {}, chronicle: Partial = {}): ArcApplicationBuilder { diff --git a/Source/Drizzle/withDrizzle.ts b/Source/Drizzle/withDrizzle.ts index 22a04f99..28af1841 100644 --- a/Source/Drizzle/withDrizzle.ts +++ b/Source/Drizzle/withDrizzle.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ArcApplicationBuilder } from '@cratis/arc.core'; import type { ExecutionContext } from '@cratis/arc.core'; +import { ArcApplicationBuilder as FetchArcApplicationBuilder } from '@cratis/arc.core/fetch'; import type { DrizzleOptions } from './DrizzleOptions.js'; import { DrizzleReadModels } from './DrizzleReadModels.js'; import { DrizzleHandle } from './DrizzleHandle.js'; @@ -42,8 +43,8 @@ export function withDrizzle(builder: ArcApplicationBuilder, options: DrizzleOpti return builder; } -declare module '@cratis/arc.core' { - interface ArcApplicationBuilder { +declare module '@cratis/arc.core/fetch' { + interface ArcBuilderExtensions { /** Attach Drizzle after importing @cratis/arc.drizzle. */ withDrizzle(options: DrizzleOptions): this; } @@ -53,3 +54,6 @@ ArcApplicationBuilder.registerExtension('drizzle', withDrizzle); ArcApplicationBuilder.prototype.withDrizzle = function (options: DrizzleOptions) { return this.extend('drizzle', options); }; +FetchArcApplicationBuilder.prototype.withDrizzle = function (options: DrizzleOptions) { + return this.extend('drizzle', options); +}; diff --git a/Source/MongoDB/withMongoDB.ts b/Source/MongoDB/withMongoDB.ts index e724a639..104ed317 100644 --- a/Source/MongoDB/withMongoDB.ts +++ b/Source/MongoDB/withMongoDB.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ArcApplicationBuilder, serviceToken } from '@cratis/arc.core'; import type { ExecutionContext } from '@cratis/arc.core'; +import { ArcApplicationBuilder as FetchArcApplicationBuilder } from '@cratis/arc.core/fetch'; import { MongoClientFactory } from './MongoClientFactory.js'; import { MongoCollection } from './MongoCollection.js'; import { MongoReadModelForCommandResolver } from './MongoReadModelForCommandResolver.js'; @@ -44,8 +45,8 @@ export function withMongoDB(builder: ArcApplicationBuilder, configured: MongoDBO return builder; } -declare module '@cratis/arc.core' { - interface ArcApplicationBuilder { +declare module '@cratis/arc.core/fetch' { + interface ArcBuilderExtensions { /** Attach MongoDB after importing @cratis/arc.mongodb. */ withMongoDB(options: MongoDBOptions): this; } @@ -55,3 +56,6 @@ ArcApplicationBuilder.registerExtension('mongodb', withMongoDB); ArcApplicationBuilder.prototype.withMongoDB = function (options: MongoDBOptions) { return this.extend('mongodb', options); }; +FetchArcApplicationBuilder.prototype.withMongoDB = function (options: MongoDBOptions) { + return this.extend('mongodb', options); +}; diff --git a/package.json b/package.json index 2397b7d4..d0310d47 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "test:legacy-decorators": "yarn build && yarn tsc -p ContractTests/LegacyDecorators/tsconfig.json && node --test ContractTests/LegacyDecorators/conformance.test.mjs", "test:conformance": "dotnet restore ContractTests/DotNET/HttpFixture.csproj --locked-mode && dotnet build ContractTests/DotNET/HttpFixture.csproj -c Debug --no-restore && yarn build && yarn tsc -p ContractTests/Http/modelBound/tsconfig.json && node --test ContractTests/Http/conformance.test.mjs", "typecheck": "tsc -b && tsc -p tsconfig.specs.json", - "build": "tsc -b && yarn check:metadata", + "build": "tsc -b Source/Core Source/Testing && tsc -b && yarn check:metadata", + "clean": "node scripts/clean.mjs", + "ci:clean": "yarn clean && yarn ci", "check:metadata": "node Samples/Tasks/check-metadata.mjs && node Source/Tools/ProxyGenerator/dist/cli.js --project Samples/Library/tsconfig.json --artifacts Samples/Library/Features --output Samples/Library/Web/src/generated --metadata Samples/Library/Features/generatedMetadata.ts --check-metadata", "check:consumers": "node scripts/check-consumers.mjs", "check:fetch": "node scripts/check-fetch-runtime.mjs", @@ -34,7 +36,7 @@ "set-version": "node scripts/set-version.mjs", "test": "vitest run", "specs": "yarn test", - "ci": "yarn tsc -b Source/Core Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn build && yarn check:consumers && yarn check:fetch && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && node --test scripts/for_release/*.test.mjs && yarn set-version --check" + "ci": "yarn tsc -b Source/Core Source/Testing Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn build && yarn check:consumers && yarn check:fetch && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && node --test scripts/for_release/*.test.mjs && yarn set-version --check" }, "devDependencies": { "@cratis/fundamentals": "7.19.6", diff --git a/scripts/check-consumers.mjs b/scripts/check-consumers.mjs index 858a4f67..cce50eec 100644 --- a/scripts/check-consumers.mjs +++ b/scripts/check-consumers.mjs @@ -217,7 +217,10 @@ import { CommandScenario, QueryScenario } from '@cratis/arc.testing'; import { analyzeSource } from '@cratis/arc.proxygenerator'; import plugin from '@cratis/eslint-plugin-arc-core'; void [attachNodeWebSockets, CommandScenario, QueryScenario, analyzeSource, plugin]; -function configureMongo(builder: ArcApplicationBuilder, options: MongoDBOptions) { withMongoDB(builder, options); } +function configureMongo(builder: ArcApplicationBuilder, options: MongoDBOptions) { + withMongoDB(builder, options); + builder.withMongoDB(options); +} void configureMongo; const adapterApp = await ArcApplication.createBuilder().build(); function attachSockets(host: HttpServer) { @@ -242,6 +245,7 @@ import { CommandScenario as CratisCommandScenario, ChronicleCommandScenario as C function configureIntegrations(builder: ArcApplicationBuilder, sql: DrizzleOptions, events: ChronicleRegistration) { withDrizzle(builder, sql); withChronicle(builder, events); + builder.withDrizzle(sql).withChronicle(events); } function configureCratis(events: ChronicleRegistration) { const composed = CratisApplication.createBuilder({ configuration: false }, events); diff --git a/scripts/clean.mjs b/scripts/clean.mjs new file mode 100644 index 00000000..752e810f --- /dev/null +++ b/scripts/clean.mjs @@ -0,0 +1,40 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { execFileSync } from 'node:child_process'; +import { existsSync, readdirSync, rmSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const { workspaces } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); +const folders = workspaces.flatMap(pattern => pattern.endsWith('/*') + ? readdirSync(join(root, pattern.slice(0, -2)), { withFileTypes: true }) + .filter(entry => entry.isDirectory()).map(entry => join(root, pattern.slice(0, -2), entry.name)) + : [join(root, pattern)]).filter(folder => existsSync(join(folder, 'package.json'))); +if (!folders.length) throw new Error('No workspaces found; refusing to clean'); +const targets = new Set(); +function scan(folder) { + for (const entry of readdirSync(folder, { withFileTypes: true })) { + const path = join(folder, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === '.ai-work' || entry.name === '.git') continue; + if (entry.name === 'dist') targets.add(path); + else scan(path); + } else if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(path); + } +} +for (const folder of folders) scan(folder); +for (const entry of readdirSync(root, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(join(root, entry.name)); +} +const paths = [...targets].sort(); +if (paths.length > 256) throw new Error(`Refusing to clean ${paths.length} outputs in one pass (limit: 256)`); +for (const path of paths) { + const tracked = execFileSync('git', ['ls-files', '--', relative(root, path)], { cwd: root, encoding: 'utf8' }); + if (tracked.trim()) throw new Error(`Refusing to remove tracked files in ${relative(root, path)}: ${tracked.trim()}`); +} +console.log(`Removing ${paths.length} build outputs:`); +for (const path of paths) { + console.log(relative(root, path)); + rmSync(path, { recursive: true }); +} From f997a94377addfbec208e250a5d98757d6795b31 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 06:11:20 +0200 Subject: [PATCH 07/15] Keep specification helpers out of package builds --- Source/Chronicle/tsconfig.json | 2 +- Source/Drizzle/tsconfig.json | 2 +- Source/Express/tsconfig.json | 2 +- Source/Fastify/tsconfig.json | 2 +- Source/Hono/tsconfig.json | 2 +- Source/MongoDB/tsconfig.json | 2 +- Source/Tools/ProxyGenerator/tsconfig.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Chronicle/tsconfig.json b/Source/Chronicle/tsconfig.json index 8f871b86..a37caf66 100644 --- a/Source/Chronicle/tsconfig.json +++ b/Source/Chronicle/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts"], "references": [{ "path": "../Core" }, { "path": "../Testing" }] } diff --git a/Source/Drizzle/tsconfig.json b/Source/Drizzle/tsconfig.json index 3a7447dd..106b06a3 100644 --- a/Source/Drizzle/tsconfig.json +++ b/Source/Drizzle/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts", "vitest.integration.config.ts", "*.integration.ts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts", "vitest.integration.config.ts", "*.integration.ts"], "references": [{ "path": "../Core" }] } diff --git a/Source/Express/tsconfig.json b/Source/Express/tsconfig.json index 11fc5cd3..2f035575 100644 --- a/Source/Express/tsconfig.json +++ b/Source/Express/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts"], "references": [{ "path": "../Core" }] } diff --git a/Source/Fastify/tsconfig.json b/Source/Fastify/tsconfig.json index 11fc5cd3..2f035575 100644 --- a/Source/Fastify/tsconfig.json +++ b/Source/Fastify/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts"], "references": [{ "path": "../Core" }] } diff --git a/Source/Hono/tsconfig.json b/Source/Hono/tsconfig.json index 11fc5cd3..2f035575 100644 --- a/Source/Hono/tsconfig.json +++ b/Source/Hono/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts"], "references": [{ "path": "../Core" }] } diff --git a/Source/MongoDB/tsconfig.json b/Source/MongoDB/tsconfig.json index 3a7447dd..106b06a3 100644 --- a/Source/MongoDB/tsconfig.json +++ b/Source/MongoDB/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts", "vitest.integration.config.ts", "*.integration.ts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts", "vitest.integration.config.ts", "*.integration.ts"], "references": [{ "path": "../Core" }] } diff --git a/Source/Tools/ProxyGenerator/tsconfig.json b/Source/Tools/ProxyGenerator/tsconfig.json index 11b9adbb..69686ec7 100644 --- a/Source/Tools/ProxyGenerator/tsconfig.json +++ b/Source/Tools/ProxyGenerator/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "dist" }, "include": ["**/*.ts"], - "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "vite.config.mts"], + "exclude": ["dist", "node_modules", "**/for_*/**", "**/given/**", "given.ts", "vite.config.mts"], "references": [{ "path": "../../Core" }] } From 21bd59b88b2d97b3b9829e8b3c250b37822aec89 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 06:16:10 +0200 Subject: [PATCH 08/15] Declare the client packages the Tasks proxies compile against --- Samples/Tasks/package.json | 8 ++++++++ yarn.lock | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/Samples/Tasks/package.json b/Samples/Tasks/package.json index 636aad1b..320fec6b 100644 --- a/Samples/Tasks/package.json +++ b/Samples/Tasks/package.json @@ -11,5 +11,13 @@ "@cratis/arc.testing": "workspace:^", "@cratis/fundamentals": "7.19.6", "rxjs": "^7.8.2" + }, + "devDependencies": { + "@cratis/arc": "22.19.1", + "@cratis/arc.react": "22.19.1", + "@types/react": "^19.2.0", + "react": "^19.2.0", + "reflect-metadata": "0.2.2", + "tsyringe": "^4.10.0" } } diff --git a/yarn.lock b/yarn.lock index db5dea7d..1a9da7c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -96,10 +96,16 @@ __metadata: version: 0.0.0-use.local resolution: "@cratis/arc.core.sample.tasks@workspace:Samples/Tasks" dependencies: + "@cratis/arc": "npm:22.19.1" "@cratis/arc.core": "workspace:^" + "@cratis/arc.react": "npm:22.19.1" "@cratis/arc.testing": "workspace:^" "@cratis/fundamentals": "npm:7.19.6" + "@types/react": "npm:^19.2.0" + react: "npm:^19.2.0" + reflect-metadata: "npm:0.2.2" rxjs: "npm:^7.8.2" + tsyringe: "npm:^4.10.0" languageName: unknown linkType: soft From a8b577576020fa098cbf334c759bf5b90b49cbc1 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 06:16:38 +0200 Subject: [PATCH 09/15] Compile the Tasks proxies against the React 18 types the client declares --- Samples/Tasks/package.json | 4 ++-- yarn.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Samples/Tasks/package.json b/Samples/Tasks/package.json index 320fec6b..35cbf5cd 100644 --- a/Samples/Tasks/package.json +++ b/Samples/Tasks/package.json @@ -15,8 +15,8 @@ "devDependencies": { "@cratis/arc": "22.19.1", "@cratis/arc.react": "22.19.1", - "@types/react": "^19.2.0", - "react": "^19.2.0", + "@types/react": "18.3.28", + "react": "18.3.1", "reflect-metadata": "0.2.2", "tsyringe": "^4.10.0" } diff --git a/yarn.lock b/yarn.lock index 1a9da7c2..88cd34fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -101,8 +101,8 @@ __metadata: "@cratis/arc.react": "npm:22.19.1" "@cratis/arc.testing": "workspace:^" "@cratis/fundamentals": "npm:7.19.6" - "@types/react": "npm:^19.2.0" - react: "npm:^19.2.0" + "@types/react": "npm:18.3.28" + react: "npm:18.3.1" reflect-metadata: "npm:0.2.2" rxjs: "npm:^7.8.2" tsyringe: "npm:^4.10.0" From ea27b9444ff9e09eda80f075e02c34c627a663bc Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:10:03 +0200 Subject: [PATCH 10/15] Build Chronicle before generating Library proxies on CI --- package.json | 3 ++- scripts/ci-linux.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 scripts/ci-linux.sh diff --git a/package.json b/package.json index d0310d47..64e2d670 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "build": "tsc -b Source/Core Source/Testing && tsc -b && yarn check:metadata", "clean": "node scripts/clean.mjs", "ci:clean": "yarn clean && yarn ci", + "ci:linux": "bash scripts/ci-linux.sh", "check:metadata": "node Samples/Tasks/check-metadata.mjs && node Source/Tools/ProxyGenerator/dist/cli.js --project Samples/Library/tsconfig.json --artifacts Samples/Library/Features --output Samples/Library/Web/src/generated --metadata Samples/Library/Features/generatedMetadata.ts --check-metadata", "check:consumers": "node scripts/check-consumers.mjs", "check:fetch": "node scripts/check-fetch-runtime.mjs", @@ -36,7 +37,7 @@ "set-version": "node scripts/set-version.mjs", "test": "vitest run", "specs": "yarn test", - "ci": "yarn tsc -b Source/Core Source/Testing Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn build && yarn check:consumers && yarn check:fetch && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && node --test scripts/for_release/*.test.mjs && yarn set-version --check" + "ci": "yarn tsc -b Source/Core Source/Testing Source/Chronicle Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn build && yarn check:consumers && yarn check:fetch && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && node --test scripts/for_release/*.test.mjs && yarn set-version --check" }, "devDependencies": { "@cratis/fundamentals": "7.19.6", diff --git a/scripts/ci-linux.sh b/scripts/ci-linux.sh new file mode 100644 index 00000000..d536636c --- /dev/null +++ b/scripts/ci-linux.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +set -euo pipefail + +cd "$(dirname "$0")/.." +if [[ -n "$(git status --porcelain)" ]]; then + printf 'Commit changes before running ci:linux; it checks a fresh clone of HEAD.\n' >&2 + exit 2 +fi + +branch=$(git branch --show-current) +clone=$(mktemp -d /tmp/arc-ci-linux-XXXXXXXX) +container="arc-ci-linux-$$" +cleanup() { + if docker ps --format '{{.Names}}' | grep -Fxq "$container"; then + printf 'Container %s still holds %s; leaving the clone intact.\n' "$container" "$clone" >&2 + else + rm -rf -- "$clone" + fi +} +trap cleanup EXIT + +git clone --quiet --no-local --single-branch --branch "$branch" "$PWD" "$clone" +docker run --rm --name "$container" --mount "type=bind,src=$clone,dst=/workspace" --workdir /workspace node:22.19.0 \ + sh -lc 'corepack enable && yarn install --immutable && yarn ci' From 962036465fd5231fee633924e50d4f981a4679f5 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:15:50 +0200 Subject: [PATCH 11/15] Install lsof for containerized client generation checks --- scripts/ci-linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-linux.sh b/scripts/ci-linux.sh index d536636c..4db741e1 100644 --- a/scripts/ci-linux.sh +++ b/scripts/ci-linux.sh @@ -23,4 +23,4 @@ trap cleanup EXIT git clone --quiet --no-local --single-branch --branch "$branch" "$PWD" "$clone" docker run --rm --name "$container" --mount "type=bind,src=$clone,dst=/workspace" --workdir /workspace node:22.19.0 \ - sh -lc 'corepack enable && yarn install --immutable && yarn ci' + sh -lc 'apt-get update -qq && apt-get install -y -qq --no-install-recommends lsof && corepack enable && yarn install --immutable && yarn ci' From 2a79a38bb1e0a0c362a8c8325df52d281948e343 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:20:35 +0200 Subject: [PATCH 12/15] Make published React client available to generated test projects --- package.json | 2 ++ yarn.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 64e2d670..f7aa6c39 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "ci": "yarn tsc -b Source/Core Source/Testing Source/Chronicle Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn build && yarn check:consumers && yarn check:fetch && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && node --test scripts/for_release/*.test.mjs && yarn set-version --check" }, "devDependencies": { + "@cratis/arc.react": "22.19.1", "@cratis/fundamentals": "7.19.6", "@eslint/js": "^10.0.1", "@opentelemetry/api": "^1.9.0", @@ -65,6 +66,7 @@ "eslint": "^10.11.0", "markdownlint-cli2": "^0.20.0", "mocha": "^11.7.5", + "react": "18.3.1", "sinon": "^21.0.0", "sinon-chai": "^4.0.1", "typescript": "npm:@typescript/typescript6@^6.0.2", diff --git a/yarn.lock b/yarn.lock index 88cd34fb..5e67e688 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,6 +2442,7 @@ __metadata: version: 0.0.0-use.local resolution: "arc-typescript-workspace@workspace:." dependencies: + "@cratis/arc.react": "npm:22.19.1" "@cratis/fundamentals": "npm:7.19.6" "@eslint/js": "npm:^10.0.1" "@opentelemetry/api": "npm:^1.9.0" @@ -2467,6 +2468,7 @@ __metadata: eslint: "npm:^10.11.0" markdownlint-cli2: "npm:^0.20.0" mocha: "npm:^11.7.5" + react: "npm:18.3.1" sinon: "npm:^21.0.0" sinon-chai: "npm:^4.0.1" typescript: "npm:@typescript/typescript6@^6.0.2" From 04fa52c77e4f0cc20cf8a4329039b46ac28dfe8a Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:20:35 +0200 Subject: [PATCH 13/15] Wait for failed WebSocket handshakes via error events --- ContractTests/Client/observable-direct-sse.test.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ContractTests/Client/observable-direct-sse.test.mjs b/ContractTests/Client/observable-direct-sse.test.mjs index d3065d80..34aa3be9 100644 --- a/ContractTests/Client/observable-direct-sse.test.mjs +++ b/ContractTests/Client/observable-direct-sse.test.mjs @@ -97,9 +97,10 @@ for (const kind of ['express', 'fastify', 'hono']) test(`generated installed cli assert.equal(timedOut.status, 408); assert.equal((await timedOut.json()).hasExceptions, true); const encoded = new WebSocket(`${listening.origin.replace('http:', 'ws:')}/api/%6eumbers`); - encoded.onerror = () => {}; - await within(new Promise(resolve => encoded.addEventListener('close', resolve, { once: true })), - 'Encoded query upgrade rejection'); + await within(new Promise((resolve, reject) => { + encoded.addEventListener('error', resolve, { once: true }); + encoded.addEventListener('open', () => reject(new Error('Encoded query unexpectedly upgraded')), { once: true }); + }), 'Encoded query upgrade rejection'); Globals.queryTransportMethod = QueryTransportMethod.WebSocket; const live = new Numbers(); live.setOrigin(listening.origin); From 048e016a4273157c90a2e2776672589688cfd635 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:26:11 +0200 Subject: [PATCH 14/15] Run the Library integration through the container host --- Samples/Library/run-integration.sh | 5 +++-- scripts/ci-linux.sh | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Samples/Library/run-integration.sh b/Samples/Library/run-integration.sh index d988f79b..2d18efac 100644 --- a/Samples/Library/run-integration.sh +++ b/Samples/Library/run-integration.sh @@ -22,9 +22,10 @@ trap 'exit 143' TERM yarn workspace @cratis/arc.sample.library build container_id=$(docker run -d --name "arc-library-chronicle-$$" -p 127.0.0.1::35000 cratis/chronicle:latest-development) port=$(docker port "$container_id" 35000/tcp) +chronicle_host=${CHRONICLE_HOST:-localhost} ready=0 for attempt in $(seq 1 90); do - if curl -kfsS --max-time 2 "https://localhost:${port##*:}/" >/dev/null 2>&1; then ready=1; break; fi + if curl -kfsS --max-time 2 "https://${chronicle_host}:${port##*:}/" >/dev/null 2>&1; then ready=1; break; fi sleep 1 done if [ "$ready" -ne 1 ]; then @@ -32,5 +33,5 @@ if [ "$ready" -ne 1 ]; then docker logs "$container_id" >&2 exit 2 fi -CHRONICLE_URL="chronicle://localhost:${port##*:}" \ +CHRONICLE_URL="chronicle://${chronicle_host}:${port##*:}" \ node --import tsx --test --test-force-exit Samples/Library/e2e.test.mjs diff --git a/scripts/ci-linux.sh b/scripts/ci-linux.sh index 4db741e1..7fdb39d3 100644 --- a/scripts/ci-linux.sh +++ b/scripts/ci-linux.sh @@ -22,5 +22,13 @@ cleanup() { trap cleanup EXIT git clone --quiet --no-local --single-branch --branch "$branch" "$PWD" "$clone" -docker run --rm --name "$container" --mount "type=bind,src=$clone,dst=/workspace" --workdir /workspace node:22.19.0 \ - sh -lc 'apt-get update -qq && apt-get install -y -qq --no-install-recommends lsof && corepack enable && yarn install --immutable && yarn ci' +network=() +if [[ "$(uname -s)" == Darwin ]]; then + network=(-e CHRONICLE_HOST=host.docker.internal) +else + network=(--network host) +fi +docker run --rm --name "$container" --mount "type=bind,src=$clone,dst=/workspace" \ + --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ + "${network[@]}" --workdir /workspace node:22.19.0 \ + sh -lc 'apt-get update -qq && apt-get install -y -qq --no-install-recommends docker.io lsof curl && corepack enable && yarn install --immutable && yarn ci' From 4536d343f84abca41807fd4dbdeeba7fef55323f Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 25 Sep 2026 07:34:02 +0200 Subject: [PATCH 15/15] Preserve Linux check clones while host processes hold them --- scripts/ci-linux.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/ci-linux.sh b/scripts/ci-linux.sh index 7fdb39d3..5a6f42f4 100644 --- a/scripts/ci-linux.sh +++ b/scripts/ci-linux.sh @@ -13,8 +13,10 @@ branch=$(git branch --show-current) clone=$(mktemp -d /tmp/arc-ci-linux-XXXXXXXX) container="arc-ci-linux-$$" cleanup() { - if docker ps --format '{{.Names}}' | grep -Fxq "$container"; then - printf 'Container %s still holds %s; leaving the clone intact.\n' "$container" "$clone" >&2 + if docker ps --format '{{.Names}}' | grep -Fxq "$container" || + ! command -v lsof >/dev/null || lsof -t +D "$clone" >/dev/null 2>&1 || + [[ -n "$(git -C "$clone" status --porcelain 2>/dev/null)" ]]; then + printf 'Container, process, or changes still hold %s; leaving the clone intact.\n' "$clone" >&2 else rm -rf -- "$clone" fi