Skip to content

Changelog v3.20.0

Version 3.20.0 (2026-07-09)

Full Changelog

This release introduces the Dart client code generator — a new NpgsqlRest.DartClient plugin that generates Dart fetch modules for Flutter projects, the same way NpgsqlRest.TsClient generates TypeScript modules. Also included: TanStack Query (React Query) hooks generation for the TypeScript client, function-calling schemas (OpenAI/Anthropic) and llms.txt generated from the MCP tool set, support for the HTTP QUERY method, and a configurable OpenAPI specification version (3.0 / 3.1) for the OpenAPI plugin.


1. Dart client code generation (NpgsqlRest.DartClient 1.0.0)

For every NpgsqlRest endpoint the generator emits idiomatic Dart built on package:http (no other dependencies, works on all Flutter targets — mobile, desktop, web):

  • Request classes with toJson() — one field per routine parameter, optional-named constructor parameters. Parameters with PostgreSQL defaults are omitted from the JSON body and query string when null (so the database default applies), mirroring the TypeScript client's optional-property semantics.
  • Response classes with a fromJson() factory — one field per column, snake_case columns become lowerCamelCase fields with the raw wire name kept as the JSON key. Composite type columns become nested model classes (flat or nested per the endpoint's nested annotation).
  • Async functions wrapping the HTTP call: query-string building (_query helper with URL encoding, repeated keys for arrays, ISO 8601 for DateTime), path-parameter interpolation resolved to the generated request fields, JSON bodies, custom headers, and @body_parameter_name handling.
  • ApiResult<T> / ApiError wrappers carrying the HTTP status code and problem-details errors (IncludeStatusCode option or the dartclient_status_code annotation).
  • Multipart upload functions for upload endpoints (http.MultipartRequest, files sent as form field file, optional progress callback reporting uploaded bytes, optional XSRF token header).
  • Server-sent events (SSE) support: SSE endpoints generate create{Name}EventSource factories returning an SseSubscription (built on streamed package:http responses — no extra dependency) and SSE-aware functions with onMessage/id/closeAfterMs/awaitConnectionMs parameters that attach the execution-id header to the triggering request — including combined upload+SSE endpoints.
  • Raw responses for proxies: proxy_out and void proxy pass-through endpoints return Future<http.Response> directly.
  • Per-call rewrite hooks: IncludeParseUrlParam/IncludeParseRequestParam options (and dartclient_parse_url/dartclient_parse_request annotations) add optional parseUrl/parseRequest named parameters that can rewrite the URI or request before sending.
  • Login/logout special-casing: login endpoints return the token String, logout endpoints return void.
  • Testability: each generated module exposes a top-level http.Client? httpClient variable — assign a MockClient from package:http/testing.dart to intercept requests in tests.

Type mapping

Unlike TypeScript (where every numeric is number), Dart distinguishes integers from floating point:

PostgreSQLDart
smallint, integer, bigintint
real, double precision, numeric, moneydouble
booleanbool
json, jsonbdynamic (configurable via DefaultJsonType)
date, timestamp, timestamptzDateTime (configurable via UseDateTimeType)
everything elseString
arraysList<T>

Dart reserved words and dart:core type names used as parameter or column names are escaped with a trailing underscore (classclass_, doubledouble_) while JSON keys keep the raw wire names.

Configuration

New NpgsqlRest:DartClientCodeGen configuration section (sibling of ClientCodeGen, which is unchanged). Full section with defaults:

json
json
{
  "NpgsqlRest": {
    "DartClientCodeGen": {
      "Enabled": false,
      "FilePath": null,
      "FileOverwrite": true,
      "IncludeHost": true,
      "CustomHost": null,
      "CommentHeader": "Simple",
      "CommentHeaderIncludeComments": true,
      "BySchema": true,
      "IncludeStatusCode": true,
      "SeparateModelsFile": false,
      "ImportBaseUrlFrom": null,
      "HeaderLines": ["// autogenerated at {0}", ""],
      "SkipRoutineNames": [],
      "SkipFunctionNames": [],
      "SkipPaths": [],
      "SkipSchemas": [],
      "DefaultJsonType": "dynamic",
      "UseRoutineNameInsteadOfEndpoint": false,
      "ExportUrls": false,
      "UniqueModels": false,
      "XsrfTokenHeaderName": null,
      "ExportEventSources": true,
      "IncludeParseUrlParam": false,
      "IncludeParseRequestParam": false,
      "CustomImports": [],
      "CustomHeaders": {},
      "IncludeSchemaInNames": true,
      "ErrorTypeName": "ApiError",
      "ResultTypeName": "ApiResult",
      "OmitAutomaticParameters": false,
      "ModelPrefix": null,
      "ModelSuffix": null,
      "UseDateTimeType": true
    }
  }
}

Set Enabled to true and point FilePath at your Flutter project, e.g. "../my_app/lib/api/{0}.dart"{0} is the PostgreSQL schema name (snake case) or the module name from the dartclient_module annotation (with BySchema).

Most options carry the same meaning as their ClientCodeGen counterparts. Dart-specific options:

  • SeparateModelsFile — emit request/response (and composite) model classes into a separate {name}_models.dart file; the client file imports and re-exports it so consumers keep a single import (the Dart analog of CreateSeparateTypeFile/ExportTypes).
  • ModelPrefix / ModelSuffix — namespace generated model class names to avoid collisions when several generated modules are imported into one file (there is no TypeScript-style I prefix).
  • UseDateTimeType — when true (default), date/timestamp/timestamptz map to DateTime (DateTime.parse in fromJson, toIso8601String in toJson); when false, they map to String.
  • ErrorTypeName / ResultTypeName — names of the generated status-wrapper classes (replaces TypeScript's free-text ErrorExpression/ErrorType, which cannot be safely injected into Dart).
  • ExportEventSources — when false, SSE event source factories get a private (underscore) name instead of a public one (the Dart analog of not exporting them).
  • IncludeParseUrlParam / IncludeParseRequestParam — add the per-call parseUrl/parseRequest rewrite hooks to every generated function.

ClientCodeGen options with no Dart counterpart (intentionally dropped): SkipTypes (there is no untyped Dart), ExportTypes (every non-underscore Dart declaration is already public), and ImportParseQueryFrom (the generated helpers are underscore-private and cannot be imported across Dart files; use ImportBaseUrlFrom for the shared baseUrl).

Comment annotations

Per-endpoint overrides in routine comments:

  • dartclient = off — exclude the endpoint from generation.
  • dartclient_module=name — group endpoints into a module file (falls back to tsclient_module, so SQL-file directory grouping applies to both generators).
  • dartclient_status_code=true — wrap this endpoint's response in ApiResult<T>.
  • dartclient_events=false — generate a plain function for an SSE endpoint (no event-source parameters).
  • dartclient_parse_url=true / dartclient_parse_request=true — add the per-call rewrite hooks to this endpoint.
  • dartclient_export_url=true — also emit a {name}Url() URL-builder function.
  • dartclient_url_only=true — emit only the URL-builder function.

Known limits

  • numeric/money map to double (the JSON wire format already carries a JSON number); Dart int on Flutter Web is limited to 2^53.
  • Multi-dimensional arrays are typed as one-dimensional List<T> (PostgreSQL does not expose array dimensionality — same limitation as the TypeScript client).

Other changes

  • docker/Dockerfile.jit: the dependency-restore layer now copies the NpgsqlRest.SqlFileSource and NpgsqlRest.Mcp project files (previously missing, breaking dotnet restore in the JIT image build), along with the new NpgsqlRest.DartClient.

2. Configurable OpenAPI specification version (NpgsqlRest.OpenApi)

The OpenAPI plugin previously always emitted "openapi": "3.0.3". A new SpecVersion option selects the specification version of the generated document:

jsonc
jsonc
{
  "NpgsqlRest": {
    "OpenApiOptions": {
      // OpenAPI specification version of the generated document: "3.0" or "3.1".
      // "3.0" emits openapi: 3.0.3 (default, backward compatible),
      // "3.1" emits openapi: 3.1.1 (JSON Schema 2020-12 alignment).
      // This is the spec version, not your API version (that is DocumentVersion).
      "SpecVersion": "3.0"
    }
  }
}
  • "3.0" (default) emits openapi: 3.0.3 — output is unchanged from previous releases.
  • "3.1" emits openapi: 3.1.1. The emitter uses no keyword whose semantics differ between OpenAPI 3.0 and 3.1 (nullable, exclusiveMinimum/exclusiveMaximum, ...), so the rest of the document is identical — the setting matters for toolchains that require a 3.1 document (JSON Schema 2020-12 alignment).
  • Values are trimmed and case-insensitive; any other value fails fast at startup with an error listing the valid values — no silent fallback.
  • The emitted version strings are exposed as constants (OpenApiSpecVersions.V30 / V31) instead of inline literals.

3. TanStack Query (React Query) hooks generation (NpgsqlRest.TsClient)

The TypeScript client generator can now emit a TanStack Query v5 hooks module alongside each generated client module: useQuery hooks for GET endpoints and useMutation hooks for everything else, with exported query-key factories. The hooks import the client functions via a relative import and derive all types from them (Parameters<typeof fn>[0] / Awaited<ReturnType<typeof fn>>), so they work regardless of the ExportTypes/CreateSeparateTypeFile settings. Generated code uses TanStack v5 object syntax exclusively and compiles under tsc --strict with only @tanstack/react-query@5 installed.

jsonc
jsonc
{
  "NpgsqlRest": {
    "ClientCodeGen": {
      "ReactQuery": {
        "Enabled": false,
        // Output file path for the hooks module. In multi-file mode (BySchema or tsclient_module
        // usage) must contain {0}, mirroring FilePath rules. Required when Enabled is true.
        "FilePath": null,
        "FileOverwrite": true,
        // Optional first segment prepended to every query key (for apps consuming multiple
        // NpgsqlRest APIs). Null to omit.
        "QueryKeyPrefix": null,
        // Export query key factory objects alongside hooks.
        "ExposeQueryKeys": true,
        // Module specifier the hooks import TanStack Query from - point at an internal
        // wrapper module when direct imports of @tanstack/react-query are not allowed.
        "ImportFrom": "@tanstack/react-query",
        "HeaderLines": ["// autogenerated at {0}", ""]
      }
    }
  }
}

Generated shape for a GET endpoint (useGetCustomerOrders-style; mutations get use{Name}Mutation with an Omit<UseMutationOptions<...>, "mutationFn"> passthrough):

typescript
typescript
export const getCustomerOrdersKeys = {
    all: ["getCustomerOrders"] as const,
    byRequest: (request: GetCustomerOrdersRequest) =>
        ["getCustomerOrders", request] as const,
};

export function useGetCustomerOrders(
    request: GetCustomerOrdersRequest,
    options?: Omit<UseQueryOptions<GetCustomerOrdersResult>, "queryKey" | "queryFn">,
) {
    return useQuery({
        queryKey: getCustomerOrdersKeys.byRequest(request),
        queryFn: () => api.getCustomerOrders(request),
        ...options,
    });
}

Details:

  • The whole request object is used as a key segment (TanStack v5 hashes keys with stable, key-order-insensitive serialization).
  • tsclient_hooks=off routine annotation excludes an endpoint from the hooks file only; the client function is still generated.
  • SSE, upload and url-only endpoints never produce hooks.
  • No automatic cache invalidation is generated — wire onSuccess through the options passthrough (a future @invalidates annotation will build on the exported key factories).
  • Hooks output is TypeScript-only: with SkipTypes enabled the run logs a warning and skips hooks generation.
  • ImportFrom replaces the @tanstack/react-query import specifier — for teams that route TanStack through an internal wrapper module (the module must re-export useQuery, useMutation, UseQueryOptions and UseMutationOptions with v5 semantics).
  • ReactQuery.Enabled with no ReactQuery.FilePath (or an empty ImportFrom) fails fast at startup.

4. Function-calling schemas (OpenAI/Anthropic) and llms.txt from the MCP tool set (NpgsqlRest.Mcp)

The MCP plugin can now project its tool catalog (the routines annotated with mcp) into three additional capability documents — without rebuilding any schema logic; every output is a projection of the tool objects the MCP catalog already contains, so parameter exclusion (claim-sourced/IP/virtual/resolved), description precedence, and type mapping are inherited exactly:

  • OpenAI Chat Completions tools array{"type":"function","function":{name, description, parameters}} per tool, where parameters is the MCP inputSchema verbatim.
  • Anthropic Messages API tools array{name, description, input_schema} per tool, same verbatim schema.
  • llms.txt — markdown capability document (H1 from ServerName/database name, blockquote summary from Instructions, one section per endpoint with method/URL/description/parameters, and a Machine-readable section linking the OpenAPI document, the /mcp endpoint, and the two tools documents).
jsonc
jsonc
{
  "NpgsqlRest": {
    "McpOptions": {
      "ToolSchemas": {
        "Enabled": false,
        "FileOverwrite": true,
        // null FileName skips writing that file; null UrlPath skips serving that document
        "OpenAiFileName": "npgsqlrest_tools_openai.json",
        "OpenAiUrlPath": "/tools/openai.json",
        "AnthropicFileName": "npgsqlrest_tools_anthropic.json",
        "AnthropicUrlPath": "/tools/anthropic.json",
        "LlmsTxtFileName": "llms.txt",
        "LlmsTxtUrlPath": "/llms.txt"
      }
    }
  }
}

Details:

  • Independent of the /mcp endpoint: documents are generated and served from mcp annotations even when McpOptions.Enabled is false.
  • Tool names are sanitized to the OpenAI function-name form ([a-zA-Z0-9_-], max 64) in both JSON documents, with a logged warning; llms.txt keeps the original names. Two names colliding after sanitization fail fast at startup.
  • Deterministic output (tools ordered by name, no timestamps): two runs against the same database produce identical bytes.
  • Documents are built once at startup and served anonymously (application/json / text/markdown), same trust level as /openapi.json.
  • No filtering, no strict/additionalProperties injection, no invalidation of any existing MCP behavior — HideUnauthorizedTools-style per-user filtering applies to tools/list only.

5. HTTP QUERY method support

The recently standardized HTTP QUERY method — safe and idempotent like GET, but carrying the query in the request body — is now supported across the stack. Annotate a routine with:

sql
sql
comment on function my_schema.search_documents(_query text, _top int) is '
HTTP QUERY
';
  • Core: QUERY joins the Method enum; the HTTP QUERY [path] annotation (and proxy QUERY / proxy_out QUERY) parse it, routing matches it, and parameters default to the JSON request body (RequestParamType.BodyJson) — the whole point of the method. request_param_type query_string still overrides per endpoint.
  • TypeScript client: emits method: "QUERY" with body: JSON.stringify(request) (fetch passes custom methods through).
  • React Query hooks: QUERY endpoints map to useQuery hooks (safe + idempotent = cacheable), not mutations.
  • Dart client: emits _send('QUERY', uri, body: jsonEncode(request.toJson())) (package:http accepts custom methods).
  • MCP: QUERY tools get readOnlyHint: true, same as GET.
  • HTTP files, llms.txt, SQL file source: work with the method string as-is (SQL files with a select-only body still default to GET; use the HTTP QUERY annotation to opt in).
  • OpenAPI: QUERY endpoints are skipped with a logged warning — OpenAPI 3.0/3.1 path items have no query operation key (representable from OpenAPI 3.2, which is not yet an emission target).

6. Required environment variables in the default connection string

The default configuration's connection string now uses the required environment variable syntax ({!NAME}):

json
json
{
  "ConnectionStrings": {
    "Default": "Host={!PGHOST};Port=5432;Database={!PGDATABASE};Username={!PGUSER};Password={!PGPASSWORD}"
  }
}

Running with the default configuration and a missing variable now fails immediately at startup with an error naming exactly what to set — Required environment variable 'PGHOST' (referenced as '{!PGHOST}' in configuration) is not set. — instead of a cryptic downstream Npgsql host-resolution failure.

Supporting changes:

  • Connection strings are now resolved through the same substitution as every other configuration value, so the {!NAME} (required) and {NAME} (optional) syntaxes both work in any ConnectionStrings entry.
  • The --config output and configuration validation never throw for unset required variables — the literal {!NAME} placeholder is kept in the rendered output, so the configuration can always be inspected before the environment is set up.

Comments