From 6fb8bee46f8b7f77c8926095edbdc742e8337493 Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Wed, 2 Sep 2026 10:09:11 +0200 Subject: [PATCH 1/9] ci: add snapshot publishing on next and JSR release pipeline - publish NuGet + JSR snapshots on every push to next - rename TS client to @intility/json-api-client, prepare for JSR - reset unpublished TypeGen version to 0.1.0 --- .github/workflows/ci-cd.yml | 14 ++- .github/workflows/next-snapshot.yml | 119 ++++++++++++++++++ .github/workflows/typescript-release.yml | 35 ++++++ .../JsonApiToolkit.TypeGen.csproj | 2 +- clients/typescript/README.md | 26 ++-- clients/typescript/build_npm.ts | 13 +- clients/typescript/deno.json | 21 +++- clients/typescript/src/index.ts | 37 ++++++ .../src/query-builder/FilterGroupBuilder.ts | 8 +- .../src/query-builder/JsonApiQueryBuilder.ts | 14 +-- 10 files changed, 252 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/next-snapshot.yml create mode 100644 .github/workflows/typescript-release.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 2431f00..e7f963c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -83,11 +83,11 @@ jobs: exit 1 publish: - if: github.event_name == 'release' + if: github.event_name == 'release' && !startsWith(github.event.release.tag_name, 'typescript-') runs-on: ubuntu-latest permissions: contents: read - id-token: write # OIDC token for NuGet trusted publishing + id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -100,10 +100,14 @@ jobs: - name: Restore run: dotnet restore --locked-mode - - name: Build and Pack + - name: Build and Pack JsonApiToolkit + if: ${{ !startsWith(github.event.release.tag_name, 'typegen-') }} run: dotnet pack JsonApiToolkit/JsonApiToolkit.csproj -c Release --no-restore - # Exchange OIDC token for a short-lived NuGet API key + - name: Build and Pack JsonApiToolkit.TypeGen + if: startsWith(github.event.release.tag_name, 'typegen-') + run: dotnet pack JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj -c Release --no-restore + - name: NuGet login id: login uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 @@ -111,7 +115,7 @@ jobs: user: ${{ secrets.NUGET_USER }} - name: Publish to NuGet.org - run: dotnet nuget push "JsonApiToolkit/bin/Release/*.nupkg" + run: dotnet nuget push "JsonApiToolkit*/bin/Release/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate diff --git a/.github/workflows/next-snapshot.yml b/.github/workflows/next-snapshot.yml new file mode 100644 index 0000000..9d1ad90 --- /dev/null +++ b/.github/workflows/next-snapshot.yml @@ -0,0 +1,119 @@ +name: Next Snapshot + +on: + push: + branches: + - next + +concurrency: + group: next-snapshot + cancel-in-progress: false + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + toolkit: ${{ steps.filter.outputs.toolkit }} + typegen: ${{ steps.filter.outputs.typegen }} + typescript: ${{ steps.filter.outputs.typescript }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: filter + with: + filters: | + toolkit: + - 'JsonApiToolkit/**' + - 'Directory.Build.props' + typegen: + - 'JsonApiToolkit.TypeGen/**' + - 'Directory.Build.props' + typescript: + - 'clients/typescript/**' + + dotnet: + name: Publish .NET Snapshot + needs: changes + if: needs.changes.outputs.toolkit == 'true' || needs.changes.outputs.typegen == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore --locked-mode + + - name: Test + run: dotnet test --configuration Release --verbosity normal --no-restore + + - name: Pack JsonApiToolkit + if: needs.changes.outputs.toolkit == 'true' + run: | + BASE=$(sed -n 's:.*\(.*\).*:\1:p' JsonApiToolkit/JsonApiToolkit.csproj) + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" + dotnet pack JsonApiToolkit/JsonApiToolkit.csproj -c Release --no-restore \ + -p:Version="$MAJOR.$MINOR.$((PATCH + 1))-next.${{ github.run_number }}" + + - name: Pack JsonApiToolkit.TypeGen + if: needs.changes.outputs.typegen == 'true' + run: | + BASE=$(sed -n 's:.*\(.*\).*:\1:p' JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj) + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" + dotnet pack JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj -c Release --no-restore \ + -p:Version="$MAJOR.$MINOR.$((PATCH + 1))-next.${{ github.run_number }}" + + - name: NuGet login + id: login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 + with: + user: ${{ secrets.NUGET_USER }} + + - name: Publish to NuGet.org + run: dotnet nuget push "JsonApiToolkit*/bin/Release/*.nupkg" + --source https://api.nuget.org/v3/index.json + --api-key ${{ steps.login.outputs.NUGET_API_KEY }} + --skip-duplicate + + typescript: + name: Publish TypeScript Snapshot + needs: changes + if: needs.changes.outputs.typescript == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + defaults: + run: + working-directory: clients/typescript + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Deno + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + with: + deno-version: v2.x + + - name: Test + run: deno task test + + - name: Set Snapshot Version + run: | + BASE=$(jq -r .version deno.json) + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" + jq --arg v "$MAJOR.$MINOR.$((PATCH + 1))-next.${{ github.run_number }}" \ + '.version = $v' deno.json > deno.json.tmp + mv deno.json.tmp deno.json + + - name: Publish to JSR + run: deno publish --allow-dirty diff --git a/.github/workflows/typescript-release.yml b/.github/workflows/typescript-release.yml new file mode 100644 index 0000000..57a8918 --- /dev/null +++ b/.github/workflows/typescript-release.yml @@ -0,0 +1,35 @@ +name: TypeScript Release + +on: + release: + types: [published] + +concurrency: + group: ts-release-${{ github.ref }} + cancel-in-progress: false + +defaults: + run: + working-directory: clients/typescript + +jobs: + publish: + if: startsWith(github.event.release.tag_name, 'typescript-v') + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout Repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Deno + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + with: + deno-version: v2.x + + - name: Test + run: deno task test + + - name: Publish to JSR + run: deno publish diff --git a/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj b/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj index 0bab50d..67c54c8 100644 --- a/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj +++ b/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj @@ -9,7 +9,7 @@ jsonapi-typegen Intility.JsonApiToolkit.TypeGen JsonApiToolkit TypeGen - 2.1.8 + 0.1.0 Intility Intility Generates TypeScript resource types from [JsonApiResource]-attributed C# models. diff --git a/clients/typescript/README.md b/clients/typescript/README.md index 3aa0784..f0fdf99 100644 --- a/clients/typescript/README.md +++ b/clients/typescript/README.md @@ -1,10 +1,10 @@ -# jsonapi-ts-tools +# json-api-client -[![🚀 Release](https://github.com/intility/jsonapi-ts-tools/actions/workflows/release.yml/badge.svg)](https://github.com/intility/jsonapi-ts-tools/actions/workflows/release.yml) +[![TypeScript Release](https://github.com/intility/json-api-toolkit/actions/workflows/typescript-release.yml/badge.svg)](https://github.com/intility/json-api-toolkit/actions/workflows/typescript-release.yml) -**jsonapi-ts-tools** is a lightweight, Deno-based TypeScript library designed to +**json-api-client** is a lightweight, Deno-based TypeScript library designed to make working with -[JsonApiToolkit](https://github.com/intility/Intility.JsonApiToolkit) responses +[JsonApiToolkit](https://github.com/intility/json-api-toolkit) responses in TypeScript applications easier. ## Features @@ -18,19 +18,25 @@ in TypeScript applications easier. ## Prerequisites - **JsonApiToolkit**: This library is designed to work with - [JsonApiToolkit](https://github.com/intility/Intility.JsonApiToolkit). Make + [JsonApiToolkit](https://github.com/intility/json-api-toolkit). Make sure the api you want to interact with is using JsonApiToolkit. ## Getting Started -You can read more about jsonapi-ts-tools & JsonApiToolkit -[**here**](https://intility.github.io/Intility.JsonApiToolkit/docs/integrations/ts-tools.html), +You can read more about json-api-client & JsonApiToolkit +[**here**](https://intility.github.io/json-api-toolkit/), or follow the instructions below for a quick start. ### Installation +The package is published on [JSR](https://jsr.io/@intility/json-api-client): + ```bash -npm install @intility/jsonapi-ts-tools +# Deno +deno add jsr:@intility/json-api-client + +# Node.js (via the jsr CLI) +npx jsr add @intility/json-api-client ``` ### Define your types @@ -73,7 +79,7 @@ import { hydrateResponse, JsonApiArrayResponse, JsonApiSingleResponse, -} from '@intility/jsonapi-ts-tools'; +} from '@intility/json-api-client'; // For list endpoints (returns array of resources) export function useHydratedListQuery( @@ -225,7 +231,7 @@ the error structure produced by import { isJsonApiErrorResponse, JsonApiErrorCodes, -} from "@intility/jsonapi-ts-tools"; +} from "@intility/json-api-client"; const response = await fetch("/api/todos"); const body = await response.json(); diff --git a/clients/typescript/build_npm.ts b/clients/typescript/build_npm.ts index d923723..5d76361 100644 --- a/clients/typescript/build_npm.ts +++ b/clients/typescript/build_npm.ts @@ -10,17 +10,20 @@ await build({ }, test: false, package: { - name: '@intility/jsonapi-ts-tools', - version: Deno.args[0] || '0.0.0', + name: '@intility/json-api-client', + version: Deno.args[0] || '0.1.0', + publishConfig: { + access: 'public', + }, description: - 'jsonapi-ts-tools is a TypeScript library for working with JSON:API.', + 'json-api-client is a TypeScript library for working with JSON:API.', license: 'MIT', repository: { type: 'git', - url: 'git+https://github.com/intility/jsonapi-ts-tools.git', + url: 'git+https://github.com/intility/json-api-toolkit.git', }, bugs: { - url: 'https://github.com/intility/jsonapi-ts-tools/issues', + url: 'https://github.com/intility/json-api-toolkit/issues', }, }, postBuild() { diff --git a/clients/typescript/deno.json b/clients/typescript/deno.json index f3b86cd..5b9a310 100644 --- a/clients/typescript/deno.json +++ b/clients/typescript/deno.json @@ -1,10 +1,21 @@ { - "name": "@intility/jsonapi-ts-tools", - "publishConfig": { - "registry": "https://npm.pkg.github.com/" - }, + "name": "@intility/json-api-client", "version": "0.6.0", - "exports": "./index.js", + "description": "TypeScript tools for JSON:API: response hydration, a type-safe query builder, and error types for JsonApiToolkit backends.", + "license": "MIT", + "exports": "./src/index.ts", + "publish": { + "exclude": [ + "contract/", + "dist/", + "build_npm.ts", + "CLAUDE.md", + "CODEOWNERS", + "CHANGELOG.md", + "deno.lock", + "**/*_test.ts" + ] + }, "tasks": { "check": "deno check src/**/*.ts contract/**/*.ts", "lint": "deno lint --ignore=README.md,.github/,CLAUDE.md,CODEOWNERS,CHANGELOG.md", diff --git a/clients/typescript/src/index.ts b/clients/typescript/src/index.ts index 430c0c2..f18d3bf 100644 --- a/clients/typescript/src/index.ts +++ b/clients/typescript/src/index.ts @@ -1,3 +1,40 @@ +/** + * TypeScript tools for JSON:API responses from + * [JsonApiToolkit](https://github.com/intility/json-api-toolkit) backends. + * + * The library has three parts: + * + * - **Hydration**: `hydrateResponse` resolves relationships from the + * `included` array and returns plain objects as `{ data, meta, links }`. + * - **Query builder**: `JsonApiQueryBuilder` builds type-safe JSON:API + * query strings with filters, sorts, includes, sparse fieldsets, and + * pagination. + * - **Error types**: JSON:API error types and the + * `isJsonApiErrorResponse` type guard, matching the C# toolkit's error + * model. + * + * ## Usage + * + * ```ts + * import { + * hydrateResponse, + * JsonApiQueryBuilder, + * } from '@intility/json-api-client'; + * + * const query = new JsonApiQueryBuilder() + * .filter('author.name', 'John') + * .include('author') + * .sort('-publishedAt') + * .page(1, 25) + * .build(); + * + * const response = await fetch(`/api/books?${query}`); + * const { data, meta } = hydrateResponse(await response.json()); + * ``` + * + * @module + */ + export * from './hydrate.ts'; export * from './errors.ts'; export * from './types/jsonapi.ts'; diff --git a/clients/typescript/src/query-builder/FilterGroupBuilder.ts b/clients/typescript/src/query-builder/FilterGroupBuilder.ts index 8c4386a..8854aed 100644 --- a/clients/typescript/src/query-builder/FilterGroupBuilder.ts +++ b/clients/typescript/src/query-builder/FilterGroupBuilder.ts @@ -12,7 +12,7 @@ export class FilterGroupBuilder { /** * Add a simple filter to this group. */ - filter>(field: K, op: FilterOp, value: any) { + filter>(field: K, op: FilterOp, value: any): this { this.groups.push({ type: 'simple', filter: { field, op, value }, @@ -23,7 +23,7 @@ export class FilterGroupBuilder { /** * Add an "or" logical group to this group. */ - or(cb: (b: FilterGroupBuilder) => void) { + or(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.groups.push({ @@ -36,7 +36,7 @@ export class FilterGroupBuilder { /** * Add an "and" logical group to this group. */ - and(cb: (b: FilterGroupBuilder) => void) { + and(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.groups.push({ @@ -49,7 +49,7 @@ export class FilterGroupBuilder { /** * Add a "not" logical group to this group. */ - not(cb: (b: FilterGroupBuilder) => void) { + not(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.groups.push({ diff --git a/clients/typescript/src/query-builder/JsonApiQueryBuilder.ts b/clients/typescript/src/query-builder/JsonApiQueryBuilder.ts index 5770f49..a4a23ba 100644 --- a/clients/typescript/src/query-builder/JsonApiQueryBuilder.ts +++ b/clients/typescript/src/query-builder/JsonApiQueryBuilder.ts @@ -73,7 +73,7 @@ export class JsonApiQueryBuilder { /** * Add an "or" logical filter group. */ - or(cb: (b: FilterGroupBuilder) => void) { + or(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.filterGroups.push({ @@ -86,7 +86,7 @@ export class JsonApiQueryBuilder { /** * Add an "and" logical filter group. */ - and(cb: (b: FilterGroupBuilder) => void) { + and(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.filterGroups.push({ @@ -99,7 +99,7 @@ export class JsonApiQueryBuilder { /** * Add a "not" logical filter group. */ - not(cb: (b: FilterGroupBuilder) => void) { + not(cb: (b: FilterGroupBuilder) => void): this { const builder = new FilterGroupBuilder(); cb(builder); this.filterGroups.push({ @@ -112,7 +112,7 @@ export class JsonApiQueryBuilder { /** * Set sort fields (comma-separated, supports -field for descending). */ - sort(...fields: Sort) { + sort(...fields: Sort): this { this.sorts = fields; return this; } @@ -120,7 +120,7 @@ export class JsonApiQueryBuilder { /** * Set included relationships (comma-separated, supports dot notation). */ - include(...fields: Include) { + include(...fields: Include): this { this.includes = fields; return this; } @@ -135,7 +135,7 @@ export class JsonApiQueryBuilder { fields( type: string, fields: (unknown extends R ? string : DirectAttributeKeys)[], - ) { + ): this { this.fieldsets.set(type, fields as string[]); return this; } @@ -143,7 +143,7 @@ export class JsonApiQueryBuilder { /** * Set pagination (page number and size). */ - page(number: number, size: number) { + page(number: number, size: number): this { this.pagination = { number, size }; return this; } From 89bf891d29c458a94490f9c44cd0e85ef62a4217 Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Wed, 2 Sep 2026 10:18:36 +0200 Subject: [PATCH 2/9] docs(typegen): add package README and NuGet metadata --- .../JsonApiToolkit.TypeGen.csproj | 10 +++++ JsonApiToolkit.TypeGen/README.md | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 JsonApiToolkit.TypeGen/README.md diff --git a/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj b/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj index 67c54c8..d3b4df2 100644 --- a/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj +++ b/JsonApiToolkit.TypeGen/JsonApiToolkit.TypeGen.csproj @@ -13,8 +13,18 @@ Intility Intility Generates TypeScript resource types from [JsonApiResource]-attributed C# models. + jsonapi;json-api;typescript;codegen;dotnet-tool + https://github.com/intility/json-api-toolkit + git + https://github.com/intility/json-api-toolkit + intility-logo.png + README.md MIT + + + +