@tablecraft-app/plugin-api 1.1.0 → 1.3.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.
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Failures the host tells apart; anything else a plugin throws is reported as `statementFailed`. */
2
- export declare const PLUGIN_ERROR_CODES: readonly ["cancelled", "connectionFailed", "statementFailed", "introspectionFailed", "definitionNotFound", "explainUnsupported", "connectionLost", "sessionBusy"];
2
+ export declare const PLUGIN_ERROR_CODES: readonly ["cancelled", "connectionFailed", "statementFailed", "introspectionFailed", "definitionNotFound", "explainUnsupported", "connectionLost", "sessionBusy", "conflict"];
3
3
  export type PluginErrorCode = (typeof PLUGIN_ERROR_CODES)[number];
4
4
  /**
5
5
  * Where the database located a failure, relative to the statement text the plugin received:
@@ -46,6 +46,15 @@ export declare class TlsSettingsError extends PluginError {
46
46
  }
47
47
  /** Structural for the same reason as `isPluginError`. */
48
48
  export declare function isTlsSettingsError(error: unknown): error is TlsSettingsError;
49
+ /**
50
+ * A guarded `Keyspace.apply` found `target` changed since it was read (decision 115): `detail`
51
+ * says what differs, such as `field name is now "Bo"`. Nothing was written.
52
+ */
53
+ export declare class ConflictError extends PluginError {
54
+ readonly target: string;
55
+ readonly detail: string;
56
+ constructor(target: string, detail: string, options?: ErrorOptions);
57
+ }
49
58
  /**
50
59
  * `Introspector.definition` asked for a relation the database does not have, usually one dropped
51
60
  * or renamed since the catalog was read (decision 029). Coded `definitionNotFound`, so the host
package/dist/errors.js CHANGED
@@ -18,6 +18,11 @@ export const PLUGIN_ERROR_CODES = [
18
18
  * reconnecting; never a message for the user.
19
19
  */
20
20
  "sessionBusy",
21
+ /**
22
+ * A guarded write found what it was to change already changed by someone else, and wrote
23
+ * nothing (decision 115); the host offers to reload or to overwrite.
24
+ */
25
+ "conflict",
21
26
  ];
22
27
  /** What a plugin throws when the host should react to the kind of failure, not just show it. */
