@venn-lang/graphql 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
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,125 @@
1
+ # @venn-lang/graphql
2
+
3
+ > The `gql` namespace: GraphQL queries, mutations and subscriptions as Venn verbs.
4
+
5
+ Three verbs that all answer with the same `{ data, errors }` envelope, plus a `noGraphqlErrors`
6
+ matcher to assert on it. Every call travels through the `GqlClient` port, so the same `.vn` file runs
7
+ against a live server or against canned responses depending only on what the host bound at startup.
8
+
9
+ ## Install
10
+
11
+ The package ships with the stdlib, so the CLI already loads it. Inside a `.vn` file, bring the
12
+ namespace in with `use`:
13
+
14
+ ```ruby
15
+ use "venn/graphql"
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ruby
21
+ module demo.profile
22
+
23
+ use "venn/graphql"
24
+
25
+ flow "Profile" {
26
+ step "read the profile" {
27
+ let res = gql.query "{ me { id plan } }" { auth: "Bearer tok123" }
28
+ expect res noGraphqlErrors
29
+ expect res.data.me.plan == "pro"
30
+ }
31
+ }
32
+ ```
33
+
34
+ The document is the single positional argument. Everything else rides the options map:
35
+
36
+ ```ruby
37
+ let res = gql.mutate "mutation Rename($id: ID!, $name: String!) { rename(id: $id, name: $name) { id } }" {
38
+ variables: { id: "u1", name: "Alice" }
39
+ auth: "Bearer tok123"
40
+ }
41
+ ```
42
+
43
+ `variables` and `auth` are the only accepted option names. Anything else is `VN3001`, reported by
44
+ `venn check` before the flow runs and refused again when the line executes.
45
+
46
+ ## Verbs
47
+
48
+ | Verb | Positional argument | Options | Result |
49
+ | --- | --- | --- | --- |
50
+ | `gql.query` | `document: string` | `variables`, `auth` | `gql.GraphqlResponse` |
51
+ | `gql.mutate` | `document: string` | `variables`, `auth` | `gql.GraphqlResponse` |
52
+ | `gql.subscribe` | `document: string` | `variables`, `auth` | `gql.GraphqlResponse` |
53
+
54
+ `query` and `mutate` both call `GqlClient.execute`; `subscribe` calls `GqlClient.subscribe`. A
55
+ subscription answers with one envelope, the same shape as the other two, not a live stream.
56
+
57
+ ## Matcher
58
+
59
+ `expect <subject> noGraphqlErrors` passes when `errors` is absent, `null`, or an empty array, and
60
+ fails when it carries at least one entry. It declares `appliesTo: "GraphqlResponse"`, which the
61
+ editor shows on hover. The matcher lives in the plugin definition (`gqlPlugin.matchers`) and is not
62
+ exported from the barrel.
63
+
64
+ ## Types
65
+
66
+ The plugin publishes two named types the checker and the editor read:
67
+
68
+ | Name | Shape |
69
+ | --- | --- |
70
+ | `gql.GraphqlResponse` | `{ data?: dynamic, errors?: list<gql.GraphqlError> }`, open |
71
+ | `gql.GraphqlError` | `{ message: string, path?: list<string \| number>, extensions?: map<dynamic> }`, open |
72
+
73
+ Both are open, because a server may answer with more than the client reads (`extensions`, above all).
74
+
75
+ ## The GqlClient port
76
+
77
+ | | |
78
+ | --- | --- |
79
+ | id | `venn.port.gql-client` |
80
+ | version | `1` |
81
+ | requires | `net` |
82
+ | methods | `execute`, `subscribe` |
83
+
84
+ Two implementations ship with the package, as the port rule demands. `createFakeClient` replays
85
+ canned envelopes; `createRealClient` is a placeholder that throws `VN8090` on every call, since no
86
+ real transport is wired in this build. The conformance suite lives in
87
+ `src/clients/gql-client.suite.ts` and the fake runs it today; the real client joins it the day it
88
+ answers instead of throwing.
89
+
90
+ `@venn-lang/stdlib` binds `createFakeClient()` with no configuration, so out of the box every call answers
91
+ `{ data: {}, errors: undefined }`: `noGraphqlErrors` passes and field assertions have nothing to read.
92
+ To assert on real data, bind a fake of your own:
93
+
94
+ ```ts
95
+ import { createFakeClient, GqlClientPort, okGraphqlResponse } from "@venn-lang/graphql";
96
+
97
+ const binding = {
98
+ port: GqlClientPort,
99
+ impl: createFakeClient({
100
+ responses: {
101
+ "{ me { id plan } }": okGraphqlResponse({ data: { me: { id: "u1", plan: "pro" } } }),
102
+ },
103
+ }),
104
+ };
105
+ ```
106
+
107
+ `responses` is keyed by the exact document text; `response` sets a single fallback for every query.
108
+
109
+ ## API
110
+
111
+ | Export | What it is |
112
+ | --- | --- |
113
+ | `gqlPlugin` (also the default export) | The `PluginDefinition`: namespace `gql`, `requires: ["net"]`, three actions, one matcher. |
114
+ | `GqlClientPort` | The port descriptor actions resolve through `ctx.port(...)`. |
115
+ | `createFakeClient({ response?, responses? })` | The test double. Returns the canned envelope for a query, or the fallback. |
116
+ | `okGraphqlResponse(overrides?)` | `{ data: {}, errors: undefined }` merged with `overrides`. |
117
+ | `createRealClient()` | The real client's slot. Every method throws `VN8090`. |
118
+ | `graphqlResponseType` | The Zod schema of the envelope, registered as the plugin's `GraphqlResponse` type. |
119
+ | `GqlClient`, `GqlRequest`, `GqlResponse`, `GqlError` | Types only. |
120
+
121
+ ## See also
122
+
123
+ - [`@venn-lang/grpc`](../std-grpc), the same port pattern over gRPC.
124
+ - [`@venn-lang/http`](../std-http), the HTTP verbs and the `Response` type.
125
+ - [`@venn-lang/sdk`](../sdk), `defineAction` / `defineMatcher` / `definePlugin`.
@@ -0,0 +1,103 @@
1
+ import { Port } from "@venn-lang/contracts";
2
+ import { PluginDefinition, ZodType } from "@venn-lang/sdk";
3
+ //#region src/port/gql-client.types.d.ts
4
+ /** One operation to run: the document text, its variables and any bearer token. */
5
+ interface GqlRequest {
6
+ query: string;
7
+ variables?: Record<string, unknown>;
8
+ auth?: string;
9
+ }
10
+ /** One entry of `errors`. `path` walks down `data` to the field that failed. */
11
+ interface GqlError {
12
+ message: string;
13
+ path?: readonly (string | number)[];
14
+ extensions?: Record<string, unknown>;
15
+ }
16
+ /**
17
+ * The `{ data, errors }` envelope, and what `res` holds after a gql verb.
18
+ *
19
+ * GraphQL answers 200 with errors inside, so a failed operation arrives here and
20
+ * not as a thrown error. `expect res noGraphqlErrors` is how a flow checks.
21
+ */
22
+ interface GqlResponse {
23
+ data?: unknown;
24
+ errors?: readonly GqlError[];
25
+ }
26
+ /**
27
+ * Running one GraphQL operation against an endpoint.
28
+ *
29
+ * `execute` and `subscribe` are separate methods because the transports differ,
30
+ * even though both answer with the same envelope.
31
+ *
32
+ * Two implementations: `createRealClient` and `createFakeClient`.
33
+ */
34
+ interface GqlClient {
35
+ execute(req: GqlRequest): Promise<GqlResponse>;
36
+ subscribe(req: GqlRequest): Promise<GqlResponse>;
37
+ }
38
+ //#endregion
39
+ //#region src/port/gql-client.port.d.ts
40
+ /**
41
+ * The port descriptor every `gql` verb resolves through `ctx.port(...)`.
42
+ *
43
+ * Declares the `net` capability, so a host that cannot reach an endpoint refuses
44
+ * the binding at load time with a readable diagnostic.
45
+ */
46
+ declare const GqlClientPort: Port<GqlClient>;
47
+ //#endregion
48
+ //#region src/clients/fake-client.d.ts
49
+ /**
50
+ * A successful envelope: an empty `data` object and no `errors`.
51
+ *
52
+ * @param overrides Fields to replace on the default envelope.
53
+ * @returns A complete {@link GqlResponse}.
54
+ */
55
+ declare function okGraphqlResponse(overrides?: Partial<GqlResponse>): GqlResponse;
56
+ /**
57
+ * The double: canned envelopes, and no endpoint.
58
+ *
59
+ * A document with no entry in `responses` falls back to `response`, so a test
60
+ * only names the documents it cares about. `execute` and `subscribe` resolve
61
+ * from the same table.
62
+ *
63
+ * @param args.responses Envelopes keyed by the exact document text.
64
+ * @param args.response What every other document gets. Defaults to
65
+ * {@link okGraphqlResponse}.
66
+ */
67
+ declare function createFakeClient(args?: {
68
+ response?: GqlResponse;
69
+ responses?: Record<string, GqlResponse>;
70
+ }): GqlClient;
71
+ //#endregion
72
+ //#region src/clients/real-client.d.ts
73
+ /**
74
+ * The real GraphQL client. Not implemented in this build.
75
+ *
76
+ * It exists so the port has its second implementation and the failure is a named
77
+ * Venn error rather than a missing method.
78
+ *
79
+ * @throws VN8090 from every method.
80
+ */
81
+ declare function createRealClient(): GqlClient;
82
+ //#endregion
83
+ //#region src/plugin.d.ts
84
+ /**
85
+ * The `gql` namespace: `query`, `mutate`, `subscribe`, the `noGraphqlErrors`
86
+ * matcher and the `gql.GraphqlResponse` type.
87
+ *
88
+ * Requires the `net` capability, so a host without it refuses the plugin at load
89
+ * time rather than failing mid-flow.
90
+ */
91
+ declare const gqlPlugin: PluginDefinition;
92
+ //#endregion
93
+ //#region src/schema/graphql-response.d.ts
94
+ /**
95
+ * Runtime validation for `gql.GraphqlResponse`, registered on the plugin.
96
+ *
97
+ * Pairs with `gqlTypeDefs.GraphqlResponse`, which tells the checker the same
98
+ * shape: this one guards a value, that one types an expression.
99
+ */
100
+ declare const graphqlResponseType: ZodType;
101
+ //#endregion
102
+ export { type GqlClient, GqlClientPort, type GqlError, type GqlRequest, type GqlResponse, createFakeClient, createRealClient, gqlPlugin as default, gqlPlugin, graphqlResponseType, okGraphqlResponse };
103
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/port/gql-client.types.ts","../src/port/gql-client.port.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/plugin.ts","../src/schema/graphql-response.ts"],"mappings":";;;;UACiB;EACf;EACA,YAAY;EACZ;;;UAIe;EACf;EACA;EACA,aAAa;;;;;;;;UASE;EACf;EACA,kBAAkB;;;;;;;;;;UAWH;EACf,QAAQ,KAAK,aAAa,QAAQ;EAClC,UAAU,KAAK,aAAa,QAAQ;;;;;;;;;;cC1BzB,eAAe,KAAK;;;;;;;;;iBCDjB,kBAAkB,YAAW,QAAQ,eAAoB;;;;;;;;;;;;iBAezD,iBACd;EAAQ,WAAW;EAAa,YAAY,eAAe;IAC1D;;;;;;;;;;;iBCda,oBAAoB;;;;;;;;;;cCEvB,WAAW;;;;;;;;;cCLX,qBAAqB"}
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import { arg, defineAction, defineMatcher, definePlugin, z } from "@venn-lang/sdk";
3
+ import { t } from "@venn-lang/types";
4
+ //#region src/clients/fake-client.ts
5
+ /**
6
+ * A successful envelope: an empty `data` object and no `errors`.
7
+ *
8
+ * @param overrides Fields to replace on the default envelope.
9
+ * @returns A complete {@link GqlResponse}.
10
+ */
11
+ function okGraphqlResponse(overrides = {}) {
12
+ return {
13
+ data: {},
14
+ errors: void 0,
15
+ ...overrides
16
+ };
17
+ }
18
+ /**
19
+ * The double: canned envelopes, and no endpoint.
20
+ *
21
+ * A document with no entry in `responses` falls back to `response`, so a test
22
+ * only names the documents it cares about. `execute` and `subscribe` resolve
23
+ * from the same table.
24
+ *
25
+ * @param args.responses Envelopes keyed by the exact document text.
26
+ * @param args.response What every other document gets. Defaults to
27
+ * {@link okGraphqlResponse}.
28
+ */
29
+ function createFakeClient(args = {}) {
30
+ const responses = args.responses ?? {};
31
+ const fallback = args.response ?? okGraphqlResponse();
32
+ const resolve = (req) => responses[req.query] ?? fallback;
33
+ return {
34
+ execute: async (req) => resolve(req),
35
+ subscribe: async (req) => resolve(req)
36
+ };
37
+ }
38
+ //#endregion
39
+ //#region src/clients/real-client.ts
40
+ /**
41
+ * The real GraphQL client. Not implemented in this build.
42
+ *
43
+ * It exists so the port has its second implementation and the failure is a named
44
+ * Venn error rather than a missing method.
45
+ *
46
+ * @throws VN8090 from every method.
47
+ */
48
+ function createRealClient() {
49
+ return {
50
+ async execute() {
51
+ return unimplemented();
52
+ },
53
+ async subscribe() {
54
+ return unimplemented();
55
+ }
56
+ };
57
+ }
58
+ function unimplemented() {
59
+ throw new VennError({
60
+ code: "VN8090",
61
+ message: "GraphQL real client not implemented in this build"
62
+ });
63
+ }
64
+ //#endregion
65
+ //#region src/port/gql-client.port.ts
66
+ /**
67
+ * The port descriptor every `gql` verb resolves through `ctx.port(...)`.
68
+ *
69
+ * Declares the `net` capability, so a host that cannot reach an endpoint refuses
70
+ * the binding at load time with a readable diagnostic.
71
+ */
72
+ const GqlClientPort = {
73
+ id: "venn.port.gql-client",
74
+ version: 1,
75
+ requires: ["net"],
76
+ methods: ["execute", "subscribe"]
77
+ };
78
+ //#endregion
79
+ //#region src/actions/gql-action.ts
80
+ const paramsSchema = z.object({
81
+ variables: z.record(z.string(), z.unknown()).optional(),
82
+ auth: z.string().optional()
83
+ });
84
+ /**
85
+ * Build one gql verb.
86
+ *
87
+ * The document is the single positional argument and everything else rides the
88
+ * options map. All three verbs return the same envelope, `subscribe` included,
89
+ * so a flow reads one shape whatever it asked for.
90
+ *
91
+ * @param config.name The verb as a flow writes it, such as `query`.
92
+ * @param config.transport Which {@link GqlClient} method carries it.
93
+ */
94
+ function gqlAction(config) {
95
+ return defineAction({
96
+ name: config.name,
97
+ doc: `GraphQL ${config.name} operation.`,
98
+ params: paramsSchema.optional(),
99
+ args: [arg("document", t.string, "The query, mutation or subscription text.")],
100
+ result: t.ref("gql.GraphqlResponse"),
101
+ run: (ctx, input) => dispatch({
102
+ client: ctx.port(GqlClientPort),
103
+ transport: config.transport,
104
+ input
105
+ })
106
+ });
107
+ }
108
+ function dispatch(args) {
109
+ const request = buildRequest(args.input);
110
+ return args.transport === "subscribe" ? args.client.subscribe(request) : args.client.execute(request);
111
+ }
112
+ function buildRequest(input) {
113
+ const params = input.params ?? {};
114
+ return {
115
+ query: String(input.args[0] ?? ""),
116
+ variables: params.variables,
117
+ auth: params.auth
118
+ };
119
+ }
120
+ //#endregion
121
+ //#region src/actions/index.ts
122
+ /** The gql namespace's verbs. Adding one is a single line here. */
123
+ const gqlActions = [
124
+ gqlAction({
125
+ name: "query",
126
+ transport: "execute"
127
+ }),
128
+ gqlAction({
129
+ name: "mutate",
130
+ transport: "execute"
131
+ }),
132
+ gqlAction({
133
+ name: "subscribe",
134
+ transport: "subscribe"
135
+ })
136
+ ];
137
+ //#endregion
138
+ //#region src/matchers/no-graphql-errors.ts
139
+ /**
140
+ * `expect res noGraphqlErrors`: passes when `errors` is absent or empty.
141
+ *
142
+ * GraphQL reports failures inside a 200 response, so this is the check that
143
+ * `expect res status 200` cannot make.
144
+ */
145
+ const noGraphqlErrors = defineMatcher({
146
+ name: "noGraphqlErrors",
147
+ appliesTo: "GraphqlResponse",
148
+ test: ({ subject }) => isEmpty(subject.errors),
149
+ message: () => "expected the GraphQL response to carry no errors"
150
+ });
151
+ function isEmpty(errors) {
152
+ if (errors === void 0 || errors === null) return true;
153
+ return Array.isArray(errors) && errors.length === 0;
154
+ }
155
+ //#endregion
156
+ //#region src/matchers/index.ts
157
+ const gqlMatchers = [noGraphqlErrors];
158
+ //#endregion
159
+ //#region src/schema/graphql-response.ts
160
+ /**
161
+ * Runtime validation for `gql.GraphqlResponse`, registered on the plugin.
162
+ *
163
+ * Pairs with `gqlTypeDefs.GraphqlResponse`, which tells the checker the same
164
+ * shape: this one guards a value, that one types an expression.
165
+ */
166
+ const graphqlResponseType = z.object({
167
+ data: z.unknown().optional(),
168
+ errors: z.array(z.unknown()).optional()
169
+ });
170
+ //#endregion
171
+ //#region src/types.ts
172
+ /**
173
+ * The types the plugin publishes to flows: `gql.GraphqlResponse` and
174
+ * `gql.GraphqlError`.
175
+ *
176
+ * They mirror `port/gql-client.types.ts` by hand, under the names the rest of
177
+ * the plugin already answers to: `noGraphqlErrors` applies to `GraphqlResponse`,
178
+ * so the type a flow reaches for is the same word.
179
+ */
180
+ const gqlTypeDefs = {
181
+ /**
182
+ * The `{ data, errors }` envelope every gql verb gives back.
183
+ *
184
+ * Open, because a server may answer with more than the two entries the client
185
+ * reads, `extensions` above all.
186
+ */
187
+ GraphqlResponse: t.record({
188
+ data: t.dynamic,
189
+ errors: t.list(t.ref("gql.GraphqlError"))
190
+ }, {
191
+ optional: ["data", "errors"],
192
+ open: true
193
+ }),
194
+ /** One entry of `errors`. `path` walks down `data` to the field that failed. */
195
+ GraphqlError: t.record({
196
+ message: t.string,
197
+ path: t.list(t.union(t.string, t.number)),
198
+ extensions: t.map(t.dynamic)
199
+ }, {
200
+ optional: ["path", "extensions"],
201
+ open: true
202
+ })
203
+ };
204
+ //#endregion
205
+ //#region src/plugin.ts
206
+ /**
207
+ * The `gql` namespace: `query`, `mutate`, `subscribe`, the `noGraphqlErrors`
208
+ * matcher and the `gql.GraphqlResponse` type.
209
+ *
210
+ * Requires the `net` capability, so a host without it refuses the plugin at load
211
+ * time rather than failing mid-flow.
212
+ */
213
+ const gqlPlugin = definePlugin({
214
+ name: "venn/graphql",
215
+ version: "0.0.0",
216
+ namespace: "gql",
217
+ requires: ["net"],
218
+ actions: gqlActions,
219
+ matchers: gqlMatchers,
220
+ types: { GraphqlResponse: graphqlResponseType },
221
+ typeDefs: gqlTypeDefs
222
+ });
223
+ //#endregion
224
+ export { GqlClientPort, createFakeClient, createRealClient, gqlPlugin as default, gqlPlugin, graphqlResponseType, okGraphqlResponse };
225
+
226
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/port/gql-client.port.ts","../src/actions/gql-action.ts","../src/actions/index.ts","../src/matchers/no-graphql-errors.ts","../src/matchers/index.ts","../src/schema/graphql-response.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import type { GqlClient, GqlRequest, GqlResponse } from \"../port/index.js\";\n\n/**\n * A successful envelope: an empty `data` object and no `errors`.\n *\n * @param overrides Fields to replace on the default envelope.\n * @returns A complete {@link GqlResponse}.\n */\nexport function okGraphqlResponse(overrides: Partial<GqlResponse> = {}): GqlResponse {\n return { data: {}, errors: undefined, ...overrides };\n}\n\n/**\n * The double: canned envelopes, and no endpoint.\n *\n * A document with no entry in `responses` falls back to `response`, so a test\n * only names the documents it cares about. `execute` and `subscribe` resolve\n * from the same table.\n *\n * @param args.responses Envelopes keyed by the exact document text.\n * @param args.response What every other document gets. Defaults to\n * {@link okGraphqlResponse}.\n */\nexport function createFakeClient(\n args: { response?: GqlResponse; responses?: Record<string, GqlResponse> } = {},\n): GqlClient {\n const responses = args.responses ?? {};\n const fallback = args.response ?? okGraphqlResponse();\n const resolve = (req: GqlRequest): GqlResponse => responses[req.query] ?? fallback;\n return {\n execute: async (req) => resolve(req),\n subscribe: async (req) => resolve(req),\n };\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { GqlClient } from \"../port/index.js\";\n\n/**\n * The real GraphQL client. Not implemented in this build.\n *\n * It exists so the port has its second implementation and the failure is a named\n * Venn error rather than a missing method.\n *\n * @throws VN8090 from every method.\n */\nexport function createRealClient(): GqlClient {\n return {\n async execute() {\n return unimplemented();\n },\n async subscribe() {\n return unimplemented();\n },\n };\n}\n\nfunction unimplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"GraphQL real client not implemented in this build\",\n });\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { GqlClient } from \"./gql-client.types.js\";\n\n/**\n * The port descriptor every `gql` verb resolves through `ctx.port(...)`.\n *\n * Declares the `net` capability, so a host that cannot reach an endpoint refuses\n * the binding at load time with a readable diagnostic.\n */\nexport const GqlClientPort: Port<GqlClient> = {\n id: \"venn.port.gql-client\",\n version: 1,\n requires: [\"net\"],\n methods: [\"execute\", \"subscribe\"],\n};\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { type GqlClient, GqlClientPort, type GqlRequest } from \"../port/index.js\";\n\nconst paramsSchema = z.object({\n variables: z.record(z.string(), z.unknown()).optional(),\n auth: z.string().optional(),\n});\n\n/** Which port method a verb rides. `query` and `mutate` execute; `subscribe` streams. */\ntype GqlTransport = \"execute\" | \"subscribe\";\n\n/**\n * Build one gql verb.\n *\n * The document is the single positional argument and everything else rides the\n * options map. All three verbs return the same envelope, `subscribe` included,\n * so a flow reads one shape whatever it asked for.\n *\n * @param config.name The verb as a flow writes it, such as `query`.\n * @param config.transport Which {@link GqlClient} method carries it.\n */\nexport function gqlAction(config: { name: string; transport: GqlTransport }): ActionDefinition {\n return defineAction({\n name: config.name,\n doc: `GraphQL ${config.name} operation.`,\n params: paramsSchema.optional(),\n args: [arg(\"document\", t.string, \"The query, mutation or subscription text.\")],\n result: t.ref(\"gql.GraphqlResponse\"),\n run: (ctx, input) =>\n dispatch({ client: ctx.port(GqlClientPort), transport: config.transport, input }),\n });\n}\n\nfunction dispatch(args: {\n client: GqlClient;\n transport: GqlTransport;\n input: ActionInput<unknown>;\n}): Promise<unknown> {\n const request = buildRequest(args.input);\n return args.transport === \"subscribe\"\n ? args.client.subscribe(request)\n : args.client.execute(request);\n}\n\nfunction buildRequest(input: ActionInput<unknown>): GqlRequest {\n const params = (input.params ?? {}) as { variables?: Record<string, unknown>; auth?: string };\n return { query: String(input.args[0] ?? \"\"), variables: params.variables, auth: params.auth };\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { gqlAction } from \"./gql-action.js\";\n\n/** The gql namespace's verbs. Adding one is a single line here. */\nexport const gqlActions: ActionDefinition[] = [\n gqlAction({ name: \"query\", transport: \"execute\" }),\n gqlAction({ name: \"mutate\", transport: \"execute\" }),\n gqlAction({ name: \"subscribe\", transport: \"subscribe\" }),\n];\n","import { defineMatcher, type MatcherDefinition } from \"@venn-lang/sdk\";\n\n/**\n * `expect res noGraphqlErrors`: passes when `errors` is absent or empty.\n *\n * GraphQL reports failures inside a 200 response, so this is the check that\n * `expect res status 200` cannot make.\n */\nexport const noGraphqlErrors: MatcherDefinition = defineMatcher({\n name: \"noGraphqlErrors\",\n appliesTo: \"GraphqlResponse\",\n test: ({ subject }) => isEmpty((subject as { errors?: unknown }).errors),\n message: () => \"expected the GraphQL response to carry no errors\",\n});\n\nfunction isEmpty(errors: unknown): boolean {\n if (errors === undefined || errors === null) return true;\n return Array.isArray(errors) && errors.length === 0;\n}\n","import type { MatcherDefinition } from \"@venn-lang/sdk\";\nimport { noGraphqlErrors } from \"./no-graphql-errors.js\";\n\nexport const gqlMatchers: MatcherDefinition[] = [noGraphqlErrors];\n","import { type ZodType, z } from \"@venn-lang/sdk\";\n\n/**\n * Runtime validation for `gql.GraphqlResponse`, registered on the plugin.\n *\n * Pairs with `gqlTypeDefs.GraphqlResponse`, which tells the checker the same\n * shape: this one guards a value, that one types an expression.\n */\nexport const graphqlResponseType: ZodType = z.object({\n data: z.unknown().optional(),\n errors: z.array(z.unknown()).optional(),\n});\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types the plugin publishes to flows: `gql.GraphqlResponse` and\n * `gql.GraphqlError`.\n *\n * They mirror `port/gql-client.types.ts` by hand, under the names the rest of\n * the plugin already answers to: `noGraphqlErrors` applies to `GraphqlResponse`,\n * so the type a flow reaches for is the same word.\n */\nexport const gqlTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * The `{ data, errors }` envelope every gql verb gives back.\n *\n * Open, because a server may answer with more than the two entries the client\n * reads, `extensions` above all.\n */\n GraphqlResponse: t.record(\n { data: t.dynamic, errors: t.list(t.ref(\"gql.GraphqlError\")) },\n { optional: [\"data\", \"errors\"], open: true },\n ),\n /** One entry of `errors`. `path` walks down `data` to the field that failed. */\n GraphqlError: t.record(\n {\n message: t.string,\n path: t.list(t.union(t.string, t.number)),\n extensions: t.map(t.dynamic),\n },\n { optional: [\"path\", \"extensions\"], open: true },\n ),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { gqlActions } from \"./actions/index.js\";\nimport { gqlMatchers } from \"./matchers/index.js\";\nimport { graphqlResponseType } from \"./schema/index.js\";\nimport { gqlTypeDefs } from \"./types.js\";\n\n/**\n * The `gql` namespace: `query`, `mutate`, `subscribe`, the `noGraphqlErrors`\n * matcher and the `gql.GraphqlResponse` type.\n *\n * Requires the `net` capability, so a host without it refuses the plugin at load\n * time rather than failing mid-flow.\n */\nexport const gqlPlugin: PluginDefinition = definePlugin({\n name: \"venn/graphql\",\n version: \"0.0.0\",\n namespace: \"gql\",\n requires: [\"net\"],\n actions: gqlActions,\n matchers: gqlMatchers,\n types: { GraphqlResponse: graphqlResponseType },\n typeDefs: gqlTypeDefs,\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/graphql",
3
+ "version": "0.1.0",
4
+ "description": "The gql namespace: GraphQL queries, mutations and subscriptions as Venn verbs.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "graphql"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-graphql#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-graphql"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/contracts": "0.1.0",
41
+ "@venn-lang/types": "0.1.0",
42
+ "@venn-lang/sdk": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,49 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { type GqlClient, GqlClientPort, type GqlRequest } from "../port/index.js";
4
+
5
+ const paramsSchema = z.object({
6
+ variables: z.record(z.string(), z.unknown()).optional(),
7
+ auth: z.string().optional(),
8
+ });
9
+
10
+ /** Which port method a verb rides. `query` and `mutate` execute; `subscribe` streams. */
11
+ type GqlTransport = "execute" | "subscribe";
12
+
13
+ /**
14
+ * Build one gql verb.
15
+ *
16
+ * The document is the single positional argument and everything else rides the
17
+ * options map. All three verbs return the same envelope, `subscribe` included,
18
+ * so a flow reads one shape whatever it asked for.
19
+ *
20
+ * @param config.name The verb as a flow writes it, such as `query`.
21
+ * @param config.transport Which {@link GqlClient} method carries it.
22
+ */
23
+ export function gqlAction(config: { name: string; transport: GqlTransport }): ActionDefinition {
24
+ return defineAction({
25
+ name: config.name,
26
+ doc: `GraphQL ${config.name} operation.`,
27
+ params: paramsSchema.optional(),
28
+ args: [arg("document", t.string, "The query, mutation or subscription text.")],
29
+ result: t.ref("gql.GraphqlResponse"),
30
+ run: (ctx, input) =>
31
+ dispatch({ client: ctx.port(GqlClientPort), transport: config.transport, input }),
32
+ });
33
+ }
34
+
35
+ function dispatch(args: {
36
+ client: GqlClient;
37
+ transport: GqlTransport;
38
+ input: ActionInput<unknown>;
39
+ }): Promise<unknown> {
40
+ const request = buildRequest(args.input);
41
+ return args.transport === "subscribe"
42
+ ? args.client.subscribe(request)
43
+ : args.client.execute(request);
44
+ }
45
+
46
+ function buildRequest(input: ActionInput<unknown>): GqlRequest {
47
+ const params = (input.params ?? {}) as { variables?: Record<string, unknown>; auth?: string };
48
+ return { query: String(input.args[0] ?? ""), variables: params.variables, auth: params.auth };
49
+ }
@@ -0,0 +1,9 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { gqlAction } from "./gql-action.js";
3
+
4
+ /** The gql namespace's verbs. Adding one is a single line here. */
5
+ export const gqlActions: ActionDefinition[] = [
6
+ gqlAction({ name: "query", transport: "execute" }),
7
+ gqlAction({ name: "mutate", transport: "execute" }),
8
+ gqlAction({ name: "subscribe", transport: "subscribe" }),
9
+ ];
@@ -0,0 +1,34 @@
1
+ import type { GqlClient, GqlRequest, GqlResponse } from "../port/index.js";
2
+
3
+ /**
4
+ * A successful envelope: an empty `data` object and no `errors`.
5
+ *
6
+ * @param overrides Fields to replace on the default envelope.
7
+ * @returns A complete {@link GqlResponse}.
8
+ */
9
+ export function okGraphqlResponse(overrides: Partial<GqlResponse> = {}): GqlResponse {
10
+ return { data: {}, errors: undefined, ...overrides };
11
+ }
12
+
13
+ /**
14
+ * The double: canned envelopes, and no endpoint.
15
+ *
16
+ * A document with no entry in `responses` falls back to `response`, so a test
17
+ * only names the documents it cares about. `execute` and `subscribe` resolve
18
+ * from the same table.
19
+ *
20
+ * @param args.responses Envelopes keyed by the exact document text.
21
+ * @param args.response What every other document gets. Defaults to
22
+ * {@link okGraphqlResponse}.
23
+ */
24
+ export function createFakeClient(
25
+ args: { response?: GqlResponse; responses?: Record<string, GqlResponse> } = {},
26
+ ): GqlClient {
27
+ const responses = args.responses ?? {};
28
+ const fallback = args.response ?? okGraphqlResponse();
29
+ const resolve = (req: GqlRequest): GqlResponse => responses[req.query] ?? fallback;
30
+ return {
31
+ execute: async (req) => resolve(req),
32
+ subscribe: async (req) => resolve(req),
33
+ };
34
+ }
@@ -0,0 +1,2 @@
1
+ export { createFakeClient, okGraphqlResponse } from "./fake-client.js";
2
+ export { createRealClient } from "./real-client.js";
@@ -0,0 +1,28 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { GqlClient } from "../port/index.js";
3
+
4
+ /**
5
+ * The real GraphQL client. Not implemented in this build.
6
+ *
7
+ * It exists so the port has its second implementation and the failure is a named
8
+ * Venn error rather than a missing method.
9
+ *
10
+ * @throws VN8090 from every method.
11
+ */
12
+ export function createRealClient(): GqlClient {
13
+ return {
14
+ async execute() {
15
+ return unimplemented();
16
+ },
17
+ async subscribe() {
18
+ return unimplemented();
19
+ },
20
+ };
21
+ }
22
+
23
+ function unimplemented(): never {
24
+ throw new VennError({
25
+ code: "VN8090",
26
+ message: "GraphQL real client not implemented in this build",
27
+ });
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The `gql` plugin: `query`, `mutate` and `subscribe`, plus the
3
+ * `noGraphqlErrors` matcher.
4
+ *
5
+ * All three verbs cross the `GqlClient` port and answer with the same
6
+ * `{ data, errors }` envelope, so a flow reads one shape whatever it asked for.
7
+ */
8
+
9
+ export * from "./clients/index.js";
10
+ export { gqlPlugin, gqlPlugin as default } from "./plugin.js";
11
+ export * from "./port/index.js";
12
+ export * from "./schema/index.js";
@@ -0,0 +1,4 @@
1
+ import type { MatcherDefinition } from "@venn-lang/sdk";
2
+ import { noGraphqlErrors } from "./no-graphql-errors.js";
3
+
4
+ export const gqlMatchers: MatcherDefinition[] = [noGraphqlErrors];
@@ -0,0 +1,19 @@
1
+ import { defineMatcher, type MatcherDefinition } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * `expect res noGraphqlErrors`: passes when `errors` is absent or empty.
5
+ *
6
+ * GraphQL reports failures inside a 200 response, so this is the check that
7
+ * `expect res status 200` cannot make.
8
+ */
9
+ export const noGraphqlErrors: MatcherDefinition = defineMatcher({
10
+ name: "noGraphqlErrors",
11
+ appliesTo: "GraphqlResponse",
12
+ test: ({ subject }) => isEmpty((subject as { errors?: unknown }).errors),
13
+ message: () => "expected the GraphQL response to carry no errors",
14
+ });
15
+
16
+ function isEmpty(errors: unknown): boolean {
17
+ if (errors === undefined || errors === null) return true;
18
+ return Array.isArray(errors) && errors.length === 0;
19
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { gqlActions } from "./actions/index.js";
3
+ import { gqlMatchers } from "./matchers/index.js";
4
+ import { graphqlResponseType } from "./schema/index.js";
5
+ import { gqlTypeDefs } from "./types.js";
6
+
7
+ /**
8
+ * The `gql` namespace: `query`, `mutate`, `subscribe`, the `noGraphqlErrors`
9
+ * matcher and the `gql.GraphqlResponse` type.
10
+ *
11
+ * Requires the `net` capability, so a host without it refuses the plugin at load
12
+ * time rather than failing mid-flow.
13
+ */
14
+ export const gqlPlugin: PluginDefinition = definePlugin({
15
+ name: "venn/graphql",
16
+ version: "0.0.0",
17
+ namespace: "gql",
18
+ requires: ["net"],
19
+ actions: gqlActions,
20
+ matchers: gqlMatchers,
21
+ types: { GraphqlResponse: graphqlResponseType },
22
+ typeDefs: gqlTypeDefs,
23
+ });
@@ -0,0 +1,15 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { GqlClient } from "./gql-client.types.js";
3
+
4
+ /**
5
+ * The port descriptor every `gql` verb resolves through `ctx.port(...)`.
6
+ *
7
+ * Declares the `net` capability, so a host that cannot reach an endpoint refuses
8
+ * the binding at load time with a readable diagnostic.
9
+ */
10
+ export const GqlClientPort: Port<GqlClient> = {
11
+ id: "venn.port.gql-client",
12
+ version: 1,
13
+ requires: ["net"],
14
+ methods: ["execute", "subscribe"],
15
+ };
@@ -0,0 +1,37 @@
1
+ /** One operation to run: the document text, its variables and any bearer token. */
2
+ export interface GqlRequest {
3
+ query: string;
4
+ variables?: Record<string, unknown>;
5
+ auth?: string;
6
+ }
7
+
8
+ /** One entry of `errors`. `path` walks down `data` to the field that failed. */
9
+ export interface GqlError {
10
+ message: string;
11
+ path?: readonly (string | number)[];
12
+ extensions?: Record<string, unknown>;
13
+ }
14
+
15
+ /**
16
+ * The `{ data, errors }` envelope, and what `res` holds after a gql verb.
17
+ *
18
+ * GraphQL answers 200 with errors inside, so a failed operation arrives here and
19
+ * not as a thrown error. `expect res noGraphqlErrors` is how a flow checks.
20
+ */
21
+ export interface GqlResponse {
22
+ data?: unknown;
23
+ errors?: readonly GqlError[];
24
+ }
25
+
26
+ /**
27
+ * Running one GraphQL operation against an endpoint.
28
+ *
29
+ * `execute` and `subscribe` are separate methods because the transports differ,
30
+ * even though both answer with the same envelope.
31
+ *
32
+ * Two implementations: `createRealClient` and `createFakeClient`.
33
+ */
34
+ export interface GqlClient {
35
+ execute(req: GqlRequest): Promise<GqlResponse>;
36
+ subscribe(req: GqlRequest): Promise<GqlResponse>;
37
+ }
@@ -0,0 +1,2 @@
1
+ export { GqlClientPort } from "./gql-client.port.js";
2
+ export type { GqlClient, GqlError, GqlRequest, GqlResponse } from "./gql-client.types.js";
@@ -0,0 +1,12 @@
1
+ import { type ZodType, z } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * Runtime validation for `gql.GraphqlResponse`, registered on the plugin.
5
+ *
6
+ * Pairs with `gqlTypeDefs.GraphqlResponse`, which tells the checker the same
7
+ * shape: this one guards a value, that one types an expression.
8
+ */
9
+ export const graphqlResponseType: ZodType = z.object({
10
+ data: z.unknown().optional(),
11
+ errors: z.array(z.unknown()).optional(),
12
+ });
@@ -0,0 +1 @@
1
+ export { graphqlResponseType } from "./graphql-response.js";
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types the plugin publishes to flows: `gql.GraphqlResponse` and
5
+ * `gql.GraphqlError`.
6
+ *
7
+ * They mirror `port/gql-client.types.ts` by hand, under the names the rest of
8
+ * the plugin already answers to: `noGraphqlErrors` applies to `GraphqlResponse`,
9
+ * so the type a flow reaches for is the same word.
10
+ */
11
+ export const gqlTypeDefs: Readonly<Record<string, TypeSpec>> = {
12
+ /**
13
+ * The `{ data, errors }` envelope every gql verb gives back.
14
+ *
15
+ * Open, because a server may answer with more than the two entries the client
16
+ * reads, `extensions` above all.
17
+ */
18
+ GraphqlResponse: t.record(
19
+ { data: t.dynamic, errors: t.list(t.ref("gql.GraphqlError")) },
20
+ { optional: ["data", "errors"], open: true },
21
+ ),
22
+ /** One entry of `errors`. `path` walks down `data` to the field that failed. */
23
+ GraphqlError: t.record(
24
+ {
25
+ message: t.string,
26
+ path: t.list(t.union(t.string, t.number)),
27
+ extensions: t.map(t.dynamic),
28
+ },
29
+ { optional: ["path", "extensions"], open: true },
30
+ ),
31
+ };