@truenas/api-client 3.0.1 → 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
@@ -3150,7 +3150,29 @@ var TrueNasApiClientV2510 = class extends TrueNasApiClient {
3150
3150
  containerQuery: () => this.api.query("virt.instance.query", [["type", "=", "CONTAINER"]]).pipe(rxjs.map((instances) => instances.map(toContainer))),
3151
3151
  containerStart: (id) => this.api.job("virt.instance.start", [id]),
3152
3152
  containerStop: (id, options) => this.api.job("virt.instance.stop", [id, options]),
3153
- containerRestart: (id, options) => this.api.job("virt.instance.restart", [id, options])
3153
+ containerRestart: (id, options) => this.api.job("virt.instance.restart", [id, options]),
3154
+ // Already a job here — `virt.instance.delete` has been one since
3155
+ // v25.10.0 — so this needs no synthesis, only the id. It takes nothing
3156
+ // else: there is no `force` and no `recursive` on this version.
3157
+ //
3158
+ // Unsupported options are reported rather than dropped. `recursive`
3159
+ // destroys child datasets, snapshots and clones irrecoverably, so a
3160
+ // caller who asked for it and silently did not get it has been told
3161
+ // something false about what just happened to their data. Reporting is
3162
+ // all this layer can do — refusing outright would make `ops.containerDelete`
3163
+ // unusable on v25.10 for the ordinary case, which is the case that works.
3164
+ containerDelete: (id, options) => {
3165
+ const unsupported = ["force", "recursive"].filter(
3166
+ (key) => options?.[key]
3167
+ );
3168
+ if (unsupported.length > 0) {
3169
+ this.logger.warn(
3170
+ "containerDelete: v25.10 has no counterpart for these options and will delete without them",
3171
+ { ignored: unsupported, id, method: "virt.instance.delete" }
3172
+ );
3173
+ }
3174
+ return this.api.job("virt.instance.delete", [id]);
3175
+ }
3154
3176
  };
3155
3177
  }
3156
3178
  };
@@ -3218,7 +3240,26 @@ var TrueNasApiClientV26 = class extends TrueNasApiClient {
3218
3240
  )
3219
3241
  )
3220
3242
  );
3221
- }
3243
+ },
3244
+ // A job since v26.0.0 — middleware made deletion long-running (it stops
3245
+ // the container when asked, tears down the libvirt domain and destroys
3246
+ // the dataset), and the generated directory moved it out of `call`
3247
+ // accordingly. `api.job` is what tracks it; `api.call` would not compile.
3248
+ //
3249
+ // Options pass straight through when given: the unified
3250
+ // `ContainerDeleteOptions` is `force`/`recursive`, exactly what the
3251
+ // generated params take.
3252
+ //
3253
+ // When they are not given the argument is *omitted* rather than passed as
3254
+ // `undefined`. `JSON.stringify` renders a trailing `undefined` array
3255
+ // element as `null`, and middleware declares `options: ContainerDeleteOptions`
3256
+ // with a model default and no `| None` — so `[id, null]` is a validation
3257
+ // error rather than "use the defaults", which is the one thing a caller
3258
+ // passing nothing is asking for.
3259
+ containerDelete: (id, options) => this.api.job(
3260
+ "container.delete",
3261
+ options ? [parseInt(id, 10), options] : [parseInt(id, 10)]
3262
+ )
3222
3263
  };
3223
3264
  }
3224
3265
  };
@@ -3280,7 +3321,26 @@ var TrueNasApiClientV27 = class extends TrueNasApiClient {
3280
3321
  )
3281
3322
  )
3282
3323
  );
3283
- }
3324
+ },
3325
+ // A job since v26.0.0 — middleware made deletion long-running (it stops
3326
+ // the container when asked, tears down the libvirt domain and destroys
3327
+ // the dataset), and the generated directory moved it out of `call`
3328
+ // accordingly. `api.job` is what tracks it; `api.call` would not compile.
3329
+ //
3330
+ // Options pass straight through when given: the unified
3331
+ // `ContainerDeleteOptions` is `force`/`recursive`, exactly what the
3332
+ // generated params take.
3333
+ //
3334
+ // When they are not given the argument is *omitted* rather than passed as
3335
+ // `undefined`. `JSON.stringify` renders a trailing `undefined` array
3336
+ // element as `null`, and middleware declares `options: ContainerDeleteOptions`
3337
+ // with a model default and no `| None` — so `[id, null]` is a validation
3338
+ // error rather than "use the defaults", which is the one thing a caller
3339
+ // passing nothing is asking for.
3340
+ containerDelete: (id, options) => this.api.job(
3341
+ "container.delete",
3342
+ options ? [parseInt(id, 10), options] : [parseInt(id, 10)]
3343
+ )
3284
3344
  };
3285
3345
  }
3286
3346
  };
@@ -3584,12 +3644,40 @@ async function createTrueNasClient(opts) {
3584
3644
  `Cannot create client for system ${uuid}: hostnames array is empty`
3585
3645
  );
3586
3646
  }
3587
- const versionDiscovery = new VersionDiscovery(logger);
3588
3647
  logger.info("Creating versioned API client", {
3589
3648
  uuid: uuid.slice(0, 8),
3590
3649
  hostnames: hostnames.join(", "),
3591
3650
  systemName
3592
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);
3593
3681
  let version;
3594
3682
  try {
3595
3683
  const winner = await discoverVersionFromAnyHostname(