23
28
  export class PluginError extends Error {
@@ -90,6 +95,19 @@ export class TlsSettingsError extends PluginError {
90
95
  export function isTlsSettingsError(error) {
91
96
  return isPluginError(error) && error.tlsSettings === true;
92
97
  }
98
+ /**
99
+ * A guarded `Keyspace.apply` found `target` changed since it was read (decision 115): `detail`
100
+ * says what differs, such as `field name is now "Bo"`. Nothing was written.
101
+ */
102
+ export class ConflictError extends PluginError {
103
+ target;
104
+ detail;
105
+ constructor(target, detail, options) {
106
+ super("conflict", `${target} changed since it was read: ${detail}. Reload it, or overwrite the change.`, options);
107
+ this.target = target;
108
+ this.detail = detail;
109
+ }
110
+ }
93
111
  /**
94
112
  * `Introspector.definition` asked for a relation the database does not have, usually one dropped
95
113
  * or renamed since the catalog was read (decision 029). Coded `definitionNotFound`, so the host
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  export { byName, type ColumnMeta, type ForeignKey, type Index, type Introspector, type Key, type Namespace, type NamespaceCatalog, type Relation, type RelationKind, ROUTINE_PARAMETER_MODES, type Routine, type RoutineParameter, type RoutineParameterMode, type Sequence, } from "./catalog.js";
2
2
  export { COMPLETION_KINDS, type CompletionCatalog, type CompletionItem, type CompletionKind, type CompletionRequest, type CompletionResult, type CompletionScope, type CompletionServer, type Declaration, type FunctionParameter, type FunctionSignature, type IncomingForeignKey, type ResolveRequest, type SignatureHelp, } from "./completion.js";
3
3
  export { INSPECTION_IDS, type InspectionId, type InspectionRequest, type Problem, PROBLEM_SEVERITIES, type ProblemSeverity, type QuickFix, type TextEdit, } from "./inspection.js";
4
- export { DefinitionNotFoundError, type ErrorPosition, errorPositionOf, isPluginError, isTlsSettingsError, offsetOfPosition, PLUGIN_ERROR_CODES, PluginError, type PluginErrorCode, type PluginErrorOptions, TlsSettingsError, } from "./errors.js";
4
+ export { ConflictError, DefinitionNotFoundError, type ErrorPosition, errorPositionOf, isPluginError, isTlsSettingsError, offsetOfPosition, PLUGIN_ERROR_CODES, PluginError, type PluginErrorCode, type PluginErrorOptions, TlsSettingsError, } from "./errors.js";
5
5
  export { type ExplainedPlan, type Explainability, type ExplainOptions, type PlanNode, type PlanProperty, } from "./explain.js";
6
+ export { KEY_TYPES, type KeyApplied, type KeyApplyOptions, type KeyBytes, type KeyChange, type KeyContents, type KeyDatabase, type KeyDetails, type KeyExpiry, type KeyInfo, type KeyName, type KeyReadRequest, type KeyScan, type KeyScanRequest, type KeySource, type Keyspace, type KeyType, } from "./keyspace.js";
6
7
  export { OBJECT_KINDS, type CatalogObject, type CheckDesign, type ColumnDesign, type DdlContext, type DdlScript, type ForeignKeyDesign, type IndexDesign, type ObjectChange, type ObjectChangeKind, type ObjectEditor, type ObjectFeatures, type ObjectKind, type TableDesign, } from "./objects.js";
7
8
  export { CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, type ConnectionField, type ConnectionFieldGroup, type ConnectionFieldKind, type ConnectionUrl, type ConnectionUrlForm, type ConnectionUrlPart, type NetworkSpec, type PluginManifest, type TlsFieldRole, type TlsSpec, } from "./manifest.js";
8
9
  export { ConnectionUrlError, formatConnectionUrl, parseConnectionUrl, type ConnectionForm, type ConnectionUrlSpec, type ConnectionValue, type ParsedConnectionUrl, } from "./url.js";
9
- export { type BoundStatement, CAPABILITIES, type Capability, type Column, type ColumnOrigin, type ConnectOptions, type DatasourcePlugin, type ExecuteOptions, type ExecuteResult, type FormatOptions, FORMAT_STYLES, type FormatStyle, GENERATED_STATEMENTS, type GeneratedStatement, IMPORT_KINDS, type ImportKind, type InferredColumn, type InsertRow, type InsertTarget, KEYWORD_CASES, type NamespaceSwitch, switchableTo, type NewColumn, type KeywordCase, PARAMETER_STYLES, type ParameterStyle, type ParameterValue, type RoutineArgument, type RoutineCall, type Row, type SavepointStatements, type SelectSource, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, type ServerMessage, type ServerMessageLevel, type SqlParameter, type Session, type SqlLanguage, STATEMENT_KINDS, type StatementClass, type StatementKind, type StatementSpan, type StatementStream, type StreamedStatement, type TlsInfo, TRANSACTION_MODES, type TransactionMode, type Transactions, } from "./plugin.js";
10
+ export { type BoundStatement, CAPABILITIES, type Capability, type Column, type ColumnOrigin, type CommandLanguage, type CommandReply, type ConnectOptions, type DatasourcePlugin, type ExecuteOptions, type ExecuteResult, type FormatOptions, FORMAT_STYLES, type FormatStyle, GENERATED_STATEMENTS, type GeneratedStatement, IMPORT_KINDS, type ImportKind, type InferredColumn, type InsertRow, type InsertTarget, KEYWORD_CASES, type NamespaceSwitch, NODE_ALL_PRIMARIES, NODE_AUTO, NODE_ROLES, type NodeRole, type ServerInfo, type ServerInfoSection, type ServerNode, type SlowCommand, switchableTo, type NewColumn, type KeywordCase, PARAMETER_STYLES, type ParameterStyle, type ParameterValue, type RoutineArgument, type RoutineCall, type Row, type SavepointStatements, type SelectSource, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, type ServerMessage, type ServerMessageLevel, type SqlParameter, type Session, type SqlLanguage, STATEMENT_KINDS, type StatementClass, type StatementKind, type StatementSpan, type StatementStream, type StreamedStatement, type TlsInfo, TRANSACTION_MODES, type TransactionMode, type Transactions, } from "./plugin.js";
10
11
  export { SERVER_SESSION_STATES, type ServerSession, type ServerSessionState, type SessionListing, type SessionMonitor, type SessionTarget, } from "./sessions.js";
11
12
  export { TLS_CHAIN_CODES, TLS_HOST_CODE, homeExpanded, tlsFailureMessage, tlsFileMessage, tlsKeyIsEncrypted, tlsKeyMessage, type TlsVerification, } from "./tls.js";
12
13
  export { BYTES_PREVIEW_LENGTH, toBytesValue, toWireInteger, toWireValue, type WireValueOptions, type BigIntValue, type BytesValue, type DateValue, type JsonValue, type TypeClass, type WireValue, } from "./values.js";
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  export { byName, ROUTINE_PARAMETER_MODES, } from "./catalog.js";
2
2
  export { COMPLETION_KINDS, } from "./completion.js";
3
3
  export { INSPECTION_IDS, PROBLEM_SEVERITIES, } from "./inspection.js";
4
- export { DefinitionNotFoundError, errorPositionOf, isPluginError, isTlsSettingsError, offsetOfPosition, PLUGIN_ERROR_CODES, PluginError, TlsSettingsError, } from "./errors.js";
4
+ export { ConflictError, DefinitionNotFoundError, errorPositionOf, isPluginError, isTlsSettingsError, offsetOfPosition, PLUGIN_ERROR_CODES, PluginError, TlsSettingsError, } from "./errors.js";
5
5
  export {} from "./explain.js";
6
+ export { KEY_TYPES, } from "./keyspace.js";
6
7
  export { OBJECT_KINDS, } from "./objects.js";
7
8
  export { CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, } from "./manifest.js";
8
9
  export { ConnectionUrlError, formatConnectionUrl, parseConnectionUrl, } from "./url.js";
9
- export { CAPABILITIES, FORMAT_STYLES, GENERATED_STATEMENTS, IMPORT_KINDS, KEYWORD_CASES, switchableTo, PARAMETER_STYLES, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, STATEMENT_KINDS, TRANSACTION_MODES, } from "./plugin.js";
10
+ export { CAPABILITIES, FORMAT_STYLES, GENERATED_STATEMENTS, IMPORT_KINDS, KEYWORD_CASES, NODE_ALL_PRIMARIES, NODE_AUTO, NODE_ROLES, switchableTo, PARAMETER_STYLES, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, STATEMENT_KINDS, TRANSACTION_MODES, } from "./plugin.js";
10
11
  export { SERVER_SESSION_STATES, } from "./sessions.js";
11
12
  export { TLS_CHAIN_CODES, TLS_HOST_CODE, homeExpanded, tlsFailureMessage, tlsFileMessage, tlsKeyIsEncrypted, tlsKeyMessage, } from "./tls.js";
12
13
  export { BYTES_PREVIEW_LENGTH, toBytesValue, toWireInteger, toWireValue, } from "./values.js";
@@ -0,0 +1,192 @@
1
+ import type { ExecuteResult } from "./plugin.js";
2
+ import type { WireValue } from "./values.js";
3
+ /** The key types the key tree tells apart (decision 113); a module type such as RedisJSON is `other`. */
4
+ export declare const KEY_TYPES: readonly ["string", "hash", "list", "set", "zset", "stream", "json", "other"];
5
+ export type KeyType = (typeof KEY_TYPES)[number];
6
+ /**
7
+ * Bytes that are usually text, such as a key name, a hash field or a value (decision 113). `text`
8
+ * is the UTF-8 reading with each invalid byte written `\xNN`, as `redis-cli` prints it; `hex` is
9
+ * every byte in lower-case hex when the bytes are not valid UTF-8, and null when they are. Sent
10
+ * back to the plugin, `hex` wins when present.
11
+ */
12
+ export interface KeyBytes {
13
+ readonly text: string;
14
+ readonly hex: string | null;
15
+ }
16
+ /** A key's name: bytes, since a key may be any binary string. */
17
+ export type KeyName = KeyBytes;
18
+ /**
19
+ * The one key a command's reply was read from (decision 119), so the host can open the key or
20
+ * edit the reply as the key. `whole` is true when the reply holds the entire key, such as
21
+ * `HGETALL` or `LRANGE k 0 -1`; its columns are then the ones `Keyspace.read` gives that type,
22
+ * without positions a key tab adds (a zset's `#`), and a string's is one `value` column.
23
+ */
24
+ export interface KeySource {
25
+ readonly database: number;
26
+ readonly key: KeyName;
27
+ readonly type: KeyType;
28
+ readonly whole: boolean;
29
+ }
30
+ /** One logical database of the keyspace: Redis' `db0`–`db15`, or the one database of a cluster. */
31
+ export interface KeyDatabase {
32
+ readonly index: number;
33
+ readonly keys: number;
34
+ /** Keys with a time to live. */
35
+ readonly expires: number;
36
+ /** The database the connection is configured with. */
37
+ readonly current: boolean;
38
+ }
39
+ export interface KeyScanRequest {
40
+ readonly database: number;
41
+ /** Null starts a pass; otherwise what the last `KeyScan.cursor` said, opaque to the host. */
42
+ readonly cursor: string | null;
43
+ /** A glob, as `SCAN … MATCH` takes it; `*` for every key. */
44
+ readonly match: string;
45
+ readonly type: KeyType | null;
46
+ /**
47
+ * The batch the server is asked to look at per round trip, not a promise of how many keys come
48
+ * back; absent, the datasource's own setting.
49
+ */
50
+ readonly count?: number;
51
+ }
52
+ export interface KeyScan {
53
+ readonly keys: readonly KeyName[];
54
+ /** Null when the pass reached the end. */
55
+ readonly cursor: string | null;
56
+ /** Keys the server looked at, matching or not, so the host can say how far the pass got. */
57
+ readonly scanned: number;
58
+ }
59
+ export interface KeyInfo {
60
+ readonly key: KeyName;
61
+ /** Null when the key does not exist (any more). */
62
+ readonly type: KeyType | null;
63
+ /** The server's name for the type, such as `ReJSON-RL` for `other` and `json`. */
64
+ readonly typeName: string | null;
65
+ /** Milliseconds left to live; null without an expiry. */
66
+ readonly ttlMs: number | null;
67
+ }
68
+ /** What the key tab's header shows; each field null when the server or its ACL would not say. */
69
+ export interface KeyDetails extends KeyInfo {
70
+ /** Fields, elements, members, entries or bytes, by type. */
71
+ readonly length: number | null;
72
+ readonly memoryBytes: number | null;
73
+ readonly encoding: string | null;
74
+ readonly idleSeconds: number | null;
75
+ }
76
+ /** Which part of a key `Keyspace.read` returns (decision 113). */
77
+ export interface KeyReadRequest {
78
+ /** A glob over hash fields and set or sorted-set members. */
79
+ readonly match?: string;
80
+ /** The first stream id, `-` by default; the last is `to`, `+` by default. */
81
+ readonly from?: string;
82
+ readonly to?: string;
83
+ /** The first list or sorted-set index read. */
84
+ readonly offset?: number;
85
+ /** At most this many elements. */
86
+ readonly window: number;
87
+ }
88
+ /**
89
+ * A key's contents: the elements as a result the host opens in its grid, or, for a string or a
90
+ * type it does not read element by element, one value.
91
+ */
92
+ export interface KeyContents {
93
+ readonly details: KeyDetails;
94
+ /** Rows of the elements: hash `field`, `value`; list `#`, `value`; and so on. Null for a string. */
95
+ readonly result: ExecuteResult | null;
96
+ /** The value of a string or of a JSON document, every byte up to the plugin's limit. */
97
+ readonly value: WireValue | null;
98
+ /** Why the contents cannot be edited, such as a value read only in part; null when they can. */
99
+ readonly readOnly: string | null;
100
+ }
101
+ /**
102
+ * One pending change to one key. `original` is what the element held when it was read, which a
103
+ * guarded apply checks first (decision 115); null for an element that did not exist.
104
+ */
105
+ export type KeyChange = {
106
+ readonly kind: "setString";
107
+ readonly value: KeyBytes;
108
+ readonly original: KeyBytes | null;
109
+ } | {
110
+ readonly kind: "setField";
111
+ readonly field: KeyBytes;
112
+ readonly value: KeyBytes;
113
+ readonly original: KeyBytes | null;
114
+ } | {
115
+ readonly kind: "deleteField";
116
+ readonly field: KeyBytes;
117
+ readonly original: KeyBytes;
118
+ } | {
119
+ readonly kind: "setIndex";
120
+ readonly index: number;
121
+ readonly value: KeyBytes;
122
+ readonly original: KeyBytes;
123
+ } | {
124
+ readonly kind: "deleteIndex";
125
+ readonly index: number;
126
+ readonly original: KeyBytes;
127
+ } | {
128
+ readonly kind: "push";
129
+ readonly value: KeyBytes;
130
+ } | {
131
+ readonly kind: "addMember";
132
+ readonly member: KeyBytes;
133
+ } | {
134
+ readonly kind: "deleteMember";
135
+ readonly member: KeyBytes;
136
+ } | {
137
+ readonly kind: "setScore";
138
+ readonly member: KeyBytes;
139
+ readonly score: number;
140
+ readonly original: number | null;
141
+ } | {
142
+ readonly kind: "addEntry";
143
+ readonly fields: readonly (readonly [KeyBytes, KeyBytes])[];
144
+ } | {
145
+ readonly kind: "deleteEntry";
146
+ readonly id: string;
147
+ };
148
+ export interface KeyApplyOptions {
149
+ /** The key's type as read, or the type of the key the changes create. */
150
+ readonly type: KeyType;
151
+ /** Check every change's `original` first and refuse with `conflict` when one differs. */
152
+ readonly guard: boolean;
153
+ /** Milliseconds to live to set with the changes, for a new key; absent leaves the expiry alone. */
154
+ readonly ttlMs?: number;
155
+ }
156
+ export interface KeyApplied {
157
+ /** The commands that ran, as `preview` lists them. */
158
+ readonly commands: readonly string[];
159
+ }
160
+ /** A new expiry: milliseconds from now, an instant, or none at all. */
161
+ export type KeyExpiry = {
162
+ readonly inMs: number;
163
+ } | {
164
+ readonly at: string;
165
+ } | null;
166
+ /**
167
+ * The keys of a key-value store, read and changed by the plugin (decision 113). Present on a
168
+ * session exactly when the plugin declares `keyValue`. Calls queue on the session they hang off.
169
+ */
170
+ export interface Keyspace {
171
+ /** What separates the levels of a key name, such as `:`, for the tree's folders. */
172
+ readonly delimiter: string;
173
+ databases(): Promise<readonly KeyDatabase[]>;
174
+ /** One batch of a pass over the keys; nothing ever runs `KEYS`. */
175
+ scan(request: KeyScanRequest): Promise<KeyScan>;
176
+ /** Type and expiry of each key, in order. */
177
+ describe(database: number, keys: readonly KeyName[]): Promise<readonly KeyInfo[]>;
178
+ /** A key that does not exist reads with a null `details.type` and no contents. */
179
+ read(database: number, key: KeyName, request: KeyReadRequest): Promise<KeyContents>;
180
+ /** The commands `apply` would run for `changes`, numbered by the host. Pure. */
181
+ preview(key: KeyName, changes: readonly KeyChange[], options: KeyApplyOptions): readonly string[];
182
+ /**
183
+ * Runs `changes` as one `MULTI`. Guarded, it first checks every `original` and rejects with
184
+ * `conflict`, writing nothing, when one differs.
185
+ */
186
+ apply(database: number, key: KeyName, changes: readonly KeyChange[], options: KeyApplyOptions): Promise<KeyApplied>;
187
+ /** Rejects with `statementFailed` when `to` exists and `overwrite` is false, or the key is gone. */
188
+ rename(database: number, key: KeyName, to: KeyName, overwrite: boolean): Promise<void>;
189
+ expire(database: number, key: KeyName, expiry: KeyExpiry): Promise<void>;
190
+ /** Removes the keys that exist and says how many did. */
191
+ remove(database: number, keys: readonly KeyName[]): Promise<number>;
192
+ }
@@ -0,0 +1,2 @@
1
+ /** The key types the key tree tells apart (decision 113); a module type such as RedisJSON is `other`. */
2
+ export const KEY_TYPES = ["string", "hash", "list", "set", "zset", "stream", "json", "other"];
@@ -36,6 +36,10 @@ export declare const connectionFieldSchema: z.ZodObject<{
36
36
  advanced: "advanced";
37
37
  }>>;
38
38
  secret: z.ZodOptional<z.ZodBoolean>;
39
+ visibleWhen: z.ZodOptional<z.ZodObject<{
40
+ key: z.ZodString;
41
+ equals: z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>;
42
+ }, z.core.$strict>>;
39
43
  }, z.core.$strict>;
40
44
  export type ConnectionField = z.infer<typeof connectionFieldSchema>;
41
45
  /** `server`: `scheme://user:password@host:port/database?params`; `file`: a path, bare or as a URL. */
@@ -69,6 +73,7 @@ export declare const connectionUrlSchema: z.ZodObject<{
69
73
  key: z.ZodString;
70
74
  values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
71
75
  }, z.core.$strict>>>;
76
+ sets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
72
77
  }, z.core.$strict>;
