@tablecraft-app/plugin-api 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/catalog.d.ts +112 -0
- package/dist/catalog.js +5 -0
- package/dist/completion.d.ts +109 -0
- package/dist/completion.js +12 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +124 -0
- package/dist/explain.d.ts +58 -0
- package/dist/explain.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/inspection.d.ts +38 -0
- package/dist/inspection.js +21 -0
- package/dist/keyspace.d.ts +180 -0
- package/dist/keyspace.js +2 -0
- package/dist/manifest.d.ts +200 -0
- package/dist/manifest.js +238 -0
- package/dist/objects.d.ts +140 -0
- package/dist/objects.js +2 -0
- package/dist/plugin.d.ts +565 -0
- package/dist/plugin.js +58 -0
- package/dist/sessions.d.ts +59 -0
- package/dist/sessions.js +2 -0
- package/dist/sql/classify.d.ts +19 -0
- package/dist/sql/classify.js +95 -0
- package/dist/sql/complete.d.ts +23 -0
- package/dist/sql/complete.js +480 -0
- package/dist/sql/explain.d.ts +42 -0
- package/dist/sql/explain.js +108 -0
- package/dist/sql/format.d.ts +26 -0
- package/dist/sql/format.js +117 -0
- package/dist/sql/functions.d.ts +47 -0
- package/dist/sql/functions.js +159 -0
- package/dist/sql/generate.d.ts +22 -0
- package/dist/sql/generate.js +64 -0
- package/dist/sql/index.d.ts +16 -0
- package/dist/sql/index.js +16 -0
- package/dist/sql/inspect.d.ts +10 -0
- package/dist/sql/inspect.js +134 -0
- package/dist/sql/inspectContext.d.ts +78 -0
- package/dist/sql/inspectContext.js +251 -0
- package/dist/sql/inspectResolve.d.ts +6 -0
- package/dist/sql/inspectResolve.js +391 -0
- package/dist/sql/inspectStyle.d.ts +3 -0
- package/dist/sql/inspectStyle.js +384 -0
- package/dist/sql/inspectSyntax.d.ts +10 -0
- package/dist/sql/inspectSyntax.js +179 -0
- package/dist/sql/layout.d.ts +8 -0
- package/dist/sql/layout.js +114 -0
- package/dist/sql/layoutNodes.d.ts +46 -0
- package/dist/sql/layoutNodes.js +317 -0
- package/dist/sql/layoutPrint.d.ts +60 -0
- package/dist/sql/layoutPrint.js +806 -0
- package/dist/sql/layoutWriter.d.ts +21 -0
- package/dist/sql/layoutWriter.js +77 -0
- package/dist/sql/literals.d.ts +28 -0
- package/dist/sql/literals.js +78 -0
- package/dist/sql/namespace.d.ts +41 -0
- package/dist/sql/namespace.js +220 -0
- package/dist/sql/objects.d.ts +42 -0
- package/dist/sql/objects.js +222 -0
- package/dist/sql/parameters.d.ts +20 -0
- package/dist/sql/parameters.js +88 -0
- package/dist/sql/resolution.d.ts +73 -0
- package/dist/sql/resolution.js +192 -0
- package/dist/sql/routines.d.ts +27 -0
- package/dist/sql/routines.js +42 -0
- package/dist/sql/scope.d.ts +117 -0
- package/dist/sql/scope.js +540 -0
- package/dist/sql/select.d.ts +41 -0
- package/dist/sql/select.js +287 -0
- package/dist/sql/signature.d.ts +18 -0
- package/dist/sql/signature.js +137 -0
- package/dist/sql/splitter.d.ts +15 -0
- package/dist/sql/splitter.js +217 -0
- package/dist/sql/suggest.d.ts +6 -0
- package/dist/sql/suggest.js +36 -0
- package/dist/sql/tokens.d.ts +62 -0
- package/dist/sql/tokens.js +227 -0
- package/dist/sql/words.d.ts +23 -0
- package/dist/sql/words.js +72 -0
- package/dist/testing/index.d.ts +274 -0
- package/dist/testing/index.js +2398 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.js +118 -0
- package/dist/url.d.ts +54 -0
- package/dist/url.js +228 -0
- package/dist/values.d.ts +44 -0
- package/dist/values.js +44 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +29 -0
- package/package.json +42 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How loudly the console marks a problem (plan 108): red wavy, amber wavy, or muted dotted. */
|
|
2
|
+
export const PROBLEM_SEVERITIES = ["error", "warning", "weak"];
|
|
3
|
+
/**
|
|
4
|
+
* The inspections `sqlInspector` runs. The ids are DataGrip's, so a `-- noinspection <Id>` comment
|
|
5
|
+
* written there silences the same check here.
|
|
6
|
+
*/
|
|
7
|
+
export const INSPECTION_IDS = [
|
|
8
|
+
"SqlSyntax",
|
|
9
|
+
"SqlResolve",
|
|
10
|
+
"SqlAmbiguousColumn",
|
|
11
|
+
"SqlReservedWordAsName",
|
|
12
|
+
"SqlInsertValues",
|
|
13
|
+
"SqlWithoutWhere",
|
|
14
|
+
"SqlNullComparison",
|
|
15
|
+
"SqlConstantCondition",
|
|
16
|
+
"SqlUnusedCte",
|
|
17
|
+
"SqlDuplicateAlias",
|
|
18
|
+
"SqlRedundantOrderingDirection",
|
|
19
|
+
"SqlRedundantAlias",
|
|
20
|
+
"SqlRedundantElseNull",
|
|
21
|
+
];
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
/** One logical database of the keyspace: Redis' `db0`–`db15`, or the one database of a cluster. */
|
|
19
|
+
export interface KeyDatabase {
|
|
20
|
+
readonly index: number;
|
|
21
|
+
readonly keys: number;
|
|
22
|
+
/** Keys with a time to live. */
|
|
23
|
+
readonly expires: number;
|
|
24
|
+
/** The database the connection is configured with. */
|
|
25
|
+
readonly current: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface KeyScanRequest {
|
|
28
|
+
readonly database: number;
|
|
29
|
+
/** Null starts a pass; otherwise what the last `KeyScan.cursor` said, opaque to the host. */
|
|
30
|
+
readonly cursor: string | null;
|
|
31
|
+
/** A glob, as `SCAN … MATCH` takes it; `*` for every key. */
|
|
32
|
+
readonly match: string;
|
|
33
|
+
readonly type: KeyType | null;
|
|
34
|
+
/**
|
|
35
|
+
* The batch the server is asked to look at per round trip, not a promise of how many keys come
|
|
36
|
+
* back; absent, the datasource's own setting.
|
|
37
|
+
*/
|
|
38
|
+
readonly count?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface KeyScan {
|
|
41
|
+
readonly keys: readonly KeyName[];
|
|
42
|
+
/** Null when the pass reached the end. */
|
|
43
|
+
readonly cursor: string | null;
|
|
44
|
+
/** Keys the server looked at, matching or not, so the host can say how far the pass got. */
|
|
45
|
+
readonly scanned: number;
|
|
46
|
+
}
|
|
47
|
+
export interface KeyInfo {
|
|
48
|
+
readonly key: KeyName;
|
|
49
|
+
/** Null when the key does not exist (any more). */
|
|
50
|
+
readonly type: KeyType | null;
|
|
51
|
+
/** The server's name for the type, such as `ReJSON-RL` for `other` and `json`. */
|
|
52
|
+
readonly typeName: string | null;
|
|
53
|
+
/** Milliseconds left to live; null without an expiry. */
|
|
54
|
+
readonly ttlMs: number | null;
|
|
55
|
+
}
|
|
56
|
+
/** What the key tab's header shows; each field null when the server or its ACL would not say. */
|
|
57
|
+
export interface KeyDetails extends KeyInfo {
|
|
58
|
+
/** Fields, elements, members, entries or bytes, by type. */
|
|
59
|
+
readonly length: number | null;
|
|
60
|
+
readonly memoryBytes: number | null;
|
|
61
|
+
readonly encoding: string | null;
|
|
62
|
+
readonly idleSeconds: number | null;
|
|
63
|
+
}
|
|
64
|
+
/** Which part of a key `Keyspace.read` returns (decision 113). */
|
|
65
|
+
export interface KeyReadRequest {
|
|
66
|
+
/** A glob over hash fields and set or sorted-set members. */
|
|
67
|
+
readonly match?: string;
|
|
68
|
+
/** The first stream id, `-` by default; the last is `to`, `+` by default. */
|
|
69
|
+
readonly from?: string;
|
|
70
|
+
readonly to?: string;
|
|
71
|
+
/** The first list or sorted-set index read. */
|
|
72
|
+
readonly offset?: number;
|
|
73
|
+
/** At most this many elements. */
|
|
74
|
+
readonly window: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A key's contents: the elements as a result the host opens in its grid, or, for a string or a
|
|
78
|
+
* type it does not read element by element, one value.
|
|
79
|
+
*/
|
|
80
|
+
export interface KeyContents {
|
|
81
|
+
readonly details: KeyDetails;
|
|
82
|
+
/** Rows of the elements: hash `field`, `value`; list `#`, `value`; and so on. Null for a string. */
|
|
83
|
+
readonly result: ExecuteResult | null;
|
|
84
|
+
/** The value of a string or of a JSON document, every byte up to the plugin's limit. */
|
|
85
|
+
readonly value: WireValue | null;
|
|
86
|
+
/** Why the contents cannot be edited, such as a value read only in part; null when they can. */
|
|
87
|
+
readonly readOnly: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One pending change to one key. `original` is what the element held when it was read, which a
|
|
91
|
+
* guarded apply checks first (decision 115); null for an element that did not exist.
|
|
92
|
+
*/
|
|
93
|
+
export type KeyChange = {
|
|
94
|
+
readonly kind: "setString";
|
|
95
|
+
readonly value: KeyBytes;
|
|
96
|
+
readonly original: KeyBytes | null;
|
|
97
|
+
} | {
|
|
98
|
+
readonly kind: "setField";
|
|
99
|
+
readonly field: KeyBytes;
|
|
100
|
+
readonly value: KeyBytes;
|
|
101
|
+
readonly original: KeyBytes | null;
|
|
102
|
+
} | {
|
|
103
|
+
readonly kind: "deleteField";
|
|
104
|
+
readonly field: KeyBytes;
|
|
105
|
+
readonly original: KeyBytes;
|
|
106
|
+
} | {
|
|
107
|
+
readonly kind: "setIndex";
|
|
108
|
+
readonly index: number;
|
|
109
|
+
readonly value: KeyBytes;
|
|
110
|
+
readonly original: KeyBytes;
|
|
111
|
+
} | {
|
|
112
|
+
readonly kind: "deleteIndex";
|
|
113
|
+
readonly index: number;
|
|
114
|
+
readonly original: KeyBytes;
|
|
115
|
+
} | {
|
|
116
|
+
readonly kind: "push";
|
|
117
|
+
readonly value: KeyBytes;
|
|
118
|
+
} | {
|
|
119
|
+
readonly kind: "addMember";
|
|
120
|
+
readonly member: KeyBytes;
|
|
121
|
+
} | {
|
|
122
|
+
readonly kind: "deleteMember";
|
|
123
|
+
readonly member: KeyBytes;
|
|
124
|
+
} | {
|
|
125
|
+
readonly kind: "setScore";
|
|
126
|
+
readonly member: KeyBytes;
|
|
127
|
+
readonly score: number;
|
|
128
|
+
readonly original: number | null;
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: "addEntry";
|
|
131
|
+
readonly fields: readonly (readonly [KeyBytes, KeyBytes])[];
|
|
132
|
+
} | {
|
|
133
|
+
readonly kind: "deleteEntry";
|
|
134
|
+
readonly id: string;
|
|
135
|
+
};
|
|
136
|
+
export interface KeyApplyOptions {
|
|
137
|
+
/** The key's type as read, or the type of the key the changes create. */
|
|
138
|
+
readonly type: KeyType;
|
|
139
|
+
/** Check every change's `original` first and refuse with `conflict` when one differs. */
|
|
140
|
+
readonly guard: boolean;
|
|
141
|
+
/** Milliseconds to live to set with the changes, for a new key; absent leaves the expiry alone. */
|
|
142
|
+
readonly ttlMs?: number;
|
|
143
|
+
}
|
|
144
|
+
export interface KeyApplied {
|
|
145
|
+
/** The commands that ran, as `preview` lists them. */
|
|
146
|
+
readonly commands: readonly string[];
|
|
147
|
+
}
|
|
148
|
+
/** A new expiry: milliseconds from now, an instant, or none at all. */
|
|
149
|
+
export type KeyExpiry = {
|
|
150
|
+
readonly inMs: number;
|
|
151
|
+
} | {
|
|
152
|
+
readonly at: string;
|
|
153
|
+
} | null;
|
|
154
|
+
/**
|
|
155
|
+
* The keys of a key-value store, read and changed by the plugin (decision 113). Present on a
|
|
156
|
+
* session exactly when the plugin declares `keyValue`. Calls queue on the session they hang off.
|
|
157
|
+
*/
|
|
158
|
+
export interface Keyspace {
|
|
159
|
+
/** What separates the levels of a key name, such as `:`, for the tree's folders. */
|
|
160
|
+
readonly delimiter: string;
|
|
161
|
+
databases(): Promise<readonly KeyDatabase[]>;
|
|
162
|
+
/** One batch of a pass over the keys; nothing ever runs `KEYS`. */
|
|
163
|
+
scan(request: KeyScanRequest): Promise<KeyScan>;
|
|
164
|
+
/** Type and expiry of each key, in order. */
|
|
165
|
+
describe(database: number, keys: readonly KeyName[]): Promise<readonly KeyInfo[]>;
|
|
166
|
+
/** A key that does not exist reads with a null `details.type` and no contents. */
|
|
167
|
+
read(database: number, key: KeyName, request: KeyReadRequest): Promise<KeyContents>;
|
|
168
|
+
/** The commands `apply` would run for `changes`, numbered by the host. Pure. */
|
|
169
|
+
preview(key: KeyName, changes: readonly KeyChange[], options: KeyApplyOptions): readonly string[];
|
|
170
|
+
/**
|
|
171
|
+
* Runs `changes` as one `MULTI`. Guarded, it first checks every `original` and rejects with
|
|
172
|
+
* `conflict`, writing nothing, when one differs.
|
|
173
|
+
*/
|
|
174
|
+
apply(database: number, key: KeyName, changes: readonly KeyChange[], options: KeyApplyOptions): Promise<KeyApplied>;
|
|
175
|
+
/** Rejects with `statementFailed` when `to` exists and `overwrite` is false, or the key is gone. */
|
|
176
|
+
rename(database: number, key: KeyName, to: KeyName, overwrite: boolean): Promise<void>;
|
|
177
|
+
expire(database: number, key: KeyName, expiry: KeyExpiry): Promise<void>;
|
|
178
|
+
/** Removes the keys that exist and says how many did. */
|
|
179
|
+
remove(database: number, keys: readonly KeyName[]): Promise<number>;
|
|
180
|
+
}
|
package/dist/keyspace.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The file that marks a directory as a plugin (decision 005). */
|
|
3
|
+
export declare const MANIFEST_FILE = "table-craft-plugin.json";
|
|
4
|
+
/**
|
|
5
|
+
* What the loader reads before executing any plugin code, so an unavailable plugin can still be
|
|
6
|
+
* named in the UI.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONNECTION_FIELD_KINDS: readonly ["text", "number", "password", "file", "select", "checkbox"];
|
|
9
|
+
export type ConnectionFieldKind = (typeof CONNECTION_FIELD_KINDS)[number];
|
|
10
|
+
export declare const CONNECTION_FIELD_GROUPS: readonly ["general", "advanced"];
|
|
11
|
+
export type ConnectionFieldGroup = (typeof CONNECTION_FIELD_GROUPS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* One control of the connection form (decision 019). `key` names the config property it fills;
|
|
14
|
+
* `secret` sends the value to the secret store instead of `datasources.json` (decision 020).
|
|
15
|
+
*/
|
|
16
|
+
export declare const connectionFieldSchema: z.ZodObject<{
|
|
17
|
+
key: z.ZodString;
|
|
18
|
+
label: z.ZodString;
|
|
19
|
+
kind: z.ZodEnum<{
|
|
20
|
+
number: "number";
|
|
21
|
+
text: "text";
|
|
22
|
+
select: "select";
|
|
23
|
+
password: "password";
|
|
24
|
+
file: "file";
|
|
25
|
+
checkbox: "checkbox";
|
|
26
|
+
}>;
|
|
27
|
+
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
28
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
29
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
30
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
31
|
+
value: z.ZodString;
|
|
32
|
+
label: z.ZodString;
|
|
33
|
+
}, z.core.$strict>>>;
|
|
34
|
+
group: z.ZodOptional<z.ZodEnum<{
|
|
35
|
+
general: "general";
|
|
36
|
+
advanced: "advanced";
|
|
37
|
+
}>>;
|
|
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>>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export type ConnectionField = z.infer<typeof connectionFieldSchema>;
|
|
45
|
+
/** `server`: `scheme://user:password@host:port/database?params`; `file`: a path, bare or as a URL. */
|
|
46
|
+
export declare const CONNECTION_URL_FORMS: readonly ["server", "file"];
|
|
47
|
+
export type ConnectionUrlForm = (typeof CONNECTION_URL_FORMS)[number];
|
|
48
|
+
/** The pieces of a connection URL a field can take; `path` belongs to the `file` form. */
|
|
49
|
+
export declare const CONNECTION_URL_PARTS: readonly ["host", "port", "database", "user", "password", "path"];
|
|
50
|
+
export type ConnectionUrlPart = (typeof CONNECTION_URL_PARTS)[number];
|
|
51
|
+
/**
|
|
52
|
+
* How a connection URL maps onto the form's fields (decision 055), so the host parses and writes
|
|
53
|
+
* one without running plugin code. `parts` names the field each piece of the URL fills; a query
|
|
54
|
+
* parameter fills `key`, its value translated through `values` (compared case-insensitively) when
|
|
55
|
+
* given, so a value outside them is reported as ignored.
|
|
56
|
+
*/
|
|
57
|
+
export declare const connectionUrlSchema: z.ZodObject<{
|
|
58
|
+
schemes: z.ZodArray<z.ZodString>;
|
|
59
|
+
form: z.ZodEnum<{
|
|
60
|
+
file: "file";
|
|
61
|
+
server: "server";
|
|
62
|
+
}>;
|
|
63
|
+
parts: z.ZodRecord<z.ZodEnum<{
|
|
64
|
+
database: "database";
|
|
65
|
+
path: "path";
|
|
66
|
+
password: "password";
|
|
67
|
+
host: "host";
|
|
68
|
+
port: "port";
|
|
69
|
+
user: "user";
|
|
70
|
+
}> & z.core.$partial, z.ZodString>;
|
|
71
|
+
params: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
72
|
+
name: z.ZodString;
|
|
73
|
+
key: z.ZodString;
|
|
74
|
+
values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
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]>>>>;
|
|
77
|
+
}, z.core.$strict>;
|
|
78
|
+
export type ConnectionUrl = z.infer<typeof connectionUrlSchema>;
|
|
79
|
+
/**
|
|
80
|
+
* Which fields hold the server's address (decision 070), so the host can open an SSH tunnel and
|
|
81
|
+
* hand the plugin `127.0.0.1` and the forwarded port instead. `connectTimeout`, in seconds, bounds
|
|
82
|
+
* the SSH hop too. Absent: the data source is not reached over the network and offers no tunnel.
|
|
83
|
+
*/
|
|
84
|
+
export declare const networkSpecSchema: z.ZodObject<{
|
|
85
|
+
host: z.ZodString;
|
|
86
|
+
port: z.ZodString;
|
|
87
|
+
connectTimeout: z.ZodOptional<z.ZodString>;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
export type NetworkSpec = z.infer<typeof networkSpecSchema>;
|
|
90
|
+
/**
|
|
91
|
+
* Which fields configure TLS (decision 072), so the dialog groups them under SSL, hints when a CA
|
|
92
|
+
* file goes unchecked, and main asks for the key passphrase only when the key file is encrypted.
|
|
93
|
+
* `mode` is the `select` of SSL modes; `verifyModes` are its values that check the server
|
|
94
|
+
* certificate. Absent: the plugin speaks no TLS the host knows about.
|
|
95
|
+
*/
|
|
96
|
+
export declare const tlsSpecSchema: z.ZodObject<{
|
|
97
|
+
mode: z.ZodString;
|
|
98
|
+
verifyModes: z.ZodArray<z.ZodString>;
|
|
99
|
+
caFile: z.ZodOptional<z.ZodString>;
|
|
100
|
+
certFile: z.ZodOptional<z.ZodString>;
|
|
101
|
+
keyFile: z.ZodOptional<z.ZodString>;
|
|
102
|
+
keyPassphrase: z.ZodOptional<z.ZodString>;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
export type TlsSpec = z.infer<typeof tlsSpecSchema>;
|
|
105
|
+
/** The roles of `TlsSpec` that name a field, in the order the dialog draws them. */
|
|
106
|
+
export declare const TLS_FIELD_ROLES: readonly ["mode", "caFile", "certFile", "keyFile", "keyPassphrase"];
|
|
107
|
+
export type TlsFieldRole = (typeof TLS_FIELD_ROLES)[number];
|
|
108
|
+
export declare const pluginManifestSchema: z.ZodObject<{
|
|
109
|
+
id: z.ZodString;
|
|
110
|
+
name: z.ZodString;
|
|
111
|
+
version: z.ZodString;
|
|
112
|
+
apiVersion: z.ZodString;
|
|
113
|
+
entry: z.ZodString;
|
|
114
|
+
capabilities: z.ZodArray<z.ZodEnum<{
|
|
115
|
+
sql: "sql";
|
|
116
|
+
cancel: "cancel";
|
|
117
|
+
schemaTree: "schemaTree";
|
|
118
|
+
transactions: "transactions";
|
|
119
|
+
ddl: "ddl";
|
|
120
|
+
explain: "explain";
|
|
121
|
+
explainAnalyze: "explainAnalyze";
|
|
122
|
+
currentNamespace: "currentNamespace";
|
|
123
|
+
keepAlive: "keepAlive";
|
|
124
|
+
bulkInsert: "bulkInsert";
|
|
125
|
+
objects: "objects";
|
|
126
|
+
serverMessages: "serverMessages";
|
|
127
|
+
sessions: "sessions";
|
|
128
|
+
routineCall: "routineCall";
|
|
129
|
+
commands: "commands";
|
|
130
|
+
keyValue: "keyValue";
|
|
131
|
+
nodes: "nodes";
|
|
132
|
+
serverInfo: "serverInfo";
|
|
133
|
+
}>>;
|
|
134
|
+
highlight: z.ZodString;
|
|
135
|
+
connection: z.ZodObject<{
|
|
136
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
137
|
+
key: z.ZodString;
|
|
138
|
+
label: z.ZodString;
|
|
139
|
+
kind: z.ZodEnum<{
|
|
140
|
+
number: "number";
|
|
141
|
+
text: "text";
|
|
142
|
+
select: "select";
|
|
143
|
+
password: "password";
|
|
144
|
+
file: "file";
|
|
145
|
+
checkbox: "checkbox";
|
|
146
|
+
}>;
|
|
147
|
+
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
148
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
149
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
150
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
151
|
+
value: z.ZodString;
|
|
152
|
+
label: z.ZodString;
|
|
153
|
+
}, z.core.$strict>>>;
|
|
154
|
+
group: z.ZodOptional<z.ZodEnum<{
|
|
155
|
+
general: "general";
|
|
156
|
+
advanced: "advanced";
|
|
157
|
+
}>>;
|
|
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>>;
|
|
163
|
+
}, z.core.$strict>>;
|
|
164
|
+
url: z.ZodOptional<z.ZodObject<{
|
|
165
|
+
schemes: z.ZodArray<z.ZodString>;
|
|
166
|
+
form: z.ZodEnum<{
|
|
167
|
+
file: "file";
|
|
168
|
+
server: "server";
|
|
169
|
+
}>;
|
|
170
|
+
parts: z.ZodRecord<z.ZodEnum<{
|
|
171
|
+
database: "database";
|
|
172
|
+
path: "path";
|
|
173
|
+
password: "password";
|
|
174
|
+
host: "host";
|
|
175
|
+
port: "port";
|
|
176
|
+
user: "user";
|
|
177
|
+
}> & z.core.$partial, z.ZodString>;
|
|
178
|
+
params: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
179
|
+
name: z.ZodString;
|
|
180
|
+
key: z.ZodString;
|
|
181
|
+
values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
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]>>>>;
|
|
184
|
+
}, z.core.$strict>>;
|
|
185
|
+
network: z.ZodOptional<z.ZodObject<{
|
|
186
|
+
host: z.ZodString;
|
|
187
|
+
port: z.ZodString;
|
|
188
|
+
connectTimeout: z.ZodOptional<z.ZodString>;
|
|
189
|
+
}, z.core.$strict>>;
|
|
190
|
+
tls: z.ZodOptional<z.ZodObject<{
|
|
191
|
+
mode: z.ZodString;
|
|
192
|
+
verifyModes: z.ZodArray<z.ZodString>;
|
|
193
|
+
caFile: z.ZodOptional<z.ZodString>;
|
|
194
|
+
certFile: z.ZodOptional<z.ZodString>;
|
|
195
|
+
keyFile: z.ZodOptional<z.ZodString>;
|
|
196
|
+
keyPassphrase: z.ZodOptional<z.ZodString>;
|
|
197
|
+
}, z.core.$strict>>;
|
|
198
|
+
}, z.core.$strict>;
|
|
199
|
+
}, z.core.$strict>;
|
|
200
|
+
export type PluginManifest = z.infer<typeof pluginManifestSchema>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CAPABILITIES } from "./plugin.js";
|
|
3
|
+
import { parseVersion } from "./version.js";
|
|
4
|
+
/** The file that marks a directory as a plugin (decision 005). */
|
|
5
|
+
export const MANIFEST_FILE = "table-craft-plugin.json";
|
|
6
|
+
const PLUGIN_ID = /^[a-z][a-z0-9-]*$/;
|
|
7
|
+
const semver = z.string().refine((text) => parseVersion(text) !== null, { message: "expected major.minor.patch" });
|
|
8
|
+
/**
|
|
9
|
+
* What the loader reads before executing any plugin code, so an unavailable plugin can still be
|
|
10
|
+
* named in the UI.
|
|
11
|
+
*/
|
|
12
|
+
export const CONNECTION_FIELD_KINDS = ["text", "number", "password", "file", "select", "checkbox"];
|
|
13
|
+
export const CONNECTION_FIELD_GROUPS = ["general", "advanced"];
|
|
14
|
+
const FIELD_KEY = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
15
|
+
/**
|
|
16
|
+
* One control of the connection form (decision 019). `key` names the config property it fills;
|
|
17
|
+
* `secret` sends the value to the secret store instead of `datasources.json` (decision 020).
|
|
18
|
+
*/
|
|
19
|
+
export const connectionFieldSchema = z.strictObject({
|
|
20
|
+
key: z.string().regex(FIELD_KEY, { message: "expected a camelCase config key" }),
|
|
21
|
+
label: z.string().min(1),
|
|
22
|
+
kind: z.enum(CONNECTION_FIELD_KINDS),
|
|
23
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
24
|
+
required: z.boolean().optional(),
|
|
25
|
+
placeholder: z.string().optional(),
|
|
26
|
+
/** The choices of a `select`, in display order. */
|
|
27
|
+
options: z
|
|
28
|
+
.array(z.strictObject({ value: z.string(), label: z.string().min(1) }))
|
|
29
|
+
.min(1)
|
|
30
|
+
.optional(),
|
|
31
|
+
group: z.enum(CONNECTION_FIELD_GROUPS).optional(),
|
|
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(),
|
|
40
|
+
});
|
|
41
|
+
/** `server`: `scheme://user:password@host:port/database?params`; `file`: a path, bare or as a URL. */
|
|
42
|
+
export const CONNECTION_URL_FORMS = ["server", "file"];
|
|
43
|
+
/** The pieces of a connection URL a field can take; `path` belongs to the `file` form. */
|
|
44
|
+
export const CONNECTION_URL_PARTS = ["host", "port", "database", "user", "password", "path"];
|
|
45
|
+
const URL_SCHEME = /^[a-z][a-z0-9+.-]*$/;
|
|
46
|
+
/**
|
|
47
|
+
* How a connection URL maps onto the form's fields (decision 055), so the host parses and writes
|
|
48
|
+
* one without running plugin code. `parts` names the field each piece of the URL fills; a query
|
|
49
|
+
* parameter fills `key`, its value translated through `values` (compared case-insensitively) when
|
|
50
|
+
* given, so a value outside them is reported as ignored.
|
|
51
|
+
*/
|
|
52
|
+
export const connectionUrlSchema = z.strictObject({
|
|
53
|
+
/** Without `://`; the first is the one the host writes. */
|
|
54
|
+
schemes: z.array(z.string().regex(URL_SCHEME, { message: "expected a lower-case URL scheme" })).min(1),
|
|
55
|
+
form: z.enum(CONNECTION_URL_FORMS),
|
|
56
|
+
parts: z.partialRecord(z.enum(CONNECTION_URL_PARTS), z.string().regex(FIELD_KEY)),
|
|
57
|
+
params: z
|
|
58
|
+
.array(z.strictObject({
|
|
59
|
+
name: z.string().min(1),
|
|
60
|
+
key: z.string().regex(FIELD_KEY),
|
|
61
|
+
values: z.record(z.string(), z.string()).optional(),
|
|
62
|
+
}))
|
|
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(),
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Which fields hold the server's address (decision 070), so the host can open an SSH tunnel and
|
|
74
|
+
* hand the plugin `127.0.0.1` and the forwarded port instead. `connectTimeout`, in seconds, bounds
|
|
75
|
+
* the SSH hop too. Absent: the data source is not reached over the network and offers no tunnel.
|
|
76
|
+
*/
|
|
77
|
+
export const networkSpecSchema = z.strictObject({
|
|
78
|
+
host: z.string().regex(FIELD_KEY),
|
|
79
|
+
port: z.string().regex(FIELD_KEY),
|
|
80
|
+
connectTimeout: z.string().regex(FIELD_KEY).optional(),
|
|
81
|
+
});
|
|
82
|
+
const NETWORK_FIELD_KINDS = { host: "text", port: "number", connectTimeout: "number" };
|
|
83
|
+
function networkFieldsDeclared(connection) {
|
|
84
|
+
const network = connection.network;
|
|
85
|
+
if (network === undefined)
|
|
86
|
+
return true;
|
|
87
|
+
return Object.keys(NETWORK_FIELD_KINDS).every((role) => {
|
|
88
|
+
const key = network[role];
|
|
89
|
+
if (key === undefined)
|
|
90
|
+
return true;
|
|
91
|
+
return connection.fields.some((field) => field.key === key && field.kind === NETWORK_FIELD_KINDS[role]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Which fields configure TLS (decision 072), so the dialog groups them under SSL, hints when a CA
|
|
96
|
+
* file goes unchecked, and main asks for the key passphrase only when the key file is encrypted.
|
|
97
|
+
* `mode` is the `select` of SSL modes; `verifyModes` are its values that check the server
|
|
98
|
+
* certificate. Absent: the plugin speaks no TLS the host knows about.
|
|
99
|
+
*/
|
|
100
|
+
export const tlsSpecSchema = z.strictObject({
|
|
101
|
+
mode: z.string().regex(FIELD_KEY),
|
|
102
|
+
verifyModes: z.array(z.string().min(1)).min(1),
|
|
103
|
+
caFile: z.string().regex(FIELD_KEY).optional(),
|
|
104
|
+
certFile: z.string().regex(FIELD_KEY).optional(),
|
|
105
|
+
keyFile: z.string().regex(FIELD_KEY).optional(),
|
|
106
|
+
keyPassphrase: z.string().regex(FIELD_KEY).optional(),
|
|
107
|
+
});
|
|
108
|
+
/** The roles of `TlsSpec` that name a field, in the order the dialog draws them. */
|
|
109
|
+
export const TLS_FIELD_ROLES = ["mode", "caFile", "certFile", "keyFile", "keyPassphrase"];
|
|
110
|
+
function tlsFieldFits(role, field, tls) {
|
|
111
|
+
switch (role) {
|
|
112
|
+
case "mode": {
|
|
113
|
+
const values = new Set((field.options ?? []).map((option) => option.value));
|
|
114
|
+
return field.kind === "select" && tls.verifyModes.every((mode) => values.has(mode));
|
|
115
|
+
}
|
|
116
|
+
case "keyPassphrase":
|
|
117
|
+
return field.kind === "password" && field.secret === true;
|
|
118
|
+
default:
|
|
119
|
+
return field.kind === "file";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function tlsFieldsDeclared(connection) {
|
|
123
|
+
const tls = connection.tls;
|
|
124
|
+
if (tls === undefined)
|
|
125
|
+
return true;
|
|
126
|
+
return TLS_FIELD_ROLES.every((role) => {
|
|
127
|
+
const key = tls[role];
|
|
128
|
+
if (key === undefined)
|
|
129
|
+
return true;
|
|
130
|
+
const field = connection.fields.find((candidate) => candidate.key === key);
|
|
131
|
+
return field !== undefined && tlsFieldFits(role, field, tls);
|
|
132
|
+
});
|
|
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
|
+
}
|
|
158
|
+
export const pluginManifestSchema = z
|
|
159
|
+
.strictObject({
|
|
160
|
+
id: z.string().regex(PLUGIN_ID, { message: "expected lower-case letters, digits and dashes" }),
|
|
161
|
+
name: z.string().min(1),
|
|
162
|
+
version: semver,
|
|
163
|
+
apiVersion: semver,
|
|
164
|
+
/** An ES module, relative to the manifest, whose default export is the `DatasourcePlugin`. */
|
|
165
|
+
entry: z.string().min(1),
|
|
166
|
+
capabilities: z.array(z.enum(CAPABILITIES)),
|
|
167
|
+
/** The highlighter the console uses for this plugin's dialect (decision 008). */
|
|
168
|
+
highlight: z.string().min(1),
|
|
169
|
+
/** The connection form, readable without running plugin code (decision 019). */
|
|
170
|
+
connection: z
|
|
171
|
+
.strictObject({
|
|
172
|
+
fields: z.array(connectionFieldSchema).min(1),
|
|
173
|
+
/** Absent: the form offers no URL to paste and the data source no Copy as URL. */
|
|
174
|
+
url: connectionUrlSchema.optional(),
|
|
175
|
+
network: networkSpecSchema.optional(),
|
|
176
|
+
tls: tlsSpecSchema.optional(),
|
|
177
|
+
})
|
|
178
|
+
.refine(networkFieldsDeclared, {
|
|
179
|
+
message: "network names a field the form does not declare, or one of the wrong kind",
|
|
180
|
+
path: ["network"],
|
|
181
|
+
})
|
|
182
|
+
.refine(tlsFieldsDeclared, {
|
|
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",
|
|
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"],
|
|
193
|
+
}),
|
|
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"] })
|
|
207
|
+
// A definition is read through the introspector, which only `schemaTree` promises (decision 029).
|
|
208
|
+
.refine((manifest) => !manifest.capabilities.includes("ddl") || manifest.capabilities.includes("schemaTree"), {
|
|
209
|
+
message: "the ddl capability requires schemaTree",
|
|
210
|
+
path: ["capabilities"],
|
|
211
|
+
})
|
|
212
|
+
// A plan is of a statement, and Analyse is Explain that also runs it (decision 031).
|
|
213
|
+
.refine((manifest) => !manifest.capabilities.includes("explain") || manifest.capabilities.includes("sql"), {
|
|
214
|
+
message: "the explain capability requires sql",
|
|
215
|
+
path: ["capabilities"],
|
|
216
|
+
})
|
|
217
|
+
.refine((manifest) => !manifest.capabilities.includes("explainAnalyze") || manifest.capabilities.includes("explain"), { message: "the explainAnalyze capability requires explain", path: ["capabilities"] })
|
|
218
|
+
// A namespace is switched by a statement, and the switch is told apart by the language (decision 048).
|
|
219
|
+
.refine((manifest) => !manifest.capabilities.includes("currentNamespace") || manifest.capabilities.includes("sql"), {
|
|
220
|
+
message: "the currentNamespace capability requires sql",
|
|
221
|
+
path: ["capabilities"],
|
|
222
|
+
})
|
|
223
|
+
// An import commits at boundaries it chooses and rolls back the rest (decision 046).
|
|
224
|
+
.refine((manifest) => !manifest.capabilities.includes("bulkInsert") || manifest.capabilities.includes("transactions"), { message: "the bulkInsert capability requires transactions", path: ["capabilities"] })
|
|
225
|
+
// Object DDL is planned against described tables and reads its names from the introspector (decision 052).
|
|
226
|
+
.refine((manifest) => !manifest.capabilities.includes("objects") ||
|
|
227
|
+
(manifest.capabilities.includes("sql") && manifest.capabilities.includes("schemaTree")), { message: "the objects capability requires sql and schemaTree", path: ["capabilities"] })
|
|
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).
|
|
230
|
+
.refine((manifest) => !manifest.capabilities.includes("sessions") ||
|
|
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
|
+
})
|
|
236
|
+
// A call runs in a console and is built from the routine the introspector describes (decision 090).
|
|
237
|
+
.refine((manifest) => !manifest.capabilities.includes("routineCall") ||
|
|
238
|
+
(manifest.capabilities.includes("sql") && manifest.capabilities.includes("schemaTree")), { message: "the routineCall capability requires sql and schemaTree", path: ["capabilities"] });
|