@tablecraft-app/plugin-api 0.0.0-stage → 1.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/catalog.d.ts +112 -0
  4. package/dist/catalog.js +5 -0
  5. package/dist/completion.d.ts +109 -0
  6. package/dist/completion.js +12 -0
  7. package/dist/errors.d.ts +58 -0
  8. package/dist/errors.js +106 -0
  9. package/dist/explain.d.ts +58 -0
  10. package/dist/explain.js +1 -0
  11. package/dist/index.d.ts +15 -0
  12. package/dist/index.js +15 -0
  13. package/dist/inspection.d.ts +38 -0
  14. package/dist/inspection.js +21 -0
  15. package/dist/manifest.d.ts +186 -0
  16. package/dist/manifest.js +176 -0
  17. package/dist/objects.d.ts +140 -0
  18. package/dist/objects.js +2 -0
  19. package/dist/plugin.d.ts +460 -0
  20. package/dist/plugin.js +49 -0
  21. package/dist/sessions.d.ts +59 -0
  22. package/dist/sessions.js +2 -0
  23. package/dist/sql/classify.d.ts +19 -0
  24. package/dist/sql/classify.js +95 -0
  25. package/dist/sql/complete.d.ts +23 -0
  26. package/dist/sql/complete.js +480 -0
  27. package/dist/sql/explain.d.ts +42 -0
  28. package/dist/sql/explain.js +108 -0
  29. package/dist/sql/format.d.ts +26 -0
  30. package/dist/sql/format.js +117 -0
  31. package/dist/sql/functions.d.ts +47 -0
  32. package/dist/sql/functions.js +159 -0
  33. package/dist/sql/generate.d.ts +22 -0
  34. package/dist/sql/generate.js +64 -0
  35. package/dist/sql/index.d.ts +16 -0
  36. package/dist/sql/index.js +16 -0
  37. package/dist/sql/inspect.d.ts +10 -0
  38. package/dist/sql/inspect.js +134 -0
  39. package/dist/sql/inspectContext.d.ts +78 -0
  40. package/dist/sql/inspectContext.js +251 -0
  41. package/dist/sql/inspectResolve.d.ts +6 -0
  42. package/dist/sql/inspectResolve.js +391 -0
  43. package/dist/sql/inspectStyle.d.ts +3 -0
  44. package/dist/sql/inspectStyle.js +384 -0
  45. package/dist/sql/inspectSyntax.d.ts +10 -0
  46. package/dist/sql/inspectSyntax.js +179 -0
  47. package/dist/sql/layout.d.ts +8 -0
  48. package/dist/sql/layout.js +114 -0
  49. package/dist/sql/layoutNodes.d.ts +46 -0
  50. package/dist/sql/layoutNodes.js +317 -0
  51. package/dist/sql/layoutPrint.d.ts +60 -0
  52. package/dist/sql/layoutPrint.js +806 -0
  53. package/dist/sql/layoutWriter.d.ts +21 -0
  54. package/dist/sql/layoutWriter.js +77 -0
  55. package/dist/sql/literals.d.ts +28 -0
  56. package/dist/sql/literals.js +78 -0
  57. package/dist/sql/namespace.d.ts +41 -0
  58. package/dist/sql/namespace.js +220 -0
  59. package/dist/sql/objects.d.ts +42 -0
  60. package/dist/sql/objects.js +222 -0
  61. package/dist/sql/parameters.d.ts +20 -0
  62. package/dist/sql/parameters.js +88 -0
  63. package/dist/sql/resolution.d.ts +73 -0
  64. package/dist/sql/resolution.js +192 -0
  65. package/dist/sql/routines.d.ts +27 -0
  66. package/dist/sql/routines.js +42 -0
  67. package/dist/sql/scope.d.ts +117 -0
  68. package/dist/sql/scope.js +540 -0
  69. package/dist/sql/select.d.ts +41 -0
  70. package/dist/sql/select.js +287 -0
  71. package/dist/sql/signature.d.ts +18 -0
  72. package/dist/sql/signature.js +137 -0
  73. package/dist/sql/splitter.d.ts +15 -0
  74. package/dist/sql/splitter.js +217 -0
  75. package/dist/sql/suggest.d.ts +6 -0
  76. package/dist/sql/suggest.js +36 -0
  77. package/dist/sql/tokens.d.ts +62 -0
  78. package/dist/sql/tokens.js +227 -0
  79. package/dist/sql/words.d.ts +23 -0
  80. package/dist/sql/words.js +72 -0
  81. package/dist/testing/index.d.ts +177 -0
  82. package/dist/testing/index.js +2070 -0
  83. package/dist/tls.d.ts +33 -0
  84. package/dist/tls.js +118 -0
  85. package/dist/url.d.ts +52 -0
  86. package/dist/url.js +219 -0
  87. package/dist/values.d.ts +44 -0
  88. package/dist/values.js +44 -0
  89. package/dist/version.d.ts +22 -0
  90. package/dist/version.js +29 -0
  91. package/package.json +42 -4