73
78
  export type ConnectionUrl = z.infer<typeof connectionUrlSchema>;
74
79
  /**
@@ -121,6 +126,10 @@ export declare const pluginManifestSchema: z.ZodObject<{
121
126
  serverMessages: "serverMessages";
122
127
  sessions: "sessions";
123
128
  routineCall: "routineCall";
129
+ commands: "commands";
130
+ keyValue: "keyValue";
131
+ nodes: "nodes";
132
+ serverInfo: "serverInfo";
124
133
  }>>;
125
134
  highlight: z.ZodString;
126
135
  connection: z.ZodObject<{
@@ -147,6 +156,10 @@ export declare const pluginManifestSchema: z.ZodObject<{
147
156
  advanced: "advanced";
148
157
  }>>;
149
158
  secret: z.ZodOptional<z.ZodBoolean>;
159
+ visibleWhen: z.ZodOptional<z.ZodObject<{
160
+ key: z.ZodString;
161
+ equals: z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>;
162
+ }, z.core.$strict>>;
150
163
  }, z.core.$strict>>;
151
164
  url: z.ZodOptional<z.ZodObject<{
152
165
  schemes: z.ZodArray<z.ZodString>;
@@ -167,6 +180,7 @@ export declare const pluginManifestSchema: z.ZodObject<{
167
180
  key: z.ZodString;
168
181
  values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
169
182
  }, z.core.$strict>>>;
183
+ sets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
170
184
  }, z.core.$strict>>;
171
185
  network: z.ZodOptional<z.ZodObject<{
172
186
  host: z.ZodString;
package/dist/manifest.js CHANGED
@@ -30,6 +30,13 @@ export const connectionFieldSchema = z.strictObject({
30
30
  .optional(),
31
31
  group: z.enum(CONNECTION_FIELD_GROUPS).optional(),
32
32
  secret: z.boolean().optional(),
33
+ /**
34
+ * Draw the field only while the `select` or `checkbox` named by `key` holds `equals` (plan 058);
35
+ * a hidden field is neither checked nor required.
36
+ */
37
+ visibleWhen: z
38
+ .strictObject({ key: z.string().regex(FIELD_KEY), equals: z.union([z.string(), z.boolean()]) })
39
+ .optional(),
33
40
  });
