Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
12 changes: 7 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand All @@ -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

Expand Down
7 changes: 4 additions & 3 deletions ContractTests/Client/observable-direct-sse.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ContractTests/Client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cratis/arc.core-client-contract",
"version": "0.23.0",
"version": "0.24.0",
"private": true,
"type": "module",
"dependencies": {
Expand Down
12 changes: 7 additions & 5 deletions Documentation/commands/calling-commands-from-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Documentation/core/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Documentation/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion Documentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Documentation/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Evidence paths are relative to the repository root. Spec folders follow `for_<Su
| Model-bound commands | Supported | `@command()` classes with Fundamentals `@field` declarations and an instance `handle()`; `defineCommand` with Zod remains the low-level path. See [Model-bound commands](../commands/model-bound/index.md). | `Source/Core/commands/modelBound/for_compileCommand`, `Source/Core/for_ArcApplicationBuilder/when_building_model_bound_artifacts`, `Samples/Tasks/Features/Tasks/Registration/for_RegisterTask` |
| Artifact discovery | Bounded | `builder.add(...)` takes an explicit catalog; `discover()` imports exported artifacts from a dedicated file-URL folder, derives namespaces from paths, and rejects conflicting namespaces and mixed JS/TS output. No bundler or assembly scanning. | `Source/Core/for_ArcApplicationBuilder/when_discovering_artifacts`, `.../when_adding_artifacts` |
| Validate without executing | Supported | `POST <route>/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` |
Expand Down
2 changes: 1 addition & 1 deletion Documentation/reference/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion Documentation/testing/low-level-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
Loading
Loading