1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226 |
x3
x3
x3
x3
x3
x3
x3
x3
x3
x46
x49
x147
x49
x46
x47
x47
x255
x46
x3
x3
x3
x30
x35
x5
x3
x8
x8
x8
x14
x14
x15
x15
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x19
x133
x152
x19
x19
x19
x19
x19
x80
x19
x19
x60
x20
x19
x19
x19
x20
x20
x19
x19
x21
x21
x22
x22
x22
x22
x22
x19
x19
x21
x21
x21
x19
x19
x19
x12
x48
x48
x12
x8
x8
x3
x30
x30
x30
x30
x30
x30
x30
x3
x44
x45
x45
x84
x44
x45
x45
x84
x44
x99
x375
x125
x297
x99
x44
x47
x47
x82
x44
x50
x50
x82
x44
x3
x104
x952
x116
x104
x945
x135
x162
x162
x162
x162
x247
x755
x251
x251
x247
x254
x254
x321
x247
x247
x262
x247
x260
x247
x261
x247
x1000
x496
x249
x249
x249
x247
x162
x104
x105
x105
x104
x105
x105
x576
x104
x3
x13
x13 |
I
I
I
I
|
import { toCamelCase } from "@std/text/to-camel-case"
import { toPascalCase as titleCase } from "@std/text/to-pascal-case"
import type { Arg, Arrayable, record, rw } from "@libs/typing"
import type { Resource } from "../resource.ts"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { printWithComments } from "@graphql-tools/utils"
import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge"
import { GraphQLHTTP } from "@deno-libs/gql"
import { schema as json_schema } from "../is/is.ts"
export { type Arg, type Arrayable, GraphQLHTTP, makeExecutableSchema, mergeResolvers, mergeTypeDefs }
export function toGraphQLDefinition(resource: Arrayable<typeof Resource>): string
export function toGraphQLDefinition(name: string, schema: ReturnType<typeof json_schema>, options?: { type: "type" | "input" }): string
export function toGraphQLDefinition(name: string | Arrayable<typeof Resource>, schema?: ReturnType<typeof json_schema>, { type = "type" as "type" | "input" } = {}): string {
if (typeof name !== "string") {
const resources = name
return toGraphQLSchema([resources].flat())
}
if ((typeof schema !== "object") || (!schema?.["$schema"])) {
throw new TypeError("Expected object to be a JSON schema.")
}
return toGraphQLType(titleCase(name), structuredClone(schema), { type })
}
export function graphql(
resources: Arrayable<typeof Resource>,
{ graphiql = true, ...options } = {} as { graphiql?: boolean; typedefs?: string; resolvers?: record },
): { schema: ReturnType<typeof makeExecutableSchema>; handler: ReturnType<typeof GraphQLHTTP> } {
const schema = makeExecutableSchema(toGraphQLSchema([resources].flat(), { raw: true, ...options }))
return { schema, handler: GraphQLHTTP({ schema, graphiql }) }
}
function toGraphQLSchema(resources: Array<typeof Resource>, options?: { raw?: false; typedefs?: string; resolvers?: record }): string
function toGraphQLSchema(resources: Array<typeof Resource>, options: { raw: true; typedefs?: string; resolvers?: record }): { typeDefs: ast; resolvers: resolvers }
function toGraphQLSchema(resources: Array<typeof Resource>, { raw = false, typedefs: _typedefs = "", resolvers: _resolvers = {} } = {} as { raw?: boolean; typedefs?: string; resolvers?: record }): string | { typeDefs: ast; resolvers: resolvers } {
const definitions = new Map<string, string>()
const resolvers = new Map<string, resolvers>()
for (const resource of resources) {
const name = titleCase(resource.name)
if (definitions.has(name)) {
throw new TypeError(`Redefining resource "${name}". Each resource should have a unique name.`)
}
let definition = toGraphQLDefinition(name, resource.schema)
definition += [
`type Query {`,
` ${toCamelCase(pluralize(name))}: [${name}]`,
` ${toCamelCase(name)}(id: ID!): ${name}`,
`}`,
`type Mutation {`,
` create${name}(input: ${name}CreateInput): ${name}`,
` update${name}(id: ID!, input: ${name}UpdateInput): ${name}`,
` delete${name}(id: ID!): ${name}`,
`}`,
toGraphQLDefinition(`${name}CreateInput`, json_schema((resource as rw).model.omit({ id: true, created: true, updated: true })), { type: "input" }),
toGraphQLDefinition(`${name}UpdateInput`, json_schema((resource as rw).model.omit({ id: true, created: true, updated: true, ...(resource as rw).readonly })), { type: "input" }),
].join("\n")
definitions.set(name, definition)
resolvers.set(name, {
Query: {
[`${toCamelCase(pluralize(name))}`]() {
return resource.list([], { array: true, raw: true })
},
[`${toCamelCase(name)}`](_: unknown, { id }: { id: string }) {
return resource.get(id, { raw: true })
},
},
Mutation: {
async [`create${name}`](_: unknown, { input }: { input: unknown }) {
const instance = await new resource(input as any).save()
return instance.data
},
async [`update${name}`](_: unknown, { id, input }: { id: string; input: unknown }) {
const instance = await resource.get(id)
if (!instance) {
return null
}
await instance.patch(input as rw)
await instance.save()
return instance.data
},
async [`delete${name}`](_: unknown, { id }: { id: string }) {
const instance = await resource.delete(id)
return instance?.data
},
},
})
}
const result = {
typeDefs: mergeTypeDefs([...definitions.values(), _typedefs]),
resolvers: mergeResolvers([...resolvers.values(), _resolvers]),
}
return raw ? result : printWithComments(result.typeDefs)
}
function documentation(description = "", { indent = "" } = {}) {
return [
`${indent}"""`,
...description.split("\n").map((line: string) => `${indent}${line}`),
`${indent}"""`,
"",
].join("\n")
}
function toGraphQLType(name: string, schema: rw, { type = "type" as string, definitions = [] as string[] } = {}) {
if (schema.type !== "object") {
throw new TypeError(`Schema must be of type "object", not "${schema.type}"`)
}
let output = ""
if (schema.description) {
output += documentation(schema.description)
}
output += `${type} ${name} {\n`
for (const property in schema.properties) {
if (schema.properties[property].description) {
output += documentation(schema.properties[property].description, { indent: " " })
}
output += toGraphQLPrimitive(name, property, schema.properties[property], { definitions })
}
if (!Object.keys(schema.properties).length) {
output += ` """\n This field has no effect.\n """\n _: Boolean\n`
}
output += "}\n"
for (const definition of definitions) {
output += `\n${definition}\n`
}
return output
}
function toGraphQLPrimitive(name: string, property: string, schema: rw, { subtype = false, definitions = [] as string[] }) {
if (Array.isArray(schema.type)) {
return toGraphQLPrimitive(name, property, { anyOf: schema.type.map((type: string) => ({ type })) }, { subtype, definitions })
}
if (!schema.anyOf) {
return toGraphQLPrimitive(name, property, { anyOf: [schema] }, { subtype, definitions })
}
const nullable = schema.anyOf.some(({ type }: { type: string }) => type === "null")
const types = new Set<string>()
const namepath = titleCase(`${name} ${property}`)
schema.anyOf.forEach((validation: rw) => {
if ((validation.enum) || (validation.const)) {
definitions.push(`enum ${namepath} {\n${(validation.enum ?? [validation.const]).map((choice: string) => ` ${choice}`).join("\n")}\n}`)
return types.add(namepath)
}
if ((validation.type === "string") && (property === "id")) {
return types.add("ID")
}
switch (validation.type) {
case "integer":
case "number":
return types.add("Float")
case "string":
return types.add("String")
case "boolean":
return types.add("Boolean")
case "array":
return types.add(`[${toGraphQLPrimitive(name, property, validation.items, { subtype: true, definitions })}]`)
case "object": {
definitions.push(toGraphQLType(namepath, validation))
return types.add(namepath)
}
}
})
if (!types.size) {
throw new TypeError(`Unsupported type: ${name}.${property}`)
}
if (types.size > 1) {
throw new TypeError(`Union types are not supported: ${name}.${property}`)
}
return subtype ? `${[...types][0]}${nullable ? "" : "!"}` : ` ${property}: ${[...types][0]}${nullable ? "" : "!"}\n`
}
type resolvers = any
type ast = any
function pluralize(word: string) {
if (/(?:s|x|ch|sh)$/.test(word)) {
return `${word}es`
}
if (/(?:[^aeiou])y$/.test(word)) {
return `${word.slice(0, -1)}ies`
}
if (/o$/.test(word)) {
return `${word}es`
}
if (/f$/.test(word)) {
return `${word.slice(0, -1)}ves`
}
return `${word}s`
}
|