34
41
  /** `server`: `scheme://user:password@host:port/database?params`; `file`: a path, bare or as a URL. */
35
42
  export const CONNECTION_URL_FORMS = ["server", "file"];
@@ -54,6 +61,13 @@ export const connectionUrlSchema = z.strictObject({
54
61
  values: z.record(z.string(), z.string()).optional(),
55
62
  }))
56
63
  .optional(),
64
+ /**
65
+ * Field values a scheme implies, keyed by scheme, such as `rediss` setting the TLS mode (plan
66
+ * 058). A pasted URL fills them; a written URL takes the first scheme whose values the config holds.
67
+ */
68
+ sets: z
69
+ .record(z.string().regex(URL_SCHEME), z.record(z.string().regex(FIELD_KEY), z.union([z.string(), z.number(), z.boolean()])))
70
+ .optional(),
57
71
  });
58
72
  /**
59
73
  * Which fields hold the server's address (decision 070), so the host can open an SSH tunnel and
@@ -117,6 +131,30 @@ function tlsFieldsDeclared(connection) {
117
131
  return field !== undefined && tlsFieldFits(role, field, tls);
118
132
  });
119
133
  }
134
+ /** Every `visibleWhen` names another field of the form, a `select` or a `checkbox`. */
135
+ function visibilityFieldsDeclared(connection) {
136
+ return connection.fields.every((field) => {
137
+ const condition = field.visibleWhen;
138
+ if (condition === undefined)
139
+ return true;
140
+ const source = connection.fields.find((candidate) => candidate.key === condition.key);
141
+ return (source !== undefined &&
142
+ source !== field &&
143
+ (source.kind === "select" || source.kind === "checkbox") &&
144
+ typeof condition.equals === (source.kind === "checkbox" ? "boolean" : "string"));
145
+ });
146
+ }
147
+ /** Every scheme `url.sets` keys is one of `url.schemes`, and every value it sets is a declared field's. */
148
+ function urlSetsDeclared(connection) {
149
+ const url = connection.url;
150
+ if (url?.sets === undefined)
151
+ return true;
152
+ return Object.entries(url.sets).every(([scheme, values]) => url.schemes.includes(scheme) &&
153
+ Object.keys(values).every((key) => connection.fields.some((field) => field.key === key)));
154
+ }
155
+ function includesAny(capabilities, ...wanted) {
156
+ return wanted.some((capability) => capabilities.includes(capability));
157
+ }
120
158
  export const pluginManifestSchema = z
