@executor-js/plugin-graphql 0.0.1 → 0.0.2

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.
@@ -0,0 +1,1090 @@
1
+ // src/sdk/errors.ts
2
+ import { Data, Schema } from "effect";
3
+ var GraphqlIntrospectionError = class extends Schema.TaggedErrorClass()(
4
+ "GraphqlIntrospectionError",
5
+ {
6
+ message: Schema.String
7
+ }
8
+ ) {
9
+ };
10
+ var GraphqlExtractionError = class extends Schema.TaggedErrorClass()(
11
+ "GraphqlExtractionError",
12
+ {
13
+ message: Schema.String
14
+ }
15
+ ) {
16
+ };
17
+ var GraphqlInvocationError = class extends Data.TaggedError("GraphqlInvocationError") {
18
+ };
19
+
20
+ // src/sdk/introspect.ts
21
+ import { Effect } from "effect";
22
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
23
+ var INTROSPECTION_QUERY = `
24
+ query IntrospectionQuery {
25
+ __schema {
26
+ queryType { name }
27
+ mutationType { name }
28
+ types {
29
+ kind
30
+ name
31
+ description
32
+ fields(includeDeprecated: false) {
33
+ name
34
+ description
35
+ args {
36
+ name
37
+ description
38
+ type {
39
+ ...TypeRef
40
+ }
41
+ defaultValue
42
+ }
43
+ type {
44
+ ...TypeRef
45
+ }
46
+ }
47
+ inputFields {
48
+ name
49
+ description
50
+ type {
51
+ ...TypeRef
52
+ }
53
+ defaultValue
54
+ }
55
+ enumValues(includeDeprecated: false) {
56
+ name
57
+ description
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ fragment TypeRef on __Type {
64
+ kind
65
+ name
66
+ ofType {
67
+ kind
68
+ name
69
+ ofType {
70
+ kind
71
+ name
72
+ ofType {
73
+ kind
74
+ name
75
+ ofType {
76
+ kind
77
+ name
78
+ ofType {
79
+ kind
80
+ name
81
+ ofType {
82
+ kind
83
+ name
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ `;
92
+ var introspect = Effect.fn("GraphQL.introspect")(function* (endpoint, headers, queryParams) {
93
+ const client = yield* HttpClient.HttpClient;
94
+ const requestEndpoint = queryParams && Object.keys(queryParams).length > 0 ? (() => {
95
+ const url = new URL(endpoint);
96
+ for (const [name, value] of Object.entries(queryParams)) {
97
+ url.searchParams.set(name, value);
98
+ }
99
+ return url.toString();
100
+ })() : endpoint;
101
+ let request = HttpClientRequest.post(requestEndpoint).pipe(
102
+ HttpClientRequest.setHeader("Content-Type", "application/json"),
103
+ HttpClientRequest.bodyJsonUnsafe({
104
+ query: INTROSPECTION_QUERY
105
+ })
106
+ );
107
+ if (headers) {
108
+ for (const [k, v] of Object.entries(headers)) {
109
+ request = HttpClientRequest.setHeader(request, k, v);
110
+ }
111
+ }
112
+ const response = yield* client.execute(request).pipe(
113
+ Effect.tapCause((cause) => Effect.logError("graphql introspection request failed", cause)),
114
+ Effect.mapError(
115
+ (err) => new GraphqlIntrospectionError({
116
+ message: `Failed to reach GraphQL endpoint: ${err.message}`
117
+ })
118
+ )
119
+ );
120
+ if (response.status !== 200) {
121
+ return yield* new GraphqlIntrospectionError({
122
+ message: `Introspection failed with status ${response.status}`
123
+ });
124
+ }
125
+ const raw = yield* response.json.pipe(
126
+ Effect.tapCause(
127
+ (cause) => Effect.logError("graphql introspection JSON parse failed", cause)
128
+ ),
129
+ Effect.mapError(
130
+ () => new GraphqlIntrospectionError({
131
+ message: `Failed to parse introspection response as JSON`
132
+ })
133
+ )
134
+ );
135
+ const json = raw;
136
+ if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {
137
+ return yield* new GraphqlIntrospectionError({
138
+ message: `Introspection returned ${json.errors.length} error(s)`
139
+ });
140
+ }
141
+ if (!json.data?.__schema) {
142
+ return yield* new GraphqlIntrospectionError({
143
+ message: "Introspection response missing __schema"
144
+ });
145
+ }
146
+ return json.data;
147
+ });
148
+ var parseIntrospectionJson = (text) => Effect.try({
149
+ try: () => {
150
+ const parsed = JSON.parse(text);
151
+ const result = parsed.data ?? parsed;
152
+ if (!result.__schema) {
153
+ throw new Error("Missing __schema in introspection JSON");
154
+ }
155
+ return result;
156
+ },
157
+ catch: (err) => new GraphqlIntrospectionError({
158
+ message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`
159
+ })
160
+ });
161
+
162
+ // src/sdk/types.ts
163
+ import { Effect as Effect2, Schema as Schema2 } from "effect";
164
+ import { SecretBackedValue } from "@executor-js/sdk/core";
165
+ var GraphqlOperationKind = Schema2.Literals(["query", "mutation"]);
166
+ var GraphqlArgument = class extends Schema2.Class("GraphqlArgument")({
167
+ name: Schema2.String,
168
+ typeName: Schema2.String,
169
+ required: Schema2.Boolean,
170
+ description: Schema2.OptionFromOptional(Schema2.String)
171
+ }) {
172
+ };
173
+ var ExtractedField = class extends Schema2.Class("ExtractedField")({
174
+ /** e.g. "user", "createUser" */
175
+ fieldName: Schema2.String,
176
+ /** "query" or "mutation" */
177
+ kind: GraphqlOperationKind,
178
+ description: Schema2.OptionFromOptional(Schema2.String),
179
+ arguments: Schema2.Array(GraphqlArgument),
180
+ /** JSON Schema for the input (built from arguments) */
181
+ inputSchema: Schema2.OptionFromOptional(Schema2.Unknown),
182
+ /** The return type name for documentation */
183
+ returnTypeName: Schema2.String
184
+ }) {
185
+ };
186
+ var ExtractionResult = class extends Schema2.Class("ExtractionResult")({
187
+ /** Schema name from introspection */
188
+ schemaName: Schema2.OptionFromOptional(Schema2.String),
189
+ fields: Schema2.Array(ExtractedField)
190
+ }) {
191
+ };
192
+ var OperationBinding = class extends Schema2.Class("OperationBinding")({
193
+ kind: GraphqlOperationKind,
194
+ fieldName: Schema2.String,
195
+ /** The full GraphQL query/mutation string */
196
+ operationString: Schema2.String,
197
+ /** Ordered variable names for mapping */
198
+ variableNames: Schema2.Array(Schema2.String)
199
+ }) {
200
+ };
201
+ var HeaderValue = SecretBackedValue;
202
+ var QueryParamValue = HeaderValue;
203
+ var GraphqlSourceAuth = Schema2.Union(
204
+ [
205
+ Schema2.Struct({ kind: Schema2.Literal("none") }),
206
+ Schema2.Struct({
207
+ kind: Schema2.Literal("oauth2"),
208
+ connectionId: Schema2.String
209
+ })
210
+ ]
211
+ );
212
+ var InvocationConfig = class extends Schema2.Class("InvocationConfig")({
213
+ /** The GraphQL endpoint URL */
214
+ endpoint: Schema2.String,
215
+ /** Headers applied to every request. Values can reference secrets. */
216
+ headers: Schema2.Record(Schema2.String, HeaderValue).pipe(
217
+ Schema2.withDecodingDefault(Effect2.succeed({})),
218
+ Schema2.withConstructorDefault(Effect2.succeed({}))
219
+ ),
220
+ /** Query parameters applied to every request. Values can reference secrets. */
221
+ queryParams: Schema2.Record(Schema2.String, QueryParamValue).pipe(
222
+ Schema2.withDecodingDefault(Effect2.succeed({})),
223
+ Schema2.withConstructorDefault(Effect2.succeed({}))
224
+ )
225
+ }) {
226
+ };
227
+ var InvocationResult = class extends Schema2.Class("InvocationResult")({
228
+ status: Schema2.Number,
229
+ data: Schema2.NullOr(Schema2.Unknown),
230
+ errors: Schema2.NullOr(Schema2.Unknown)
231
+ }) {
232
+ };
233
+
234
+ // src/sdk/extract.ts
235
+ import { Effect as Effect3, Option } from "effect";
236
+ var unwrapTypeName = (ref) => {
237
+ if (ref.name) return ref.name;
238
+ if (ref.ofType) return unwrapTypeName(ref.ofType);
239
+ return "Unknown";
240
+ };
241
+ var isNonNull = (ref) => ref.kind === "NON_NULL";
242
+ var buildDefinitions = (types) => {
243
+ const defs = {};
244
+ for (const [name, type] of types) {
245
+ if (name.startsWith("__")) continue;
246
+ if (type.kind === "INPUT_OBJECT" && type.inputFields) {
247
+ const properties = {};
248
+ const required = [];
249
+ for (const field of type.inputFields) {
250
+ const schema = typeRefToJsonSchema(field.type, types);
251
+ if (field.description) {
252
+ schema.description = field.description;
253
+ }
254
+ properties[field.name] = schema;
255
+ if (isNonNull(field.type)) {
256
+ required.push(field.name);
257
+ }
258
+ }
259
+ const def = { type: "object", properties };
260
+ if (required.length > 0) def.required = required;
261
+ if (type.description) def.description = type.description;
262
+ defs[name] = def;
263
+ }
264
+ if (type.kind === "ENUM" && type.enumValues) {
265
+ defs[name] = {
266
+ type: "string",
267
+ enum: type.enumValues.map((v) => v.name),
268
+ ...type.description ? { description: type.description } : {}
269
+ };
270
+ }
271
+ }
272
+ return defs;
273
+ };
274
+ var typeRefToJsonSchema = (ref, types) => {
275
+ switch (ref.kind) {
276
+ case "NON_NULL":
277
+ return ref.ofType ? typeRefToJsonSchema(ref.ofType, types) : {};
278
+ case "LIST":
279
+ return {
280
+ type: "array",
281
+ items: ref.ofType ? typeRefToJsonSchema(ref.ofType, types) : {}
282
+ };
283
+ case "SCALAR":
284
+ return scalarToJsonSchema(ref.name ?? "String");
285
+ case "ENUM":
286
+ return ref.name ? { $ref: `#/$defs/${ref.name}` } : { type: "string" };
287
+ case "INPUT_OBJECT":
288
+ return ref.name ? { $ref: `#/$defs/${ref.name}` } : { type: "object" };
289
+ case "OBJECT":
290
+ case "INTERFACE":
291
+ case "UNION":
292
+ return { type: "object" };
293
+ default:
294
+ return {};
295
+ }
296
+ };
297
+ var scalarToJsonSchema = (name) => {
298
+ switch (name) {
299
+ case "String":
300
+ case "ID":
301
+ return { type: "string" };
302
+ case "Int":
303
+ return { type: "integer" };
304
+ case "Float":
305
+ return { type: "number" };
306
+ case "Boolean":
307
+ return { type: "boolean" };
308
+ default:
309
+ return { type: "string", description: `Custom scalar: ${name}` };
310
+ }
311
+ };
312
+ var buildInputSchema = (args, types) => {
313
+ if (args.length === 0) return void 0;
314
+ const properties = {};
315
+ const required = [];
316
+ for (const arg of args) {
317
+ const schema = typeRefToJsonSchema(arg.type, types);
318
+ if (arg.description) {
319
+ schema.description = arg.description;
320
+ }
321
+ properties[arg.name] = schema;
322
+ if (isNonNull(arg.type)) {
323
+ required.push(arg.name);
324
+ }
325
+ }
326
+ const inputSchema = {
327
+ type: "object",
328
+ properties
329
+ };
330
+ if (required.length > 0) inputSchema.required = required;
331
+ return inputSchema;
332
+ };
333
+ var formatTypeRef = (ref) => {
334
+ switch (ref.kind) {
335
+ case "NON_NULL":
336
+ return ref.ofType ? `${formatTypeRef(ref.ofType)}!` : "Unknown!";
337
+ case "LIST":
338
+ return ref.ofType ? `[${formatTypeRef(ref.ofType)}]` : "[Unknown]";
339
+ default:
340
+ return ref.name ?? "Unknown";
341
+ }
342
+ };
343
+ var extractFields = (_schema, kind, typeName, types) => {
344
+ if (!typeName) return [];
345
+ const type = types.get(typeName);
346
+ if (!type?.fields) return [];
347
+ return type.fields.filter((f) => !f.name.startsWith("__")).map((field) => {
348
+ const args = field.args.map(
349
+ (arg) => new GraphqlArgument({
350
+ name: arg.name,
351
+ typeName: formatTypeRef(arg.type),
352
+ required: isNonNull(arg.type),
353
+ description: arg.description ? Option.some(arg.description) : Option.none()
354
+ })
355
+ );
356
+ const inputSchema = buildInputSchema(field.args, types);
357
+ return new ExtractedField({
358
+ fieldName: field.name,
359
+ kind,
360
+ description: field.description ? Option.some(field.description) : Option.none(),
361
+ arguments: args,
362
+ inputSchema: inputSchema ? Option.some(inputSchema) : Option.none(),
363
+ returnTypeName: unwrapTypeName(field.type)
364
+ });
365
+ });
366
+ };
367
+ var extract = (introspection) => Effect3.try({
368
+ try: () => {
369
+ const schema = introspection.__schema;
370
+ const typeMap = /* @__PURE__ */ new Map();
371
+ for (const t of schema.types) {
372
+ typeMap.set(t.name, t);
373
+ }
374
+ const definitions = buildDefinitions(typeMap);
375
+ const queryFields = extractFields(schema, "query", schema.queryType?.name, typeMap);
376
+ const mutationFields = extractFields(schema, "mutation", schema.mutationType?.name, typeMap);
377
+ return {
378
+ result: new ExtractionResult({
379
+ schemaName: Option.none(),
380
+ fields: [...queryFields, ...mutationFields]
381
+ }),
382
+ definitions
383
+ };
384
+ },
385
+ catch: (err) => new GraphqlExtractionError({
386
+ message: `Failed to extract GraphQL schema: ${err instanceof Error ? err.message : String(err)}`
387
+ })
388
+ });
389
+
390
+ // src/sdk/invoke.ts
391
+ import { Effect as Effect4, Option as Option2 } from "effect";
392
+ import { HttpClient as HttpClient2, HttpClientRequest as HttpClientRequest2 } from "effect/unstable/http";
393
+ import { resolveSecretBackedMap } from "@executor-js/sdk/core";
394
+ var resolveHeaders = (headers, secrets) => {
395
+ const entries = Object.entries(headers);
396
+ const secretCount = entries.reduce(
397
+ (acc, [, value]) => typeof value === "string" ? acc : acc + 1,
398
+ 0
399
+ );
400
+ return resolveSecretBackedMap({
401
+ values: headers,
402
+ getSecret: (secretId) => secrets.get(secretId).pipe(Effect4.catch(() => Effect4.succeed(null))),
403
+ missing: "drop",
404
+ onMissing: (name) => new GraphqlInvocationError({
405
+ message: `Missing secret for header "${name}"`,
406
+ statusCode: Option2.none()
407
+ })
408
+ }).pipe(
409
+ Effect4.catch(() => Effect4.succeed(void 0)),
410
+ Effect4.map((resolved) => resolved ?? {}),
411
+ Effect4.withSpan("plugin.graphql.secret.resolve", {
412
+ attributes: {
413
+ "plugin.graphql.headers.total": entries.length,
414
+ "plugin.graphql.headers.secret_count": secretCount
415
+ }
416
+ })
417
+ );
418
+ };
419
+ var endpointWithQueryParams = (endpoint, queryParams) => {
420
+ if (Object.keys(queryParams).length === 0) return endpoint;
421
+ const url = new URL(endpoint);
422
+ for (const [name, value] of Object.entries(queryParams)) {
423
+ url.searchParams.set(name, value);
424
+ }
425
+ return url.toString();
426
+ };
427
+ var isJsonContentType = (ct) => {
428
+ if (!ct) return false;
429
+ const normalized = ct.split(";")[0]?.trim().toLowerCase() ?? "";
430
+ return normalized === "application/json" || normalized.includes("+json") || normalized.includes("json");
431
+ };
432
+ var invoke = Effect4.fn("GraphQL.invoke")(function* (operation, args, endpoint, resolvedHeaders, resolvedQueryParams = {}) {
433
+ const client = yield* HttpClient2.HttpClient;
434
+ const requestEndpoint = endpointWithQueryParams(endpoint, resolvedQueryParams);
435
+ yield* Effect4.annotateCurrentSpan({
436
+ "http.method": "POST",
437
+ "http.url": requestEndpoint,
438
+ "plugin.graphql.endpoint": endpoint,
439
+ "plugin.graphql.operation_kind": operation.kind,
440
+ "plugin.graphql.field_name": operation.fieldName,
441
+ "plugin.graphql.headers.resolved_count": Object.keys(resolvedHeaders).length,
442
+ "plugin.graphql.query_params.resolved_count": Object.keys(resolvedQueryParams).length
443
+ });
444
+ const variables = {};
445
+ for (const varName of operation.variableNames) {
446
+ if (args[varName] !== void 0) {
447
+ variables[varName] = args[varName];
448
+ }
449
+ }
450
+ if (typeof args.variables === "object" && args.variables !== null) {
451
+ Object.assign(variables, args.variables);
452
+ }
453
+ let request = HttpClientRequest2.post(requestEndpoint).pipe(
454
+ HttpClientRequest2.setHeader("Content-Type", "application/json"),
455
+ HttpClientRequest2.bodyJsonUnsafe({
456
+ query: operation.operationString,
457
+ variables: Object.keys(variables).length > 0 ? variables : void 0
458
+ })
459
+ );
460
+ for (const [name, value] of Object.entries(resolvedHeaders)) {
461
+ request = HttpClientRequest2.setHeader(request, name, value);
462
+ }
463
+ const response = yield* client.execute(request).pipe(
464
+ Effect4.mapError(
465
+ (err) => new GraphqlInvocationError({
466
+ message: `GraphQL request failed: ${err.message}`,
467
+ statusCode: Option2.none(),
468
+ cause: err
469
+ })
470
+ )
471
+ );
472
+ const status = response.status;
473
+ const contentType = response.headers["content-type"] ?? null;
474
+ const body = isJsonContentType(contentType) ? yield* response.json.pipe(Effect4.catch(() => response.text)) : yield* response.text;
475
+ const gqlBody = body;
476
+ const hasErrors = Array.isArray(gqlBody?.errors) && gqlBody.errors.length > 0;
477
+ yield* Effect4.annotateCurrentSpan({
478
+ "http.status_code": status,
479
+ "plugin.graphql.has_errors": hasErrors,
480
+ "plugin.graphql.error_count": hasErrors ? gqlBody.errors.length : 0
481
+ });
482
+ return new InvocationResult({
483
+ status,
484
+ data: gqlBody?.data ?? null,
485
+ errors: hasErrors ? gqlBody.errors : null
486
+ });
487
+ });
488
+ var invokeWithLayer = (operation, args, endpoint, resolvedHeaders, resolvedQueryParams, httpClientLayer) => invoke(operation, args, endpoint, resolvedHeaders, resolvedQueryParams).pipe(
489
+ Effect4.provide(httpClientLayer),
490
+ Effect4.mapError(
491
+ (err) => err instanceof GraphqlInvocationError ? err : new GraphqlInvocationError({
492
+ message: err instanceof Error ? err.message : String(err),
493
+ statusCode: Option2.none(),
494
+ cause: err
495
+ })
496
+ ),
497
+ Effect4.withSpan("plugin.graphql.invoke", {
498
+ attributes: {
499
+ "plugin.graphql.endpoint": endpoint,
500
+ "plugin.graphql.operation_kind": operation.kind,
501
+ "plugin.graphql.field_name": operation.fieldName
502
+ }
503
+ })
504
+ );
505
+
506
+ // src/sdk/store.ts
507
+ import { Effect as Effect5 } from "effect";
508
+ import {
509
+ defineSchema
510
+ } from "@executor-js/sdk/core";
511
+ var graphqlSchema = defineSchema({
512
+ graphql_source: {
513
+ fields: {
514
+ id: { type: "string", required: true },
515
+ scope_id: { type: "string", required: true, index: true },
516
+ name: { type: "string", required: true },
517
+ endpoint: { type: "string", required: true },
518
+ headers: { type: "json", required: false },
519
+ query_params: { type: "json", required: false },
520
+ auth: { type: "json", required: false }
521
+ }
522
+ },
523
+ graphql_operation: {
524
+ fields: {
525
+ id: { type: "string", required: true },
526
+ scope_id: { type: "string", required: true, index: true },
527
+ source_id: { type: "string", required: true, index: true },
528
+ binding: { type: "json", required: true }
529
+ }
530
+ }
531
+ });
532
+ var decodeBinding = (value) => {
533
+ const data = typeof value === "string" ? JSON.parse(value) : value;
534
+ return new OperationBinding({
535
+ kind: data.kind,
536
+ fieldName: data.fieldName,
537
+ operationString: data.operationString,
538
+ variableNames: [...data.variableNames]
539
+ });
540
+ };
541
+ var encodeBinding = (binding) => ({
542
+ kind: binding.kind,
543
+ fieldName: binding.fieldName,
544
+ operationString: binding.operationString,
545
+ variableNames: [...binding.variableNames]
546
+ });
547
+ var toJsonRecord = (value) => value;
548
+ var decodeHeaders = (value) => {
549
+ if (value == null) return {};
550
+ if (typeof value === "string")
551
+ return JSON.parse(value);
552
+ return value;
553
+ };
554
+ var decodeQueryParams = (value) => {
555
+ if (value == null) return {};
556
+ if (typeof value === "string")
557
+ return JSON.parse(value);
558
+ return value;
559
+ };
560
+ var decodeAuth = (value) => {
561
+ if (value == null) return { kind: "none" };
562
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
563
+ return parsed?.kind === "oauth2" && typeof parsed.connectionId === "string" ? { kind: "oauth2", connectionId: parsed.connectionId } : { kind: "none" };
564
+ };
565
+ var makeDefaultGraphqlStore = ({
566
+ adapter: db
567
+ }) => {
568
+ const rowToSource = (row) => ({
569
+ namespace: row.id,
570
+ scope: row.scope_id,
571
+ name: row.name,
572
+ endpoint: row.endpoint,
573
+ headers: decodeHeaders(row.headers),
574
+ queryParams: decodeQueryParams(row.query_params),
575
+ auth: decodeAuth(row.auth)
576
+ });
577
+ const rowToOperation = (row) => ({
578
+ toolId: row.id,
579
+ sourceId: row.source_id,
580
+ binding: decodeBinding(row.binding)
581
+ });
582
+ const deleteSource = (namespace, scope) => Effect5.gen(function* () {
583
+ yield* db.deleteMany({
584
+ model: "graphql_operation",
585
+ where: [
586
+ { field: "source_id", value: namespace },
587
+ { field: "scope_id", value: scope }
588
+ ]
589
+ });
590
+ yield* db.delete({
591
+ model: "graphql_source",
592
+ where: [
593
+ { field: "id", value: namespace },
594
+ { field: "scope_id", value: scope }
595
+ ]
596
+ });
597
+ });
598
+ return {
599
+ upsertSource: (input, operations) => Effect5.gen(function* () {
600
+ yield* deleteSource(input.namespace, input.scope);
601
+ yield* db.create({
602
+ model: "graphql_source",
603
+ data: {
604
+ id: input.namespace,
605
+ scope_id: input.scope,
606
+ name: input.name,
607
+ endpoint: input.endpoint,
608
+ headers: input.headers,
609
+ query_params: input.queryParams,
610
+ auth: toJsonRecord(input.auth)
611
+ },
612
+ forceAllowId: true
613
+ });
614
+ if (operations.length > 0) {
615
+ yield* db.createMany({
616
+ model: "graphql_operation",
617
+ data: operations.map((op) => ({
618
+ id: op.toolId,
619
+ scope_id: input.scope,
620
+ source_id: op.sourceId,
621
+ binding: toJsonRecord(encodeBinding(op.binding))
622
+ })),
623
+ forceAllowId: true
624
+ });
625
+ }
626
+ }),
627
+ updateSourceMeta: (namespace, scope, patch) => Effect5.gen(function* () {
628
+ const existing = yield* db.findOne({
629
+ model: "graphql_source",
630
+ where: [
631
+ { field: "id", value: namespace },
632
+ { field: "scope_id", value: scope }
633
+ ]
634
+ });
635
+ if (!existing) return;
636
+ const update = {};
637
+ if (patch.name !== void 0) update.name = patch.name;
638
+ if (patch.endpoint !== void 0) update.endpoint = patch.endpoint;
639
+ if (patch.headers !== void 0) {
640
+ update.headers = patch.headers;
641
+ }
642
+ if (patch.queryParams !== void 0) {
643
+ update.query_params = patch.queryParams;
644
+ }
645
+ if (patch.auth !== void 0) {
646
+ update.auth = toJsonRecord(patch.auth);
647
+ }
648
+ if (Object.keys(update).length === 0) return;
649
+ yield* db.update({
650
+ model: "graphql_source",
651
+ where: [
652
+ { field: "id", value: namespace },
653
+ { field: "scope_id", value: scope }
654
+ ],
655
+ update
656
+ });
657
+ }),
658
+ getSource: (namespace, scope) => db.findOne({
659
+ model: "graphql_source",
660
+ where: [
661
+ { field: "id", value: namespace },
662
+ { field: "scope_id", value: scope }
663
+ ]
664
+ }).pipe(Effect5.map((row) => row ? rowToSource(row) : null)),
665
+ listSources: () => db.findMany({ model: "graphql_source" }).pipe(Effect5.map((rows) => rows.map(rowToSource))),
666
+ getOperationByToolId: (toolId, scope) => db.findOne({
667
+ model: "graphql_operation",
668
+ where: [
669
+ { field: "id", value: toolId },
670
+ { field: "scope_id", value: scope }
671
+ ]
672
+ }).pipe(Effect5.map((row) => row ? rowToOperation(row) : null)),
673
+ listOperationsBySource: (sourceId, scope) => db.findMany({
674
+ model: "graphql_operation",
675
+ where: [
676
+ { field: "source_id", value: sourceId },
677
+ { field: "scope_id", value: scope }
678
+ ]
679
+ }).pipe(Effect5.map((rows) => rows.map(rowToOperation))),
680
+ removeSource: (namespace, scope) => deleteSource(namespace, scope)
681
+ };
682
+ };
683
+
684
+ // src/sdk/plugin.ts
685
+ import { Effect as Effect6, Option as Option3 } from "effect";
686
+ import { FetchHttpClient } from "effect/unstable/http";
687
+ import {
688
+ definePlugin,
689
+ SourceDetectionResult
690
+ } from "@executor-js/sdk/core";
691
+ import {
692
+ headersToConfigValues
693
+ } from "@executor-js/config";
694
+ var namespaceFromEndpoint = (endpoint) => {
695
+ try {
696
+ const url = new URL(endpoint);
697
+ return url.hostname.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
698
+ } catch {
699
+ return "graphql";
700
+ }
701
+ };
702
+ var formatTypeRef2 = (ref) => {
703
+ switch (ref.kind) {
704
+ case "NON_NULL":
705
+ return ref.ofType ? `${formatTypeRef2(ref.ofType)}!` : "Unknown!";
706
+ case "LIST":
707
+ return ref.ofType ? `[${formatTypeRef2(ref.ofType)}]` : "[Unknown]";
708
+ default:
709
+ return ref.name ?? "Unknown";
710
+ }
711
+ };
712
+ var unwrapTypeName2 = (ref) => {
713
+ if (ref.name) return ref.name;
714
+ if (ref.ofType) return unwrapTypeName2(ref.ofType);
715
+ return "Unknown";
716
+ };
717
+ var buildSelectionSet = (ref, types, depth, seen) => {
718
+ if (depth > 2) return "";
719
+ const leafName = unwrapTypeName2(ref);
720
+ if (seen.has(leafName)) return "";
721
+ const objectType = types.get(leafName);
722
+ if (!objectType?.fields) return "";
723
+ const kind = objectType.kind;
724
+ if (kind === "SCALAR" || kind === "ENUM") return "";
725
+ seen.add(leafName);
726
+ const subFields = objectType.fields.filter((f) => !f.name.startsWith("__")).slice(0, 12).map((f) => {
727
+ const sub = buildSelectionSet(f.type, types, depth + 1, seen);
728
+ return sub ? `${f.name} ${sub}` : f.name;
729
+ });
730
+ seen.delete(leafName);
731
+ return subFields.length > 0 ? `{ ${subFields.join(" ")} }` : "";
732
+ };
733
+ var buildOperationStringForField = (kind, field, types) => {
734
+ const opType = kind === "query" ? "query" : "mutation";
735
+ const varDefs = field.args.map((arg) => {
736
+ const typeName = formatTypeRef2(arg.type);
737
+ return `$${arg.name}: ${typeName}`;
738
+ });
739
+ const argPasses = field.args.map((arg) => `${arg.name}: $${arg.name}`);
740
+ const selectionSet = buildSelectionSet(field.type, types, 0, /* @__PURE__ */ new Set());
741
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(", ")})` : "";
742
+ const argPassStr = argPasses.length > 0 ? `(${argPasses.join(", ")})` : "";
743
+ return `${opType}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`;
744
+ };
745
+ var prepareOperations = (fields, introspection) => {
746
+ const typeMap = /* @__PURE__ */ new Map();
747
+ for (const t of introspection.__schema.types) {
748
+ typeMap.set(t.name, t);
749
+ }
750
+ const fieldMap = /* @__PURE__ */ new Map();
751
+ const schema = introspection.__schema;
752
+ for (const rootKind of ["query", "mutation"]) {
753
+ const typeName = rootKind === "query" ? schema.queryType?.name : schema.mutationType?.name;
754
+ if (!typeName) continue;
755
+ const rootType = typeMap.get(typeName);
756
+ if (!rootType?.fields) continue;
757
+ for (const f of rootType.fields) {
758
+ if (!f.name.startsWith("__")) {
759
+ fieldMap.set(`${rootKind}.${f.name}`, { kind: rootKind, field: f });
760
+ }
761
+ }
762
+ }
763
+ return fields.map((extracted) => {
764
+ const prefix = extracted.kind === "mutation" ? "mutation" : "query";
765
+ const toolPath = `${prefix}.${extracted.fieldName}`;
766
+ const description = Option3.getOrElse(
767
+ extracted.description,
768
+ () => `GraphQL ${extracted.kind}: ${extracted.fieldName} -> ${extracted.returnTypeName}`
769
+ );
770
+ const key = `${extracted.kind}.${extracted.fieldName}`;
771
+ const entry = fieldMap.get(key);
772
+ const operationString = entry ? buildOperationStringForField(entry.kind, entry.field, typeMap) : `${extracted.kind} { ${extracted.fieldName} }`;
773
+ const binding = new OperationBinding({
774
+ kind: extracted.kind,
775
+ fieldName: extracted.fieldName,
776
+ operationString,
777
+ variableNames: extracted.arguments.map((a) => a.name)
778
+ });
779
+ return {
780
+ toolPath,
781
+ description,
782
+ inputSchema: Option3.getOrUndefined(extracted.inputSchema),
783
+ binding
784
+ };
785
+ });
786
+ };
787
+ var annotationsFor = (binding) => {
788
+ if (binding.kind === "mutation") {
789
+ return {
790
+ requiresApproval: true,
791
+ approvalDescription: `mutation ${binding.fieldName}`
792
+ };
793
+ }
794
+ return {};
795
+ };
796
+ var toGraphqlConfigEntry = (namespace, config) => ({
797
+ kind: "graphql",
798
+ endpoint: config.endpoint,
799
+ introspectionJson: config.introspectionJson,
800
+ namespace,
801
+ headers: headersToConfigValues(config.headers)
802
+ });
803
+ var graphqlPlugin = definePlugin((options) => {
804
+ const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;
805
+ return {
806
+ id: "graphql",
807
+ schema: graphqlSchema,
808
+ storage: (deps) => makeDefaultGraphqlStore(deps),
809
+ extension: (ctx) => {
810
+ const resolveConfigValues = (values) => Effect6.gen(function* () {
811
+ if (!values) return void 0;
812
+ const resolved = yield* resolveHeaders(values, ctx.secrets);
813
+ return Object.keys(resolved).length > 0 ? resolved : void 0;
814
+ });
815
+ const resolveOAuthHeader = (auth) => Effect6.gen(function* () {
816
+ if (!auth || auth.kind === "none") return void 0;
817
+ const accessToken = yield* ctx.connections.accessToken(auth.connectionId).pipe(
818
+ Effect6.mapError(
819
+ (err) => new GraphqlIntrospectionError({
820
+ message: `Failed to resolve OAuth connection "${auth.connectionId}": ${"message" in err ? err.message : String(err)}`
821
+ })
822
+ )
823
+ );
824
+ return { Authorization: `Bearer ${accessToken}` };
825
+ });
826
+ const resolveRequestHeaders = (headers, auth) => Effect6.gen(function* () {
827
+ const resolvedHeaders = yield* resolveConfigValues(headers);
828
+ const oauthHeader = yield* resolveOAuthHeader(auth);
829
+ return { ...resolvedHeaders ?? {}, ...oauthHeader ?? {} };
830
+ });
831
+ const addSourceInternal = (config) => ctx.transaction(
832
+ Effect6.gen(function* () {
833
+ let introspectionResult;
834
+ if (config.introspectionJson) {
835
+ introspectionResult = yield* parseIntrospectionJson(
836
+ config.introspectionJson
837
+ );
838
+ } else {
839
+ const resolvedHeaders = yield* resolveRequestHeaders(
840
+ config.headers,
841
+ config.auth
842
+ );
843
+ const resolvedQueryParams = yield* resolveConfigValues(
844
+ config.queryParams
845
+ );
846
+ introspectionResult = yield* introspect(
847
+ config.endpoint,
848
+ Object.keys(resolvedHeaders).length > 0 ? resolvedHeaders : void 0,
849
+ resolvedQueryParams
850
+ ).pipe(Effect6.provide(httpClientLayer));
851
+ }
852
+ const { result, definitions } = yield* extract(introspectionResult);
853
+ const namespace = config.namespace ?? namespaceFromEndpoint(config.endpoint);
854
+ const prepared = prepareOperations(
855
+ result.fields,
856
+ introspectionResult
857
+ );
858
+ const displayName = config.name?.trim() || namespace;
859
+ const storedSource = {
860
+ namespace,
861
+ scope: config.scope,
862
+ name: displayName,
863
+ endpoint: config.endpoint,
864
+ headers: config.headers ?? {},
865
+ queryParams: config.queryParams ?? {},
866
+ auth: config.auth ?? { kind: "none" }
867
+ };
868
+ const storedOps = prepared.map((p) => ({
869
+ toolId: `${namespace}.${p.toolPath}`,
870
+ sourceId: namespace,
871
+ binding: p.binding
872
+ }));
873
+ yield* ctx.storage.upsertSource(storedSource, storedOps);
874
+ yield* ctx.core.sources.register({
875
+ id: namespace,
876
+ scope: config.scope,
877
+ kind: "graphql",
878
+ name: displayName,
879
+ url: config.endpoint,
880
+ canRemove: true,
881
+ canRefresh: false,
882
+ canEdit: true,
883
+ tools: prepared.map((p) => ({
884
+ name: p.toolPath,
885
+ description: p.description,
886
+ inputSchema: p.inputSchema
887
+ }))
888
+ });
889
+ if (Object.keys(definitions).length > 0) {
890
+ yield* ctx.core.definitions.register({
891
+ sourceId: namespace,
892
+ scope: config.scope,
893
+ definitions
894
+ });
895
+ }
896
+ return { toolCount: prepared.length, namespace };
897
+ })
898
+ );
899
+ const configFile = options?.configFile;
900
+ return {
901
+ addSource: (config) => addSourceInternal(config).pipe(
902
+ Effect6.tap(
903
+ (result) => configFile ? configFile.upsertSource(
904
+ toGraphqlConfigEntry(result.namespace, config)
905
+ ) : Effect6.void
906
+ )
907
+ ),
908
+ removeSource: (namespace, scope) => Effect6.gen(function* () {
909
+ yield* ctx.transaction(
910
+ Effect6.gen(function* () {
911
+ yield* ctx.storage.removeSource(namespace, scope);
912
+ yield* ctx.core.sources.unregister(namespace);
913
+ })
914
+ );
915
+ if (configFile) {
916
+ yield* configFile.removeSource(namespace);
917
+ }
918
+ }),
919
+ getSource: (namespace, scope) => ctx.storage.getSource(namespace, scope),
920
+ updateSource: (namespace, scope, input) => ctx.storage.updateSourceMeta(namespace, scope, {
921
+ name: input.name?.trim() || void 0,
922
+ endpoint: input.endpoint,
923
+ headers: input.headers,
924
+ queryParams: input.queryParams,
925
+ auth: input.auth
926
+ })
927
+ };
928
+ },
929
+ staticSources: (self) => [
930
+ {
931
+ id: "graphql",
932
+ kind: "control",
933
+ name: "GraphQL",
934
+ tools: [
935
+ {
936
+ name: "addSource",
937
+ description: "Add a GraphQL endpoint and register its operations as tools",
938
+ inputSchema: {
939
+ type: "object",
940
+ properties: {
941
+ endpoint: { type: "string" },
942
+ name: { type: "string" },
943
+ introspectionJson: { type: "string" },
944
+ namespace: { type: "string" },
945
+ headers: { type: "object" },
946
+ queryParams: { type: "object" },
947
+ auth: { type: "object" }
948
+ },
949
+ required: ["endpoint"]
950
+ },
951
+ outputSchema: {
952
+ type: "object",
953
+ properties: {
954
+ toolCount: { type: "number" }
955
+ },
956
+ required: ["toolCount"]
957
+ },
958
+ // Static-tool callers don't name a scope. Default to the
959
+ // outermost scope in the executor's stack — for a single-
960
+ // scope executor that's the only scope; for a per-user
961
+ // stack `[user, org]` it writes at `org` so the source is
962
+ // visible across every user.
963
+ handler: ({ ctx, args }) => self.addSource({
964
+ ...args,
965
+ scope: ctx.scopes.at(-1).id
966
+ })
967
+ }
968
+ ]
969
+ }
970
+ ],
971
+ invokeTool: ({ ctx, toolRow, args }) => Effect6.gen(function* () {
972
+ const toolScope = toolRow.scope_id;
973
+ const op = yield* ctx.storage.getOperationByToolId(
974
+ toolRow.id,
975
+ toolScope
976
+ );
977
+ if (!op) {
978
+ return yield* Effect6.fail(
979
+ new Error(`No GraphQL operation found for tool "${toolRow.id}"`)
980
+ );
981
+ }
982
+ const source = yield* ctx.storage.getSource(op.sourceId, toolScope);
983
+ if (!source) {
984
+ return yield* Effect6.fail(
985
+ new Error(`No GraphQL source found for "${op.sourceId}"`)
986
+ );
987
+ }
988
+ const resolvedHeaders = yield* resolveHeaders(
989
+ source.headers,
990
+ ctx.secrets
991
+ );
992
+ const resolvedQueryParams = yield* resolveHeaders(
993
+ source.queryParams,
994
+ ctx.secrets
995
+ );
996
+ if (source.auth.kind === "oauth2") {
997
+ const accessToken = yield* ctx.connections.accessToken(
998
+ source.auth.connectionId
999
+ );
1000
+ resolvedHeaders.Authorization = `Bearer ${accessToken}`;
1001
+ }
1002
+ const result = yield* invokeWithLayer(
1003
+ op.binding,
1004
+ args ?? {},
1005
+ source.endpoint,
1006
+ resolvedHeaders,
1007
+ resolvedQueryParams,
1008
+ httpClientLayer
1009
+ );
1010
+ return result;
1011
+ }),
1012
+ resolveAnnotations: ({ ctx, sourceId, toolRows }) => Effect6.gen(function* () {
1013
+ const scopes = /* @__PURE__ */ new Set();
1014
+ for (const row of toolRows) {
1015
+ scopes.add(row.scope_id);
1016
+ }
1017
+ const entries = yield* Effect6.forEach(
1018
+ [...scopes],
1019
+ (scope) => Effect6.gen(function* () {
1020
+ const ops = yield* ctx.storage.listOperationsBySource(
1021
+ sourceId,
1022
+ scope
1023
+ );
1024
+ const byId = /* @__PURE__ */ new Map();
1025
+ for (const op of ops) byId.set(op.toolId, op.binding);
1026
+ return [scope, byId];
1027
+ }),
1028
+ { concurrency: "unbounded" }
1029
+ );
1030
+ const byScope = new Map(entries);
1031
+ const out = {};
1032
+ for (const row of toolRows) {
1033
+ const binding = byScope.get(row.scope_id)?.get(row.id);
1034
+ if (binding) out[row.id] = annotationsFor(binding);
1035
+ }
1036
+ return out;
1037
+ }),
1038
+ removeSource: ({ ctx, sourceId, scope }) => ctx.storage.removeSource(sourceId, scope),
1039
+ detect: ({ url }) => Effect6.gen(function* () {
1040
+ const trimmed = url.trim();
1041
+ if (!trimmed) return null;
1042
+ const parsed = yield* Effect6.try({
1043
+ try: () => new URL(trimmed),
1044
+ catch: (cause) => cause
1045
+ }).pipe(
1046
+ Effect6.option
1047
+ );
1048
+ if (Option3.isNone(parsed)) return null;
1049
+ const ok = yield* introspect(trimmed).pipe(
1050
+ Effect6.provide(httpClientLayer),
1051
+ Effect6.map(() => true),
1052
+ Effect6.catch(() => Effect6.succeed(false))
1053
+ );
1054
+ if (!ok) return null;
1055
+ const name = namespaceFromEndpoint(trimmed);
1056
+ return new SourceDetectionResult({
1057
+ kind: "graphql",
1058
+ confidence: "high",
1059
+ endpoint: trimmed,
1060
+ name,
1061
+ namespace: name
1062
+ });
1063
+ })
1064
+ };
1065
+ });
1066
+
1067
+ export {
1068
+ GraphqlIntrospectionError,
1069
+ GraphqlExtractionError,
1070
+ GraphqlInvocationError,
1071
+ introspect,
1072
+ parseIntrospectionJson,
1073
+ GraphqlOperationKind,
1074
+ GraphqlArgument,
1075
+ ExtractedField,
1076
+ ExtractionResult,
1077
+ OperationBinding,
1078
+ HeaderValue,
1079
+ GraphqlSourceAuth,
1080
+ InvocationConfig,
1081
+ InvocationResult,
1082
+ extract,
1083
+ resolveHeaders,
1084
+ invoke,
1085
+ invokeWithLayer,
1086
+ graphqlSchema,
1087
+ makeDefaultGraphqlStore,
1088
+ graphqlPlugin
1089
+ };
1090
+ //# sourceMappingURL=chunk-ILBZO52O.js.map