@zocomputer/system-api 0.0.0-stage → 0.1.0

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.
Files changed (57) hide show
  1. package/README.md +51 -2
  2. package/dist/catalog/index.d.ts +106 -0
  3. package/dist/catalog/index.js +237 -0
  4. package/dist/core/catalog.d.ts +155 -0
  5. package/dist/core/catalog.js +24 -0
  6. package/dist/core/client.d.ts +156 -0
  7. package/dist/core/client.js +87 -0
  8. package/dist/core/database.d.ts +124 -0
  9. package/dist/core/database.js +314 -0
  10. package/dist/core/envelope.d.ts +45 -0
  11. package/dist/core/envelope.js +112 -0
  12. package/dist/core/errors.d.ts +52 -0
  13. package/dist/core/errors.js +33 -0
  14. package/dist/core/index.d.ts +14 -0
  15. package/dist/core/index.js +13 -0
  16. package/dist/core/json-call.d.ts +4 -0
  17. package/dist/core/json-call.js +109 -0
  18. package/dist/core/json.d.ts +17 -0
  19. package/dist/core/json.js +4 -0
  20. package/dist/core/transport.d.ts +47 -0
  21. package/dist/core/transport.js +367 -0
  22. package/dist/core/validator.d.ts +52 -0
  23. package/dist/core/validator.js +1 -0
  24. package/dist/generate/cli.d.ts +44 -0
  25. package/dist/generate/cli.js +358 -0
  26. package/dist/generate/generate.d.ts +46 -0
  27. package/dist/generate/generate.js +215 -0
  28. package/dist/generate/identifiers.d.ts +27 -0
  29. package/dist/generate/identifiers.js +67 -0
  30. package/dist/generate/index.d.ts +15 -0
  31. package/dist/generate/index.js +15 -0
  32. package/dist/generate/manifest.d.ts +1 -0
  33. package/dist/generate/manifest.js +1 -0
  34. package/dist/generate/types.d.ts +57 -0
  35. package/dist/generate/types.js +357 -0
  36. package/dist/generated/catalog.d.ts +2 -0
  37. package/dist/generated/catalog.js +842 -0
  38. package/dist/generated/index.d.ts +2 -0
  39. package/dist/generated/index.js +4 -0
  40. package/dist/generated/manifest.json +789 -0
  41. package/dist/generated/operations.d.ts +8407 -0
  42. package/dist/generated/operations.js +3 -0
  43. package/dist/generated/schema.json +39395 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +11 -0
  46. package/dist/json/index.d.ts +81 -0
  47. package/dist/json/index.js +404 -0
  48. package/dist/validation/index.d.ts +19 -0
  49. package/dist/validation/index.js +149 -0
  50. package/dist/validation/json.d.ts +1 -0
  51. package/dist/validation/json.js +1 -0
  52. package/dist/validation/pattern.d.ts +56 -0
  53. package/dist/validation/pattern.js +382 -0
  54. package/dist/validation/schema.d.ts +103 -0
  55. package/dist/validation/schema.js +1300 -0
  56. package/openapi.json +40698 -0
  57. package/package.json +93 -4
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The typed invocation client.
3
+ *
4
+ * `call` is correlated by action and version: the action comes from the first
5
+ * argument and the version from the (optional) options, never from the input.
6
+ * The input is checked for an exact match against that action/version's
7
+ * contract — a foreign action's input, a wrong version's input, or an excess
8
+ * property is a compile error — and a union-valued action or version is
9
+ * rejected so a single call always resolves one concrete contract. Omitting
10
+ * options uses the action's declared default-version pin.
11
+ */
12
+ import type { DefaultVersionsShape, GuaranteedPinKeys, OperationsShape, PinnedVersion, RuntimeCatalog, TypedCatalog, TypedCatalogBase } from "./catalog.ts";
13
+ import type { JsonObject } from "./json.ts";
14
+ import { type AccessTokenProvider, type FetchLike } from "./transport.ts";
15
+ import type { InvocationValidator } from "./validator.ts";
16
+ export interface ClientConfig<C> {
17
+ /** The generated (or fixture) typed catalog. Supplies default-version pins. */
18
+ readonly catalog: C;
19
+ /** Application-provided bearer token source; the SDK never obtains tokens itself. */
20
+ readonly accessToken: AccessTokenProvider;
21
+ /** Override the default `https://api.zo.computer/zo-system-api/v1` base URL. */
22
+ readonly baseUrl?: string;
23
+ /** Select a workspace covered by a multi-workspace access token. */
24
+ readonly workspaceId?: string;
25
+ /** Inject a `fetch` implementation (tests, non-browser runtimes). */
26
+ readonly fetch?: FetchLike;
27
+ /**
28
+ * Optional structural validator. When present, its hooks validate the exact
29
+ * request/response bytes; when absent, the same client runs with no validation
30
+ * and bundles nothing from a validator. Typing (above) is orthogonal to this.
31
+ */
32
+ readonly validator?: InvocationValidator;
33
+ }
34
+ /** True iff `T` is a union type (used to reject union-valued action/version). */
35
+ type IsUnion<T, B = T> = T extends B ? ([B] extends [T] ? false : true) : never;
36
+ /** `Shape`, but any key present on `I` and absent from `Shape` is rejected. */
37
+ type Exact<I, Shape> = Shape & Record<Exclude<keyof I, keyof Shape>, never>;
38
+ /** The Zo built-in whose envelope may carry a caller idempotency key. */
39
+ declare const IDEMPOTENT_ACTION = "zo.function.invoke";
40
+ /**
41
+ * Zo built-ins other than `zo.function.invoke` reject keys. Custom action
42
+ * names may carry one; the server checks whether their binding is a run.
43
+ */
44
+ type KeyOption<A> = A extends string ? A extends `zo.${string}` ? A extends typeof IDEMPOTENT_ACTION ? {
45
+ idempotencyKey?: string;
46
+ } : {
47
+ idempotencyKey?: never;
48
+ } : {
49
+ idempotencyKey?: string;
50
+ } : {
51
+ idempotencyKey?: never;
52
+ };
53
+ /**
54
+ * A version that declares list query options takes `query` of its declared
55
+ * type; any other version takes none.
56
+ */
57
+ type QueryOption<E> = E extends {
58
+ readonly query: infer Q;
59
+ } ? {
60
+ query?: Q;
61
+ } : {
62
+ query?: never;
63
+ };
64
+ /** When `T` is a union, demand an impossible property so the call fails. */
65
+ type SingleOnly<T, Tag extends string> = IsUnion<T> extends true ? Record<Tag, never> : unknown;
66
+ export interface TypedClient<Ops extends OperationsShape, Defaults extends DefaultVersionsShape<Ops>> {
67
+ /** Invoke `action` at an explicit `version`. Input must match that version exactly. */
68
+ call<A extends keyof Ops, V extends keyof Ops[A] & number, I>(action: A, input: I & Exact<I, Ops[A][V]["input"]> & SingleOnly<A, "action must be a single literal">, options: {
69
+ version: V;
70
+ } & KeyOption<A> & QueryOption<Ops[A][V]> & SingleOnly<V, "version must be a single literal">): Promise<Ops[A][V]["output"]>;
71
+ /**
72
+ * Invoke `action` at its default-version pin (options omitted). Available only
73
+ * for an action with a *guaranteed* pin ({@link GuaranteedPinKeys}); a visible
74
+ * but unpinned action is excluded here, so omitting the version for it is a
75
+ * compile error and the explicit-version overload above must be used. A
76
+ * widened/optional `Defaults` exposes no default calls at all.
77
+ */
78
+ call<A extends GuaranteedPinKeys<Defaults> & keyof Ops, I>(action: A, input: I & Exact<I, Ops[A][PinnedVersion<Ops, A, Defaults>]["input"]> & SingleOnly<A, "action must be a single literal">, options?: KeyOption<A> & QueryOption<Ops[A][PinnedVersion<Ops, A, Defaults>]>): Promise<Ops[A][PinnedVersion<Ops, A, Defaults>]["output"]>;
79
+ }
80
+ /** Version, idempotency key and exact query JSON for a runtime invocation. */
81
+ export interface JsonCallOptions {
82
+ readonly version?: number;
83
+ readonly idempotencyKey?: string;
84
+ readonly queryJson?: string;
85
+ }
86
+ /** A correlated response with exact JSON text and request metadata. */
87
+ export interface JsonCallResult {
88
+ readonly envelopeJson: string;
89
+ readonly resultJson: string;
90
+ readonly requestId: string;
91
+ readonly action: string;
92
+ readonly version: number;
93
+ /** The query the server applied, present exactly when a query was sent. */
94
+ readonly queryJson?: string;
95
+ readonly httpStatus: number;
96
+ }
97
+ /**
98
+ * The untyped invocation client, built from a server-loaded `RuntimeCatalog`.
99
+ * The wire behavior is identical to `TypedClient` — same transport, envelope,
100
+ * and digest-bound target — but with no generated types the action is an
101
+ * arbitrary string, the input is `unknown`, and the result is `unknown` (never
102
+ * `any`). This surface is reachable only through a runtime catalog; a typed
103
+ * client has no `call(string, unknown)` escape hatch.
104
+ */
105
+ export interface RuntimeClient {
106
+ /** Invoke exact JSON without converting input, query or result numbers to JS numbers. */
107
+ callJson(action: string, inputJson: string, options?: JsonCallOptions): Promise<JsonCallResult>;
108
+ /**
109
+ * Custom action keys are sent for server-side run eligibility; other Zo
110
+ * built-ins reject them before sending. `query` is sent as `options.query`;
111
+ * the server refuses it for an action without list query options.
112
+ */
113
+ call(action: string, input: unknown, options?: {
114
+ version?: number;
115
+ idempotencyKey?: string;
116
+ query?: JsonObject;
117
+ }): Promise<unknown>;
118
+ }
119
+ /**
120
+ * Every catalog `createClient` accepts: a generated typed one, or a runtime one.
121
+ * The typed member is the shallow {@link TypedCatalogBase}, not a fully
122
+ * pin-parameterized `TypedCatalog`, so a concrete typed catalog — pinned or with
123
+ * unpinned actions — is assignable to the accept-constraint; `createClient`'s
124
+ * conditional still infers `Ops`/`Defaults` from the concrete argument type.
125
+ */
126
+ type AnyCatalog = TypedCatalogBase | RuntimeCatalog;
127
+ /**
128
+ * The single public client constructor, with two ways to fix the call types.
129
+ *
130
+ * Inference form (no explicit type arguments): the catalog value's brand selects
131
+ * the client type — a `TypedCatalog` yields a `TypedClient` (compile-time-checked
132
+ * calls, typed results); a server-loaded `RuntimeCatalog` yields a `RuntimeClient`
133
+ * (string action, `unknown` result). There is one runtime — the untyped surface
134
+ * is not a catch-all overload that bypasses typing on a typed client.
135
+ *
136
+ * Explicit operation-map form (explicit type arguments): a consumer that already
137
+ * has an `Operations` map type — hand-written, or imported ahead of a generated
138
+ * catalog — binds it directly, `createClient<Ops, Defaults>(config)`, and gets a
139
+ * `TypedClient<Ops, Defaults>` regardless of the catalog value's own brand. The
140
+ * declared `Operations` are the caller's asserted contract; no authority is taken
141
+ * from the catalog value. Both type arguments are required: generics are erased,
142
+ * so they cannot invent the runtime default-version pins (`Defaults`) the way the
143
+ * inference form reads them off a typed catalog value — the widened
144
+ * `DefaultVersionsShape<Ops>` has no guaranteed pin for any action, so a default
145
+ * (options-omitted) call would resolve for none of them. The pins must be stated
146
+ * explicitly, so a consumer over a runtime catalog value passes both `Ops` and
147
+ * `Defaults`.
148
+ *
149
+ * The inference form is declared first, so overload resolution keeps the exact
150
+ * prior behavior when no type arguments are given; the explicit form is reachable
151
+ * only through explicit type arguments (its two-parameter arity does not match a
152
+ * bare or single-argument call against the inference form).
153
+ */
154
+ export declare function createClient<C extends AnyCatalog>(config: ClientConfig<C>): C extends TypedCatalog<infer Ops, infer Defaults> ? TypedClient<Ops, Defaults> : RuntimeClient;
155
+ export declare function createClient<Ops extends OperationsShape, Defaults extends DefaultVersionsShape<Ops>>(config: ClientConfig<TypedCatalog<Ops, Defaults> | RuntimeCatalog>): TypedClient<Ops, Defaults>;
156
+ export {};
@@ -0,0 +1,87 @@
1
+ import { SystemAPIError } from "./errors.js";
2
+ import { createTransport, } from "./transport.js";
3
+ /** The Zo built-in whose envelope may carry a caller idempotency key. */
4
+ const IDEMPOTENT_ACTION = "zo.function.invoke";
5
+ export function createClient(config) {
6
+ const transport = createTransport(config);
7
+ // One generic runtime body serves both surfaces; the overload signatures above
8
+ // are what distinguish the typed client from the untyped one.
9
+ const actions = config.catalog.actions;
10
+ function resolveTarget(action, options) {
11
+ const entry = actions[action];
12
+ if (entry === undefined) {
13
+ // Unreachable through the typed surface; guards a fixture/catalog mismatch.
14
+ throw new SystemAPIError({
15
+ code: "action_not_found",
16
+ message: `unknown action: ${action}`,
17
+ outcome: "not_started",
18
+ });
19
+ }
20
+ if (options?.idempotencyKey !== undefined &&
21
+ action.startsWith("zo.") &&
22
+ action !== IDEMPOTENT_ACTION) {
23
+ throw new SystemAPIError({
24
+ code: "invalid_envelope",
25
+ message: `action '${action}' does not accept an idempotency key`,
26
+ outcome: "not_started",
27
+ });
28
+ }
29
+ let version = options?.version;
30
+ if (version === undefined) {
31
+ version = entry.defaultVersion;
32
+ if (version === undefined)
33
+ // Reachable through the untyped surface or a fixture/catalog mismatch:
34
+ // an unpinned action has no default, so an omitted version is rejected
35
+ // before any request. The typed surface forbids this at compile time.
36
+ throw new SystemAPIError({
37
+ code: "version_required",
38
+ message: `action '${action}' has no default version; pass an explicit version`,
39
+ outcome: "not_started",
40
+ });
41
+ }
42
+ // Resolve the contract the client is committing to — action, concrete
43
+ // version, and that version's catalog digest — so an optional validator can
44
+ // bind its schema bundle to this exact pinned entry (digest agreement).
45
+ const versionEntry = entry.versions[version];
46
+ if (versionEntry === undefined) {
47
+ // Unreachable through the typed surface; guards a fixture/catalog mismatch.
48
+ throw new SystemAPIError({
49
+ code: "unsupported_version",
50
+ message: `unknown version ${version} for action: ${action}`,
51
+ outcome: "not_started",
52
+ });
53
+ }
54
+ return {
55
+ action,
56
+ version,
57
+ digest: versionEntry.digest,
58
+ };
59
+ }
60
+ async function call(action, input, options) {
61
+ const target = resolveTarget(action, options);
62
+ const envelope = {
63
+ action,
64
+ input: input,
65
+ options: {
66
+ version: target.version,
67
+ ...(options?.idempotencyKey !== undefined
68
+ ? { idempotency_key: options.idempotencyKey }
69
+ : {}),
70
+ // The server reads `query: null` as no query and echoes none, so
71
+ // null is left off like undefined.
72
+ ...(options?.query !== undefined && options.query !== null
73
+ ? { query: options.query }
74
+ : {}),
75
+ },
76
+ };
77
+ const success = await transport.invoke(envelope, target);
78
+ return success.result;
79
+ }
80
+ async function callJson(action, inputJson, options) {
81
+ const target = resolveTarget(action, options);
82
+ const { invokeJson } = await import("./json-call.js");
83
+ return invokeJson(transport, target, inputJson, options);
84
+ }
85
+ // The overloads expose callJson only for runtime catalogs.
86
+ return { call, callJson };
87
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Decoding of database SQL results by column type.
3
+ *
4
+ * `zo.database.query`, `zo.database.execute` and `zo.database.migrate` return
5
+ * values in the System API's value encoding: `NULL` as `null`, `boolean` as a
6
+ * JSON boolean, `smallint` and `integer` as numbers, arrays as nested JSON
7
+ * arrays, and every other type as the string Postgres prints under Zo's output
8
+ * settings (`DateStyle = ISO`, `IntervalStyle = iso_8601`, `TimeZone = UTC`,
9
+ * `bytea_output = hex`, `extra_float_digits = 1`). These functions turn those
10
+ * strings into JavaScript values by `columns[].type` without losing anything
11
+ * by default: a `Date` would drop microseconds and cannot hold every Postgres
12
+ * year, so it is opt-in. A call that changes those settings itself gets text
13
+ * the parsers here refuse; decode it with the `"string"` modes.
14
+ */
15
+ import type { JsonValue } from "./json.ts";
16
+ /** One entry of a SQL result's `columns`. */
17
+ export interface DatabaseColumn {
18
+ readonly name: string;
19
+ /** The type as `format_type` prints it, without modifiers: `integer`, `text[]`, `timestamp with time zone`. */
20
+ readonly type: string;
21
+ }
22
+ /** The part of one SQL result (an entry of `results`) that decoding reads. */
23
+ export interface DatabaseResult {
24
+ readonly columns: readonly DatabaseColumn[];
25
+ readonly rows: readonly (readonly unknown[])[];
26
+ }
27
+ export interface DatabaseDecodeOptions {
28
+ /**
29
+ * `"parse"` (the default) returns `JSON.parse`'s result for `json` and
30
+ * `jsonb`. `"string"` keeps Postgres's text, for documents holding numbers a
31
+ * JavaScript number cannot represent exactly, such as integers past 2^53.
32
+ */
33
+ readonly json?: "parse" | "string";
34
+ /**
35
+ * How `date`, `timestamp` and `timestamp with time zone` values decode.
36
+ * `"parse"` (the default) returns a `DatabaseDate` or `DatabaseTimestamp`.
37
+ * `"string"` keeps Postgres's text. `"date"` returns a `Date`, dropping
38
+ * microseconds, and throws `RangeError` for a value past a `Date`'s range
39
+ * (after the year 275760). `infinity` and `-infinity` stay those strings in
40
+ * every mode.
41
+ */
42
+ readonly timestamps?: "parse" | "string" | "date";
43
+ }
44
+ /** A decoded value. Which member a column holds follows from its type; see `decodeDatabaseValue`. */
45
+ export type DatabaseValue = JsonValue | bigint | Uint8Array | Date | DatabaseDate | DatabaseTimestamp | readonly DatabaseValue[];
46
+ export interface DatabaseDateFields {
47
+ /** ISO year: 1 BC is 0, 2 BC is -1. */
48
+ readonly year: number;
49
+ /** 1 to 12. */
50
+ readonly month: number;
51
+ /** 1 to 31. */
52
+ readonly day: number;
53
+ }
54
+ export interface DatabaseTimestampFields extends DatabaseDateFields {
55
+ readonly hour: number;
56
+ readonly minute: number;
57
+ readonly second: number;
58
+ /** 0 to 999999. */
59
+ readonly microsecond: number;
60
+ /**
61
+ * Seconds east of UTC for a `timestamp with time zone`, whose other fields
62
+ * are local time at that offset (always 0 under Zo's settings). `null` for a
63
+ * `timestamp`, which has no zone.
64
+ */
65
+ readonly offsetSeconds: number | null;
66
+ }
67
+ /** A finite Postgres `date`, with every year Postgres holds. */
68
+ export declare class DatabaseDate implements DatabaseDateFields {
69
+ readonly year: number;
70
+ readonly month: number;
71
+ readonly day: number;
72
+ constructor(fields: DatabaseDateFields);
73
+ /** UTC midnight of this date. Throws `RangeError` past a `Date`'s range. */
74
+ toDate(): Date;
75
+ /** The date as Postgres prints it under `DateStyle = ISO`: `2026-11-02`, `0044-03-15 BC`. */
76
+ toString(): string;
77
+ toJSON(): string;
78
+ }
79
+ /** A finite Postgres `timestamp` or `timestamp with time zone`, to the microsecond. */
80
+ export declare class DatabaseTimestamp implements DatabaseTimestampFields {
81
+ readonly year: number;
82
+ readonly month: number;
83
+ readonly day: number;
84
+ readonly hour: number;
85
+ readonly minute: number;
86
+ readonly second: number;
87
+ readonly microsecond: number;
88
+ readonly offsetSeconds: number | null;
89
+ constructor(fields: DatabaseTimestampFields);
90
+ /**
91
+ * The instant as a `Date`, dropping microseconds. A `timestamp` without a
92
+ * zone is read as UTC. Throws `RangeError` past a `Date`'s range.
93
+ */
94
+ toDate(): Date;
95
+ /** The timestamp as Postgres prints it under `DateStyle = ISO`: `2026-11-02 18:04:11.52+00`. */
96
+ toString(): string;
97
+ toJSON(): string;
98
+ }
99
+ /**
100
+ * Decode one value of a column whose `columns[].type` is `type`.
101
+ *
102
+ * | Type | Decoded |
103
+ * | --- | --- |
104
+ * | any `NULL` | `null` |
105
+ * | `boolean`, `smallint`, `integer` | as sent |
106
+ * | `bigint` | `bigint` |
107
+ * | `real`, `double precision` | `number`, including `NaN`, `Infinity` and `-0` |
108
+ * | `json`, `jsonb` | the parsed document, or its text with `json: "string"` |
109
+ * | `date` | `DatabaseDate`, `Date`, or text, by `timestamps` |
110
+ * | `timestamp without time zone`, `timestamp with time zone` | `DatabaseTimestamp`, `Date`, or text, by `timestamps` |
111
+ * | `bytea` | `Uint8Array` |
112
+ * | arrays (`text[]`) | arrays, nested as sent, with elements decoded by the element type |
113
+ * | everything else, including `numeric` | the string as sent |
114
+ *
115
+ * Throws `TypeError` when the value does not have the form its type's encoding
116
+ * gives, and `RangeError` when `timestamps: "date"` meets a value a `Date`
117
+ * cannot hold.
118
+ */
119
+ export declare function decodeDatabaseValue(type: string, value: unknown, options?: DatabaseDecodeOptions): DatabaseValue;
120
+ /**
121
+ * Decode every row of one SQL result by its columns' types. Rows stay arrays
122
+ * in column order, as sent, so duplicate column names survive.
123
+ */
124
+ export declare function decodeDatabaseRows(result: DatabaseResult, options?: DatabaseDecodeOptions): DatabaseValue[][];
@@ -0,0 +1,314 @@
1
+ /** A finite Postgres `date`, with every year Postgres holds. */
2
+ export class DatabaseDate {
3
+ year;
4
+ month;
5
+ day;
6
+ constructor(fields) {
7
+ checkDateFields(fields);
8
+ this.year = fields.year;
9
+ this.month = fields.month;
10
+ this.day = fields.day;
11
+ }
12
+ /** UTC midnight of this date. Throws `RangeError` past a `Date`'s range. */
13
+ toDate() {
14
+ return utcDate(this, 0, 0, 0, 0, 0, this);
15
+ }
16
+ /** The date as Postgres prints it under `DateStyle = ISO`: `2026-11-02`, `0044-03-15 BC`. */
17
+ toString() {
18
+ return `${formatYearDate(this)}${bcSuffix(this.year)}`;
19
+ }
20
+ toJSON() {
21
+ return this.toString();
22
+ }
23
+ }
24
+ /** A finite Postgres `timestamp` or `timestamp with time zone`, to the microsecond. */
25
+ export class DatabaseTimestamp {
26
+ year;
27
+ month;
28
+ day;
29
+ hour;
30
+ minute;
31
+ second;
32
+ microsecond;
33
+ offsetSeconds;
34
+ constructor(fields) {
35
+ checkDateFields(fields);
36
+ checkField("hour", fields.hour, 0, 23);
37
+ checkField("minute", fields.minute, 0, 59);
38
+ checkField("second", fields.second, 0, 59);
39
+ checkField("microsecond", fields.microsecond, 0, 999_999);
40
+ if (fields.offsetSeconds !== null)
41
+ checkField("offsetSeconds", fields.offsetSeconds, -MAX_OFFSET_SECONDS, MAX_OFFSET_SECONDS);
42
+ this.year = fields.year;
43
+ this.month = fields.month;
44
+ this.day = fields.day;
45
+ this.hour = fields.hour;
46
+ this.minute = fields.minute;
47
+ this.second = fields.second;
48
+ this.microsecond = fields.microsecond;
49
+ this.offsetSeconds = fields.offsetSeconds;
50
+ }
51
+ /**
52
+ * The instant as a `Date`, dropping microseconds. A `timestamp` without a
53
+ * zone is read as UTC. Throws `RangeError` past a `Date`'s range.
54
+ */
55
+ toDate() {
56
+ return utcDate(this, this.hour, this.minute, this.second, Math.floor(this.microsecond / 1000), this.offsetSeconds ?? 0, this);
57
+ }
58
+ /** The timestamp as Postgres prints it under `DateStyle = ISO`: `2026-11-02 18:04:11.52+00`. */
59
+ toString() {
60
+ const fraction = this.microsecond === 0
61
+ ? ""
62
+ : `.${String(this.microsecond).padStart(6, "0").replace(/0+$/, "")}`;
63
+ const offset = this.offsetSeconds === null ? "" : formatOffset(this.offsetSeconds);
64
+ return `${formatYearDate(this)} ${pad2(this.hour)}:${pad2(this.minute)}:${pad2(this.second)}${fraction}${offset}${bcSuffix(this.year)}`;
65
+ }
66
+ toJSON() {
67
+ return this.toString();
68
+ }
69
+ }
70
+ /**
71
+ * Decode one value of a column whose `columns[].type` is `type`.
72
+ *
73
+ * | Type | Decoded |
74
+ * | --- | --- |
75
+ * | any `NULL` | `null` |
76
+ * | `boolean`, `smallint`, `integer` | as sent |
77
+ * | `bigint` | `bigint` |
78
+ * | `real`, `double precision` | `number`, including `NaN`, `Infinity` and `-0` |
79
+ * | `json`, `jsonb` | the parsed document, or its text with `json: "string"` |
80
+ * | `date` | `DatabaseDate`, `Date`, or text, by `timestamps` |
81
+ * | `timestamp without time zone`, `timestamp with time zone` | `DatabaseTimestamp`, `Date`, or text, by `timestamps` |
82
+ * | `bytea` | `Uint8Array` |
83
+ * | arrays (`text[]`) | arrays, nested as sent, with elements decoded by the element type |
84
+ * | everything else, including `numeric` | the string as sent |
85
+ *
86
+ * Throws `TypeError` when the value does not have the form its type's encoding
87
+ * gives, and `RangeError` when `timestamps: "date"` meets a value a `Date`
88
+ * cannot hold.
89
+ */
90
+ export function decodeDatabaseValue(type, value, options = {}) {
91
+ const settings = resolveOptions(options);
92
+ try {
93
+ return decodeAny(type, value, settings);
94
+ }
95
+ catch (error) {
96
+ throw withContext(error, `Cannot decode a value of type ${type}`);
97
+ }
98
+ }
99
+ /**
100
+ * Decode every row of one SQL result by its columns' types. Rows stay arrays
101
+ * in column order, as sent, so duplicate column names survive.
102
+ */
103
+ export function decodeDatabaseRows(result, options = {}) {
104
+ const settings = resolveOptions(options);
105
+ const { columns } = result;
106
+ return result.rows.map((row, rowIndex) => {
107
+ if (row.length !== columns.length)
108
+ throw new TypeError(`Cannot decode row ${rowIndex}: it has ${row.length} values for ${columns.length} columns.`);
109
+ return columns.map((column, columnIndex) => {
110
+ try {
111
+ return decodeAny(column.type, row[columnIndex], settings);
112
+ }
113
+ catch (error) {
114
+ throw withContext(error, `Cannot decode row ${rowIndex}, column ${columnIndex} (${JSON.stringify(column.name)}, ${column.type})`);
115
+ }
116
+ });
117
+ });
118
+ }
119
+ const JSON_MODES = ["parse", "string"];
120
+ const TIMESTAMP_MODES = ["parse", "string", "date"];
121
+ /** Postgres refuses a UTC offset past 15:59:59. */
122
+ const MAX_OFFSET_SECONDS = 15 * 3600 + 59 * 60 + 59;
123
+ function resolveOptions(options) {
124
+ const json = options.json ?? "parse";
125
+ const timestamps = options.timestamps ?? "parse";
126
+ if (!JSON_MODES.includes(json))
127
+ throw new TypeError(`json must be "parse" or "string", not ${JSON.stringify(json)}.`);
128
+ if (!TIMESTAMP_MODES.includes(timestamps))
129
+ throw new TypeError(`timestamps must be "parse", "string" or "date", not ${JSON.stringify(timestamps)}.`);
130
+ return { json, timestamps };
131
+ }
132
+ function decodeAny(type, value, settings) {
133
+ if (value === null)
134
+ return null;
135
+ const element = type.replace(/(\[\])+$/, "");
136
+ if (element !== type)
137
+ return decodeArray(element, value, settings);
138
+ return decodeScalar(type, value, settings);
139
+ }
140
+ function decodeArray(element, value, settings) {
141
+ if (!Array.isArray(value))
142
+ throw new TypeError("expected an array");
143
+ return value.map((item) => {
144
+ if (item === null)
145
+ return null;
146
+ if (Array.isArray(item))
147
+ return decodeArray(element, item, settings);
148
+ return decodeScalar(element, item, settings);
149
+ });
150
+ }
151
+ function decodeScalar(type, value, settings) {
152
+ switch (type) {
153
+ case "boolean":
154
+ if (typeof value !== "boolean")
155
+ throw new TypeError("expected a boolean");
156
+ return value;
157
+ case "smallint":
158
+ case "integer":
159
+ if (typeof value !== "number" || !Number.isInteger(value))
160
+ throw new TypeError("expected an integer");
161
+ return value;
162
+ case "bigint":
163
+ return BigInt(match(value, BIGINT, "an integer string")[0]);
164
+ case "real":
165
+ case "double precision":
166
+ return Number(match(value, FLOAT, "a float string")[0]);
167
+ case "json":
168
+ case "jsonb":
169
+ return settings.json === "string" ? text(value) : parseJson(text(value));
170
+ case "bytea":
171
+ return decodeBytea(value);
172
+ case "date":
173
+ return decodeDate(value, settings);
174
+ case "timestamp without time zone":
175
+ return decodeTimestamp(value, settings, false);
176
+ case "timestamp with time zone":
177
+ return decodeTimestamp(value, settings, true);
178
+ default:
179
+ // `numeric` and every other type are sent as text. A domain is named
180
+ // by its base type, so only the cases above carry numbers or booleans.
181
+ return text(value);
182
+ }
183
+ }
184
+ const BIGINT = /^-?\d+$/;
185
+ // Exactly what float8out prints; Number() also reads "infinity" and "nan" as NaN.
186
+ const FLOAT = /^(?:-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|NaN|-?Infinity)$/;
187
+ const BYTEA = /^\\x(?<hex>(?:[0-9a-f]{2})*)$/i;
188
+ const DATE = /^(?<year>\d{4,})-(?<month>\d{2})-(?<day>\d{2})(?<bc> BC)?$/;
189
+ const TIMESTAMP = /^(?<year>\d{4,})-(?<month>\d{2})-(?<day>\d{2}) (?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.(?<fraction>\d{1,6}))?(?:(?<sign>[+-])(?<offsetHour>\d{2})(?::(?<offsetMinute>\d{2}))?(?::(?<offsetSecond>\d{2}))?)?(?<bc> BC)?$/;
190
+ function text(value) {
191
+ if (typeof value !== "string")
192
+ throw new TypeError("expected a string");
193
+ return value;
194
+ }
195
+ function match(value, pattern, expected) {
196
+ const found = pattern.exec(text(value));
197
+ if (!found)
198
+ throw new TypeError(`expected ${expected}`);
199
+ return found;
200
+ }
201
+ /** A matched pattern's named groups; an unmatched optional group is absent. */
202
+ function groups(pattern, source) {
203
+ return pattern.exec(source)?.groups;
204
+ }
205
+ function parseJson(source) {
206
+ try {
207
+ return JSON.parse(source);
208
+ }
209
+ catch (error) {
210
+ throw new TypeError("expected a JSON document", { cause: error });
211
+ }
212
+ }
213
+ function decodeBytea(value) {
214
+ const hex = match(value, BYTEA, 'hex output ("\\x…")').groups?.hex ?? "";
215
+ const bytes = new Uint8Array(hex.length / 2);
216
+ for (let i = 0; i < bytes.length; i++)
217
+ bytes[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
218
+ return bytes;
219
+ }
220
+ function isInfinity(value) {
221
+ return value === "infinity" || value === "-infinity";
222
+ }
223
+ function decodeDate(value, settings) {
224
+ const source = text(value);
225
+ if (settings.timestamps === "string" || isInfinity(source))
226
+ return source;
227
+ const parts = groups(DATE, source);
228
+ if (!parts)
229
+ throw new TypeError(NOT_ISO);
230
+ const { year = "", month, day, bc } = parts;
231
+ const date = new DatabaseDate({
232
+ year: isoYear(year, bc),
233
+ month: Number(month),
234
+ day: Number(day),
235
+ });
236
+ return settings.timestamps === "date" ? date.toDate() : date;
237
+ }
238
+ function decodeTimestamp(value, settings, zoned) {
239
+ const source = text(value);
240
+ if (settings.timestamps === "string" || isInfinity(source))
241
+ return source;
242
+ const parts = groups(TIMESTAMP, source);
243
+ if (!parts || (parts.sign !== undefined) !== zoned)
244
+ throw new TypeError(NOT_ISO);
245
+ const { year = "", month, day, hour, minute, second, fraction = "", sign, bc, } = parts;
246
+ let offsetSeconds = null;
247
+ if (sign !== undefined) {
248
+ const { offsetHour, offsetMinute = "0", offsetSecond = "0" } = parts;
249
+ const magnitude = Number(offsetHour) * 3600 +
250
+ Number(offsetMinute) * 60 +
251
+ Number(offsetSecond);
252
+ offsetSeconds = sign === "-" && magnitude !== 0 ? -magnitude : magnitude;
253
+ }
254
+ const timestamp = new DatabaseTimestamp({
255
+ year: isoYear(year, bc),
256
+ month: Number(month),
257
+ day: Number(day),
258
+ hour: Number(hour),
259
+ minute: Number(minute),
260
+ second: Number(second),
261
+ microsecond: Number(fraction.padEnd(6, "0")),
262
+ offsetSeconds,
263
+ });
264
+ return settings.timestamps === "date" ? timestamp.toDate() : timestamp;
265
+ }
266
+ const NOT_ISO = 'expected Postgres ISO output; if the call changed DateStyle or TimeZone, decode with timestamps: "string"';
267
+ /** Postgres prints 1 BC as `0001 BC`; ISO numbers it 0. */
268
+ function isoYear(digits, bc) {
269
+ const year = Number(digits);
270
+ return bc === undefined ? year : 1 - year;
271
+ }
272
+ function utcDate(date, hour, minute, second, millisecond, offsetSeconds, source) {
273
+ // Date.UTC maps years 0 to 99 onto 1900 to 1999, so set the year separately.
274
+ const local = new Date(0);
275
+ local.setUTCFullYear(date.year, date.month - 1, date.day);
276
+ local.setUTCHours(hour, minute, second, millisecond);
277
+ const instant = new Date(local.getTime() - offsetSeconds * 1000);
278
+ if (Number.isNaN(instant.getTime()))
279
+ throw new RangeError(`${source} is outside the range of a Date`);
280
+ return instant;
281
+ }
282
+ function checkDateFields(fields) {
283
+ checkField("year", fields.year, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
284
+ checkField("month", fields.month, 1, 12);
285
+ checkField("day", fields.day, 1, 31);
286
+ }
287
+ function checkField(name, value, min, max) {
288
+ if (!Number.isInteger(value) || value < min || value > max)
289
+ throw new RangeError(`${name} must be an integer from ${min} to ${max}, not ${value}.`);
290
+ }
291
+ function pad2(value) {
292
+ return String(value).padStart(2, "0");
293
+ }
294
+ function formatYearDate(date) {
295
+ const year = date.year > 0 ? date.year : 1 - date.year;
296
+ return `${String(year).padStart(4, "0")}-${pad2(date.month)}-${pad2(date.day)}`;
297
+ }
298
+ function bcSuffix(year) {
299
+ return year > 0 ? "" : " BC";
300
+ }
301
+ function formatOffset(offsetSeconds) {
302
+ const sign = offsetSeconds < 0 ? "-" : "+";
303
+ const total = Math.abs(offsetSeconds);
304
+ const hours = Math.floor(total / 3600);
305
+ const minutes = Math.floor((total % 3600) / 60);
306
+ const seconds = total % 60;
307
+ return `${sign}${pad2(hours)}${minutes || seconds ? `:${pad2(minutes)}` : ""}${seconds ? `:${pad2(seconds)}` : ""}`;
308
+ }
309
+ function withContext(error, context) {
310
+ if (!(error instanceof Error))
311
+ return new TypeError(context, { cause: error });
312
+ const ErrorType = error instanceof RangeError ? RangeError : TypeError;
313
+ return new ErrorType(`${context}: ${error.message}`, { cause: error });
314
+ }