@riducms/plugin-graphql 0.5.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 +26 -0
- package/package.json +52 -0
- package/src/editor.ts +211 -0
- package/src/index.ts +22 -0
- package/src/loader.ts +26 -0
- package/src/messages.ts +115 -0
- package/src/operation.ts +90 -0
- package/src/playground-controller.svelte.ts +140 -0
- package/src/playground-route.svelte +47 -0
- package/src/playground.scss +301 -0
- package/src/playground.svelte +252 -0
- package/src/schema-docs.ts +171 -0
- package/src/schema-explorer.svelte +189 -0
- package/src/starter-query.ts +53 -0
- package/src/vite-env.d.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haniel Ubogu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# `@riducms/plugin-graphql`
|
|
2
|
+
|
|
3
|
+
The admin GraphQL playground for Ridu's Go GraphQL plugin. Admins open **GraphQL** in the
|
|
4
|
+
sidebar to write operations with schema-aware completion and validation, run them under the current
|
|
5
|
+
admin access rules, inspect responses, and browse the schema docs.
|
|
6
|
+
|
|
7
|
+
Install it with the Go plugin:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
ridu add graphql \
|
|
11
|
+
--go-package github.com/riducms/ridu/plugins/graphql \
|
|
12
|
+
--admin-package @riducms/plugin-graphql
|
|
13
|
+
bun run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The generated registry imports `graphqlAdminPlugin`; do not register it again. The Go plugin serves
|
|
17
|
+
the schema and endpoint through the `graphql-playground` admin loader, so the playground works with
|
|
18
|
+
a custom `Path` and never needs public introspection. Set `DisablePlayground` on the Go plugin to
|
|
19
|
+
remove the route and loader, then remove this package.
|
|
20
|
+
|
|
21
|
+
The editor, GraphQL language tools, and docs explorer load only when the playground opens. Its
|
|
22
|
+
`ridu-graphql*` presentation is semantic SCSS in the shared component layer; applications can
|
|
23
|
+
override those hooks.
|
|
24
|
+
|
|
25
|
+
See the [GraphQL guide](../../website/src/content/docs/graphql.md#playground) for setup, keyboard
|
|
26
|
+
shortcuts, and security.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bugs": {
|
|
3
|
+
"url": "https://github.com/riducms/ridu/issues"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@codemirror/autocomplete": "^6.20.3",
|
|
7
|
+
"@codemirror/commands": "^6.11.1",
|
|
8
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
9
|
+
"@codemirror/language": "^6.12.4",
|
|
10
|
+
"@codemirror/lint": "^6.9.7",
|
|
11
|
+
"@codemirror/state": "^6.7.5",
|
|
12
|
+
"@codemirror/view": "^6.43.12",
|
|
13
|
+
"@lezer/highlight": "^1.2.3",
|
|
14
|
+
"@riducms/plugin": "0.5.0",
|
|
15
|
+
"@riducms/protocol": "0.5.0",
|
|
16
|
+
"@riducms/sdk": "0.5.0",
|
|
17
|
+
"@riducms/ui": "0.5.0",
|
|
18
|
+
"cm6-graphql": "^0.2.2",
|
|
19
|
+
"graphql": "^16.14.2",
|
|
20
|
+
"svelte": "^5.56.8"
|
|
21
|
+
},
|
|
22
|
+
"description": "GraphQL playground for Ridu's official GraphQL plugin.",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@riducms/build": "0.5.0"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://riducms.com",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"name": "@riducms/plugin-graphql",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"directory": "packages/plugin-graphql",
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/riducms/ridu.git"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "vite build",
|
|
47
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
48
|
+
"test": "bun test"
|
|
49
|
+
},
|
|
50
|
+
"type": "module",
|
|
51
|
+
"version": "0.5.0"
|
|
52
|
+
}
|
package/src/editor.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { autocompletion, closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
2
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
3
|
+
import { json } from "@codemirror/lang-json";
|
|
4
|
+
import {
|
|
5
|
+
bracketMatching,
|
|
6
|
+
HighlightStyle,
|
|
7
|
+
indentOnInput,
|
|
8
|
+
syntaxHighlighting,
|
|
9
|
+
} from "@codemirror/language";
|
|
10
|
+
import { lintGutter } from "@codemirror/lint";
|
|
11
|
+
import { Compartment, EditorState, type Extension } from "@codemirror/state";
|
|
12
|
+
import {
|
|
13
|
+
drawSelection,
|
|
14
|
+
EditorView,
|
|
15
|
+
highlightActiveLineGutter,
|
|
16
|
+
keymap,
|
|
17
|
+
lineNumbers,
|
|
18
|
+
placeholder,
|
|
19
|
+
} from "@codemirror/view";
|
|
20
|
+
import { tags } from "@lezer/highlight";
|
|
21
|
+
import { graphql, updateSchema } from "cm6-graphql";
|
|
22
|
+
import type { GraphQLSchema } from "graphql";
|
|
23
|
+
|
|
24
|
+
// Matches the admin's code editor so the playground reads as part of the same interface.
|
|
25
|
+
const highlighting = HighlightStyle.define([
|
|
26
|
+
{ tag: [tags.keyword, tags.bool, tags.null], color: "var(--code-keyword)" },
|
|
27
|
+
{ tag: [tags.string, tags.special(tags.string)], color: "var(--code-string)" },
|
|
28
|
+
{ tag: [tags.number, tags.literal], color: "var(--code-number)" },
|
|
29
|
+
{ tag: [tags.propertyName, tags.attributeName], color: "var(--code-property)" },
|
|
30
|
+
{ tag: tags.comment, color: "var(--code-comment)", fontStyle: "italic" },
|
|
31
|
+
{ tag: [tags.typeName, tags.className], color: "var(--code-type)" },
|
|
32
|
+
{ tag: [tags.variableName, tags.definition(tags.variableName)], color: "var(--syntax-variable)" },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// CodeMirror injects unlayered base CSS; its theme API overrides those defaults with Ridu tokens.
|
|
36
|
+
const theme = EditorView.theme({
|
|
37
|
+
"&": { height: "100%", fontSize: "13px", backgroundColor: "transparent" },
|
|
38
|
+
"&.cm-focused": { outline: "none" },
|
|
39
|
+
".cm-scroller": { fontFamily: "var(--font-mono)", lineHeight: "20px", overflow: "auto" },
|
|
40
|
+
".cm-content": { padding: "12px 0", caretColor: "var(--foreground)" },
|
|
41
|
+
".cm-line": { paddingInline: "10px 16px" },
|
|
42
|
+
// The gutter stays in place while long lines scroll sideways, so it must be opaque.
|
|
43
|
+
".cm-gutters": {
|
|
44
|
+
backgroundColor: "var(--background-surface, var(--card))",
|
|
45
|
+
color: "var(--foreground-faint, var(--muted-foreground))",
|
|
46
|
+
border: "0",
|
|
47
|
+
},
|
|
48
|
+
".cm-lineNumbers .cm-gutterElement": { padding: "0 8px 0 12px" },
|
|
49
|
+
".cm-activeLineGutter": { backgroundColor: "transparent", color: "var(--foreground)" },
|
|
50
|
+
".cm-selectionBackground": { backgroundColor: "var(--control-surface-hover)" },
|
|
51
|
+
"&.cm-focused .cm-selectionBackground": {
|
|
52
|
+
backgroundColor: "color-mix(in srgb, var(--foreground) 20%, transparent)",
|
|
53
|
+
},
|
|
54
|
+
".cm-cursor": { borderLeftColor: "var(--foreground)" },
|
|
55
|
+
".cm-matchingBracket": { outline: "1px solid var(--muted-foreground)" },
|
|
56
|
+
".cm-placeholder": { color: "var(--muted-foreground)" },
|
|
57
|
+
".cm-tooltip": {
|
|
58
|
+
backgroundColor: "var(--popover)",
|
|
59
|
+
color: "var(--popover-foreground)",
|
|
60
|
+
border: "1px solid var(--border)",
|
|
61
|
+
borderRadius: "var(--radius)",
|
|
62
|
+
boxShadow: "var(--shadow-popover)",
|
|
63
|
+
},
|
|
64
|
+
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
|
|
65
|
+
backgroundColor: "var(--control-surface-hover)",
|
|
66
|
+
color: "var(--foreground)",
|
|
67
|
+
},
|
|
68
|
+
".cm-diagnostic-error": { borderLeftColor: "var(--destructive)" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
interface EditorOptions {
|
|
72
|
+
doc: string;
|
|
73
|
+
label: string;
|
|
74
|
+
onChange?: (value: string) => void;
|
|
75
|
+
/** Runs the current operation; bound to Ctrl/⌘+Enter in every playground editor. */
|
|
76
|
+
onRun?: () => void;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface EditorPresentation {
|
|
81
|
+
label: string;
|
|
82
|
+
placeholder?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const editorConfigurations = new WeakMap<
|
|
86
|
+
EditorView,
|
|
87
|
+
{ compartment: Compartment; readOnly: boolean }
|
|
88
|
+
>();
|
|
89
|
+
|
|
90
|
+
function presentationExtensions(options: EditorPresentation, readOnly: boolean): Extension {
|
|
91
|
+
return [
|
|
92
|
+
EditorState.readOnly.of(readOnly),
|
|
93
|
+
EditorView.editable.of(!readOnly),
|
|
94
|
+
EditorView.contentAttributes.of({
|
|
95
|
+
"aria-label": options.label,
|
|
96
|
+
"aria-readonly": String(readOnly),
|
|
97
|
+
spellcheck: "false",
|
|
98
|
+
...(readOnly ? { tabindex: "0" } : {}),
|
|
99
|
+
}),
|
|
100
|
+
...(options.placeholder ? [placeholder(options.placeholder)] : []),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function baseExtensions(
|
|
105
|
+
options: EditorOptions,
|
|
106
|
+
readOnly: boolean,
|
|
107
|
+
configuration: Compartment
|
|
108
|
+
): Extension[] {
|
|
109
|
+
const run = options.onRun;
|
|
110
|
+
return [
|
|
111
|
+
theme,
|
|
112
|
+
syntaxHighlighting(highlighting),
|
|
113
|
+
lineNumbers(),
|
|
114
|
+
highlightActiveLineGutter(),
|
|
115
|
+
drawSelection(),
|
|
116
|
+
bracketMatching(),
|
|
117
|
+
configuration.of(presentationExtensions(options, readOnly)),
|
|
118
|
+
keymap.of([
|
|
119
|
+
...(run
|
|
120
|
+
? [
|
|
121
|
+
{
|
|
122
|
+
key: "Mod-Enter",
|
|
123
|
+
run: () => {
|
|
124
|
+
run();
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
: []),
|
|
130
|
+
]),
|
|
131
|
+
...(readOnly
|
|
132
|
+
? []
|
|
133
|
+
: [
|
|
134
|
+
history(),
|
|
135
|
+
indentOnInput(),
|
|
136
|
+
closeBrackets(),
|
|
137
|
+
keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap]),
|
|
138
|
+
]),
|
|
139
|
+
EditorView.updateListener.of((update) => {
|
|
140
|
+
if (update.docChanged) options.onChange?.(update.state.doc.toString());
|
|
141
|
+
}),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface QueryEditorOptions extends EditorOptions {
|
|
146
|
+
schema: GraphQLSchema | undefined;
|
|
147
|
+
/** Opens the docs explorer at a type, from Ctrl/⌘+click or a completion's details. */
|
|
148
|
+
onShowInDocs: (type: string) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The GraphQL editor: schema-aware completion, validation, and jump-to-docs. */
|
|
152
|
+
export function createQueryEditor(parent: HTMLElement, options: QueryEditorOptions) {
|
|
153
|
+
const configuration = new Compartment();
|
|
154
|
+
const view = new EditorView({
|
|
155
|
+
parent,
|
|
156
|
+
state: EditorState.create({
|
|
157
|
+
doc: options.doc,
|
|
158
|
+
extensions: [
|
|
159
|
+
...baseExtensions(options, false, configuration),
|
|
160
|
+
graphql(options.schema, {
|
|
161
|
+
onShowInDocs: (field, type, parentType) => {
|
|
162
|
+
const target = parentType ?? type ?? field;
|
|
163
|
+
if (target) options.onShowInDocs(target);
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
autocompletion({ activateOnTyping: true }),
|
|
167
|
+
lintGutter(),
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
editorConfigurations.set(view, { compartment: configuration, readOnly: false });
|
|
172
|
+
return view;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function setQuerySchema(view: EditorView, schema: GraphQLSchema | undefined) {
|
|
176
|
+
updateSchema(view, schema);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A JSON editor for variables, or a read-only viewer for responses. */
|
|
180
|
+
export function createJSONEditor(
|
|
181
|
+
parent: HTMLElement,
|
|
182
|
+
options: EditorOptions & { readOnly: boolean }
|
|
183
|
+
) {
|
|
184
|
+
const configuration = new Compartment();
|
|
185
|
+
const view = new EditorView({
|
|
186
|
+
parent,
|
|
187
|
+
state: EditorState.create({
|
|
188
|
+
doc: options.doc,
|
|
189
|
+
extensions: [...baseExtensions(options, options.readOnly, configuration), json()],
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
editorConfigurations.set(view, { compartment: configuration, readOnly: options.readOnly });
|
|
193
|
+
return view;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Updates translated editor chrome without recreating the editor or losing its history. */
|
|
197
|
+
export function configureEditor(view: EditorView, options: EditorPresentation) {
|
|
198
|
+
const configuration = editorConfigurations.get(view);
|
|
199
|
+
if (configuration === undefined) return;
|
|
200
|
+
view.dispatch({
|
|
201
|
+
effects: configuration.compartment.reconfigure(
|
|
202
|
+
presentationExtensions(options, configuration.readOnly)
|
|
203
|
+
),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Replaces the whole document when an externally owned value changes. */
|
|
208
|
+
export function replaceDocument(view: EditorView, text: string) {
|
|
209
|
+
if (view.state.doc.toString() === text) return;
|
|
210
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
|
|
211
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { withAdminLoader } from "@riducms/plugin";
|
|
2
|
+
import { defineAdminPlugin } from "@riducms/plugin/authoring/v1";
|
|
3
|
+
|
|
4
|
+
import { playgroundLoader } from "./loader";
|
|
5
|
+
import { graphqlMessages } from "./messages";
|
|
6
|
+
import PlaygroundRoute from "./playground-route.svelte";
|
|
7
|
+
|
|
8
|
+
/** The admin companion of `github.com/riducms/ridu/plugins/graphql`: the GraphQL playground. */
|
|
9
|
+
export const graphqlAdminPlugin = defineAdminPlugin({
|
|
10
|
+
key: "graphql",
|
|
11
|
+
pairingVersion: 1,
|
|
12
|
+
routes: [
|
|
13
|
+
{
|
|
14
|
+
path: "graphql",
|
|
15
|
+
navigation: { label: "GraphQL", labelKey: "plugin.graphql:navigation" },
|
|
16
|
+
...withAdminLoader(playgroundLoader, PlaygroundRoute),
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
messages: graphqlMessages,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export { graphqlMessages } from "./messages";
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SchemaAdminLoader } from "@riducms/protocol";
|
|
2
|
+
import { createAdminLoader } from "@riducms/sdk";
|
|
3
|
+
|
|
4
|
+
/** What the Go plugin's playground loader returns. */
|
|
5
|
+
export interface PlaygroundData {
|
|
6
|
+
/** Same-origin path of the GraphQL transport, such as `/api/graphql`. */
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/** The transport's SDL, identical to the file `ridu generate` writes. */
|
|
9
|
+
schema: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Mirrors the Go loader's manifest contract property for property. The admin build compares the
|
|
13
|
+
// two exactly, so a mismatch fails before the playground can request data.
|
|
14
|
+
const playgroundContract: SchemaAdminLoader = {
|
|
15
|
+
key: "graphql-playground",
|
|
16
|
+
input: { kind: "object" },
|
|
17
|
+
output: {
|
|
18
|
+
kind: "object",
|
|
19
|
+
fields: { endpoint: { kind: "string" }, schema: { kind: "string" } },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Reads the playground's endpoint and schema through the admin's existing access policy. */
|
|
24
|
+
export const playgroundLoader = createAdminLoader<Record<string, never>, PlaygroundData>(
|
|
25
|
+
playgroundContract
|
|
26
|
+
);
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { defineAdminMessages } from "@riducms/plugin";
|
|
2
|
+
|
|
3
|
+
/** English fallback text for the playground, translatable under `plugin.graphql:<key>`. */
|
|
4
|
+
export const graphqlMessages = defineAdminMessages({
|
|
5
|
+
fallback: {
|
|
6
|
+
navigation: "GraphQL",
|
|
7
|
+
title: "GraphQL playground",
|
|
8
|
+
loading: "Loading the playground…",
|
|
9
|
+
loadFailed: "The playground could not be loaded.",
|
|
10
|
+
reload: "Reload page",
|
|
11
|
+
description: "Operations use your current admin access.",
|
|
12
|
+
run: "Run",
|
|
13
|
+
runShortcut: "Ctrl+Enter",
|
|
14
|
+
cancel: "Cancel",
|
|
15
|
+
docs: "Docs",
|
|
16
|
+
closeDocs: "Close docs",
|
|
17
|
+
query: "Query",
|
|
18
|
+
variables: "Variables",
|
|
19
|
+
variablesPlaceholder: 'Variables as JSON, such as { "limit": 5 }',
|
|
20
|
+
response: "Response",
|
|
21
|
+
noResponse: "Run an operation to see its response here.",
|
|
22
|
+
running: "Running…",
|
|
23
|
+
status: "{status} · {duration} ms",
|
|
24
|
+
responseReady: "Response received: {status}.",
|
|
25
|
+
invalidVariables: "Variables must be a single JSON object.",
|
|
26
|
+
requestFailed: "The request could not be sent: {message}",
|
|
27
|
+
schemaUnavailable:
|
|
28
|
+
"The schema could not be read, so suggestions and docs are unavailable. Operations still run.",
|
|
29
|
+
searchSchema: "Search types and fields",
|
|
30
|
+
noMatches: "No types or fields match.",
|
|
31
|
+
rootTypes: "Root types",
|
|
32
|
+
back: "Back",
|
|
33
|
+
fields: "Fields",
|
|
34
|
+
arguments: "Arguments",
|
|
35
|
+
values: "Values",
|
|
36
|
+
members: "Possible types",
|
|
37
|
+
defaultValue: "Default: {value}",
|
|
38
|
+
deprecated: "Deprecated: {reason}",
|
|
39
|
+
typeNotFound: "{type} is not in this schema.",
|
|
40
|
+
},
|
|
41
|
+
translations: {
|
|
42
|
+
fr: {
|
|
43
|
+
navigation: "GraphQL",
|
|
44
|
+
title: "Bac à sable GraphQL",
|
|
45
|
+
loading: "Chargement du bac à sable…",
|
|
46
|
+
loadFailed: "Impossible de charger le bac à sable.",
|
|
47
|
+
reload: "Recharger la page",
|
|
48
|
+
description: "Les opérations utilisent vos accès d’administration actuels.",
|
|
49
|
+
run: "Exécuter",
|
|
50
|
+
runShortcut: "Ctrl+Entrée",
|
|
51
|
+
cancel: "Annuler",
|
|
52
|
+
docs: "Documentation",
|
|
53
|
+
closeDocs: "Fermer la documentation",
|
|
54
|
+
query: "Requête",
|
|
55
|
+
variables: "Variables",
|
|
56
|
+
variablesPlaceholder: 'Variables en JSON, par exemple { "limit": 5 }',
|
|
57
|
+
response: "Réponse",
|
|
58
|
+
noResponse: "Exécutez une opération pour afficher sa réponse ici.",
|
|
59
|
+
running: "Exécution…",
|
|
60
|
+
status: "{status} · {duration} ms",
|
|
61
|
+
responseReady: "Réponse reçue : {status}.",
|
|
62
|
+
invalidVariables: "Les variables doivent former un seul objet JSON.",
|
|
63
|
+
requestFailed: "Impossible d’envoyer la requête : {message}",
|
|
64
|
+
schemaUnavailable:
|
|
65
|
+
"Le schéma est illisible : les suggestions et la documentation sont indisponibles. Les opérations s’exécutent toujours.",
|
|
66
|
+
searchSchema: "Rechercher des types et des champs",
|
|
67
|
+
noMatches: "Aucun type ni champ ne correspond.",
|
|
68
|
+
rootTypes: "Types racines",
|
|
69
|
+
back: "Retour",
|
|
70
|
+
fields: "Champs",
|
|
71
|
+
arguments: "Arguments",
|
|
72
|
+
values: "Valeurs",
|
|
73
|
+
members: "Types possibles",
|
|
74
|
+
defaultValue: "Par défaut : {value}",
|
|
75
|
+
deprecated: "Obsolète : {reason}",
|
|
76
|
+
typeNotFound: "{type} ne figure pas dans ce schéma.",
|
|
77
|
+
},
|
|
78
|
+
ar: {
|
|
79
|
+
navigation: "GraphQL",
|
|
80
|
+
title: "ساحة تجربة GraphQL",
|
|
81
|
+
loading: "جارٍ تحميل ساحة التجربة…",
|
|
82
|
+
loadFailed: "تعذر تحميل ساحة التجربة.",
|
|
83
|
+
reload: "إعادة تحميل الصفحة",
|
|
84
|
+
description: "تستخدم العمليات صلاحيات الإدارة الحالية.",
|
|
85
|
+
run: "تشغيل",
|
|
86
|
+
runShortcut: "Ctrl+Enter",
|
|
87
|
+
cancel: "إلغاء",
|
|
88
|
+
docs: "التوثيق",
|
|
89
|
+
closeDocs: "إغلاق التوثيق",
|
|
90
|
+
query: "الاستعلام",
|
|
91
|
+
variables: "المتغيرات",
|
|
92
|
+
variablesPlaceholder: 'المتغيرات بصيغة JSON، مثل { "limit": 5 }',
|
|
93
|
+
response: "الاستجابة",
|
|
94
|
+
noResponse: "شغّل عملية لعرض استجابتها هنا.",
|
|
95
|
+
running: "جارٍ التشغيل…",
|
|
96
|
+
status: "{status} · {duration} مللي ثانية",
|
|
97
|
+
responseReady: "تم استلام الاستجابة: {status}.",
|
|
98
|
+
invalidVariables: "يجب أن تكون المتغيرات كائن JSON واحدًا.",
|
|
99
|
+
requestFailed: "تعذر إرسال الطلب: {message}",
|
|
100
|
+
schemaUnavailable:
|
|
101
|
+
"تعذرت قراءة المخطط، لذا لا تتوفر الاقتراحات والتوثيق. لا تزال العمليات تعمل.",
|
|
102
|
+
searchSchema: "البحث في الأنواع والحقول",
|
|
103
|
+
noMatches: "لا توجد أنواع أو حقول مطابقة.",
|
|
104
|
+
rootTypes: "الأنواع الجذرية",
|
|
105
|
+
back: "رجوع",
|
|
106
|
+
fields: "الحقول",
|
|
107
|
+
arguments: "الوسائط",
|
|
108
|
+
values: "القيم",
|
|
109
|
+
members: "الأنواع الممكنة",
|
|
110
|
+
defaultValue: "الافتراضي: {value}",
|
|
111
|
+
deprecated: "مُهمَل: {reason}",
|
|
112
|
+
typeNotFound: "{type} غير موجود في هذا المخطط.",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
package/src/operation.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isRecord } from "@riducms/protocol";
|
|
2
|
+
import { Kind, parse, type OperationDefinitionNode } from "graphql";
|
|
3
|
+
|
|
4
|
+
export interface OperationRequest {
|
|
5
|
+
query: string;
|
|
6
|
+
variables?: Record<string, unknown>;
|
|
7
|
+
operationName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OperationResponse {
|
|
11
|
+
status: number;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
/** The response body, indented when it is JSON. */
|
|
14
|
+
body: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type VariablesResult = { ok: true; value?: Record<string, unknown> } | { ok: false };
|
|
18
|
+
|
|
19
|
+
/** Variables are optional; when present they must be one JSON object. */
|
|
20
|
+
export function parseVariables(text: string): VariablesResult {
|
|
21
|
+
if (text.trim() === "") return { ok: true };
|
|
22
|
+
try {
|
|
23
|
+
const value: unknown = JSON.parse(text);
|
|
24
|
+
if (isRecord(value)) return { ok: true, value };
|
|
25
|
+
} catch {
|
|
26
|
+
// Reported below as invalid variables.
|
|
27
|
+
}
|
|
28
|
+
return { ok: false };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GraphQL needs an operation name when a document holds several operations. Pick the operation
|
|
33
|
+
* under the cursor, as GraphiQL does. A document that does not parse is sent unchanged so the
|
|
34
|
+
* server reports the syntax error in its usual response shape.
|
|
35
|
+
*/
|
|
36
|
+
export function operationNameAt(query: string, offset: number): string | undefined {
|
|
37
|
+
let operations: OperationDefinitionNode[];
|
|
38
|
+
try {
|
|
39
|
+
operations = parse(query).definitions.filter(
|
|
40
|
+
(definition): definition is OperationDefinitionNode =>
|
|
41
|
+
definition.kind === Kind.OPERATION_DEFINITION
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let selected = operations[0];
|
|
47
|
+
for (const operation of operations) {
|
|
48
|
+
if (operation.loc === undefined) continue;
|
|
49
|
+
if (offset >= operation.loc.start && offset < operation.loc.end) {
|
|
50
|
+
selected = operation;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (offset >= operation.loc.end) selected = operation;
|
|
54
|
+
}
|
|
55
|
+
return operations.length > 1 ? selected?.name?.value : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send one operation with the current admin credentials. The admin is served by the same Go binary
|
|
60
|
+
* as the API, so same-origin credentials authenticate the request when present.
|
|
61
|
+
*/
|
|
62
|
+
export async function executeOperation(
|
|
63
|
+
endpoint: string,
|
|
64
|
+
request: OperationRequest,
|
|
65
|
+
signal: AbortSignal,
|
|
66
|
+
fetchOperation: typeof fetch = fetch
|
|
67
|
+
): Promise<OperationResponse> {
|
|
68
|
+
const started = performance.now();
|
|
69
|
+
const response = await fetchOperation(endpoint, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
credentials: "same-origin",
|
|
72
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
73
|
+
body: JSON.stringify(request),
|
|
74
|
+
signal,
|
|
75
|
+
});
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
return {
|
|
78
|
+
status: response.status,
|
|
79
|
+
durationMs: Math.round(performance.now() - started),
|
|
80
|
+
body: indentJSON(text),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function indentJSON(text: string) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
87
|
+
} catch {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
}
|