@model-ts/graphql 0.0.0-stage → 7.0.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1280 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/Assembly.d.ts +24 -0
  5. package/dist/Assembly.d.ts.map +1 -0
  6. package/dist/Assembly.js +236 -0
  7. package/dist/Assembly.js.map +1 -0
  8. package/dist/Builder.d.ts +27 -0
  9. package/dist/Builder.d.ts.map +1 -0
  10. package/dist/Builder.js +40 -0
  11. package/dist/Builder.js.map +1 -0
  12. package/dist/Connections.d.ts +41 -0
  13. package/dist/Connections.d.ts.map +1 -0
  14. package/dist/Connections.js +243 -0
  15. package/dist/Connections.js.map +1 -0
  16. package/dist/Definitions.d.ts +139 -0
  17. package/dist/Definitions.d.ts.map +1 -0
  18. package/dist/Definitions.js +117 -0
  19. package/dist/Definitions.js.map +1 -0
  20. package/dist/Errors.d.ts +39 -0
  21. package/dist/Errors.d.ts.map +1 -0
  22. package/dist/Errors.js +103 -0
  23. package/dist/Errors.js.map +1 -0
  24. package/dist/Execution.d.ts +52 -0
  25. package/dist/Execution.d.ts.map +1 -0
  26. package/dist/Execution.js +174 -0
  27. package/dist/Execution.js.map +1 -0
  28. package/dist/FieldRuntime.d.ts +5 -0
  29. package/dist/FieldRuntime.d.ts.map +1 -0
  30. package/dist/FieldRuntime.js +27 -0
  31. package/dist/FieldRuntime.js.map +1 -0
  32. package/dist/FieldTypes.d.ts +232 -0
  33. package/dist/FieldTypes.d.ts.map +1 -0
  34. package/dist/FieldTypes.js +6 -0
  35. package/dist/FieldTypes.js.map +1 -0
  36. package/dist/Fields.d.ts +8 -0
  37. package/dist/Fields.d.ts.map +1 -0
  38. package/dist/Fields.js +72 -0
  39. package/dist/Fields.js.map +1 -0
  40. package/dist/Formatting.d.ts +25 -0
  41. package/dist/Formatting.d.ts.map +1 -0
  42. package/dist/Formatting.js +29 -0
  43. package/dist/Formatting.js.map +1 -0
  44. package/dist/GraphQL.d.ts +10 -0
  45. package/dist/GraphQL.d.ts.map +1 -0
  46. package/dist/GraphQL.js +5 -0
  47. package/dist/GraphQL.js.map +1 -0
  48. package/dist/InputTypes.d.ts +27 -0
  49. package/dist/InputTypes.d.ts.map +1 -0
  50. package/dist/InputTypes.js +203 -0
  51. package/dist/InputTypes.js.map +1 -0
  52. package/dist/Registry.d.ts +70 -0
  53. package/dist/Registry.d.ts.map +1 -0
  54. package/dist/Registry.js +3 -0
  55. package/dist/Registry.js.map +1 -0
  56. package/dist/Resolver.d.ts +30 -0
  57. package/dist/Resolver.d.ts.map +1 -0
  58. package/dist/Resolver.js +76 -0
  59. package/dist/Resolver.js.map +1 -0
  60. package/dist/Results.d.ts +20 -0
  61. package/dist/Results.d.ts.map +1 -0
  62. package/dist/Results.js +79 -0
  63. package/dist/Results.js.map +1 -0
  64. package/dist/ScalarBoundary.d.ts +9 -0
  65. package/dist/ScalarBoundary.d.ts.map +1 -0
  66. package/dist/ScalarBoundary.js +31 -0
  67. package/dist/ScalarBoundary.js.map +1 -0
  68. package/dist/Scalars.d.ts +9 -0
  69. package/dist/Scalars.d.ts.map +1 -0
  70. package/dist/Scalars.js +104 -0
  71. package/dist/Scalars.js.map +1 -0
  72. package/dist/SchemaTypes.d.ts +55 -0
  73. package/dist/SchemaTypes.d.ts.map +1 -0
  74. package/dist/SchemaTypes.js +47 -0
  75. package/dist/SchemaTypes.js.map +1 -0
  76. package/dist/Subscription.d.ts +6 -0
  77. package/dist/Subscription.d.ts.map +1 -0
  78. package/dist/Subscription.js +60 -0
  79. package/dist/Subscription.js.map +1 -0
  80. package/dist/Testing.d.ts +52 -0
  81. package/dist/Testing.d.ts.map +1 -0
  82. package/dist/Testing.js +74 -0
  83. package/dist/Testing.js.map +1 -0
  84. package/dist/index.d.ts +2 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +2 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/internal/SubscriptionRule.d.ts +8 -0
  89. package/dist/internal/SubscriptionRule.d.ts.map +1 -0
  90. package/dist/internal/SubscriptionRule.js +73 -0
  91. package/dist/internal/SubscriptionRule.js.map +1 -0
  92. package/package.json +55 -4
  93. package/src/Assembly.ts +352 -0
  94. package/src/Builder.ts +110 -0
  95. package/src/Connections.ts +374 -0
  96. package/src/Definitions.ts +465 -0
  97. package/src/Errors.ts +188 -0
  98. package/src/Execution.ts +354 -0
  99. package/src/FieldRuntime.ts +32 -0
  100. package/src/FieldTypes.ts +667 -0
  101. package/src/Fields.ts +117 -0
  102. package/src/Formatting.ts +54 -0
  103. package/src/GraphQL.ts +50 -0
  104. package/src/InputTypes.ts +266 -0
  105. package/src/Registry.ts +157 -0
  106. package/src/Resolver.ts +176 -0
  107. package/src/Results.ts +128 -0
  108. package/src/ScalarBoundary.ts +30 -0
  109. package/src/Scalars.ts +143 -0
  110. package/src/SchemaTypes.ts +90 -0
  111. package/src/Subscription.ts +93 -0
  112. package/src/Testing.ts +144 -0
  113. package/src/index.ts +1 -0
  114. package/src/internal/SubscriptionRule.ts +90 -0
