@coldsmirk/inkstone-elasticsearch 0.22.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/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @coldsmirk/inkstone-elasticsearch
2
+
3
+ Elasticsearch request editing intelligence for Monaco: requests in Kibana's console spelling (`GET /orders/_search` on the first line, the body after), endpoint and index completion on the path, body keys by where the caret stands (the query DSL's clauses under `query`, a `bool`'s clauses, the aggregation types, a mapping's field options …), the request read as the engine reads it, markers, and a drop-in React `<ElasticsearchEditor>`.
4
+
5
+ Part of [inkstone](https://github.com/coldsmirk/inkstone). Built for the [**polyglot**](https://github.com/coldsmirk/polyglot) sidecar's Elasticsearch engine, which takes one request in this very spelling.
6
+
7
+ ## Entries
8
+
9
+ - **`@coldsmirk/inkstone-elasticsearch`** — the framework-free core: the request reader, the endpoint table, the body vocabulary, the assist, the markers. Zero editor dependencies.
10
+ - **`@coldsmirk/inkstone-elasticsearch/monaco`** — `defineElasticsearchLanguage(monaco)` (the `elasticsearch` id with its Monarch grammar, once per monaco module), `registerElasticsearchLanguage(monaco, modelPath, hooks)` (completion and hover for one model), `setElasticsearchMarkers`. Peer: `monaco-editor`.
11
+ - **`@coldsmirk/inkstone-elasticsearch/react`** — `<ElasticsearchEditor>`: a controlled request editor built on `@coldsmirk/inkstone-react`'s `<MonacoEditor>`, with a run key (Ctrl/Cmd-Enter handing over the live caret), caret tracking, an insert handle for index trees, and the markers refreshed on every change. Peers: `monaco-editor`, `react`.
12
+
13
+ ## The shape
14
+
15
+ ```ts
16
+ import { createElasticsearchAssist, requestMarkers, runnableRequest } from "@coldsmirk/inkstone-elasticsearch";
17
+
18
+ // The catalog is YOURS to fetch — and it MUST be a cache-backed read (the tree's own list),
19
+ // never a fresh fetch: completion reads it on every keystroke in a path.
20
+ const assist = createElasticsearchAssist({ indices: () => cachedIndices() }); // CatalogIndex[]
21
+
22
+ // What one run should send: the selection where there is one, else the request the caret is in
23
+ // — a document may hold several, each starting at its request line.
24
+ const runnable = runnableRequest(text, from, to);
25
+
26
+ if (runnable.kind === "one") {
27
+ send(runnable.text);
28
+ }
29
+
30
+ // A code per fault — the host owns the wording.
31
+ for (const marker of requestMarkers(text)) {
32
+ console.log(marker.code, marker.severity, marker.from, marker.to);
33
+ }
34
+ ```
35
+
36
+ ## The mirror invariant
37
+
38
+ `readRequest` reads a request exactly as the engine does before sending it: the text stripped, the first line split into a method and a path on its first blank, the method uppercased and one of `GET` / `POST` / `PUT` / `DELETE` / `HEAD` / `PATCH`, the path starting with `/`, the rest the body — newline-delimited under `_bulk` and `_msearch`, JSON otherwise. What it refuses, the engine refuses, as a code (`"no-request-line"`, `"unknown-method"`, `"path-without-slash"`). A change to the engine's reading requires a matching release of this package; `request.test.ts` carries the cases.
39
+
40
+ ## The vocabulary
41
+
42
+ The endpoint table and the body keys are curated in `api.ts` — the paths a bench reaches for and the keys the query DSL, the aggregations and the mappings take, by context. They are a hint of the API's shape, never its authority: a key the table does not know is still sent, and the server's answer is what counts.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pnpm add @coldsmirk/inkstone-elasticsearch monaco-editor
48
+ ```
49
+
50
+ Every peer is optional, because the root entry needs none of them: `/monaco` needs `monaco-editor`, and `/react` additionally needs `react` (>= 19) and `@coldsmirk/inkstone-react`. Never import `monaco-editor` statically — take the module from `onMount` / `ensureMonacoHost()`; type-only imports erase and are fine.
@@ -0,0 +1,34 @@
1
+ import { _ as readRequest, a as ElasticsearchAssist, b as runnableRequest, c as ElasticsearchHoverCard, d as REQUEST_METHODS, f as RequestMethod, g as caretContext, h as RunnableRequest, i as CatalogIndex, l as createElasticsearchAssist, m as RequestRefusal, n as ElasticsearchMarkerCode, o as ElasticsearchCatalog, p as RequestRead, r as requestMarkers, s as ElasticsearchCompletionCandidate, t as ElasticsearchMarker, u as CaretContext, v as requestAt, x as takesLines, y as requestSpans } from "./markers-BctX905j.js";
2
+ //#region src/api.d.ts
3
+ /**
4
+ * The vocabulary the assist offers: the REST endpoints a bench reaches for, and the keys of a
5
+ * body by where the caret stands in it. Curated, not exhaustive — the console's own
6
+ * completion is a hint of the API's shape, and the server's answer is the authority.
7
+ */
8
+ /**
9
+ * One endpoint: its path with `{index}`, `{id}` and `{name}` for the parts the reader fills
10
+ * in, the methods it takes, and one line on what it does.
11
+ */
12
+ interface ApiEndpoint {
13
+ path: string;
14
+ methods: RequestMethod[];
15
+ summary: string;
16
+ }
17
+ declare const API_ENDPOINTS: readonly ApiEndpoint[];
18
+ /**
19
+ * The endpoint a path names, `{…}` parts matching one segment each; null where none does.
20
+ * The query string is not part of the match.
21
+ */
22
+ declare function endpointOf(path: string): ApiEndpoint | null;
23
+ /**
24
+ * What the request is for, read off its path — which decides the top-level keys of its body.
25
+ */
26
+ type BodyKind = "search" | "count" | "index" | "mapping" | "settings" | "document" | "update" | "update-by-query" | "reindex" | "aliases" | "analyze" | "sql" | "cluster-settings" | "template" | "explain" | "mget" | "other";
27
+ declare function bodyKindOf(method: string, path: string): BodyKind;
28
+ /**
29
+ * The keys to offer at a key position in the body, by the request the body belongs to and the
30
+ * keys enclosing the caret; empty where the names are the reader's own (a field, an aggregation).
31
+ */
32
+ declare function bodyKeys(kind: BodyKind, path: readonly string[]): string[];
33
+ //#endregion
34
+ export { API_ENDPOINTS, type ApiEndpoint, type BodyKind, type CaretContext, type CatalogIndex, type ElasticsearchAssist, type ElasticsearchCatalog, type ElasticsearchCompletionCandidate, type ElasticsearchHoverCard, type ElasticsearchMarker, type ElasticsearchMarkerCode, REQUEST_METHODS, type RequestMethod, type RequestRead, type RequestRefusal, type RunnableRequest, bodyKeys, bodyKindOf, caretContext, createElasticsearchAssist, endpointOf, readRequest, requestAt, requestMarkers, requestSpans, runnableRequest, takesLines };
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ import { a as requestAt, c as takesLines, d as bodyKindOf, f as endpointOf, i as readRequest, l as API_ENDPOINTS, n as REQUEST_METHODS, o as requestSpans, r as caretContext, s as runnableRequest, t as requestMarkers, u as bodyKeys } from "./markers-DBJshw-Y.js";
2
+ //#region src/assist.ts
3
+ const NO_CATALOG = { indices: () => [] };
4
+ function tailOf(endpoint, prefix) {
5
+ return endpoint.path.slice(prefix.length).replaceAll(/\/\{[^}]+\}/gu, "").replace(/^\//u, "");
6
+ }
7
+ function endpointCandidate(endpoint, label, replace) {
8
+ return {
9
+ label,
10
+ kind: "endpoint",
11
+ insertText: label,
12
+ replace,
13
+ detail: endpoint.methods.join(" | "),
14
+ documentation: endpoint.summary,
15
+ sortGroup: 1
16
+ };
17
+ }
18
+ function createElasticsearchAssist(catalog = NO_CATALOG) {
19
+ return {
20
+ complete: (text, offset) => {
21
+ const context = caretContext(text, offset);
22
+ switch (context.kind) {
23
+ case "method": return REQUEST_METHODS.map((method) => {
24
+ return {
25
+ label: method,
26
+ kind: "method",
27
+ insertText: `${method} `,
28
+ replace: context.typed.length,
29
+ sortGroup: 0
30
+ };
31
+ });
32
+ case "path": {
33
+ const replace = context.typed.length;
34
+ if (context.segments.length === 0) {
35
+ const indices = catalog.indices().map((index) => {
36
+ return {
37
+ label: index.name,
38
+ kind: index.alias ? "alias" : "index",
39
+ insertText: index.name,
40
+ replace,
41
+ sortGroup: 0
42
+ };
43
+ });
44
+ const roots = API_ENDPOINTS.filter((endpoint) => endpoint.path !== "/" && !endpoint.path.startsWith("/{index}")).map((endpoint) => endpointCandidate(endpoint, tailOf(endpoint, "/"), replace));
45
+ return [...indices, ...roots];
46
+ }
47
+ if (context.segments.length === 1 && !context.segments[0].startsWith("_")) {
48
+ const seen = /* @__PURE__ */ new Set();
49
+ const under = [];
50
+ for (const endpoint of API_ENDPOINTS) {
51
+ if (!endpoint.path.startsWith("/{index}/")) continue;
52
+ const label = tailOf(endpoint, "/{index}/");
53
+ if (!seen.has(label)) {
54
+ seen.add(label);
55
+ under.push(endpointCandidate(endpoint, label, replace));
56
+ }
57
+ }
58
+ return under;
59
+ }
60
+ return [];
61
+ }
62
+ case "body-key": {
63
+ const kind = bodyKindOf(context.request.method, context.request.path);
64
+ return bodyKeys(kind, context.path).map((key) => context.quoted ? {
65
+ label: key,
66
+ kind: "key",
67
+ insertText: key,
68
+ replace: context.typed.length,
69
+ sortGroup: 0
70
+ } : {
71
+ label: key,
72
+ kind: "key",
73
+ insertText: `"${key}": $0`,
74
+ snippet: true,
75
+ replace: 0,
76
+ sortGroup: 0
77
+ });
78
+ }
79
+ default: return [];
80
+ }
81
+ },
82
+ hover: (text, offset) => {
83
+ const span = requestAt(text, offset);
84
+ const request = readRequest(text.slice(span.from, span.to));
85
+ if (request.refusal !== null && request.refusal !== "unknown-method") return null;
86
+ const lineFrom = span.from + request.line.from;
87
+ const lineTo = span.from + request.line.to;
88
+ const line = text.slice(lineFrom, lineTo);
89
+ const pathAt = line.search(/\s/u) + (line.slice(line.search(/\s/u)).length - line.slice(line.search(/\s/u)).trimStart().length);
90
+ if (offset < lineFrom + pathAt || offset > lineTo) return null;
91
+ const endpoint = endpointOf(request.path);
92
+ if (endpoint === null) return null;
93
+ return {
94
+ from: lineFrom + pathAt,
95
+ to: lineTo,
96
+ markdown: [`\`${endpoint.methods.join(" | ")} ${endpoint.path}\``, endpoint.summary]
97
+ };
98
+ }
99
+ };
100
+ }
101
+ //#endregion
102
+ export { API_ENDPOINTS, REQUEST_METHODS, bodyKeys, bodyKindOf, caretContext, createElasticsearchAssist, endpointOf, readRequest, requestAt, requestMarkers, requestSpans, runnableRequest, takesLines };
@@ -0,0 +1,204 @@
1
+ //#region src/request.d.ts
2
+ /**
3
+ * An Elasticsearch command is one request in Kibana's console spelling — `METHOD /path` on
4
+ * the first line, the body on the lines after — read here exactly as the polyglot sidecar's
5
+ * Elasticsearch engine reads it before sending: the text stripped, the first line split into
6
+ * a method and a path on its first blank, the method uppercased and one of six, the path
7
+ * starting with `/`, the rest the body, sent as ndjson under `_bulk` and `_msearch` and as
8
+ * JSON otherwise. A request this reader refuses is a request the engine refuses, for the
9
+ * same reason.
10
+ */
11
+ declare const REQUEST_METHODS: readonly ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH"];
12
+ type RequestMethod = (typeof REQUEST_METHODS)[number];
13
+ /**
14
+ * Why a request could not be read — a code, never a sentence; the host owns the wording.
15
+ */
16
+ type RequestRefusal = "empty" | "no-request-line" | "unknown-method" | "path-without-slash";
17
+ interface RequestRead {
18
+ /**
19
+ * The method, uppercased; empty where the first line has none.
20
+ */
21
+ method: string;
22
+ /**
23
+ * The path with its query string, as written.
24
+ */
25
+ path: string;
26
+ /**
27
+ * The body, stripped; empty where there is none.
28
+ */
29
+ body: string;
30
+ bodyKind: "none" | "json" | "ndjson";
31
+ /**
32
+ * The request line's `[from, to)` in the text handed in, leading blanks left out.
33
+ */
34
+ line: {
35
+ from: number;
36
+ to: number;
37
+ };
38
+ /**
39
+ * The body's `[from, to)`, or null where there is none.
40
+ */
41
+ bodySpan: {
42
+ from: number;
43
+ to: number;
44
+ } | null;
45
+ refusal: RequestRefusal | null;
46
+ }
47
+ /**
48
+ * Whether the path names an endpoint that takes newline-delimited JSON, as the engine decides
49
+ * the content type.
50
+ */
51
+ declare function takesLines(path: string): boolean;
52
+ /**
53
+ * One request, read as the engine reads it.
54
+ */
55
+ declare function readRequest(text: string): RequestRead;
56
+ /**
57
+ * The `[from, to)` of every request in the document — each from its request line to the line
58
+ * before the next request's, blanks at either end left out; a document with no request line
59
+ * is one request whole, and text before the first request line belongs to none.
60
+ */
61
+ declare function requestSpans(text: string): Array<{
62
+ from: number;
63
+ to: number;
64
+ }>;
65
+ /**
66
+ * The `[from, to)` of the request the caret is in (`requestSpans`); the first where the caret
67
+ * stands before every request line.
68
+ */
69
+ declare function requestAt(text: string, offset: number): {
70
+ from: number;
71
+ to: number;
72
+ };
73
+ /**
74
+ * What one run should send: the selection where there is one, else the request the caret is
75
+ * in; `none` where either is blank.
76
+ */
77
+ type RunnableRequest = {
78
+ kind: "one";
79
+ text: string;
80
+ from: number;
81
+ to: number;
82
+ } | {
83
+ kind: "none";
84
+ };
85
+ declare function runnableRequest(text: string, from: number, to: number): RunnableRequest;
86
+ /**
87
+ * Where the caret stands, for completion: in the request line's method or path, at a key
88
+ * position in the body, or nowhere the assist has anything to say.
89
+ */
90
+ type CaretContext = {
91
+ kind: "method";
92
+ /**
93
+ * The characters of the method typed before the caret.
94
+ */
95
+ typed: string;
96
+ } | {
97
+ kind: "path";
98
+ /**
99
+ * The path's segments before the caret's own, the leading `/` dropped: `[]` in the first
100
+ * segment, `["orders"]` in the second.
101
+ */
102
+ segments: string[];
103
+ /**
104
+ * The caret's own segment as typed before the caret.
105
+ */
106
+ typed: string;
107
+ } | {
108
+ kind: "body-key";
109
+ /**
110
+ * The keys of the objects enclosing the caret, outermost first, arrays passed through:
111
+ * `["query", "bool"]` inside the bool's own object.
112
+ */
113
+ path: string[];
114
+ /**
115
+ * The request the body belongs to, for the top-level vocabulary.
116
+ */
117
+ request: RequestRead;
118
+ /**
119
+ * The key's characters typed before the caret, inside an opened quote where one stands.
120
+ */
121
+ typed: string;
122
+ /**
123
+ * Whether the caret is inside an opened quote.
124
+ */
125
+ quoted: boolean;
126
+ } | {
127
+ kind: "none";
128
+ };
129
+ declare function caretContext(text: string, offset: number): CaretContext;
130
+ //#endregion
131
+ //#region src/assist.d.ts
132
+ /**
133
+ * One index or alias as the catalog lists it.
134
+ */
135
+ interface CatalogIndex {
136
+ name: string;
137
+ alias: boolean;
138
+ }
139
+ /**
140
+ * The catalog is the host's to fetch — and it MUST be a cache-backed read (a query cache, a
141
+ * memo the tree's refresh invalidates), never a fresh fetch: completion reads it on every
142
+ * keystroke in a path, and the package deliberately holds no cache of its own.
143
+ */
144
+ interface ElasticsearchCatalog {
145
+ indices: () => readonly CatalogIndex[];
146
+ }
147
+ /**
148
+ * One suggestion: a method, an endpoint's path, an index or alias, or a body key.
149
+ */
150
+ interface ElasticsearchCompletionCandidate {
151
+ label: string;
152
+ kind: "method" | "endpoint" | "index" | "alias" | "key";
153
+ insertText: string;
154
+ /**
155
+ * `insertText` is a snippet (`$0` marks the caret) rather than plain text.
156
+ */
157
+ snippet?: boolean;
158
+ /**
159
+ * How many characters before the caret the insert replaces — the segment or the key typed
160
+ * so far, which the editor's own word rules do not see whole (a path segment holds `_` and
161
+ * `.`, a key stands inside quotes).
162
+ */
163
+ replace: number;
164
+ detail?: string;
165
+ documentation?: string;
166
+ sortGroup: number;
167
+ }
168
+ interface ElasticsearchHoverCard {
169
+ from: number;
170
+ to: number;
171
+ markdown: string[];
172
+ }
173
+ interface ElasticsearchAssist {
174
+ complete: (text: string, offset: number) => ElasticsearchCompletionCandidate[];
175
+ hover: (text: string, offset: number) => ElasticsearchHoverCard | null;
176
+ }
177
+ /**
178
+ * Completion for the request line and the body, hover for the endpoint on the request line —
179
+ * both reading the request through `request.ts`, as the engine reads it.
180
+ */
181
+ declare function createElasticsearchAssist(catalog?: ElasticsearchCatalog): ElasticsearchAssist;
182
+ //#endregion
183
+ //#region src/markers.d.ts
184
+ /**
185
+ * What a marker says — a code, never a sentence; the host owns the wording:
186
+ *
187
+ * - the request-line faults `readRequest` refuses, which the engine refuses too (`error`);
188
+ * - `"body-not-json"` — a body (or, under `_bulk` and `_msearch`, a line of it) that is not
189
+ * one JSON value; the server will refuse it (`warning`);
190
+ * - `"method-not-taken"` — a known endpoint under a method it does not list (`hint`).
191
+ */
192
+ type ElasticsearchMarkerCode = Exclude<RequestRefusal, "empty"> | "body-not-json" | "method-not-taken";
193
+ interface ElasticsearchMarker {
194
+ from: number;
195
+ to: number;
196
+ code: ElasticsearchMarkerCode;
197
+ severity: "error" | "warning" | "hint";
198
+ }
199
+ /**
200
+ * The markers of every request in the text.
201
+ */
202
+ declare function requestMarkers(text: string): ElasticsearchMarker[];
203
+ //#endregion
204
+ export { readRequest as _, ElasticsearchAssist as a, runnableRequest as b, ElasticsearchHoverCard as c, REQUEST_METHODS as d, RequestMethod as f, caretContext as g, RunnableRequest as h, CatalogIndex as i, createElasticsearchAssist as l, RequestRefusal as m, ElasticsearchMarkerCode as n, ElasticsearchCatalog as o, RequestRead as p, requestMarkers as r, ElasticsearchCompletionCandidate as s, ElasticsearchMarker as t, CaretContext as u, requestAt as v, takesLines as x, requestSpans as y };