@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.
- package/CHANGELOG.md +21 -0
- package/README.md +1280 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/Assembly.d.ts +24 -0
- package/dist/Assembly.d.ts.map +1 -0
- package/dist/Assembly.js +236 -0
- package/dist/Assembly.js.map +1 -0
- package/dist/Builder.d.ts +27 -0
- package/dist/Builder.d.ts.map +1 -0
- package/dist/Builder.js +40 -0
- package/dist/Builder.js.map +1 -0
- package/dist/Connections.d.ts +41 -0
- package/dist/Connections.d.ts.map +1 -0
- package/dist/Connections.js +243 -0
- package/dist/Connections.js.map +1 -0
- package/dist/Definitions.d.ts +139 -0
- package/dist/Definitions.d.ts.map +1 -0
- package/dist/Definitions.js +117 -0
- package/dist/Definitions.js.map +1 -0
- package/dist/Errors.d.ts +39 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +103 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Execution.d.ts +52 -0
- package/dist/Execution.d.ts.map +1 -0
- package/dist/Execution.js +174 -0
- package/dist/Execution.js.map +1 -0
- package/dist/FieldRuntime.d.ts +5 -0
- package/dist/FieldRuntime.d.ts.map +1 -0
- package/dist/FieldRuntime.js +27 -0
- package/dist/FieldRuntime.js.map +1 -0
- package/dist/FieldTypes.d.ts +232 -0
- package/dist/FieldTypes.d.ts.map +1 -0
- package/dist/FieldTypes.js +6 -0
- package/dist/FieldTypes.js.map +1 -0
- package/dist/Fields.d.ts +8 -0
- package/dist/Fields.d.ts.map +1 -0
- package/dist/Fields.js +72 -0
- package/dist/Fields.js.map +1 -0
- package/dist/Formatting.d.ts +25 -0
- package/dist/Formatting.d.ts.map +1 -0
- package/dist/Formatting.js +29 -0
- package/dist/Formatting.js.map +1 -0
- package/dist/GraphQL.d.ts +10 -0
- package/dist/GraphQL.d.ts.map +1 -0
- package/dist/GraphQL.js +5 -0
- package/dist/GraphQL.js.map +1 -0
- package/dist/InputTypes.d.ts +27 -0
- package/dist/InputTypes.d.ts.map +1 -0
- package/dist/InputTypes.js +203 -0
- package/dist/InputTypes.js.map +1 -0
- package/dist/Registry.d.ts +70 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +3 -0
- package/dist/Registry.js.map +1 -0
- package/dist/Resolver.d.ts +30 -0
- package/dist/Resolver.d.ts.map +1 -0
- package/dist/Resolver.js +76 -0
- package/dist/Resolver.js.map +1 -0
- package/dist/Results.d.ts +20 -0
- package/dist/Results.d.ts.map +1 -0
- package/dist/Results.js +79 -0
- package/dist/Results.js.map +1 -0
- package/dist/ScalarBoundary.d.ts +9 -0
- package/dist/ScalarBoundary.d.ts.map +1 -0
- package/dist/ScalarBoundary.js +31 -0
- package/dist/ScalarBoundary.js.map +1 -0
- package/dist/Scalars.d.ts +9 -0
- package/dist/Scalars.d.ts.map +1 -0
- package/dist/Scalars.js +104 -0
- package/dist/Scalars.js.map +1 -0
- package/dist/SchemaTypes.d.ts +55 -0
- package/dist/SchemaTypes.d.ts.map +1 -0
- package/dist/SchemaTypes.js +47 -0
- package/dist/SchemaTypes.js.map +1 -0
- package/dist/Subscription.d.ts +6 -0
- package/dist/Subscription.d.ts.map +1 -0
- package/dist/Subscription.js +60 -0
- package/dist/Subscription.js.map +1 -0
- package/dist/Testing.d.ts +52 -0
- package/dist/Testing.d.ts.map +1 -0
- package/dist/Testing.js +74 -0
- package/dist/Testing.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/SubscriptionRule.d.ts +8 -0
- package/dist/internal/SubscriptionRule.d.ts.map +1 -0
- package/dist/internal/SubscriptionRule.js +73 -0
- package/dist/internal/SubscriptionRule.js.map +1 -0
- package/package.json +55 -4
- package/src/Assembly.ts +352 -0
- package/src/Builder.ts +110 -0
- package/src/Connections.ts +374 -0
- package/src/Definitions.ts +465 -0
- package/src/Errors.ts +188 -0
- package/src/Execution.ts +354 -0
- package/src/FieldRuntime.ts +32 -0
- package/src/FieldTypes.ts +667 -0
- package/src/Fields.ts +117 -0
- package/src/Formatting.ts +54 -0
- package/src/GraphQL.ts +50 -0
- package/src/InputTypes.ts +266 -0
- package/src/Registry.ts +157 -0
- package/src/Resolver.ts +176 -0
- package/src/Results.ts +128 -0
- package/src/ScalarBoundary.ts +30 -0
- package/src/Scalars.ts +143 -0
- package/src/SchemaTypes.ts +90 -0
- package/src/Subscription.ts +93 -0
- package/src/Testing.ts +144 -0
- package/src/index.ts +1 -0
- package/src/internal/SubscriptionRule.ts +90 -0
package/src/Resolver.ts
ADDED
|
@@ -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
|
+
})
|