121
159
  .strictObject({
122
160
  id: z.string().regex(PLUGIN_ID, { message: "expected lower-case letters, digits and dashes" }),
@@ -144,8 +182,28 @@ export const pluginManifestSchema = z
144
182
  .refine(tlsFieldsDeclared, {
145
183
  message: "tls names a field the form does not declare or one of the wrong kind: mode a select offering every verify mode, files of kind file, the passphrase a secret password",
146
184
  path: ["tls"],
185
+ })
186
+ .refine(visibilityFieldsDeclared, {
187
+ message: "visibleWhen names a field the form does not declare, the field itself, or one that is not a select or checkbox, or compares it with a value of the wrong type",
188
+ path: ["fields"],
189
+ })
190
+ .refine(urlSetsDeclared, {
191
+ message: "url.sets names a scheme the url does not list, or a field the form does not declare",
192
+ path: ["url"],
147
193
  }),
148
194
  })
195
+ // A console speaks one language: SQL statements or a store's commands (decision 112).
196
+ .refine((manifest) => !(manifest.capabilities.includes("commands") && manifest.capabilities.includes("sql")), {
197
+ message: "the commands and sql capabilities exclude each other",
198
+ path: ["capabilities"],
199
+ })
200
+ // The key tree's edits and actions are previewed and confirmed as commands (decision 113).
201
+ .refine((manifest) => !manifest.capabilities.includes("keyValue") || manifest.capabilities.includes("commands"), {
202
+ message: "the keyValue capability requires commands",
203
+ path: ["capabilities"],
204
+ })
205
+ .refine((manifest) => !includesAny(manifest.capabilities, "nodes", "serverInfo") ||
206
+ includesAny(manifest.capabilities, "sql", "commands"), { message: "the nodes and serverInfo capabilities require sql or commands", path: ["capabilities"] })
149
207
  // A definition is read through the introspector, which only `schemaTree` promises (decision 029).