@@ -0,0 +1,176 @@
1
+ import type { Stream } from "effect"
2
+ import { Effect, Schema, SchemaIssue } from "effect"
3
+ import {
4
+ type GraphQLFieldConfigArgumentMap,
5
+ type GraphQLOutputType,
6
+ } from "graphql"
7
+ import {
8
+ type ErrorPolicy,
9
+ fieldError,
10
+ protect,
11
+ forbidden,
12
+ resultSource,
13
+ } from "./Errors.ts"
14
+ import { describe } from "./FieldRuntime.ts"
15
+ import {
16
+ type ArgFields,
17
+ type Description,
18
+ type BuildContext,
19
+ fieldRequirements,
20
+ type Field,
21
+ } from "./FieldTypes.ts"
22
+ import { argumentDecoder, optionalNullMessage } from "./InputTypes.ts"
23
+ import { type ErrorSchema, type ResultOptions, errorName } from "./Results.ts"
24
+
25
+ export interface Resolver {
26
+ readonly type: (context: BuildContext, name: string) => GraphQLOutputType
27
+ readonly args: ArgFields
28
+ readonly extraArgs?: GraphQLFieldConfigArgumentMap
29
+ readonly members: readonly ErrorSchema[]
30
+ readonly call: (
31
+ source: unknown,
32
+ args: Readonly<Record<string, unknown>>,
33
+ decoded: unknown,
34
+ coordinate: string,
35
+ context: unknown,
36
+ ) => unknown
37
+ readonly finish: (
38
+ value: unknown,
39
+ args: Readonly<Record<string, unknown>>,
40
+ ) => unknown
41
+ }
42
+
43
+ export type AnyResolve = (
44
+ source: unknown,
45
+ args: unknown,
46
+ context: unknown,
47
+ ) => unknown
48
+ /** The runtime view of every field input, before type parameters are fixed. */
49
+
50
+ export interface AnyConfig extends Description {
51
+ readonly edgeName?: string
52
+ readonly name?: string
53
+ readonly args?: ArgFields
54
+ readonly result?: ResultOptions
55
+ readonly errors?: readonly ErrorSchema[]
56
+ readonly resolve?: AnyResolve
57
+ readonly nodes?: AnyResolve
58
+ readonly authorize?: AnyResolve
59
+ readonly totalCount?: AnyResolve
60
+ readonly subscribe?: (
61
+ source: unknown,
62
+ args: unknown,
63
+ context: unknown,
64
+ ) => Stream.Stream<unknown, unknown, unknown>
65
+ }
66
+
67
+ export const config = (input: AnyResolve | AnyConfig): AnyConfig =>
68
+ typeof input === "function" ? { resolve: input } : input
69
+
70
+ export const resolverField = <R>(
71
+ policy: ErrorPolicy,
72
+ options: AnyConfig,
73
+ resolver: Resolver,
74
+ ): Field<R> => ({
75
+ kind: "field",
76
+ [fieldRequirements]: undefined,
77
+ build(context, name, coordinate = name) {
78
+ const decode = argumentDecoder(resolver.args)
79
+ return {
80
+ type: resolver.type(context, coordinate),
81
+ ...describe(options),
82
+ args: {
83
+ ...resolver.extraArgs,
84
+ ...Object.fromEntries(
85
+ Object.entries(resolver.args).map(([key, schema]) => [
86
+ key,
87
+ context.input.argument(schema, `${name}(${key})`),
88
+ ]),
89
+ ),
90
+ },
91
+ resolve(source, raw: Readonly<Record<string, unknown>>, request, info) {
92
+ const coordinate = `${info.parentType.name}.${info.fieldName}`
93
+ return request.run(
94
+ protect(
95
+ () => {
96
+ source = resultSource(source)
97
+ let decoded: unknown
98
+ try {
99
+ decoded = decode(raw)
100
+ } catch (error) {
101
+ if (Schema.isSchemaError(error)) {
102
+ const member = resolver.members.find(
103
+ (member) => errorName(member) === "UserInputError",
104
+ )
105
+ if (member) {
106
+ const issue = SchemaIssue.makeFormatterStandardSchemaV1()(
107
+ error.issue,
108
+ ).issues[0]
109
+ const path =
110
+ issue?.path?.map((key) =>
111
+ typeof key === "object" ? String(key.key) : String(key),
112
+ ) ?? []
113
+ const field =
114
+ (path[0] === "input" ? path.slice(1) : path).join(".") ||
115
+ name
116
+ return Effect.fail(
117
+ Schema.decodeUnknownSync(member)({
118
+ _tag: "UserInputError",
119
+ message:
120
+ optionalNullMessage(resolver.args, raw) ??
121
+ issue?.message ??
122
+ "Invalid arguments",
123
+ field,
124
+ }),
125
+ )
126
+ }
127
+ }
128
+ throw fieldError(
129
+ optionalNullMessage(resolver.args, raw) ??
130
+ "Invalid arguments",
131
+ {
132
+ code: "BAD_USER_INPUT",
133
+ },
134
+ )
135
+ }
136
+ return Effect.gen(function* () {
137
+ const allowed = yield* protect(
138
+ () =>
139
+ options.authorize
140
+ ? options.authorize(source, decoded, request.context)
141
+ : true,
142
+ [],
143
+ policy,
144
+ { coordinate, phase: "authorize" },
145
+ )
146
+ if (allowed instanceof Error) return allowed
147
+ if (allowed !== true) return forbidden(info.fieldName)
148
+ return yield* Effect.suspend(() => {
149
+ const output = resolver.call(
150
+ source,
151
+ raw,
152
+ decoded,
153
+ coordinate,
154
+ request.context,
155
+ )
156
+ return Effect.isEffect(output)
157
+ ? output
158
+ : Effect.succeed(output)
159
+ }).pipe(
160
+ Effect.map((value) =>
161
+ value instanceof Error
162
+ ? value
163
+ : resolver.finish(value, raw),
164
+ ),
165
+ )
166
+ })
167
+ },
168
+ resolver.members,
169
+ policy,
170
+ { coordinate, phase: "resolve" },
171
+ ),
172
+ )
173
+ },
174
+ }
175
+ },
176
+ })
package/src/Results.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type { Schema } from "effect"
2
+ import {
3
+ GraphQLObjectType,
4
+ GraphQLNonNull,
5
+ GraphQLUnionType,
6
+ isOutputType,
7
+ type GraphQLNamedType,
8
+ type GraphQLAbstractType,
9
+ } from "graphql"
10
+ import type { BuildContext } from "./FieldTypes.ts"
11
+ import type { InputTypes } from "./InputTypes.ts"
12
+ import { metadata, invalid } from "./SchemaTypes.ts"
13
+
14
+ export interface ErrorSchema extends Schema.ConstraintDecoder<unknown, never> {
15
+ readonly Type: Error & { readonly _tag: string }
16
+ readonly fields: Schema.Struct.Fields
17
+ }
18
+
19
+ export type ErrorsOf<E extends readonly ErrorSchema[]> = E[number]["Type"]
20
+
21
+ export interface ResultOptions {
22
+ /** Override the generated result name to preserve an existing schema contract. */
23
+ readonly name?: string
24
+ readonly description?: string
25
+ }
26
+
27
+ export const errorMember = Symbol("GraphQL error member")
28
+
29
+ export function errorName(error: ErrorSchema): string {
30
+ const tag = error.fields._tag?.ast
31
+ if (tag?._tag !== "Literal" || typeof tag.literal !== "string")
32
+ return invalid("Expected a Schema.TaggedError with a literal string _tag")
33
+ return tag.literal
34
+ }
35
+
36
+ export function makeResultTypes(
37
+ inputs: InputTypes,
38
+ add: <T extends GraphQLNamedType>(type: T) => T,
39
+ objectType: (name: string, coordinate: string) => GraphQLObjectType,
40
+ named: ReadonlyMap<string, GraphQLNamedType>,
41
+ ): BuildContext["result"] {
42
+ const errorTypes = new Map<ErrorSchema, GraphQLObjectType>()
43
+ const errorType = (error: ErrorSchema) => {
44
+ const existing = errorTypes.get(error)
45
+ if (existing) return existing
46
+ const name = errorName(error)
47
+ const registered = named.get(name)
48
+ if (registered) {
49
+ if (!(registered instanceof GraphQLObjectType))
50
+ return invalid(
51
+ `Result error ${name} must be an object; register its error boundary as an object`,
52
+ )
53
+ errorTypes.set(error, registered)
54
+ return registered
55
+ }
56
+ const type = add(
57
+ new GraphQLObjectType({
58
+ name,
59
+ fields: Object.fromEntries(
60
+ Object.entries(error.fields)
61
+ .filter(([key]) => key !== "_tag")
62
+ .map(([key, field]) => [
63
+ key,
64
+ (() => {
65
+ const type = inputs.type(field.ast, `${name}.${key}`)
66
+ if (!isOutputType(type))
67
+ return invalid(
68
+ `Error fields need an output scalar at ${name}.${key}`,
69
+ )
70
+ return { type, ...metadata(field.ast) }
71
+ })(),
72
+ ]),
73
+ ),
74
+ }),
75
+ )
76
+ errorTypes.set(error, type)
77
+ return type
78
+ }
79
+ return (ref, errors, coordinate, options = {}, nullable = false) => {
80
+ const success = named.get(ref)
81
+ const successMembers =
82
+ success instanceof GraphQLUnionType
83
+ ? success.getTypes()
84
+ : [objectType(ref, coordinate)]
85
+ const [parent, field = parent!] = coordinate.split(".")
86
+ // Mutation results keep the conventional <field>Result name; other parents
87
+ // are prefixed to avoid collisions across roots.
88
+ const prefix = parent === "Mutation" ? "" : parent
89
+ const name =
90
+ options.name ??
91
+ `${prefix}${field[0]!.toUpperCase()}${field.slice(1)}Result`
92
+ const members = [...successMembers, ...errors.map(errorType)]
93
+ if (new Set(members).size !== members.length)
94
+ invalid(`Duplicate union member: ${name}`)
95
+ const type = add(
96
+ new GraphQLUnionType({
97
+ name,
98
+ ...(options.description === undefined
99
+ ? {}
100
+ : { description: options.description }),
101
+ types: members,
102
+ resolveType: (
103
+ value,
104
+ context,
105
+ info,
106
+ abstractType: GraphQLAbstractType,
107
+ ) => {
108
+ if (
109
+ typeof value === "object" &&
110
+ value !== null &&
111
+ errorMember in value
112
+ )
113
+ return Reflect.get(value, errorMember)
114
+ if (success instanceof GraphQLUnionType) {
115
+ if (success.resolveType)
116
+ return success.resolveType(value, context, info, abstractType)
117
+ const tag: unknown =
118
+ Reflect.get(Object(value), "__typename") ??
119
+ Reflect.get(Object(value), "_tag")
120
+ return typeof tag === "string" ? tag : undefined
121
+ }
122
+ return ref
123
+ },
124
+ }),
125
+ )
126
+ return nullable ? type : new GraphQLNonNull(type)
127
+ }
128
+ }
@@ -0,0 +1,30 @@
1
+ import { GraphQLScalarType, specifiedScalarTypes } from "graphql"
2
+
3
+ /** Retain callback provenance even when graphql-js forwards a thrown GraphQLError unchanged. */
4
+ export class ScalarInputFailure extends Error {
5
+ constructor(readonly original: unknown) {
6
+ super("Scalar input failed")
7
+ }
8
+ }
9
+
10
+ const guarded = new WeakSet<GraphQLScalarType>()
11
+
12
+ /** Clone adopted scalars so validation cannot mistake application exceptions for document diagnostics. */
13
+ export function guardScalar(type: GraphQLScalarType): GraphQLScalarType {
14
+ if (specifiedScalarTypes.includes(type) || guarded.has(type)) return type
15
+ const input = <A>(evaluate: () => A): A => {
16
+ try {
17
+ return evaluate()
18
+ } catch (error) {
19
+ throw new ScalarInputFailure(error)
20
+ }
21
+ }
22
+ const result = new GraphQLScalarType({
23
+ ...type.toConfig(),
24
+ parseValue: (value) => input(() => type.parseValue(value)),
25
+ parseLiteral: (node, variables) =>
26
+ input(() => type.parseLiteral(node, variables)),
27
+ })
28
+ guarded.add(result)
29
+ return result
30
+ }
package/src/Scalars.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { Schema } from "effect"
2
+ import {
3
+ GraphQLScalarType,
4
+ GraphQLEnumType,
5
+ valueFromASTUntyped,
6
+ Kind,
7
+ type ValueNode,
8
+ } from "graphql"
9
+ import { fieldError } from "./Errors.ts"
10
+ import { guardScalar } from "./ScalarBoundary.ts"
11
+ import { invalid, metadata, annotations } from "./SchemaTypes.ts"
12
+
13
+ export const date = (value: unknown) => {
14
+ if (typeof value !== "string")
15
+ return invalid("DateTime expects an ISO string")
16
+ const result = new Date(value)
17
+ return Number.isFinite(result.getTime())
18
+ ? result
19
+ : invalid("Invalid DateTime")
20
+ }
21
+
22
+ const invalidInput = (message: string): never => {
23
+ throw fieldError(message, { code: "BAD_USER_INPUT" })
24
+ }
25
+
26
+ const dateInput = (value: unknown): string => {
27
+ try {
28
+ return date(value).toISOString()
29
+ } catch {
30
+ return invalidInput("DateTime expects a valid ISO string")
31
+ }
32
+ }
33
+
34
+ export const dateTime = () =>
35
+ guardScalar(
36
+ new GraphQLScalarType({
37
+ name: "DateTime",
38
+ description: "A valid DateTime value",
39
+ serialize: (value) =>
40
+ value instanceof Date && Number.isFinite(value.getTime())
41
+ ? value.toISOString()
42
+ : date(value).toISOString(),
43
+ parseValue: dateInput,
44
+ parseLiteral: (node) =>
45
+ node.kind === Kind.STRING
46
+ ? dateInput(node.value)
47
+ : invalidInput("DateTime expects a string literal"),
48
+ }),
49
+ )
50
+
51
+ export function scalar<A, I>(
52
+ name: string,
53
+ codec: Schema.Codec<A, I>,
54
+ ): GraphQLScalarType {
55
+ const decode = Schema.decodeUnknownSync(codec)
56
+ const encode = Schema.encodeUnknownSync(codec)
57
+ const decodeInput = (value: unknown) => {
58
+ try {
59
+ return decode(value)
60
+ } catch (error) {
61
+ if (Schema.isSchemaError(error))
62
+ return invalidInput(`Invalid value for ${name}`)
63
+ throw error
64
+ }
65
+ }
66
+ return guardScalar(
67
+ new GraphQLScalarType({
68
+ name,
69
+ ...metadata(codec.ast),
70
+ specifiedByURL: annotations(codec.ast).specifiedByURL,
71
+ serialize: encode,
72
+ // Keep the encoded representation until the argument Struct converts it for the resolver.
73
+ parseValue: (value) => {
74
+ decodeInput(value)
75
+ return value
76
+ },
77
+ parseLiteral: (node, variables) => {
78
+ if (node.kind === Kind.ENUM)
79
+ return invalidInput(
80
+ `${name} cannot represent an enum literal; quote the value as a string`,
81
+ )
82
+ const value: unknown = valueFromASTUntyped(node, variables)
83
+ decodeInput(value)
84
+ return value
85
+ },
86
+ }),
87
+ )
88
+ }
89
+
90
+ function jsonLiteral(
91
+ node: ValueNode,
92
+ variables?: Readonly<Record<string, unknown>> | null,
93
+ ): unknown {
94
+ switch (node.kind) {
95
+ case Kind.ENUM:
96
+ return invalidInput(
97
+ "JSON cannot represent an enum literal; quote the value as a string",
98
+ )
99
+ case Kind.OBJECT:
100
+ return Object.fromEntries(
101
+ node.fields.map((field) => [
102
+ field.name.value,
103
+ jsonLiteral(field.value, variables),
104
+ ]),
105
+ )
106
+ case Kind.LIST:
107
+ return node.values.map((value) => jsonLiteral(value, variables))
108
+ default:
109
+ return valueFromASTUntyped(node, variables)
110
+ }
111
+ }
112
+
113
+ /** Like graphql-type-json, variables and outputs are deliberately unrestricted JS values. */
114
+ export const jsonScalar = () =>
115
+ guardScalar(
116
+ new GraphQLScalarType({
117
+ name: "JSON",
118
+ description:
119
+ "The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).",
120
+ serialize: (value) => value,
121
+ parseValue: (value) => value,
122
+ parseLiteral: jsonLiteral,
123
+ }),
124
+ )
125
+
126
+ export function enumeration(
127
+ name: string,
128
+ schema: Schema.Schema<string>,
129
+ ): GraphQLEnumType {
130
+ const ast = schema.ast
131
+ const values = ast._tag === "Union" ? ast.types : [ast]
132
+ return new GraphQLEnumType({
133
+ name,
134
+ ...metadata(ast),
135
+ values: Object.fromEntries(
136
+ values.map((value) => {
137
+ if (value._tag !== "Literal" || typeof value.literal !== "string")
138
+ return invalid(`${name}: enums require string Schema.Literals`)
139
+ return [value.literal, { value: value.literal, ...metadata(value) }]
140
+ }),
141
+ ),
142
+ })
143
+ }
@@ -0,0 +1,90 @@
1
+ import type { SchemaAST } from "effect"
2
+ import { Schema } from "effect"
3
+ import {
4
+ type GraphQLOutputType,
5
+ type GraphQLNullableType,
6
+ type GraphQLInputType,
7
+ isNonNullType,
8
+ } from "graphql"
9
+
10
+ declare module "effect/Schema" {
11
+ namespace Annotations {
12
+ interface Annotations {
13
+ readonly specifiedByURL?: string
14
+ readonly graphqlType?: string
15
+ readonly graphqlDefault?: unknown
16
+ readonly deprecationReason?: string
17
+ }
18
+ }
19
+ }
20
+
21
+ export const ID = Schema.String.annotate({ graphqlType: "ID" })
22
+
23
+ export function id<S extends Schema.Top & Schema.Schema<string>>(
24
+ schema: S,
25
+ ): S["Rebuild"] {
26
+ return schema.annotate({ graphqlType: "ID" })
27
+ }
28
+
29
+ export const JSONValue = Schema.Unknown.annotate({ graphqlType: "JSON" })
30
+
31
+ /** DateTime inputs decode to normalized ISO strings. */
32
+ export const DateTime = Schema.String.annotate({ graphqlType: "DateTime" })
33
+
34
+ export function input<S extends Schema.Top>(
35
+ name: string,
36
+ schema: S,
37
+ ): S["Rebuild"] {
38
+ // Schema annotations preserve the codec and its fields (including Schema classes).
39
+ return schema.annotate({ graphqlType: name })
40
+ }
41
+
42
+ export function defaulted<
43
+ S extends Schema.Top & Schema.Codec<unknown, unknown>,
44
+ >(schema: S, value: NoInfer<S["Type"]>): S["Rebuild"] {
45
+ Schema.decodeUnknownSync(Schema.toType(schema))(value)
46
+ return schema.annotate({ graphqlDefault: value })
47
+ }
48
+
49
+ export const invalid = (message: string): never => {
50
+ throw new Error(message)
51
+ }
52
+
53
+ export const annotations = (ast: SchemaAST.AST) => ({
54
+ ...Schema.resolveAnnotations(Schema.make(ast)),
55
+ ...ast.context?.annotations,
56
+ })
57
+
58
+ export function descriptionOf(ast: SchemaAST.AST): string | undefined {
59
+ const own = annotations(ast).description
60
+ return typeof own === "string"
61
+ ? own
62
+ : ast._tag === "Union"
63
+ ? ast.types.map(descriptionOf).find((v) => v !== undefined)
64
+ : undefined
65
+ }
66
+
67
+ export function metadata(ast: SchemaAST.AST) {
68
+ const description = descriptionOf(ast)
69
+ const deprecationReason = annotations(ast).deprecationReason
70
+ return {
71
+ ...(description === undefined ? {} : { description }),
72
+ ...(deprecationReason === undefined ? {} : { deprecationReason }),
73
+ }
74
+ }
75
+
76
+ export function representation(node: {
77
+ readonly annotations?: Schema.Annotations.Annotations | undefined
78
+ }): unknown {
79
+ return Reflect.get(Object(node.annotations?.representation), "id")
80
+ }
81
+
82
+ export function unwrap(
83
+ type: GraphQLOutputType,
84
+ ): GraphQLOutputType & GraphQLNullableType
85
+ export function unwrap(
86
+ type: GraphQLInputType,
87
+ ): GraphQLInputType & GraphQLNullableType
88
+ export function unwrap(type: GraphQLOutputType | GraphQLInputType) {
89
+ return isNonNullType(type) ? type.ofType : type
90
+ }
@@ -0,0 +1,93 @@
1
+ import { Effect, Stream } from "effect"
2
+ import {
3
+ type ErrorPolicy,
4
+ fieldError,
5
+ forbidden,
6
+ protect,
7
+ recoverCause,
8
+ } from "./Errors.ts"
9
+ import { describe, normalize } from "./FieldRuntime.ts"
10
+ import { type RuntimeRef, type Field, fieldRequirements } from "./FieldTypes.ts"
11
+ import { argumentDecoder, optionalNullMessage } from "./InputTypes.ts"
12
+ import { type AnyConfig } from "./Resolver.ts"
13
+
14
+ /** Setup and delivery share the resolver policy; authorization runs on every event. */
15
+ export const eventField = (
16
+ policy: ErrorPolicy,
17
+ ref: RuntimeRef,
18
+ options: AnyConfig,
19
+ ): Field<unknown> => ({
20
+ kind: "field",
21
+ [fieldRequirements]: undefined,
22
+ build(context, name) {
23
+ const decode = argumentDecoder(options.args ?? {})
24
+ return {
25
+ type: context.output(ref, name),
26
+ ...describe(options),
27
+ args: Object.fromEntries(
28
+ Object.entries(options.args ?? {}).map(([key, schema]) => [
29
+ key,
30
+ context.input.argument(schema, `${name}(${key})`),
31
+ ]),
32
+ ),
33
+ subscribe(source, raw, request, info) {
34
+ const coordinate = `${info.parentType.name}.${info.fieldName}`
35
+ return request.stream(
36
+ Stream.suspend(() => {
37
+ let decoded: unknown
38
+ try {
39
+ decoded = decode(raw)
40
+ } catch {
41
+ throw fieldError(
42
+ optionalNullMessage(options.args ?? {}, raw) ??
43
+ "Invalid arguments",
44
+ { code: "BAD_USER_INPUT" },
45
+ )
46
+ }
47
+ if (!options.subscribe)
48
+ throw new Error("Subscription needs a stream")
49
+ return options.subscribe(source, decoded, request.context)
50
+ }).pipe(
51
+ Stream.catchCause((cause) =>
52
+ Stream.fromEffect(
53
+ recoverCause(cause, [], policy, {
54
+ coordinate,
55
+ phase: "subscribe",
56
+ }),
57
+ ),
58
+ ),
59
+ ),
60
+ )
61
+ },
62
+ resolve(value, raw, request, info) {
63
+ const coordinate = `${info.parentType.name}.${info.fieldName}`
64
+ return request.run(
65
+ protect(
66
+ () => {
67
+ if (value instanceof Error) return value
68
+ const decoded = decode(raw)
69
+ return Effect.gen(function* () {
70
+ const allowed = yield* protect(
71
+ () =>
72
+ options.authorize
73
+ ? options.authorize(value, decoded, request.context)
74
+ : true,
75
+ [],
76
+ policy,
77
+ { coordinate, phase: "authorize" },
78
+ )
79
+ if (allowed instanceof Error) return allowed
80
+ return allowed === true
81
+ ? normalize(ref, value)
82
+ : forbidden(info.fieldName)
83
+ })
84
+ },
85
+ [],
86
+ policy,
87
+ { coordinate, phase: "resolve" },
88
+ ),
89
+ )
90
+ },
91
+ }
92
+ },
93
+ })