package/dist/tls.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wording for TLS failures shared by the network plugins (decision 072). Pure: each plugin reads
3
+ * its files and drives its own driver, and hands the error here to say what did not match.
4
+ */
5
+ /** Node's codes for a server certificate whose chain the trusted CAs do not vouch for. */
6
+ export declare const TLS_CHAIN_CODES: readonly string[];
7
+ /** Node's code for a certificate that does not name the host it was checked against. */
8
+ export declare const TLS_HOST_CODE = "ERR_TLS_CERT_ALTNAME_INVALID";
9
+ /** `path` with a leading `~` replaced by `home`, as a shell would; any other path unchanged. */
10
+ export declare function homeExpanded(path: string, home: string): string;
11
+ /** What a verifying connection checked the server certificate against. */
12
+ export interface TlsVerification {
13
+ /** The CA file field's form label, such as `SSL root certificate`. */
14
+ readonly caLabel: string;
15
+ /** The CA file's path; null when the system's trusted CAs were used. */
16
+ readonly caPath: string | null;
17
+ /** The host name the certificate had to carry. */
18
+ readonly host: string;
19
+ }
20
+ /**
21
+ * Says what a verification failure did not match, or null when `cause` is not one: the CA file
22
+ * (or the system's CAs) for a chain error, the host name for a name mismatch.
23
+ */
24
+ export declare function tlsFailureMessage(cause: unknown, verification: TlsVerification): string | null;
25
+ /** Words a failure to read one of the TLS files, naming the field and the path. */
26
+ export declare function tlsFileMessage(label: string, path: string, cause: unknown): string;
27
+ /** Whether a PEM private key needs a passphrase: PKCS#8 encrypted, or traditional with `Proc-Type`. */
28
+ export declare function tlsKeyIsEncrypted(text: string): boolean;
29
+ /**
30
+ * Words a client key or certificate OpenSSL refused, naming the key file. Any failure on an
31
+ * `encrypted` key is put down to the passphrase, since OpenSSL's codes for it vary by version.
32
+ */
33
+ export declare function tlsKeyMessage(label: string, path: string, cause: unknown, encrypted?: boolean): string;
package/dist/tls.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Wording for TLS failures shared by the network plugins (decision 072). Pure: each plugin reads
3
+ * its files and drives its own driver, and hands the error here to say what did not match.
4
+ */
5
+ /** Node's codes for a server certificate whose chain the trusted CAs do not vouch for. */
6
+ export const TLS_CHAIN_CODES = [
7
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
8
+ "UNABLE_TO_GET_ISSUER_CERT",
9
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
10
+ "SELF_SIGNED_CERT_IN_CHAIN",
11
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
12
+ "CERT_SIGNATURE_FAILURE",
13
+ "CERT_UNTRUSTED",
14
+ ];
15
+ /** Node's codes for a server certificate outside its validity dates. */
16
+ const TLS_DATE_CODES = ["CERT_HAS_EXPIRED", "CERT_NOT_YET_VALID"];
17
+ /** Node's code for a certificate that does not name the host it was checked against. */
18
+ export const TLS_HOST_CODE = "ERR_TLS_CERT_ALTNAME_INVALID";
19
+ /** Node's codes for a private key it could not decrypt with the passphrase given, or without one. */
20
+ const BAD_DECRYPT_CODES = [
21
+ "ERR_OSSL_BAD_DECRYPT",
22
+ "ERR_OSSL_EVP_BAD_DECRYPT",
23
+ "ERR_MISSING_PASSPHRASE",
24
+ ];
25
+ const HOME_PREFIX = /^~(?=$|[/\\])/;
26
+ /** `path` with a leading `~` replaced by `home`, as a shell would; any other path unchanged. */
27
+ export function homeExpanded(path, home) {
28
+ return path.replace(HOME_PREFIX, () => home);
29
+ }
30
+ const FILE_REASONS = {
31
+ ENOENT: "the file does not exist",
32
+ EACCES: "permission denied",
33
+ EPERM: "permission denied",
34
+ EISDIR: "it is a folder, not a file",
35
+ };
36
+ function codeOf(cause) {
37
+ if (typeof cause !== "object" || cause === null || !("code" in cause))
38
+ return null;
39
+ return typeof cause.code === "string" ? cause.code : null;
40
+ }
41
+ function reasonOf(cause) {
42
+ return cause instanceof Error ? cause.message : String(cause);
43
+ }
44
+ /**
45
+ * OpenSSL's `X509_verify_cert_error_string` for each code, lower-cased and with hyphens as spaces:
46
+ * OpenSSL 3 writes "self-signed", BoringSSL (Electron) "self signed".
47
+ */
48
+ const VERIFY_REASONS = {
49
+ "unable to verify the first certificate": "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
50
+ "unable to get issuer certificate": "UNABLE_TO_GET_ISSUER_CERT",
51
+ "unable to get local issuer certificate": "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
52
+ "self signed certificate in certificate chain": "SELF_SIGNED_CERT_IN_CHAIN",
53
+ "self signed certificate": "DEPTH_ZERO_SELF_SIGNED_CERT",
54
+ "certificate signature failure": "CERT_SIGNATURE_FAILURE",
55
+ "certificate not trusted": "CERT_UNTRUSTED",
56
+ "certificate has expired": "CERT_HAS_EXPIRED",
57
+ "certificate is not yet valid": "CERT_NOT_YET_VALID",
58
+ };
59
+ const KNOWN_CODES = [...TLS_CHAIN_CODES, ...TLS_DATE_CODES, TLS_HOST_CODE];
60
+ /**
61
+ * Node's code for a TLS failure. A driver may overwrite it (mysql2 sets `HANDSHAKE_SSL_ERROR`,
62
+ * `lib/commands/client_handshake.js`), so the error is recognised by what survives: the
63
+ * `reason`, `host` and `cert` of a `checkServerIdentity` error, or OpenSSL's message.
64
+ */
65
+ function tlsCodeOf(cause) {
66
+ const code = codeOf(cause);
67
+ if (code !== null && KNOWN_CODES.includes(code))
68
+ return code;
69
+ if (typeof cause !== "object" || cause === null)
70
+ return null;
71
+ if ("reason" in cause && "host" in cause && "cert" in cause)
72
+ return TLS_HOST_CODE;
73
+ return VERIFY_REASONS[reasonOf(cause).toLowerCase().replaceAll("-", " ")] ?? null;
74
+ }
75
+ /**
76
+ * Says what a verification failure did not match, or null when `cause` is not one: the CA file
77
+ * (or the system's CAs) for a chain error, the host name for a name mismatch.
78
+ */
79
+ export function tlsFailureMessage(cause, verification) {
80
+ const code = tlsCodeOf(cause);
81
+ if (code === null)
82
+ return null;
83
+ const reason = reasonOf(cause);
84
+ if (code === TLS_HOST_CODE) {
85
+ return `The server certificate is not valid for host ${verification.host} (${reason}). Use Verify CA to check the certificate without its host name.`;
86
+ }
87
+ if (TLS_DATE_CODES.includes(code))
88
+ return `The server certificate is outside its validity dates (${reason}).`;
89
+ if (!TLS_CHAIN_CODES.includes(code))
90
+ return null;
91
+ const { caLabel, caPath } = verification;
92
+ if (caPath === null) {
93
+ return `The server certificate is not signed by a CA this system trusts (${reason}). Choose the server's CA file as the ${caLabel}.`;
94
+ }
95
+ return `The server certificate is not signed by the CA in ${caPath} (${caLabel}): ${reason}.`;
96
+ }
97
+ /** Words a failure to read one of the TLS files, naming the field and the path. */
98
+ export function tlsFileMessage(label, path, cause) {
99
+ const code = codeOf(cause);
100
+ const reason = (code === null ? undefined : FILE_REASONS[code]) ?? reasonOf(cause);
101
+ return `Cannot read the ${label} ${path}: ${reason}.`;
102
+ }
103
+ const ENCRYPTED_KEY = /-----BEGIN ENCRYPTED PRIVATE KEY-----|^Proc-Type: 4,ENCRYPTED/m;
104
+ /** Whether a PEM private key needs a passphrase: PKCS#8 encrypted, or traditional with `Proc-Type`. */
105
+ export function tlsKeyIsEncrypted(text) {
106
+ return ENCRYPTED_KEY.test(text);
107
+ }
108
+ /**
109
+ * Words a client key or certificate OpenSSL refused, naming the key file. Any failure on an
110
+ * `encrypted` key is put down to the passphrase, since OpenSSL's codes for it vary by version.
111
+ */
112
+ export function tlsKeyMessage(label, path, cause, encrypted = false) {
113
+ const code = codeOf(cause);
114
+ if (encrypted || (code !== null && BAD_DECRYPT_CODES.includes(code))) {
115
+ return `Cannot decrypt the ${label} ${path}: the passphrase is wrong or missing.`;
116
+ }
117
+ return `Cannot use the ${label} ${path}: ${reasonOf(cause)}.`;
118
+ }
package/dist/url.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { ConnectionFieldKind, ConnectionUrlForm, ConnectionUrlPart } from "./manifest.js";
2
+ /** A form value as `datasources.json` holds it. */
3
+ export type ConnectionValue = string | number | boolean;
4
+ /** What a pasted connection URL fills in (decision 055). */
5
+ export interface ParsedConnectionUrl {
6
+ /** Field key to value, for the fields the URL names; secrets are not among them. */
7
+ readonly values: Readonly<Record<string, ConnectionValue>>;
8
+ /** Secret field key to value, such as the password; for the secret store, never `datasources.json`. */
9
+ readonly secrets: Readonly<Record<string, string>>;
10
+ /** The query parameters, or their values, no field takes, as the user should see them listed. */
11
+ readonly ignored: readonly string[];
12
+ }
13
+ /** The text is not a connection URL this plugin reads; the message says what it expected. */
14
+ export declare class ConnectionUrlError extends Error {
15
+ readonly name = "ConnectionUrlError";
16
+ }
17
+ /** A manifest's `connection.url`, read-only so a host's copy of it serves as well. */
18
+ export interface ConnectionUrlSpec {
19
+ readonly schemes: readonly string[];
20
+ readonly form: ConnectionUrlForm;
21
+ readonly parts: Readonly<Partial<Record<ConnectionUrlPart, string>>>;
22
+ readonly params?: readonly {
23
+ readonly name: string;
24
+ readonly key: string;
25
+ readonly values?: Readonly<Record<string, string>> | undefined;
26
+ }[] | undefined;
27
+ }
28
+ interface UrlField {
29
+ readonly key: string;
30
+ readonly kind: ConnectionFieldKind;
31
+ readonly default?: ConnectionValue | undefined;
32
+ readonly secret?: boolean | undefined;
33
+ }
34
+ /** The part of a manifest the URL functions read: its connection fields and URL. */
35
+ export interface ConnectionForm {
36
+ readonly connection: {
37
+ readonly fields: readonly UrlField[];
38
+ readonly url?: ConnectionUrlSpec | undefined;
39
+ };
40
+ }
41
+ /**
42
+ * Reads `text` as the plugin's connection URL: `scheme://user:password@host:port/database?params`
43
+ * for a server, or a path, bare or as a `sqlite:`/`file:` URL, for a file. A `jdbc:` prefix is
44
+ * dropped. Throws `ConnectionUrlError` when it is none.
45
+ */
46
+ export declare function parseConnectionUrl(form: ConnectionForm, text: string): ParsedConnectionUrl;
47
+ /**
48
+ * The connection URL of `config`, in the plugin's first scheme. Never carries a secret field, so
49
+ * the result is safe to copy, show and log; params equal to their field's default are left out.
50
+ */
51
+ export declare function formatConnectionUrl(form: ConnectionForm, config: Readonly<Record<string, unknown>>): string;
52
+ export {};
package/dist/url.js ADDED
@@ -0,0 +1,219 @@
1
+ /** The text is not a connection URL this plugin reads; the message says what it expected. */
2
+ export class ConnectionUrlError extends Error {
3
+ name = "ConnectionUrlError";
4
+ }
5
+ /** Tools written for the JVM prefix their URLs with it; the rest of the URL is the usual one. */
6
+ const JDBC_PREFIX = "jdbc:";
7
+ const MEMORY_PATH = ":memory:";
8
+ const SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
9
+ const WINDOWS_DRIVE_PATH = /^\/[A-Za-z]:[/\\]/;
10
+ const LEADING_SLASHES = /^\/+/;
11
+ const LOCAL_HOSTS = new Set(["", "localhost"]);
12
+ /** libpq's names for the parts of the URL, accepted as query parameters by every server form. */
13
+ const PART_PARAMS = {
14
+ host: "host",
15
+ port: "port",
16
+ dbname: "database",
17
+ database: "database",
18
+ user: "user",
19
+ password: "password",
20
+ };
21
+ function urlOf(form) {
22
+ const url = form.connection.url;
23
+ if (url === undefined)
24
+ throw new ConnectionUrlError("This data source type has no connection URL.");
25
+ return url;
26
+ }
27
+ function schemeList(url) {
28
+ const written = url.schemes.map((scheme) => (url.form === "server" ? `${scheme}://` : `${scheme}:`));
29
+ return written.length === 1 ? (written[0] ?? "") : `${written.slice(0, -1).join(", ")} or ${written.at(-1) ?? ""}`;
30
+ }
31
+ function decoded(text) {
32
+ try {
33
+ return decodeURIComponent(text);
34
+ }
35
+ catch {
36
+ throw new ConnectionUrlError(`"${text}" is not valid percent-encoding; write % as %25.`);
37
+ }
38
+ }
39
+ function fieldOf(form, key) {
40
+ return form.connection.fields.find((field) => field.key === key);
41
+ }
42
+ function coerced(field, raw, label) {
43
+ if (field?.kind === "number") {
44
+ const value = Number(raw);
45
+ if (raw.trim() === "" || !Number.isFinite(value))
46
+ throw new ConnectionUrlError(`The ${label} "${raw}" is not a number.`);
47
+ return value;
48
+ }
49
+ if (field?.kind === "checkbox")
50
+ return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
51
+ return raw;
52
+ }
53
+ class Collected {
54
+ form;
55
+ url;
56
+ values = {};
57
+ secrets = {};
58
+ ignored = [];
59
+ constructor(form, url) {
60
+ this.form = form;
61
+ this.url = url;
62
+ }
63
+ part(part, raw) {
64
+ const key = this.url.parts[part];
65
+ if (key === undefined || raw === "")
66
+ return;
67
+ const field = fieldOf(this.form, key);
68
+ if (field?.secret === true)
69
+ this.secrets[key] = raw;
70
+ else
71
+ this.values[key] = coerced(field, raw, part);
72
+ }
73
+ param(name, raw) {
74
+ const lower = name.toLowerCase();
75
+ const param = this.url.params?.find((candidate) => candidate.name.toLowerCase() === lower);
76
+ if (param === undefined) {
77
+ const part = this.url.form === "server" ? PART_PARAMS[lower] : undefined;
78
+ if (part !== undefined && this.url.parts[part] !== undefined)
79
+ this.part(part, raw);
80
+ else
81
+ this.ignored.push(name);
82
+ return;
83
+ }
84
+ if (param.values === undefined) {
85
+ this.values[param.key] = coerced(fieldOf(this.form, param.key), raw, name);
86
+ return;
87
+ }
88
+ const match = Object.entries(param.values).find(([value]) => value.toLowerCase() === raw.toLowerCase());
89
+ if (match === undefined)
90
+ this.ignored.push(`${name}=${raw}`);
91
+ else
92
+ this.values[param.key] = match[1];
93
+ }
94
+ query(search) {
95
+ for (const [name, value] of new URLSearchParams(search))
96
+ this.param(name, value);
97
+ }
98
+ done() {
99
+ return { values: this.values, secrets: this.secrets, ignored: this.ignored };
100
+ }
101
+ }
102
+ function parseServer(form, url, text) {
103
+ const scheme = SCHEME.exec(text)?.[1]?.toLowerCase();
104
+ if (scheme === undefined || !url.schemes.includes(scheme) || !text.slice(scheme.length + 1).startsWith("//")) {
105
+ throw new ConnectionUrlError(`Expected a URL starting with ${schemeList(url)}.`);
106
+ }
107
+ let parsed;
108
+ try {
109
+ parsed = new URL(text);
110
+ }
111
+ catch {
112
+ throw new ConnectionUrlError(`That is not a valid ${scheme}:// URL; check the port is a number and special characters are percent-encoded.`);
113
+ }
114
+ const collected = new Collected(form, url);
115
+ collected.part("host", decoded(parsed.hostname.replace(/^\[(.*)\]$/, "$1")));
116
+ collected.part("port", parsed.port);
117
+ collected.part("database", decoded(parsed.pathname.replace(LEADING_SLASHES, "")));
118
+ collected.part("user", decoded(parsed.username));
119
+ collected.part("password", decoded(parsed.password));
120
+ collected.query(parsed.search);
121
+ return collected.done();
122
+ }
123
+ function parseFile(form, url, text) {
124
+ const collected = new Collected(form, url);
125
+ const scheme = SCHEME.exec(text)?.[1]?.toLowerCase();
126
+ // One letter before the colon is a Windows drive, not a scheme.
127
+ if (scheme === undefined || scheme.length === 1 || text === MEMORY_PATH) {
128
+ collected.part("path", text);
129
+ return collected.done();
130
+ }
131
+ if (!url.schemes.includes(scheme)) {
132
+ throw new ConnectionUrlError(`Expected a file path, or a URL starting with ${schemeList(url)}.`);
133
+ }
134
+ let rest = text.slice(scheme.length + 1);
135
+ const queryAt = rest.search(/[?#]/);
136
+ if (queryAt >= 0) {
137
+ const query = rest.slice(queryAt);
138
+ rest = rest.slice(0, queryAt);
139
+ if (query.startsWith("?"))
140
+ collected.query(query.split("#")[0] ?? "");
141
+ }
142
+ if (rest.startsWith("//")) {
143
+ const authorityEnd = rest.indexOf("/", 2);
144
+ const authority = authorityEnd < 0 ? rest.slice(2) : rest.slice(2, authorityEnd);
145
+ if (authority === MEMORY_PATH)
146
+ rest = MEMORY_PATH;
147
+ else if (!LOCAL_HOSTS.has(authority.toLowerCase())) {
148
+ throw new ConnectionUrlError(`"${authority}" names another machine; a file URL reads a local file.`);
149
+ }
150
+ else
151
+ rest = authorityEnd < 0 ? "" : rest.slice(authorityEnd).replace(LEADING_SLASHES, "/");
152
+ }
153
+ const path = decoded(rest);
154
+ collected.part("path", WINDOWS_DRIVE_PATH.test(path) ? path.slice(1) : path);
155
+ return collected.done();
156
+ }
157
+ /**
158
+ * Reads `text` as the plugin's connection URL: `scheme://user:password@host:port/database?params`
159
+ * for a server, or a path, bare or as a `sqlite:`/`file:` URL, for a file. A `jdbc:` prefix is
160
+ * dropped. Throws `ConnectionUrlError` when it is none.
161
+ */
162
+ export function parseConnectionUrl(form, text) {
163
+ const url = urlOf(form);
164
+ let trimmed = text.trim();
165
+ if (trimmed.toLowerCase().startsWith(JDBC_PREFIX))
166
+ trimmed = trimmed.slice(JDBC_PREFIX.length);
167
+ if (trimmed === "")
168
+ throw new ConnectionUrlError(`Expected a URL starting with ${schemeList(url)}.`);
169
+ return url.form === "server" ? parseServer(form, url, trimmed) : parseFile(form, url, trimmed);
170
+ }
171
+ function isConnectionValue(value) {
172
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
173
+ }
174
+ function written(value) {
175
+ return String(value);
176
+ }
177
+ function encodedPath(path) {
178
+ return encodeURI(path).replaceAll("?", "%3F").replaceAll("#", "%23");
179
+ }
180
+ /**
181
+ * The connection URL of `config`, in the plugin's first scheme. Never carries a secret field, so
182
+ * the result is safe to copy, show and log; params equal to their field's default are left out.
183
+ */
184
+ export function formatConnectionUrl(form, config) {
185
+ const url = urlOf(form);
186
+ const scheme = url.schemes[0] ?? "";
187
+ const valueOf = (part) => {
188
+ const key = url.parts[part];
189
+ if (key === undefined || fieldOf(form, key)?.secret === true)
190
+ return "";
191
+ const value = config[key];
192
+ return isConnectionValue(value) ? written(value) : "";
193
+ };
194
+ const params = new URLSearchParams();
195
+ for (const param of url.params ?? []) {
196
+ const value = config[param.key];
197
+ const field = fieldOf(form, param.key);
198
+ if (field?.secret === true || !isConnectionValue(value) || value === "" || value === field?.default)
199
+ continue;
200
+ const text = param.values === undefined
201
+ ? written(value)
202
+ : Object.entries(param.values).find(([, mapped]) => mapped === value)?.[0];
203
+ if (text !== undefined)
204
+ params.append(param.name, text);
205
+ }
206
+ const query = params.size === 0 ? "" : `?${params.toString()}`;
207
+ if (url.form === "file") {
208
+ const path = valueOf("path");
209
+ if (path === MEMORY_PATH || !path.startsWith("/"))
210
+ return `${scheme}:${encodedPath(path)}${query}`;
211
+ return `${scheme}://${encodedPath(path)}${query}`;
212
+ }
213
+ const user = valueOf("user");
214
+ const host = valueOf("host");
215
+ const port = valueOf("port");
216
+ const database = valueOf("database");
217
+ const authority = `${user === "" ? "" : `${encodeURIComponent(user)}@`}${host.includes(":") ? `[${host}]` : encodeURIComponent(host)}${port === "" ? "" : `:${port}`}`;
218
+ return `${scheme}://${authority}${database === "" ? "" : `/${encodeURIComponent(database)}`}${query}`;
219
+ }
@@ -0,0 +1,44 @@
1
+ /** Bytes shown of a binary value; the rest stays in the database (decision 009). */
2
+ export declare const BYTES_PREVIEW_LENGTH = 32;
3
+ export interface BigIntValue {
4
+ readonly kind: "bigint";
5
+ readonly value: string;
6
+ }
7
+ export interface BytesValue {
8
+ readonly kind: "bytes";
9
+ /**
10
+ * Lower-case hex of the first `BYTES_PREVIEW_LENGTH` bytes, or of every byte when the statement
11
+ * ran with `ExecuteOptions.fullBytes`; the value is complete exactly when `preview.length` is
12
+ * twice `length`.
13
+ */
14
+ readonly preview: string;
15
+ readonly length: number;
16
+ }
17
+ export interface JsonValue {
18
+ readonly kind: "json";
19
+ readonly text: string;
20
+ }
21
+ export interface DateValue {
22
+ readonly kind: "date";
23
+ readonly iso: string;
24
+ }
25
+ /**
26
+ * One cell as it crosses a process boundary. Anything structured clone would mangle, or that is
27
+ * too large to ship whole, is tagged.
28
+ */
29
+ export type WireValue = null | boolean | number | string | BigIntValue | BytesValue | JsonValue | DateValue;
30
+ /**
31
+ * What a column's values are. The grid sorts, aligns and draws its header glyph by this. The host
32
+ * reads it from the type name unless the plugin names it on the `Column` (decision 101).
33
+ */
34
+ export type TypeClass = "integer" | "float" | "decimal" | "boolean" | "temporal" | "json" | "bytes" | "text";
35
+ export interface WireValueOptions {
36
+ /** Ship every byte of a binary value instead of the preview (`ExecuteOptions.fullBytes`). */
37
+ readonly fullBytes?: boolean;
38
+ }
39
+ /** A binary value as it crosses the wire: the preview, or every byte with `fullBytes`. */
40
+ export declare function toBytesValue(bytes: Uint8Array, options?: WireValueOptions): BytesValue;
41
+ /** A 64-bit integer as a number when it is exact in a double, tagged otherwise. */
42
+ export declare function toWireInteger(value: bigint | string): number | BigIntValue;
43
+ /** Converts what a JavaScript driver typically returns into a `WireValue`. */
44
+ export declare function toWireValue(value: unknown, options?: WireValueOptions): WireValue;
package/dist/values.js ADDED
@@ -0,0 +1,44 @@
1
+ /** Bytes shown of a binary value; the rest stays in the database (decision 009). */
2
+ export const BYTES_PREVIEW_LENGTH = 32;
3
+ const HEX_RADIX = 16;
4
+ const HEX_BYTE_WIDTH = 2;
5
+ const BYTE_VALUES = 256;
6
+ const HEX_DIGITS = Array.from({ length: BYTE_VALUES }, (_, byte) => byte.toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, "0"));
7
+ function toHex(bytes) {
8
+ const digits = new Array(bytes.byteLength);
9
+ for (let index = 0; index < bytes.byteLength; index += 1)
10
+ digits[index] = HEX_DIGITS[bytes[index] ?? 0] ?? "";
11
+ return digits.join("");
12
+ }
13
+ /** A binary value as it crosses the wire: the preview, or every byte with `fullBytes`. */
14
+ export function toBytesValue(bytes, options = {}) {
15
+ const shown = options.fullBytes === true ? bytes : bytes.subarray(0, BYTES_PREVIEW_LENGTH);
16
+ return { kind: "bytes", preview: toHex(shown), length: bytes.byteLength };
17
+ }
18
+ /** A 64-bit integer as a number when it is exact in a double, tagged otherwise. */
19
+ export function toWireInteger(value) {
20
+ const exact = BigInt(value);
21
+ if (exact >= BigInt(Number.MIN_SAFE_INTEGER) && exact <= BigInt(Number.MAX_SAFE_INTEGER))
22
+ return Number(exact);
23
+ return { kind: "bigint", value: exact.toString() };
24
+ }
25
+ /** Converts what a JavaScript driver typically returns into a `WireValue`. */
26
+ export function toWireValue(value, options = {}) {
27
+ if (value === null || value === undefined)
28
+ return null;
29
+ switch (typeof value) {
30
+ case "boolean":
31
+ case "number":
32
+ case "string":
33
+ return value;
34
+ case "bigint":
35
+ return { kind: "bigint", value: value.toString() };
36
+ default:
37
+ break;
38
+ }
39
+ if (value instanceof Uint8Array)
40
+ return toBytesValue(value, options);
41
+ if (value instanceof Date)
42
+ return { kind: "date", iso: value.toISOString() };
43
+ return { kind: "json", text: JSON.stringify(value) };
44
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The plugin contract's version, by semver (decision 106): a host loads a plugin built for the same
3
+ * major and an older or equal minor. Typed `string` so a bump does not change the API report.
4
+ */
5
+ export declare const API_VERSION: string;
6
+ interface Version {
7
+ readonly major: number;
8
+ readonly minor: number;
9
+ readonly patch: number;
10
+ }
11
+ /** `major.minor.patch` and nothing else; pre-release and build tags are not accepted. */
12
+ export declare function parseVersion(text: string): Version | null;
13
+ /**
14
+ * Why a plugin built against `apiVersion` cannot load on `hostVersion`: it needs a `newer` host
15
+ * (a later minor, or a later major), it was built for an `olderMajor`, or the version is `invalid`.
16
+ * Null when it loads.
17
+ */
18
+ export type Incompatibility = "newer" | "olderMajor" | "invalid";
19
+ export declare function incompatibility(apiVersion: string, hostVersion?: string): Incompatibility | null;
20
+ /** Whether a plugin built against `apiVersion` can be loaded by this `API_VERSION`, by semver. */
21
+ export declare function isCompatible(apiVersion: string, hostVersion?: string): boolean;
22
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The plugin contract's version, by semver (decision 106): a host loads a plugin built for the same
3
+ * major and an older or equal minor. Typed `string` so a bump does not change the API report.
4
+ */
5
+ export const API_VERSION = "1.1.0";
6
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
7
+ /** `major.minor.patch` and nothing else; pre-release and build tags are not accepted. */
8
+ export function parseVersion(text) {
9
+ const match = SEMVER.exec(text);
10
+ if (match === null)
11
+ return null;
12
+ const [, major, minor, patch] = match;
13
+ return { major: Number(major), minor: Number(minor), patch: Number(patch) };
14
+ }
15
+ export function incompatibility(apiVersion, hostVersion = API_VERSION) {
16
+ const plugin = parseVersion(apiVersion);
17
+ const host = parseVersion(hostVersion);
18
+ if (plugin === null || host === null)
19
+ return "invalid";
20
+ if (plugin.major < host.major)
21
+ return "olderMajor";
22
+ if (plugin.major > host.major || plugin.minor > host.minor)
23
+ return "newer";
24
+ return null;
25
+ }
26
+ /** Whether a plugin built against `apiVersion` can be loaded by this `API_VERSION`, by semver. */
27
+ export function isCompatible(apiVersion, hostVersion = API_VERSION) {
28
+ return incompatibility(apiVersion, hostVersion) === null;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@tablecraft-app/plugin-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "The datasource plugin contract for TableCraft",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tranphucbol/table-craft.git",
10
+ "directory": "packages/plugin-api"
11
+ },
12
+ "dependencies": {
13
+ "sql-formatter": "^15.9.0",
14
+ "zod": "^4.1.0"
15
+ },
16
+ "peerDependencies": {
17
+ "vitest": "^4.1.11"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "vitest": {
21
+ "optional": true
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./sql": {
36
+ "types": "./dist/sql/index.d.ts",
37
+ "default": "./dist/sql/index.js"
38
+ },
39
+ "./testing": {
40
+ "types": "./dist/testing/index.d.ts",
41
+ "default": "./dist/testing/index.js"
42
+ }
43
+ }
44
+ }