@truenas/api-client 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,19 +84,61 @@ client.api.events('app.query').subscribe(event => {
84
84
 
85
85
  ### Naming a version
86
86
 
87
- The version is discovered at runtime; the types are fixed at compile time.
88
- `createTrueNasClient` defaults to the oldest supported version, which
89
- understates a newer server rather than promising methods it lacks. Name a
90
- version to reach the rest:
87
+ By default the version is discovered at runtime while the types are fixed at
88
+ compile time, and `createTrueNasClient` assumes the oldest supported version
89
+ which understates a newer server rather than promising methods it lacks. There
90
+ are two ways to reach the rest, and they differ in more than syntax.
91
+
92
+ **Assert the surface** when you do not know the version but intend to write
93
+ against a particular one:
91
94
 
92
95
  ```typescript
93
96
  const client = await createTrueNasClient<ApiDirectoryV26_0_0>(opts);
94
97
  client.api.query('container.query'); // v26-only, reachable
95
98
  ```
96
99
 
97
- That is a claim about the server, not a guarantee the client you get is
98
- whichever version discovery found. Operations that must work across versions
99
- belong on `client.ops`, which resolves them at runtime.
100
+ Discovery still runs and still decides which client is built. The type argument
101
+ is a claim about the server, not a guarantee — the client you get is whichever
102
+ version discovery found, so a wrong claim fails at runtime.
103
+
104
+ **State the version** when you already know it — a UI served by the appliance,
105
+ a harness against a pinned image:
106
+
107
+ ```typescript
108
+ const client = await createTrueNasClient({
109
+ uuid, hostnames, enabled: true, version: 'v27.0.0',
110
+ });
111
+ client.api.query('container.query'); // typed v27, derived from the string
112
+ ```
113
+
114
+ This skips discovery entirely: no `GET /api/versions`, no CORS fallback. The
115
+ surface is *derived* rather than asserted, so there is no type argument to get
116
+ wrong, and a version the package ships no types for does not compile.
117
+
118
+ It is the stronger claim of the two, because the version also selects the
119
+ websocket path. Naming `v27.0.0` at a v26 appliance connects on `/api/v27.0.0`
120
+ with v27 types over a v26 server, and discovery cannot correct it — declining
121
+ discovery is the point.
122
+
123
+ The derivation needs the version to be literal at the call site. Passing a type
124
+ argument as well, forwarding `version` through a wrapper, or annotating the
125
+ options object as `CreateClientOptions` all compile, all connect to the version
126
+ you named, and all type as the default surface instead. That
127
+ errs safely — understated types fail at the method call, not at runtime — but
128
+ silently, so keep the literal where the call is.
129
+
130
+ Compatibility is still checked, and two kinds of refusal reach a caller. A string that
131
+ is not a supported version — reachable only from JavaScript — throws a plain
132
+ `Error` naming the ones that are. A supported version this build has no client
133
+ for throws `VersionTooNewError`, the same type discovery raises; that happens
134
+ when types have been generated for a release before its client was written.
135
+ There is no `VersionTooOldError` here, because the oldest version you can name
136
+ is the oldest one supported.
137
+
138
+ Operations that must work across versions belong on `client.ops`. On the
139
+ discovery route that resolves against whatever the appliance turned out to be.
140
+ On the named route it cannot: the client class is picked from the version you
141
+ stated, so `ops` is that version's mappings whether or not the server agrees.
100
142
 
101
143
  ## Documentation
102
144
 
package/dist/index.cjs CHANGED
@@ -3644,12 +3644,40 @@ async function createTrueNasClient(opts) {
3644
3644
  `Cannot create client for system ${uuid}: hostnames array is empty`
3645
3645
  );
3646
3646
  }
3647
- const versionDiscovery = new VersionDiscovery(logger);
3648
3647
  logger.info("Creating versioned API client", {
3649
3648
  uuid: uuid.slice(0, 8),
3650
3649
  hostnames: hostnames.join(", "),
3651
3650
  systemName
3652
3651
  });
3652
+ if (opts.version !== void 0) {
3653
+ if (!SUPPORTED_API_VERSIONS.includes(opts.version)) {
3654
+ throw new Error(
3655
+ `Cannot create client for system ${uuid}: '${opts.version}' is not a version this package ships types for. Supported: ${SUPPORTED_API_VERSIONS.join(", ")}.`
3656
+ );
3657
+ }
3658
+ const known = parseApiVersion(opts.version);
3659
+ if (!known) {
3660
+ throw new Error(
3661
+ `Cannot create client for system ${uuid}: supported version '${opts.version}' failed to parse.`
3662
+ );
3663
+ }
3664
+ const compatibility = checkVersionCompatibility(known);
3665
+ if (compatibility === "too-new" /* TooNew */) {
3666
+ throw new VersionTooNewError(hostnames[0], [known.version]);
3667
+ }
3668
+ if (compatibility !== "compatible" /* Compatible */) {
3669
+ throw new Error(
3670
+ `Cannot create client for system ${uuid}: the supported version range is not usable (${apiVersionConfig.MIN_SUPPORTED_VERSION}..${apiVersionConfig.MAX_SUPPORTED_VERSION}).`
3671
+ );
3672
+ }
3673
+ logger.info("API version supplied by the caller, skipping discovery", {
3674
+ uuid: uuid.slice(0, 8),
3675
+ version: known.version,
3676
+ websocketPath: known.websocketPath
3677
+ });
3678
+ return instantiateClientForVersion(known, opts, logger);
3679
+ }
3680
+ const versionDiscovery = new VersionDiscovery(logger);
3653
3681
  let version;
3654
3682
  try {
3655
3683
  const winner = await discoverVersionFromAnyHostname(