diff --git a/ContractTests/Client/package.json b/ContractTests/Client/package.json index 78cb6287..799d0e1b 100644 --- a/ContractTests/Client/package.json +++ b/ContractTests/Client/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.core-client-contract", - "version": "0.57.1", + "version": "0.57.2", "private": true, "type": "module", "dependencies": { diff --git a/Documentation/index.md b/Documentation/index.md index 2735de70..c4639c68 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.57.1 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.57.2 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 ddfd28e0..7f39b0d9 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.57.1, the version of the source preview. **None is published to npm.** They ship ES modules only. Clone this repository, run `yarn install` and `yarn build`, and then use the packages in one of two ways: +Every package in this repository is at version 0.57.2, the version of the source preview. **None is published to npm.** They ship ES modules only. Clone this repository, run `yarn install` and `yarn build`, and then use the packages in one of two ways: - **Inside the clone.** Put your application in a folder under `Samples/`, which the root `workspaces` list includes, and reference the packages with the `workspace:^` protocol, as [`Samples/Tasks/package.json`](https://github.com/Cratis/Arc.TypeScript/blob/main/Samples/Tasks/package.json) does. `workspace:^` resolves only inside this repository's Yarn workspace. - **In your own project.** Pack each package you need with `yarn workspace pack --out ` and install the tarballs with npm. Use `yarn pack`: it rewrites `workspace:^` dependencies to version ranges, and `npm pack` does not. `yarn check:consumers` installs packed packages this way to check NodeNext and Bundler consumers. diff --git a/README.md b/README.md index f8830df4..5d56b836 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.19.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.57.1. 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. Core, host adapters, MongoDB, and Drizzle 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 run in a Next.js App Router route handler on the Node.js runtime, with Bun and Deno smoke checks; Cloudflare Workers and the Next.js Edge runtime are not supported. See [Fetch API runtimes](Documentation/hosts/fetch-runtimes.md). 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.57.2. 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. Core, host adapters, MongoDB, and Drizzle 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 run in a Next.js App Router route handler on the Node.js runtime, with Bun and Deno smoke checks; Cloudflare Workers and the Next.js Edge runtime are not supported. See [Fetch API runtimes](Documentation/hosts/fetch-runtimes.md). 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 afddbc51..0090ecff 100644 --- a/Source/Chronicle/package.json +++ b/Source/Chronicle/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.chronicle", - "version": "0.57.1", + "version": "0.57.2", "publishConfig": { "access": "public" }, @@ -34,8 +34,8 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.57.1", - "@cratis/arc.testing": "^0.57.1", + "@cratis/arc.core": "^0.57.2", + "@cratis/arc.testing": "^0.57.2", "@cratis/chronicle": "^6.29.0", "@cratis/fundamentals": "^7.19.6", "rxjs": "^7.8.2", diff --git a/Source/CodeAnalysis/Version.ts b/Source/CodeAnalysis/Version.ts index 8c2256c4..95be4b25 100644 --- a/Source/CodeAnalysis/Version.ts +++ b/Source/CodeAnalysis/Version.ts @@ -1,4 +1,4 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // Generated by yarn set-version. Do not edit. -export const packageVersion = '0.57.1'; +export const packageVersion = '0.57.2'; diff --git a/Source/CodeAnalysis/package.json b/Source/CodeAnalysis/package.json index 576f4f07..60b72abf 100644 --- a/Source/CodeAnalysis/package.json +++ b/Source/CodeAnalysis/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/eslint-plugin-arc-core", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "description": "ESLint diagnostics for Arc for TypeScript server artifacts", diff --git a/Source/Core/Version.ts b/Source/Core/Version.ts index 8c2256c4..95be4b25 100644 --- a/Source/Core/Version.ts +++ b/Source/Core/Version.ts @@ -1,4 +1,4 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // Generated by yarn set-version. Do not edit. -export const packageVersion = '0.57.1'; +export const packageVersion = '0.57.2'; diff --git a/Source/Core/package.json b/Source/Core/package.json index 9b79e298..1c3f18fc 100644 --- a/Source/Core/package.json +++ b/Source/Core/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.core", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_encoded_wire_values.ts b/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_encoded_wire_values.ts new file mode 100644 index 00000000..7f8a44e4 --- /dev/null +++ b/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_encoded_wire_values.ts @@ -0,0 +1,35 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { describe } from 'vitest'; +import type { ClientField } from '../../../introspection/ClientField.js'; +import type { QueryResult } from '../../QueryResult.js'; +import { renderQueryData } from '../../queryRendering.js'; +import { SortDirection } from '../../SortDirection.js'; +import { Severity } from '../../../validation/Severity.js'; + +const cases: { kind: string; type: ClientField['type']; values: (string | number)[]; order: number[] }[] = [ + { kind: 'numeric strings', type: { kind: 'string' }, values: ['2', '10'], order: [1, 0] }, + { kind: 'numbers', type: { kind: 'number' }, values: [10, 2], order: [1, 0] }, + { kind: 'date strings', type: { kind: 'string' }, values: ['2026-01-02T00:00:00.000Z', '2026-01-01T00:00:00.000Z'], order: [1, 0] }, + { kind: 'date only strings', type: { kind: 'string' }, values: ['2026-02-01', '2026-01-01'], order: [1, 0] }, + { kind: 'time only strings', type: { kind: 'string' }, values: ['12:00:00', '08:00:00'], order: [1, 0] }, + { kind: 'time span strings', type: { kind: 'string' }, values: ['2.00:00:00', '10.00:00:00'], order: [1, 0] } +]; +const directions = [SortDirection.Ascending, SortDirection.Descending]; + +describe.each(cases.flatMap(item => directions.map(direction => ({ ...item, direction }))))( + 'when sorting in memory with encoded $kind in $direction order', ({ type, values, order, direction }) => { + let result: QueryResult; + beforeEach(() => { + result = renderQueryData({ clientOutput: { output: { kind: 'array', element: { kind: 'dto', name: 'Row', fields: [ + { name: 'value', type }, { name: 'index', type: { kind: 'number' } } + ] } } } }, values.map((value, index) => ({ value, index })), { + tenantId: 'test', correlationId: 'test', principal: undefined, signal: new AbortController().signal, allowedSeverity: Severity.Warning + }, { sorting: { field: 'value', direction } }); + }); + it('should preserve the wire values and their existing comparison order', () => { + const expected = direction === SortDirection.Ascending ? order : [...order].reverse(); + result.isSuccess.should.equal(true); + result.data!.should.deep.equal(expected.map(index => ({ value: values[index], index }))); + }); +}); diff --git a/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_scalar_values.ts b/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_scalar_values.ts index a3e9d728..a697d01f 100644 --- a/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_scalar_values.ts +++ b/Source/Core/queries/for_queryRendering/when_sorting_in_memory/with_scalar_values.ts @@ -12,30 +12,35 @@ const cases = [ { kind: 'string', values: ['b', 'a'], order: [1, 0] }, { kind: 'number', values: [10, 2], order: [1, 0] }, { kind: 'boolean', values: [true, false], order: [1, 0] }, - { kind: 'date', values: [new Date(1000), new Date(0)], order: [1, 0] }, + { kind: 'date', values: [new Date('2026-01-02T00:00:00Z'), new Date('2026-01-01T00:00:00Z')], order: [1, 0] }, { kind: 'guid', values: [Guid.parse('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'), Guid.parse('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')], order: [1, 0] }, - { kind: 'date only', values: [DateOnly.parse('2026-02-01'), DateOnly.parse('2026-01-01')], order: [1, 0] }, - { kind: 'time only', values: [TimeOnly.parse('12:00:00'), TimeOnly.parse('08:00:00')], order: [1, 0] }, - { kind: 'time span', values: [TimeSpan.parse('12:00:00'), TimeSpan.parse('08:00:00')], order: [1, 0] }, - { kind: 'null and undefined', values: [2, null, undefined, 1], order: [1, 2, 3, 0] }, + { kind: 'date only', values: ['2026-02-01', '2026-01-02', '2025-12-31', '2026-01-01'].map(DateOnly.parse), order: [2, 3, 1, 0] }, + { kind: 'time only', values: ['12:00:00', '08:01:00', '08:00:01', '08:00:00.001', '08:00:00'].map(TimeOnly.parse), order: [4, 3, 2, 1, 0] }, + { kind: 'time span', values: ['10.00:00:00', '2.00:00:00', '12:00:00', '00:00:00.0000001', '00:00:00', '-01:00:00', '-02:00:00'].map(TimeSpan.parse), order: [6, 5, 4, 3, 2, 1, 0] }, + { kind: 'null and undefined', values: [2, null, undefined, 1], order: [1, 2, 3, 0], descendingOrder: [0, 3, 1, 2] }, + { kind: 'nullable concept', values: [new ScalarConcept(2), new ScalarConcept(null), new ScalarConcept(1)], order: [1, 2, 0] }, { kind: 'mixed scalar kinds', values: [2, '10', 1], order: [2, 1, 0] }, { kind: 'existing bigint comparison', values: [10n, 2n], order: [1, 0] } ]; const concepts = cases.filter(item => !['null and undefined', 'mixed scalar kinds', 'existing bigint comparison'].includes(item.kind)) - .map(item => ({ kind: `concept over ${item.kind}`, values: item.values.map(value => new ScalarConcept(value)), order: undefined })); + .map(item => ({ ...item, kind: `concept over ${item.kind}`, values: item.values.map(value => new ScalarConcept(value)) })); +const nestedConcepts = cases.filter(item => ['number', 'date'].includes(item.kind)) + .map(item => ({ ...item, kind: `nested concept over ${item.kind}`, values: item.values.map(value => new ScalarConcept(new ScalarConcept(value))) })); +const directions = [SortDirection.Ascending, SortDirection.Descending]; -describe.each([...cases, ...concepts])('when sorting in memory with $kind values', ({ values, order }) => { +describe.each([...cases, ...concepts, ...nestedConcepts].flatMap(item => directions.map(direction => ({ ...item, direction }))))( + 'when sorting in memory with $kind values in $direction order', ({ values, order, descendingOrder, direction }) => { let result: QueryResult; beforeEach(() => { result = renderQueryData({}, values.map((value, index) => ({ value, index })), { tenantId: 'test', correlationId: 'test', principal: undefined, signal: new AbortController().signal, allowedSeverity: Severity.Warning - }, { sorting: { field: 'value', direction: SortDirection.Ascending } }); + }, { sorting: { field: 'value', direction } }); }); it('should accept the scalar values', () => { result.isSuccess.should.equal(true); }); - // Raw concept ordering is tracked separately in https://github.com/Cratis/Arc.TypeScript/issues/176. - if (order) it('should preserve the scalar comparison order', () => { - (result.data as { index: number }[]).map(item => item.index).should.deep.equal(order); + it('should preserve the scalar comparison order', () => { + const expected = direction === SortDirection.Ascending ? order : descendingOrder ?? [...order].reverse(); + (result.data as { index: number }[]).map(item => item.index).should.deep.equal(expected); }); }); diff --git a/Source/Core/queries/queryRendering.ts b/Source/Core/queries/queryRendering.ts index 3f6d2a36..6f7f7afe 100644 --- a/Source/Core/queries/queryRendering.ts +++ b/Source/Core/queries/queryRendering.ts @@ -29,8 +29,15 @@ function isScalarSortValue(value: unknown): boolean { } function compareValues(left: unknown, right: unknown): number { + while (left instanceof ConceptAs) left = left.value; + while (right instanceof ConceptAs) right = right.value; if (left instanceof Date && right instanceof Date) return left.getTime() - right.getTime(); if (left == null || right == null) return left == null ? right == null ? 0 : -1 : 1; + if (left instanceof DateOnly && right instanceof DateOnly) + return left.year - right.year || left.month - right.month || left.day - right.day; + if (left instanceof TimeOnly && right instanceof TimeOnly) + return left.hour - right.hour || left.minute - right.minute || left.second - right.second || left.millisecond - right.millisecond; + if (left instanceof TimeSpan && right instanceof TimeSpan) return left.ticks - right.ticks; if (typeof left === 'number' && typeof right === 'number') return left - right; if (typeof left === 'bigint' && typeof right === 'bigint') return left < right ? -1 : left > right ? 1 : 0; if (typeof left === 'boolean' && typeof right === 'boolean') return Number(left) - Number(right); diff --git a/Source/Cratis/package.json b/Source/Cratis/package.json index b3f550d6..3ef076aa 100644 --- a/Source/Cratis/package.json +++ b/Source/Cratis/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/cratis", - "version": "0.57.1", + "version": "0.57.2", "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 9b80714a..88afed80 100644 --- a/Source/Drizzle/package.json +++ b/Source/Drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.drizzle", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { @@ -29,7 +29,7 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.57.1", + "@cratis/arc.core": "^0.57.2", "@cratis/fundamentals": "^7.19.6", "drizzle-orm": "^0.45.0", "rxjs": "^7.8.2" diff --git a/Source/Express/package.json b/Source/Express/package.json index 5461796d..9dff5863 100644 --- a/Source/Express/package.json +++ b/Source/Express/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.express", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Fastify/package.json b/Source/Fastify/package.json index f59b197b..762fd63a 100644 --- a/Source/Fastify/package.json +++ b/Source/Fastify/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.fastify", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Hono/package.json b/Source/Hono/package.json index 04967a0c..d7151bc4 100644 --- a/Source/Hono/package.json +++ b/Source/Hono/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.hono", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/MongoDB/package.json b/Source/MongoDB/package.json index 971922c7..f302ff6f 100644 --- a/Source/MongoDB/package.json +++ b/Source/MongoDB/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.mongodb", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { @@ -29,7 +29,7 @@ "README.md" ], "peerDependencies": { - "@cratis/arc.core": "^0.57.1", + "@cratis/arc.core": "^0.57.2", "@cratis/fundamentals": "^7.19.6", "@opentelemetry/api": "^1.9.0", "mongodb": "^6.21.0", diff --git a/Source/Testing/package.json b/Source/Testing/package.json index 1dd4fb32..d3e539bf 100644 --- a/Source/Testing/package.json +++ b/Source/Testing/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.testing", - "version": "0.57.1", + "version": "0.57.2", "type": "module", "license": "MIT", "publishConfig": { diff --git a/Source/Tools/ProxyGenerator/package.json b/Source/Tools/ProxyGenerator/package.json index 5296678c..9becac55 100644 --- a/Source/Tools/ProxyGenerator/package.json +++ b/Source/Tools/ProxyGenerator/package.json @@ -1,6 +1,6 @@ { "name": "@cratis/arc.proxygenerator", - "version": "0.57.1", + "version": "0.57.2", "description": "TypeScript source analyzer and deterministic Arc client proxy generator", "repository": { "type": "git", diff --git a/yarn.lock b/yarn.lock index 8dc51c14..9dbe5941 100644 --- a/yarn.lock +++ b/yarn.lock @@ -120,8 +120,8 @@ __metadata: rxjs: "npm:^7.8.2" zod: "npm:^4.1.0" peerDependencies: - "@cratis/arc.core": ^0.57.1 - "@cratis/arc.testing": ^0.57.1 + "@cratis/arc.core": ^0.57.2 + "@cratis/arc.testing": ^0.57.2 "@cratis/chronicle": ^6.29.0 "@cratis/fundamentals": ^7.19.6 rxjs: ^7.8.2 @@ -226,7 +226,7 @@ __metadata: rxjs: "npm:^7.8.2" sql.js: "npm:^1.14.2" peerDependencies: - "@cratis/arc.core": ^0.57.1 + "@cratis/arc.core": ^0.57.2 "@cratis/fundamentals": ^7.19.6 drizzle-orm: ^0.45.0 rxjs: ^7.8.2 @@ -292,7 +292,7 @@ __metadata: mongodb: "npm:^6.21.0" rxjs: "npm:^7.8.2" peerDependencies: - "@cratis/arc.core": ^0.57.1 + "@cratis/arc.core": ^0.57.2 "@cratis/fundamentals": ^7.19.6 "@opentelemetry/api": ^1.9.0 mongodb: ^6.21.0