GraphQL: query/mutation/fragments/vars/executor + parser spec + tests
New graphql application. 676-line test-graphql.sx covers parser, executor, fetch-gql integration. lib/graphql.sx (686L) is the core parser/AST; lib/graphql-exec.sx (219L) runs resolvers. applications/graphql/spec.sx declares the application. sx/sx/applications/graphql/ provides the doc pages (parser, queries, mutation, fragments, vars, fetch-gql, executor). Includes rebuilt sx_browser.bc.js / sx_browser.bc.wasm.js bundles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
sx/sx/applications/graphql/executor/index.sx
Normal file
91
sx/sx/applications/graphql/executor/index.sx
Normal file
@@ -0,0 +1,91 @@
|
||||
;; GraphQL: executor — live execute against /api.execute-demo
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Executor"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"The executor in "
|
||||
(code "lib/graphql-exec.sx")
|
||||
" walks the parsed AST, dispatches root fields to a resolver, "
|
||||
"and projects each result down to the selected fields.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 mb-6")
|
||||
"The endpoint below uses a static seed dataset (users, posts, comments) "
|
||||
"and a resolver written in plain SX. The same bytecode runs the docs site test suite.")
|
||||
(~docs/section
|
||||
:title "Seed data"
|
||||
:id "seed"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Three tables, hardcoded in the handler file. No database required — "
|
||||
"every query and mutation hits this in-memory store.")
|
||||
(~docs/code
|
||||
:src "(define gql-seed-users\n (list\n {:id 1 :name \"Alice\" :email \"alice@test.com\" :role \"admin\"}\n {:id 2 :name \"Bob\" :email \"bob@test.com\" :role \"user\"}\n {:id 3 :name \"Carol\" :email \"carol@test.com\" :role \"user\"}))\n\n(define gql-seed-posts\n (list\n {:id 101 :authorId 1 :title \"Hello, world\" :body \"First post in SX GraphQL.\"}\n {:id 102 :authorId 2 :title \"GraphQL in SX\" :body \"Every op compiles to bytecode.\"}\n {:id 103 :authorId 1 :title \"Quiet night\" :body \"Just thinking about parsers.\"}))\n\n(define gql-seed-comments\n (list\n {:id 1001 :postId 101 :authorId 2 :text \"Nice one.\"}\n {:id 1002 :postId 102 :authorId 3 :text \"Compiled to bytecode?!\"}\n {:id 1003 :postId 102 :authorId 1 :text \"Yes — every op is CEK.\"}))"))
|
||||
(~docs/section
|
||||
:title "Resolver"
|
||||
:id "resolver"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The resolver receives "
|
||||
(code "(field-name args-dict op-type)")
|
||||
" per root field. Return a scalar, a dict, or a list of dicts — "
|
||||
"the executor projects it down to the requested selection set.")
|
||||
(~docs/code
|
||||
:src "(define gql-seed-resolve\n (fn (field-name args op-type)\n (cond\n ((= field-name \"user\")\n (gql-find-by-id gql-seed-users (get args :id)))\n ((= field-name \"users\") gql-seed-users)\n ((= field-name \"post\")\n (gql-find-by-id gql-seed-posts (get args :id)))\n ((= field-name \"posts\")\n (let ((author-id (get args :authorId)))\n (if author-id\n (gql-filter-by gql-seed-posts :authorId author-id)\n gql-seed-posts)))\n ((= field-name \"comments\")\n (let ((post-id (get args :postId)))\n (if post-id\n (gql-filter-by gql-seed-comments :postId post-id)\n gql-seed-comments)))\n ((= field-name \"echo\") (get args :message))\n ((= field-name \"createPost\")\n {:id 999\n :title (get args :title)\n :body (get args :body)\n :authorId (get args :authorId)})\n ((= field-name \"likePost\")\n {:id (get args :id) :liked true})\n (true nil))))"))
|
||||
(~docs/section
|
||||
:title "Live endpoint"
|
||||
:id "endpoint"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"POST "
|
||||
(code "/sx/(applications.(graphql.(api.execute-demo)))")
|
||||
" with form fields "
|
||||
(code "query")
|
||||
" and optional "
|
||||
(code "variables")
|
||||
" (an SX dict literal like "
|
||||
(code "{:id 1}")
|
||||
").")
|
||||
(~docs/code
|
||||
:src "(defhandler\n gql-execute-demo\n :path \"/sx/(applications.(graphql.(api.execute-demo)))\"\n :method :post\n :csrf false\n :returns \"text\"\n (&key query variables)\n (let ((doc (gql-parse query))\n (vars-dict (gql-parse-vars variables)))\n (let ((result (gql-execute doc vars-dict gql-seed-resolve)))\n (str\n \"<pre>\" (escape (sx-serialize vars-dict)) \"</pre>\"\n \"<pre>\" (escape (sx-serialize result)) \"</pre>\")))))"))
|
||||
(~docs/section
|
||||
:title "Try it"
|
||||
:id "try"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The query is sent straight to the handler; the result is projected to your selection set.")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#exec-out"
|
||||
:hx-swap "innerHTML"
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700")
|
||||
"Query")
|
||||
(textarea
|
||||
:name "query"
|
||||
:rows "7"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"query Q($id: ID!) {\n post(id: $id) {\n title\n body\n authorId\n }\n}")
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700")
|
||||
"Variables")
|
||||
(textarea
|
||||
:name "variables"
|
||||
:rows "2"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"{:id 102}")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Execute"))
|
||||
(div
|
||||
:id "exec-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Variables + result will appear here.")))))
|
||||
64
sx/sx/applications/graphql/fetch-gql/index.sx
Normal file
64
sx/sx/applications/graphql/fetch-gql/index.sx
Normal file
@@ -0,0 +1,64 @@
|
||||
;; GraphQL: hyperscript integration — fetch gql command
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · fetch gql"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"The "
|
||||
(code "fetch gql")
|
||||
" hyperscript command embeds GraphQL queries directly in attribute syntax. "
|
||||
"The hyperscript parser collects the GraphQL body between "
|
||||
(code "{")
|
||||
" and "
|
||||
(code "}")
|
||||
" and the compiler emits a "
|
||||
(code "hs-fetch-gql")
|
||||
" call.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 mb-6")
|
||||
"Compilation happens server-side at page boot; the runtime dispatch "
|
||||
"hits the same "
|
||||
(code "/api.execute-demo")
|
||||
" endpoint these pages use.")
|
||||
(~docs/section
|
||||
:title "Shorthand query"
|
||||
:id "shorthand"
|
||||
(~docs/code
|
||||
:src "<button _=\"on click\n fetch gql { users { name role } }\n put result.data.users into #fetch-out\">\n Fetch users\n</button>"))
|
||||
(~docs/section
|
||||
:title "Named operation"
|
||||
:id "named"
|
||||
(~docs/code
|
||||
:src "<input _=\"on input\n fetch gql query Search($q: String!) { posts(authorId: 1) { title } } with vars\n put result.data.posts into #search-out\">"))
|
||||
(~docs/section
|
||||
:title "Mutation"
|
||||
:id "mutation"
|
||||
(~docs/code
|
||||
:src "<form _=\"on submit\n fetch gql mutation { createPost(title: 'Hi', body: 'body', authorId: 1) { id } }\n put it into #create-out\">"))
|
||||
(~docs/section
|
||||
:title "Custom endpoint"
|
||||
:id "from"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Override the default "
|
||||
(code "/graphql")
|
||||
" path per-call with "
|
||||
(code "from"))
|
||||
(~docs/code
|
||||
:src "<button _=\"on click\n fetch gql { users { name } } from '/sx/(applications.(graphql.(api.execute-demo)))'\n put result into #users-out\">"))
|
||||
(~docs/section
|
||||
:title "Compilation pipeline"
|
||||
:id "pipeline"
|
||||
(~docs/note
|
||||
(p
|
||||
(strong "Same bytecode path. ")
|
||||
"The hyperscript tokenizer, parser, compiler, and bytecode runtime "
|
||||
"all live in "
|
||||
(code "lib/hyperscript/")
|
||||
". The GraphQL parser and executor "
|
||||
"live in "
|
||||
(code "lib/graphql.sx")
|
||||
" and "
|
||||
(code "lib/graphql-exec.sx")
|
||||
". Every piece compiles to the same kernel.")))))
|
||||
70
sx/sx/applications/graphql/fragments/index.sx
Normal file
70
sx/sx/applications/graphql/fragments/index.sx
Normal file
@@ -0,0 +1,70 @@
|
||||
;; GraphQL: fragments — live fragment spreads against /api.execute-demo
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Fragments"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-6")
|
||||
"Fragment definitions collect reusable field sets. Spreads inline them during projection. "
|
||||
"The executor applies them unconditionally — type checking is available in "
|
||||
(code "spec/types.sx")
|
||||
" but not required here.")
|
||||
(~docs/section
|
||||
:title "Named fragment"
|
||||
:id "named"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Define "
|
||||
(code "UserFields")
|
||||
" once, use it wherever a "
|
||||
(code "User")
|
||||
" appears.")
|
||||
(~docs/code
|
||||
:src "query {\n alice: user(id: 1) { ...UserFields }\n bob: user(id: 2) { ...UserFields }\n}\n\nfragment UserFields on User {\n name\n email\n role\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#frag-named-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "query { alice: user(id: 1) { ...UserFields } bob: user(id: 2) { ...UserFields } } fragment UserFields on User { name email role }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "frag-named-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Result will appear here.")))
|
||||
(~docs/section
|
||||
:title "Inline fragment"
|
||||
:id "inline"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Inline fragments work without a name — the executor treats them the same way "
|
||||
"and projects their selection set into the parent.")
|
||||
(~docs/code
|
||||
:src "{\n post(id: 101) {\n title\n ... on Post {\n authorId\n body\n }\n }\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#frag-inline-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "{ post(id: 101) { title ... on Post { authorId body } } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "frag-inline-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Result will appear here.")))))
|
||||
78
sx/sx/applications/graphql/mutation/index.sx
Normal file
78
sx/sx/applications/graphql/mutation/index.sx
Normal file
@@ -0,0 +1,78 @@
|
||||
;; GraphQL: mutation — live createPost, likePost
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Mutations"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-6")
|
||||
"Mutations reach the resolver with "
|
||||
(code "op-type")
|
||||
" = "
|
||||
(code "'gql-mutation")
|
||||
". The demo resolver returns a synthesized record. "
|
||||
"In production SX, mutations map to "
|
||||
(code "defaction")
|
||||
" via IO suspension.")
|
||||
(~docs/section
|
||||
:title "createPost"
|
||||
:id "create"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Typed variables let the client pass structured input without inlining strings.")
|
||||
(~docs/code
|
||||
:src "mutation NewPost($title: String!, $body: String!, $authorId: ID!) {\n createPost(title: $title, body: $body, authorId: $authorId) {\n id\n title\n authorId\n }\n}")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#mut-create-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "mutation NewPost($title: String!, $body: String!, $authorId: ID!) { createPost(title: $title, body: $body, authorId: $authorId) { id title authorId } }")
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700")
|
||||
"Variables")
|
||||
(textarea
|
||||
:name "variables"
|
||||
:rows "3"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"{:title \"Fresh take\" :body \"Bytecode FTW\" :authorId 1}")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run mutation"))
|
||||
(div
|
||||
:id "mut-create-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Response will appear here.")))
|
||||
(~docs/section
|
||||
:title "likePost"
|
||||
:id "like"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"A minimal mutation with a single argument.")
|
||||
(~docs/code :src "mutation { likePost(id: 102) { id liked } }")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#mut-like-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "mutation { likePost(id: 102) { id liked } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "mut-like-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Response will appear here.")))))
|
||||
64
sx/sx/applications/graphql/parser/index.sx
Normal file
64
sx/sx/applications/graphql/parser/index.sx
Normal file
@@ -0,0 +1,64 @@
|
||||
;; GraphQL: parser — live parse against /api.parse-demo
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Parser"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"The tokenizer and recursive-descent parser live in "
|
||||
(code "lib/graphql.sx")
|
||||
". Feed them any GraphQL source and get an s-expression AST.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 mb-6")
|
||||
"The server endpoint below parses the query, pretty-prints the AST, "
|
||||
"and round-trips it back through "
|
||||
(code "gql-serialize")
|
||||
".")
|
||||
(~docs/section
|
||||
:title "Server endpoint"
|
||||
:id "endpoint"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"This is the live handler. It's the same bytecode the test suite exercises.")
|
||||
(~docs/code
|
||||
:src "(defhandler\n gql-parse-demo\n :path \"/sx/(applications.(graphql.(api.parse-demo)))\"\n :method :get\n :returns \"text\"\n (&key source)\n (if (or (nil? source) (empty? source))\n \"<p>Enter a GraphQL query and click Parse.</p>\"\n (let ((doc (gql-parse source)))\n (str\n \"<pre>\" (escape (sx-serialize doc)) \"</pre>\"\n \"<pre>\" (escape (gql-serialize doc)) \"</pre>\")))))"))
|
||||
(~docs/section
|
||||
:title "Try it"
|
||||
:id "try"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Type a query, click Parse. The form posts via HTMX to the handler above — "
|
||||
"response drops straight into "
|
||||
(code "#parse-out")
|
||||
".")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-get "/sx/(applications.(graphql.(api.parse-demo)))"
|
||||
:hx-target "#parse-out"
|
||||
:hx-swap "innerHTML"
|
||||
:hx-trigger "submit, input from:textarea[name='source'] changed delay:400ms"
|
||||
(textarea
|
||||
:name "source"
|
||||
:rows "6"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"{\n user(id: 1) {\n name\n email\n }\n}")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Parse"))
|
||||
(div
|
||||
:id "parse-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Output will appear here.")))
|
||||
(~docs/section
|
||||
:title "AST node types"
|
||||
:id "nodes"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Every GraphQL concept becomes a named s-expression:")
|
||||
(~docs/code
|
||||
:src "(gql-doc definitions...)\n(gql-query name vars directives selections)\n(gql-mutation name vars directives selections)\n(gql-subscription name vars directives selections)\n(gql-field name args directives selections [alias])\n(gql-fragment name on-type directives selections)\n(gql-fragment-spread name directives)\n(gql-inline-fragment on-type directives selections)\n(gql-var name)\n(gql-var-def name type default)\n(gql-directive name args)"))))
|
||||
113
sx/sx/applications/graphql/queries/index.sx
Normal file
113
sx/sx/applications/graphql/queries/index.sx
Normal file
@@ -0,0 +1,113 @@
|
||||
;; GraphQL: query — three live queries against /api.execute-demo
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Queries"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-6")
|
||||
"Three live queries. Each button posts a canned query to "
|
||||
(code "/api.execute-demo")
|
||||
" and drops the raw SX result into the output below.")
|
||||
(~docs/section
|
||||
:title "Single record"
|
||||
:id "single"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Fetch one user by id. The executor projects the seed row to just the selected fields.")
|
||||
(~docs/code :src "{\n user(id: 1) {\n name\n role\n }\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#q-single-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "{ user(id: 1) { name role } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "q-single-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span (~tw :tokens "text-gray-400 text-sm") "Click Run to execute.")))
|
||||
(~docs/section
|
||||
:title "List"
|
||||
:id "list"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Fetch all posts. The resolver returns a list of dicts; "
|
||||
"the executor maps projection over each element.")
|
||||
(~docs/code :src "{\n posts {\n id\n title\n authorId\n }\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#q-list-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "{ posts { id title authorId } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "q-list-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span (~tw :tokens "text-gray-400 text-sm") "Click Run to execute.")))
|
||||
(~docs/section
|
||||
:title "Filtered list"
|
||||
:id "filter"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Arguments reach the resolver via the "
|
||||
(code "args")
|
||||
" dict. "
|
||||
"Here, "
|
||||
(code "authorId: 1")
|
||||
" narrows to posts by Alice.")
|
||||
(~docs/code :src "{\n posts(authorId: 1) {\n title\n body\n }\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#q-filter-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "{ posts(authorId: 1) { title body } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "q-filter-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span (~tw :tokens "text-gray-400 text-sm") "Click Run to execute.")))
|
||||
(~docs/section
|
||||
:title "Multiple root fields"
|
||||
:id "multi"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"A single query can dispatch many root fields. "
|
||||
"The executor resolves each independently and merges the results.")
|
||||
(~docs/code :src "{\n users { name }\n posts { title }\n}")
|
||||
(form
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#q-multi-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "{ users { name } posts { title } }")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "q-multi-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span (~tw :tokens "text-gray-400 text-sm") "Click Run to execute.")))))
|
||||
100
sx/sx/applications/graphql/vars/index.sx
Normal file
100
sx/sx/applications/graphql/vars/index.sx
Normal file
@@ -0,0 +1,100 @@
|
||||
;; GraphQL: variables — live variable substitution
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL · Variables"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-6")
|
||||
"Variable references become "
|
||||
(code "(gql-var name)")
|
||||
" nodes. "
|
||||
"At execution, the executor walks the AST and substitutes bound values. "
|
||||
"The demo accepts an SX dict literal in the "
|
||||
(code "variables")
|
||||
" form field.")
|
||||
(~docs/section
|
||||
:title "Scalar variable"
|
||||
:id "scalar"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"One typed variable, used in a field argument. "
|
||||
"Change the dict on the right and click Run — the resolver receives the new value.")
|
||||
(~docs/code
|
||||
:src "query ByAuthor($author: Int!) {\n posts(authorId: $author) {\n title\n body\n }\n}\n\n# Variables (SX dict literal):\n# {:author 2}")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#var-scalar-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "query ByAuthor($author: Int!) { posts(authorId: $author) { title body } }")
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700")
|
||||
"Variables")
|
||||
(textarea
|
||||
:name "variables"
|
||||
:rows "2"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"{:author 2}")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "var-scalar-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Result will appear here.")))
|
||||
(~docs/section
|
||||
:title "Default value"
|
||||
:id "default"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Leave the variables empty and the parser default kicks in — "
|
||||
"executed against the seed, "
|
||||
(code "id: 1")
|
||||
" resolves to Alice.")
|
||||
(~docs/code
|
||||
:src "query GetUser($id: ID = 1) {\n user(id: $id) { name role }\n}")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-post "/sx/(applications.(graphql.(api.execute-demo)))"
|
||||
:hx-target "#var-default-out"
|
||||
:hx-swap "innerHTML"
|
||||
(input
|
||||
:type "hidden"
|
||||
:name "query"
|
||||
:value "query GetUser($id: ID = 1) { user(id: $id) { name role } }")
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700")
|
||||
"Variables (leave blank for default)")
|
||||
(textarea
|
||||
:name "variables"
|
||||
:rows "2"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg")
|
||||
"")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700")
|
||||
"Run"))
|
||||
(div
|
||||
:id "var-default-out"
|
||||
(~tw :tokens "mt-4")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Result will appear here.")))
|
||||
(~docs/section
|
||||
:title "Pure SX"
|
||||
:id "parse"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The variables payload is parsed with the same SX reader the rest of the language uses:")
|
||||
(~docs/code
|
||||
:src "(define gql-parse-vars\n (fn (source)\n (if (or (nil? source) (empty? source))\n {}\n (let ((parsed (parse source)))\n (if (dict? parsed) parsed {})))))"))))
|
||||
Reference in New Issue
Block a user