150
208
  .refine((manifest) => !manifest.capabilities.includes("ddl") || manifest.capabilities.includes("schemaTree"), {
151
209
  message: "the ddl capability requires schemaTree",
@@ -167,10 +225,14 @@ export const pluginManifestSchema = z
167
225
  // Object DDL is planned against described tables and reads its names from the introspector (decision 052).
168
226
  .refine((manifest) => !manifest.capabilities.includes("objects") ||
169
227
  (manifest.capabilities.includes("sql") && manifest.capabilities.includes("schemaTree")), { message: "the objects capability requires sql and schemaTree", path: ["capabilities"] })
170
- // A listed session's query opens in a console, and the engine lists sessions on its introspection
171
- // session, which only a schemaTree plugin opens (decision 088).
228
+ // A listed session's query opens in a console, and the engine lists sessions on its catalog or
229
+ // key-tree session (decision 088, plan 058).
172
230
  .refine((manifest) => !manifest.capabilities.includes("sessions") ||
173
- (manifest.capabilities.includes("sql") && manifest.capabilities.includes("schemaTree")), { message: "the sessions capability requires sql and schemaTree", path: ["capabilities"] })
231
+ (includesAny(manifest.capabilities, "sql", "commands") &&
232
+ includesAny(manifest.capabilities, "schemaTree", "keyValue")), {
233
+ message: "the sessions capability requires sql or commands, and schemaTree or keyValue",
234
+ path: ["capabilities"],
235
+ })
174
236
  // A call runs in a console and is built from the routine the introspector describes (decision 090).
175
237
  .refine((manifest) => !manifest.capabilities.includes("routineCall") ||
176
238
  (manifest.capabilities.includes("sql") && manifest.capabilities.includes("schemaTree")), { message: "the routineCall capability requires sql and schemaTree", path: ["capabilities"] });
package/dist/plugin.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { Introspector, Namespace, Relation, Routine } from "./catalog.js";
3
3
  import type { CompletionCatalog, CompletionRequest, CompletionResult, Declaration, ResolveRequest, SignatureHelp } from "./completion.js";
4
4
  import type { ExplainedPlan, Explainability, ExplainOptions } from "./explain.js";
5
5
  import type { InspectionRequest, Problem } from "./inspection.js";
6
+ import type { KeySource, Keyspace } from "./keyspace.js";
6
7
  import type { ObjectEditor } from "./objects.js";
7
8
  import type { SessionMonitor } from "./sessions.js";
8
9
  import type { TypeClass, WireValue } from "./values.js";
@@ -10,7 +11,7 @@ import type { TypeClass, WireValue } from "./values.js";
10
11
  * What a plugin can do. The UI branches on these, never on a plugin id (decision 007), and a
11
12
  * capability without contract-suite coverage cannot be declared (decision 013).
12
13
  */
13
- export declare const CAPABILITIES: readonly ["sql", "cancel", "schemaTree", "transactions", "ddl", "explain", "explainAnalyze", "currentNamespace", "keepAlive", "bulkInsert", "objects", "serverMessages", "sessions", "routineCall"];
14
+ export declare const CAPABILITIES: readonly ["sql", "cancel", "schemaTree", "transactions", "ddl", "explain", "explainAnalyze", "currentNamespace", "keepAlive", "bulkInsert", "objects", "serverMessages", "sessions", "routineCall", "commands", "keyValue", "nodes", "serverInfo"];
14
15
  export type Capability = (typeof CAPABILITIES)[number];
15
16
  /** Statements `SqlLanguage.generate` writes from a catalog relation (plan 010). */
16
17
  export declare const GENERATED_STATEMENTS: readonly ["drop", "select", "insert", "update"];
@@ -55,6 +56,29 @@ export interface ExecuteResult {
55
56
  * the plugin declares `serverMessages`.
56
57
  */
57
58
  readonly messages?: readonly ServerMessage[];
59
+ /**
60
+ * A command's one-value answer, such as a status, a number or a string (decision 112): the
61
+ * console shows it in Output and opens no result unless it is `read`. Only a `commands` plugin sets it, and then
62
+ * `columns` and `rows` are empty.
63
+ */
64
+ readonly reply?: CommandReply;
65
+ /**
66
+ * The one key the command read, when it read exactly one (decision 119); absent for writes,
67
+ * keyless and multi-key commands. Only a `keyValue` plugin sets it.
68
+ */
69
+ readonly source?: KeySource;
70
+ }
71
+ /** A scalar reply to a command (decision 112). */
72
+ export interface CommandReply {
73
+ readonly value: WireValue;
74
+ /** The reply as the server's own client prints it: `"Ann"`, `(integer) 5`, `(nil)`, `OK`. */
75
+ readonly text: string;
76
+ /**
77
+ * The reply is a value read, such as a bulk string from `GET` or `HGET` or the nil of a missing
78
+ * one (decision 119): the console opens it as a one-row `value` result besides Output. Absent
79
+ * for statuses, numbers and errors, which stay in Output.
80
+ */
81
+ readonly read?: boolean;
58
82
  }
59
83
  /** How serious a `ServerMessage` is, from the server's own word for it. */
60
84
  export declare const SERVER_MESSAGE_LEVELS: readonly ["error", "warning", "notice", "info"];
@@ -92,6 +116,56 @@ export interface ExecuteOptions {
92
116
  * preview. For an export, which writes values whole (decision 040); a grid never asks.
93
117
  */
94
118
  readonly fullBytes?: boolean;
119
+ /**
120
+ * Where a command runs, for a plugin that declares `nodes` (decision 114): `NODE_AUTO` (absent
121
+ * means the same) lets the plugin route it, `NODE_ALL_PRIMARIES` runs it on every primary and
122
+ * leads each row with a `node` column, and anything else is a `ServerNode.id`.
123
+ */
124
+ readonly node?: string;
125
+ }
126
+ /** `ExecuteOptions.node`: the plugin routes the command by its keys (decision 114). */
127
+ export declare const NODE_AUTO = "auto";
128
+ /** `ExecuteOptions.node`: the command runs once on every primary (decision 114). */
129
+ export declare const NODE_ALL_PRIMARIES = "allPrimaries";
130
+ export declare const NODE_ROLES: readonly ["primary", "replica"];
131
+ export type NodeRole = (typeof NODE_ROLES)[number];
132
+ /** One server of the datasource (decision 114); a standalone server is one primary. */
133
+ export interface ServerNode {
134
+ /** Stable while the cluster is: Redis' node id, or the address of a standalone server. */
135
+ readonly id: string;
136
+ /** `host:port`, as the client reaches it. */
137
+ readonly address: string;
138
+ readonly role: NodeRole;
139
+ /** Inclusive hash-slot ranges a primary serves; empty for a replica and a standalone server. */
140
+ readonly slots: readonly (readonly [number, number])[];
141
+ /** The primary a replica follows; null for a primary. */
142
+ readonly primaryId: string | null;
143
+ /** The server's word for its health, such as `connected` or `fail`. */
144
+ readonly state: string;
145
+ }
146
+ /** A titled group of a server's figures, such as Redis' `# Memory` section of `INFO` (decision 114). */
147
+ export interface ServerInfoSection {
148
+ readonly title: string;
149
+ readonly entries: readonly {
150
+ readonly name: string;
151
+ readonly value: string;
152
+ }[];
153
+ }
154
+ /** One slow-log entry: when, for how long, what ran, and from where. */
155
+ export interface SlowCommand {
156
+ readonly id: number;
157
+ /** ISO-8601. */
158
+ readonly at: string;
159
+ readonly durationMicros: number;
160
+ readonly command: string;
161
+ readonly client: string | null;
162
+ readonly node: string | null;
163
+ }
164
+ /** What `Session.serverInfo` reads about one node. */
165
+ export interface ServerInfo {
166
+ /** The node, or null for a standalone server. */
167
+ readonly node: string | null;
168
+ readonly sections: readonly ServerInfoSection[];
95
169
  }
96
170
  /** Kinds a file importer infers from a column's text, narrowest first within each family (plan 021). */
97
171
  export declare const IMPORT_KINDS: readonly ["boolean", "integer", "bigint", "decimal", "double", "date", "timestamp", "text"];
@@ -174,6 +248,20 @@ export interface Session {
174
248
  * not say, and the host then matches by id alone.
175
249
  */
176
250
  readonly serverInstance?: string;
251
+ /** Present exactly when the plugin declares `keyValue` (decision 113). */
252
+ readonly keyspace?: Keyspace;
253
+ /**
254
+ * Present exactly when the plugin declares `nodes` (decision 114): every server of the
255
+ * datasource, primaries first; one entry for a single server.
256
+ */
257
+ nodes?(): Promise<readonly ServerNode[]>;
258
+ /**
259
+ * Present exactly when the plugin declares `serverInfo`: what `node` (a `ServerNode.id`, or null
260
+ * for the one server or a primary of the plugin's choice) reports about itself.
261
+ */
262
+ serverInfo?(node: string | null): Promise<ServerInfo>;
263
+ /** Present exactly when the plugin declares `serverInfo`: the newest `limit` slow commands, of every primary for a null `node`. */
264
+ slowLog?(node: string | null, limit: number): Promise<readonly SlowCommand[]>;
177
265
  /**
178
266
  * Settles in bounded time even when the network is gone: a graceful goodbye that does not
179
267
  * answer within a few seconds gives way to dropping the socket (decision 040).
@@ -254,6 +342,13 @@ export interface StatementClass {
254
342
  * `SET search_path` (plan 022); the host then asks `Session.currentNamespace` again.
255
343
  */
256
344
  readonly setsNamespace?: boolean;
345
+ /**
346
+ * Why the statement may be slow or heavy for the server, such as `KEYS walks every key`; the
347
+ * editor marks it and never blocks the run (decision 112). Null or absent when it is not.
348
+ */
349
+ readonly costly?: string | null;
350
+ /** With `destructive`: confirming it asks for the datasource's name, not just OK (decision 112). */
351
+ readonly confirmByName?: boolean;
257
352
  }
258
353
  export declare const KEYWORD_CASES: readonly ["upper", "lower", "preserve"];
259
354
  export type KeywordCase = (typeof KEYWORD_CASES)[number];
@@ -447,12 +542,33 @@ export interface Transactions {
447
542
  /** Whether a transaction is open after the last statement, commit or rollback. */
448
543
  readonly open: boolean;
449
544
  }
545
+ /**
546
+ * The console language of a datasource that speaks commands, not SQL (decision 112): one command
547
+ * per line, as the server's own client reads them.
548
+ */
549
+ export interface CommandLanguage {
550
+ /** Every command in `text`, in order; comments and blank lines belong to none. */
551
+ split(text: string): readonly StatementSpan[];
552
+ classify(command: string): StatementClass;
553
+ /**
554
+ * What fits at the caret. `session` is a connection the host keeps for such questions, null
555
+ * when it cannot open one; the plugin then completes from what it knows offline. Never rejects
556
+ * for text it cannot read.
557
+ */
558
+ complete(request: CompletionRequest, session: Session | null): Promise<CompletionResult>;
559
+ /** The command the caret is in and the argument it is at, for the parameter hint (decision 098). */
560
+ signatureAt?(request: CompletionRequest, session: Session | null): Promise<SignatureHelp | null>;
561
+ /** Problems in `request.text` found without running it, such as a costly command, with quick fixes. */
562
+ inspect?(request: InspectionRequest): Promise<readonly Problem[]>;
563
+ }
450
564
  /** The default export of a plugin's entry module (decision 005). */
451
565
  export interface DatasourcePlugin {
452
566
  readonly id: string;
453
567
  readonly capabilities: readonly Capability[];
454
568
  /** Present exactly when the plugin declares `sql`. */
455
569
  readonly language?: SqlLanguage;
570
+ /** Present exactly when the plugin declares `commands` (decision 112). */
571
+ readonly commandLanguage?: CommandLanguage;
456
572
  /** Validates the configuration `connect` receives; the host parses before it calls. */
457
573
  readonly connectionSchema: z.ZodType;
458
574
  /** Rejects with a `PluginError` coded `connectionFailed` when the database cannot be reached. */
package/dist/plugin.js CHANGED
@@ -17,6 +17,10 @@ export const CAPABILITIES = [
17
17
  "serverMessages",
18
18
  "sessions",
19
19
  "routineCall",
20
+ "commands",
21
+ "keyValue",
22
+ "nodes",
23
+ "serverInfo",
20
24
  ];
21
25
  /** Statements `SqlLanguage.generate` writes from a catalog relation (plan 010). */
22
26
  export const GENERATED_STATEMENTS = ["drop", "select", "insert", "update"];
@@ -32,6 +36,11 @@ export function omittedMessages(count) {
32
36
  text: `${String(count)} more server ${count === 1 ? "message is" : "messages are"} not shown.`,
33
37
  };
34
38
  }
39
+ /** `ExecuteOptions.node`: the plugin routes the command by its keys (decision 114). */
40
+ export const NODE_AUTO = "auto";
41
+ /** `ExecuteOptions.node`: the command runs once on every primary (decision 114). */
42
+ export const NODE_ALL_PRIMARIES = "allPrimaries";
43
+ export const NODE_ROLES = ["primary", "replica"];
35
44
  /** Kinds a file importer infers from a column's text, narrowest first within each family (plan 021). */
36
45
  export const IMPORT_KINDS = ["boolean", "integer", "bigint", "decimal", "double", "date", "timestamp", "text"];
37
46
  export const STATEMENT_KINDS = ["read", "write", "ddl", "transaction", "other"];