@ultimabase/graphql 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -2
- package/dist/data-types.d.ts +15 -0
- package/dist/data-types.d.ts.map +1 -0
- package/dist/data-types.js +53 -0
- package/dist/data.d.ts +34 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +207 -0
- package/dist/depth.d.ts +6 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +67 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/functions.d.ts +28 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +195 -0
- package/dist/graphiql.d.ts +3 -0
- package/dist/graphiql.d.ts.map +1 -0
- package/dist/graphiql.js +46 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/inputs.d.ts +20 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +236 -0
- package/dist/json-fields.d.ts +12 -0
- package/dist/json-fields.d.ts.map +1 -0
- package/dist/json-fields.js +66 -0
- package/dist/json-schema.d.ts +7 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +16 -0
- package/dist/live-stream.d.ts +59 -0
- package/dist/live-stream.d.ts.map +1 -0
- package/dist/live-stream.js +136 -0
- package/dist/mutations.d.ts +9 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +228 -0
- package/dist/names.d.ts +8 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +14 -0
- package/dist/outputs.d.ts +16 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +177 -0
- package/dist/overlap.d.ts +5 -0
- package/dist/overlap.d.ts.map +1 -0
- package/dist/overlap.js +82 -0
- package/dist/pages.d.ts +11 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +22 -0
- package/dist/registry.d.ts +80 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +97 -0
- package/dist/reread.d.ts +9 -0
- package/dist/reread.d.ts.map +1 -0
- package/dist/reread.js +116 -0
- package/dist/rows.d.ts +23 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +106 -0
- package/dist/scalars.d.ts +13 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/scalars.js +88 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +130 -0
- package/dist/select.d.ts +24 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +152 -0
- package/dist/sockets.d.ts +25 -0
- package/dist/sockets.d.ts.map +1 -0
- package/dist/sockets.js +234 -0
- package/dist/subscriptions.d.ts +14 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +242 -0
- package/package.json +58 -3
- package/src/data-types.ts +100 -0
- package/src/data.ts +366 -0
- package/src/depth.ts +98 -0
- package/src/errors.ts +48 -0
- package/src/functions.ts +291 -0
- package/src/graphiql.ts +54 -0
- package/src/index.ts +231 -0
- package/src/inputs.ts +348 -0
- package/src/json-fields.ts +95 -0
- package/src/json-schema.ts +21 -0
- package/src/live-stream.ts +192 -0
- package/src/mutations.ts +317 -0
- package/src/names.ts +25 -0
- package/src/outputs.ts +267 -0
- package/src/overlap.ts +97 -0
- package/src/pages.ts +39 -0
- package/src/registry.ts +189 -0
- package/src/reread.ts +159 -0
- package/src/rows.ts +151 -0
- package/src/scalars.ts +110 -0
- package/src/schema.ts +189 -0
- package/src/select.ts +230 -0
- package/src/sockets.ts +291 -0
- package/src/subscriptions.ts +334 -0
package/src/mutations.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// The writes (D-107): `createPost(data)`, `updatePost(where, data)`,
|
|
2
|
+
// `upsertPost(where, create, update)` (D-142) and `deletePost(where)`, each
|
|
3
|
+
// one kernel write run as its caller. A create, an update and an upsert
|
|
4
|
+
// return the row as the query asks for it, a delete the row as it was,
|
|
5
|
+
// without its relations.
|
|
6
|
+
import { run } from '@ultimabase/http/internal'
|
|
7
|
+
import type {
|
|
8
|
+
GraphQLFieldConfig,
|
|
9
|
+
GraphQLInputFieldConfigMap,
|
|
10
|
+
GraphQLNullableInputType,
|
|
11
|
+
} from 'graphql'
|
|
12
|
+
import {
|
|
13
|
+
GraphQLBoolean,
|
|
14
|
+
GraphQLID,
|
|
15
|
+
GraphQLInputObjectType,
|
|
16
|
+
GraphQLList,
|
|
17
|
+
GraphQLNonNull,
|
|
18
|
+
} from 'graphql'
|
|
19
|
+
import type { SchemaField } from 'ultimabase'
|
|
20
|
+
import { objectOf, uniqueInput, uniqueWhere } from './inputs.ts'
|
|
21
|
+
import { jsonInput, plainData } from './json-fields.ts'
|
|
22
|
+
import { upper } from './names.ts'
|
|
23
|
+
import { deletedType, isMaster, objectType } from './outputs.ts'
|
|
24
|
+
import type { Args, Context, Entry, Registry } from './registry.ts'
|
|
25
|
+
import { as, described, valueType } from './registry.ts'
|
|
26
|
+
import { JSONScalar } from './scalars.ts'
|
|
27
|
+
import { selectOf } from './select.ts'
|
|
28
|
+
|
|
29
|
+
type Fields = { [field: string]: GraphQLFieldConfig<unknown, Context, Args> }
|
|
30
|
+
|
|
31
|
+
/** `ConnectInput { id }`: what a relation write names a row by. */
|
|
32
|
+
const connectInput = (registry: Registry): GraphQLInputObjectType =>
|
|
33
|
+
registry.input(
|
|
34
|
+
'ConnectInput',
|
|
35
|
+
() =>
|
|
36
|
+
new GraphQLInputObjectType({
|
|
37
|
+
name: 'ConnectInput',
|
|
38
|
+
fields: { id: { type: new GraphQLNonNull(GraphQLID) } },
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
type Relation = Exclude<SchemaField, { readonly kind: 'scalar' }>
|
|
43
|
+
|
|
44
|
+
const facing = (field: Relation): string => `Without${upper(field.inverse)}`
|
|
45
|
+
|
|
46
|
+
/** `CommentCreateOrConnectWithoutPostInput { where, create }` (D-138). */
|
|
47
|
+
const orCreateInput = (
|
|
48
|
+
registry: Registry,
|
|
49
|
+
field: Relation,
|
|
50
|
+
): GraphQLInputObjectType => {
|
|
51
|
+
const name = `${field.target}CreateOrConnect${facing(field)}Input`
|
|
52
|
+
return registry.input(
|
|
53
|
+
name,
|
|
54
|
+
() =>
|
|
55
|
+
new GraphQLInputObjectType({
|
|
56
|
+
name,
|
|
57
|
+
fields: () => ({
|
|
58
|
+
where: {
|
|
59
|
+
type: new GraphQLNonNull(uniqueInput(registry, field.target)),
|
|
60
|
+
},
|
|
61
|
+
create: {
|
|
62
|
+
type: new GraphQLNonNull(
|
|
63
|
+
dataInput(registry, field.target, true, field.inverse),
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `CommentUpdateWithWhereUniqueWithoutPostInput { where, data }` (D-138). */
|
|
72
|
+
const updateWhereInput = (
|
|
73
|
+
registry: Registry,
|
|
74
|
+
field: Relation,
|
|
75
|
+
): GraphQLInputObjectType => {
|
|
76
|
+
const name = `${field.target}UpdateWithWhereUnique${facing(field)}Input`
|
|
77
|
+
return registry.input(
|
|
78
|
+
name,
|
|
79
|
+
() =>
|
|
80
|
+
new GraphQLInputObjectType({
|
|
81
|
+
name,
|
|
82
|
+
fields: () => ({
|
|
83
|
+
where: {
|
|
84
|
+
type: new GraphQLNonNull(uniqueInput(registry, field.target)),
|
|
85
|
+
},
|
|
86
|
+
data: {
|
|
87
|
+
type: new GraphQLNonNull(
|
|
88
|
+
dataInput(registry, field.target, false, field.inverse),
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A relation's write, named as Prisma names it (D-20, D-128, D-138): a
|
|
98
|
+
* create connects or creates, an update also disconnects, unless the
|
|
99
|
+
* relation is a required `one`, and updates or deletes a linked row; a
|
|
100
|
+
* `many` is also `set`. `create` takes the target's data without the
|
|
101
|
+
* relation back: `PostCreateInput.comments` is
|
|
102
|
+
* `CommentCreateNestedManyWithoutPostInput { connect, connectOrCreate,
|
|
103
|
+
* create: [CommentCreateWithoutPostInput!] }`. A hidden model is not
|
|
104
|
+
* created, updated or deleted over GraphQL, through a relation either, as
|
|
105
|
+
* in Parse.
|
|
106
|
+
*/
|
|
107
|
+
function relationInput(
|
|
108
|
+
registry: Registry,
|
|
109
|
+
field: Relation,
|
|
110
|
+
creating: boolean,
|
|
111
|
+
): GraphQLInputObjectType {
|
|
112
|
+
const many = field.kind === 'many'
|
|
113
|
+
const shape = many ? 'Many' : field.optional ? 'One' : 'OneRequired'
|
|
114
|
+
const name = creating
|
|
115
|
+
? `${field.target}CreateNested${many ? 'Many' : 'One'}${facing(field)}Input`
|
|
116
|
+
: `${field.target}Update${shape}${facing(field)}NestedInput`
|
|
117
|
+
return registry.input(name, () => {
|
|
118
|
+
const listed = <T extends GraphQLInputObjectType>(type: T) =>
|
|
119
|
+
many ? new GraphQLList(new GraphQLNonNull(type)) : type
|
|
120
|
+
const row = listed(connectInput(registry))
|
|
121
|
+
const visible = registry.entry(field.target).visible
|
|
122
|
+
const updating = !creating && visible
|
|
123
|
+
const fields: GraphQLInputFieldConfigMap = { connect: { type: row } }
|
|
124
|
+
if (visible) {
|
|
125
|
+
fields['connectOrCreate'] = {
|
|
126
|
+
type: listed(orCreateInput(registry, field)),
|
|
127
|
+
}
|
|
128
|
+
const created = dataInput(registry, field.target, true, field.inverse)
|
|
129
|
+
fields['create'] = { type: listed(created) }
|
|
130
|
+
}
|
|
131
|
+
if (updating && shape !== 'OneRequired') {
|
|
132
|
+
const unique = uniqueInput(registry, field.target)
|
|
133
|
+
fields['delete'] = { type: many ? listed(unique) : GraphQLBoolean }
|
|
134
|
+
}
|
|
135
|
+
if (!creating && shape !== 'OneRequired') {
|
|
136
|
+
fields['disconnect'] = { type: many ? row : GraphQLBoolean }
|
|
137
|
+
}
|
|
138
|
+
// A filtered relation takes no `set` (D-151).
|
|
139
|
+
if (!creating && many && field.filtered !== true) {
|
|
140
|
+
fields['set'] = { type: row }
|
|
141
|
+
}
|
|
142
|
+
if (updating) {
|
|
143
|
+
fields['update'] = {
|
|
144
|
+
type: many
|
|
145
|
+
? listed(updateWhereInput(registry, field))
|
|
146
|
+
: dataInput(registry, field.target, false, field.inverse),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// A `one` relation takes one of them.
|
|
150
|
+
return new GraphQLInputObjectType({ name, fields, isOneOf: !many })
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The kernel's data from a mutation's: its json values plain, and each
|
|
156
|
+
* nested row's data and unique target too (D-128, D-138).
|
|
157
|
+
*/
|
|
158
|
+
function kernelData(registry: Registry, model: string, data: Args): Args {
|
|
159
|
+
const plained: { [field: string]: unknown } = plainData(registry, model, data)
|
|
160
|
+
for (const field of registry.entry(model).model.fields) {
|
|
161
|
+
const value = plained[field.name]
|
|
162
|
+
if (field.kind === 'scalar' || typeof value !== 'object' || value === null)
|
|
163
|
+
continue
|
|
164
|
+
const { target } = field
|
|
165
|
+
const row = (item: unknown) => kernelData(registry, target, objectOf(item))
|
|
166
|
+
const unique = (item: unknown) =>
|
|
167
|
+
uniqueWhere(registry, target, objectOf(item))
|
|
168
|
+
const write: { [key: string]: unknown } = { ...value }
|
|
169
|
+
const map = (key: string, read: (item: unknown) => unknown) => {
|
|
170
|
+
const operand = write[key]
|
|
171
|
+
if (operand === undefined || operand === null) return
|
|
172
|
+
write[key] = Array.isArray(operand) ? operand.map(read) : read(operand)
|
|
173
|
+
}
|
|
174
|
+
map('create', row)
|
|
175
|
+
map('connectOrCreate', (item) => {
|
|
176
|
+
const { where, create } = objectOf(item)
|
|
177
|
+
return { where: unique(where), create: row(create) }
|
|
178
|
+
})
|
|
179
|
+
if (field.kind === 'many') {
|
|
180
|
+
map('update', (item) => {
|
|
181
|
+
const { where, data: changes } = objectOf(item)
|
|
182
|
+
return { where: unique(where), data: row(changes) }
|
|
183
|
+
})
|
|
184
|
+
map('delete', unique)
|
|
185
|
+
} else map('update', row)
|
|
186
|
+
plained[field.name] = write
|
|
187
|
+
}
|
|
188
|
+
return plained
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fieldInput(
|
|
192
|
+
registry: Registry,
|
|
193
|
+
model: string,
|
|
194
|
+
field: SchemaField,
|
|
195
|
+
creating: boolean,
|
|
196
|
+
): GraphQLNullableInputType {
|
|
197
|
+
if (field.kind === 'scalar' && field.type === 'json') {
|
|
198
|
+
return jsonInput(registry, model, field).type
|
|
199
|
+
}
|
|
200
|
+
// A plugin field's schema takes its JSON (D-122).
|
|
201
|
+
if (registry.entry(model).exposed.fieldTypes[field.name] !== undefined) {
|
|
202
|
+
return JSONScalar
|
|
203
|
+
}
|
|
204
|
+
if (field.kind === 'scalar') return valueType(registry, model, field)
|
|
205
|
+
return relationInput(registry, field, creating)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `PostCreateInput` and `PostUpdateInput`: every field, those only Master
|
|
210
|
+
* reads included. A create needs the fields its type needs, neither optional
|
|
211
|
+
* nor with a default (D-53); the others are optional. A row created or
|
|
212
|
+
* updated through a relation takes `CommentCreateWithoutPostInput` or
|
|
213
|
+
* `CommentUpdateWithoutPostInput`, without that relation (D-128, D-138).
|
|
214
|
+
*/
|
|
215
|
+
function dataInput(
|
|
216
|
+
registry: Registry,
|
|
217
|
+
model: string,
|
|
218
|
+
creating: boolean,
|
|
219
|
+
without?: string,
|
|
220
|
+
): GraphQLInputObjectType {
|
|
221
|
+
const name = `${model}${creating ? 'Create' : 'Update'}${
|
|
222
|
+
without === undefined ? '' : `Without${upper(without)}`
|
|
223
|
+
}Input`
|
|
224
|
+
return registry.input(
|
|
225
|
+
name,
|
|
226
|
+
() =>
|
|
227
|
+
new GraphQLInputObjectType({
|
|
228
|
+
name,
|
|
229
|
+
fields: () => {
|
|
230
|
+
const { exposed } = registry.entry(model)
|
|
231
|
+
const required = new Set(creating ? exposed.required : [])
|
|
232
|
+
const fields: GraphQLInputFieldConfigMap = {}
|
|
233
|
+
for (const field of exposed.schema.fields) {
|
|
234
|
+
// A view is read-only (D-151).
|
|
235
|
+
const view = field.kind === 'many' && field.from !== undefined
|
|
236
|
+
if (field.name === without || view) continue
|
|
237
|
+
const type = fieldInput(registry, model, field, creating)
|
|
238
|
+
fields[field.name] = {
|
|
239
|
+
type: required.has(field.name) ? new GraphQLNonNull(type) : type,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return described(registry.entry(model), fields)
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A visible model's four mutations. */
|
|
249
|
+
export function mutationFields(registry: Registry, entry: Entry): Fields {
|
|
250
|
+
const name = entry.model.name
|
|
251
|
+
const type = objectType(registry, name)
|
|
252
|
+
const { queries } = entry.exposed
|
|
253
|
+
const where = { type: new GraphQLNonNull(uniqueInput(registry, name)) }
|
|
254
|
+
const data = (creating: boolean) => ({
|
|
255
|
+
type: new GraphQLNonNull(dataInput(registry, name, creating)),
|
|
256
|
+
})
|
|
257
|
+
const written =
|
|
258
|
+
(
|
|
259
|
+
write: (args: Args, select: Args, context: Context) => Promise<unknown>,
|
|
260
|
+
): GraphQLFieldConfig<unknown, Context, Args>['resolve'] =>
|
|
261
|
+
async (_, args, context, info) => {
|
|
262
|
+
const select = selectOf(
|
|
263
|
+
registry,
|
|
264
|
+
info,
|
|
265
|
+
type,
|
|
266
|
+
info.fieldNodes,
|
|
267
|
+
isMaster(context),
|
|
268
|
+
)
|
|
269
|
+
return run(() => write(args, select, context))
|
|
270
|
+
}
|
|
271
|
+
const unique = (args: Args): Args =>
|
|
272
|
+
uniqueWhere(registry, name, objectOf(args['where']))
|
|
273
|
+
const dataOf = (args: Args): Args =>
|
|
274
|
+
kernelData(registry, name, objectOf(args['data']))
|
|
275
|
+
return {
|
|
276
|
+
[`create${name}`]: {
|
|
277
|
+
type: new GraphQLNonNull(type),
|
|
278
|
+
args: { data: data(true) },
|
|
279
|
+
resolve: written(async (args, select, context) =>
|
|
280
|
+
queries.create({ data: dataOf(args), select }, as(context)),
|
|
281
|
+
),
|
|
282
|
+
},
|
|
283
|
+
[`update${name}`]: {
|
|
284
|
+
type: new GraphQLNonNull(type),
|
|
285
|
+
args: { where, data: data(false) },
|
|
286
|
+
resolve: written(async (args, select, context) =>
|
|
287
|
+
queries.update(
|
|
288
|
+
{ where: unique(args), data: dataOf(args), select },
|
|
289
|
+
as(context),
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
},
|
|
293
|
+
// The row it created or updated, as either branch returns it (D-142).
|
|
294
|
+
[`upsert${name}`]: {
|
|
295
|
+
type: new GraphQLNonNull(type),
|
|
296
|
+
args: { where, create: data(true), update: data(false) },
|
|
297
|
+
resolve: written(async (args, select, context) => {
|
|
298
|
+
const { row } = await entry.exposed.upsert(
|
|
299
|
+
{
|
|
300
|
+
where: unique(args),
|
|
301
|
+
create: kernelData(registry, name, objectOf(args['create'])),
|
|
302
|
+
update: kernelData(registry, name, objectOf(args['update'])),
|
|
303
|
+
select,
|
|
304
|
+
},
|
|
305
|
+
as(context),
|
|
306
|
+
)
|
|
307
|
+
return row
|
|
308
|
+
}),
|
|
309
|
+
},
|
|
310
|
+
[`delete${name}`]: {
|
|
311
|
+
type: new GraphQLNonNull(deletedType(registry, name)),
|
|
312
|
+
args: { where },
|
|
313
|
+
resolve: async (_, args, context) =>
|
|
314
|
+
run(async () => queries.delete({ where: unique(args) }, as(context))),
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
}
|
package/src/names.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The names the schema gives each model's types and root fields (D-107).
|
|
2
|
+
|
|
3
|
+
import { plural } from '@ultimabase/http/internal'
|
|
4
|
+
import { Refused } from 'ultimabase'
|
|
5
|
+
|
|
6
|
+
export const upper = (name: string): string =>
|
|
7
|
+
name.charAt(0).toUpperCase() + name.slice(1)
|
|
8
|
+
|
|
9
|
+
export const lower = (name: string): string =>
|
|
10
|
+
name.charAt(0).toLowerCase() + name.slice(1)
|
|
11
|
+
|
|
12
|
+
/** A model's root fields: its one-row name and its list name. */
|
|
13
|
+
export function rootNames(
|
|
14
|
+
model: string,
|
|
15
|
+
given: string | undefined,
|
|
16
|
+
): { readonly single: string; readonly list: string } {
|
|
17
|
+
const single = lower(model)
|
|
18
|
+
const list = given ?? plural(single)
|
|
19
|
+
if (list === single) {
|
|
20
|
+
throw new Refused(
|
|
21
|
+
`${model}: its list field would be named ${single} too; set .with({ graphql: { plural } }).`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return { single, list }
|
|
25
|
+
}
|
package/src/outputs.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// The output types (D-107): each model's object type, its edges and
|
|
2
|
+
// connections, and the arguments of its lists.
|
|
3
|
+
import { connection, readable, rowsOf } from '@ultimabase/http/internal'
|
|
4
|
+
import type {
|
|
5
|
+
GraphQLFieldConfig,
|
|
6
|
+
GraphQLFieldConfigArgumentMap,
|
|
7
|
+
GraphQLFieldConfigMap,
|
|
8
|
+
} from 'graphql'
|
|
9
|
+
import {
|
|
10
|
+
GraphQLBoolean,
|
|
11
|
+
GraphQLID,
|
|
12
|
+
GraphQLInt,
|
|
13
|
+
GraphQLList,
|
|
14
|
+
GraphQLNonNull,
|
|
15
|
+
GraphQLObjectType,
|
|
16
|
+
GraphQLString,
|
|
17
|
+
} from 'graphql'
|
|
18
|
+
import type { SchemaField } from 'ultimabase'
|
|
19
|
+
import { orderByInput, whereInput } from './inputs.ts'
|
|
20
|
+
import { upper } from './names.ts'
|
|
21
|
+
import { listRead } from './pages.ts'
|
|
22
|
+
import { jsonOutput } from './json-fields.ts'
|
|
23
|
+
import type { Args, Context, Registry } from './registry.ts'
|
|
24
|
+
import { described, valueType } from './registry.ts'
|
|
25
|
+
import { DateTime } from './scalars.ts'
|
|
26
|
+
|
|
27
|
+
const required = <T extends GraphQLObjectType>(type: T) =>
|
|
28
|
+
new GraphQLNonNull(type)
|
|
29
|
+
|
|
30
|
+
export const isMaster = (context: Context): boolean =>
|
|
31
|
+
context.caller.kind === 'master'
|
|
32
|
+
|
|
33
|
+
/** `first`, `after`, `last`, `before`, or `take` and `skip`; then filters. */
|
|
34
|
+
export function listArgs(
|
|
35
|
+
registry: Registry,
|
|
36
|
+
model: string,
|
|
37
|
+
): GraphQLFieldConfigArgumentMap {
|
|
38
|
+
const pages: GraphQLFieldConfigArgumentMap =
|
|
39
|
+
registry.settings.pagination === 'list'
|
|
40
|
+
? { take: { type: GraphQLInt }, skip: { type: GraphQLInt } }
|
|
41
|
+
: {
|
|
42
|
+
first: { type: GraphQLInt },
|
|
43
|
+
after: { type: GraphQLString },
|
|
44
|
+
last: { type: GraphQLInt },
|
|
45
|
+
before: { type: GraphQLString },
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...pages,
|
|
49
|
+
where: { type: whereInput(registry, model) },
|
|
50
|
+
orderBy: {
|
|
51
|
+
type: new GraphQLList(new GraphQLNonNull(orderByInput(registry, model))),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pageInfo = (registry: Registry): GraphQLObjectType =>
|
|
57
|
+
registry.object(
|
|
58
|
+
'PageInfo',
|
|
59
|
+
() =>
|
|
60
|
+
new GraphQLObjectType({
|
|
61
|
+
name: 'PageInfo',
|
|
62
|
+
fields: {
|
|
63
|
+
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
|
64
|
+
hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
|
65
|
+
startCursor: { type: GraphQLString },
|
|
66
|
+
endCursor: { type: GraphQLString },
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const edge = (registry: Registry, model: string): GraphQLObjectType =>
|
|
72
|
+
registry.object(
|
|
73
|
+
`${model}Edge`,
|
|
74
|
+
() =>
|
|
75
|
+
new GraphQLObjectType({
|
|
76
|
+
name: `${model}Edge`,
|
|
77
|
+
fields: () => ({
|
|
78
|
+
cursor: { type: new GraphQLNonNull(GraphQLString) },
|
|
79
|
+
node: { type: required(objectType(registry, model)) },
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `PostConnection` at the root, `UserPostsConnection` on a relation, each
|
|
86
|
+
* with `totalCount` (D-107, D-134).
|
|
87
|
+
*/
|
|
88
|
+
export function connectionType(
|
|
89
|
+
registry: Registry,
|
|
90
|
+
name: string,
|
|
91
|
+
model: string,
|
|
92
|
+
): GraphQLObjectType {
|
|
93
|
+
return registry.object(
|
|
94
|
+
name,
|
|
95
|
+
() =>
|
|
96
|
+
new GraphQLObjectType({
|
|
97
|
+
name,
|
|
98
|
+
fields: () => ({
|
|
99
|
+
edges: {
|
|
100
|
+
type: new GraphQLNonNull(
|
|
101
|
+
new GraphQLList(required(edge(registry, model))),
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
pageInfo: { type: required(pageInfo(registry)) },
|
|
105
|
+
// A function the default resolver calls: counted when selected.
|
|
106
|
+
totalCount: { type: new GraphQLNonNull(GraphQLInt) },
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A relation's value in its row: `null` when absent and its read rule may
|
|
114
|
+
* hide it. Absent otherwise, the resolver that returned the row did not read
|
|
115
|
+
* it, such as one `extend` added.
|
|
116
|
+
*/
|
|
117
|
+
function readOf(
|
|
118
|
+
source: unknown,
|
|
119
|
+
model: string,
|
|
120
|
+
field: string,
|
|
121
|
+
ruled: boolean,
|
|
122
|
+
): unknown {
|
|
123
|
+
const value: unknown = Reflect.get(Object(source), field)
|
|
124
|
+
if (value !== undefined) return value
|
|
125
|
+
if (ruled) return null
|
|
126
|
+
throw new Error(
|
|
127
|
+
`${model}.${field} was not read; select it with context.select(info).`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A `many` relation: a connection, or a list with `pagination: 'list'`. */
|
|
132
|
+
function manyField(
|
|
133
|
+
registry: Registry,
|
|
134
|
+
model: string,
|
|
135
|
+
field: Extract<SchemaField, { readonly kind: 'many' }>,
|
|
136
|
+
ruled: boolean,
|
|
137
|
+
): GraphQLFieldConfig<unknown, Context, Args> {
|
|
138
|
+
const target = objectType(registry, field.target)
|
|
139
|
+
const type =
|
|
140
|
+
registry.settings.pagination === 'list'
|
|
141
|
+
? new GraphQLList(required(target))
|
|
142
|
+
: connectionType(
|
|
143
|
+
registry,
|
|
144
|
+
`${model}${upper(field.name)}Connection`,
|
|
145
|
+
field.target,
|
|
146
|
+
)
|
|
147
|
+
return {
|
|
148
|
+
// Absent when the field's read rule hides it (D-44).
|
|
149
|
+
type: ruled ? type : new GraphQLNonNull(type),
|
|
150
|
+
args: listArgs(registry, field.target),
|
|
151
|
+
resolve: (source, args, context) => {
|
|
152
|
+
const value = readOf(source, model, field.name, ruled)
|
|
153
|
+
if (value === null) return null
|
|
154
|
+
const rows = rowsOf(value)
|
|
155
|
+
const { page } = listRead(
|
|
156
|
+
registry,
|
|
157
|
+
registry.entry(field.target),
|
|
158
|
+
args,
|
|
159
|
+
isMaster(context),
|
|
160
|
+
)
|
|
161
|
+
if (page === undefined) return rows
|
|
162
|
+
return {
|
|
163
|
+
...connection(rows, page, (row) => row),
|
|
164
|
+
// The row's `_count`, read when the connection selects it (D-134).
|
|
165
|
+
totalCount: () => {
|
|
166
|
+
const counts: unknown = Reflect.get(Object(source), '_count')
|
|
167
|
+
const count: unknown = Reflect.get(Object(counts), field.name)
|
|
168
|
+
if (typeof count === 'number') return count
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${model}.${field.name} was not counted; select it with context.select(info).`,
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `id`, the dates and each scalar field a caller other than Master reads. */
|
|
179
|
+
function scalarFields(
|
|
180
|
+
registry: Registry,
|
|
181
|
+
model: string,
|
|
182
|
+
): GraphQLFieldConfigMap<unknown, Context> {
|
|
183
|
+
const entry = registry.entry(model)
|
|
184
|
+
const fields: GraphQLFieldConfigMap<unknown, Context> = {
|
|
185
|
+
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
186
|
+
createdAt: { type: new GraphQLNonNull(DateTime) },
|
|
187
|
+
updatedAt: { type: new GraphQLNonNull(DateTime) },
|
|
188
|
+
}
|
|
189
|
+
for (const field of readable(entry)) {
|
|
190
|
+
if (field.kind !== 'scalar') continue
|
|
191
|
+
const ruled = entry.exposed.readRules[field.name] === 'rule'
|
|
192
|
+
const fieldType = entry.exposed.fieldTypes[field.name]
|
|
193
|
+
if (fieldType !== undefined) {
|
|
194
|
+
// A plugin field reads as its toJSON gives (D-122), typed by its
|
|
195
|
+
// shape (D-152).
|
|
196
|
+
const { type, nullable } = jsonOutput(registry, model, field)
|
|
197
|
+
fields[field.name] = {
|
|
198
|
+
type:
|
|
199
|
+
field.optional || ruled || nullable ? type : new GraphQLNonNull(type),
|
|
200
|
+
resolve: (source) => {
|
|
201
|
+
const value = readOf(source, model, field.name, ruled)
|
|
202
|
+
return value === null ? null : fieldType.toJSON(value)
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const { type, nullable } =
|
|
208
|
+
field.type === 'json'
|
|
209
|
+
? jsonOutput(registry, model, field)
|
|
210
|
+
: { type: valueType(registry, model, field), nullable: false }
|
|
211
|
+
fields[field.name] = {
|
|
212
|
+
type:
|
|
213
|
+
field.optional || ruled || nullable ? type : new GraphQLNonNull(type),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return fields
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `Post`: a field only Master reads is absent, a ruled one nullable. */
|
|
220
|
+
export function objectType(
|
|
221
|
+
registry: Registry,
|
|
222
|
+
model: string,
|
|
223
|
+
): GraphQLObjectType {
|
|
224
|
+
return registry.object(
|
|
225
|
+
model,
|
|
226
|
+
() =>
|
|
227
|
+
new GraphQLObjectType<unknown, Context>({
|
|
228
|
+
name: model,
|
|
229
|
+
description: registry.entry(model).exposed.description,
|
|
230
|
+
fields: () => {
|
|
231
|
+
const entry = registry.entry(model)
|
|
232
|
+
const fields = scalarFields(registry, model)
|
|
233
|
+
for (const field of readable(entry)) {
|
|
234
|
+
const ruled = entry.exposed.readRules[field.name] === 'rule'
|
|
235
|
+
if (field.kind === 'one') {
|
|
236
|
+
// Its target's read rule may hide the row (D-86).
|
|
237
|
+
fields[field.name] = {
|
|
238
|
+
type: objectType(registry, field.target),
|
|
239
|
+
resolve: (source) => readOf(source, model, field.name, ruled),
|
|
240
|
+
}
|
|
241
|
+
} else if (field.kind === 'many') {
|
|
242
|
+
fields[field.name] = manyField(registry, model, field, ruled)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
Object.assign(fields, registry.virtualFields(model))
|
|
246
|
+
return described(entry, fields)
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** `DeletedPost`: a deleted row as it was, without its relations (D-102). */
|
|
253
|
+
export function deletedType(
|
|
254
|
+
registry: Registry,
|
|
255
|
+
model: string,
|
|
256
|
+
): GraphQLObjectType {
|
|
257
|
+
const name = `Deleted${model}`
|
|
258
|
+
return registry.object(
|
|
259
|
+
name,
|
|
260
|
+
() =>
|
|
261
|
+
new GraphQLObjectType<unknown, Context>({
|
|
262
|
+
name,
|
|
263
|
+
fields: () =>
|
|
264
|
+
described(registry.entry(model), scalarFields(registry, model)),
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
}
|
package/src/overlap.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The bound on graphql-js's overlapping-fields rule (D-107): the rule
|
|
2
|
+
// compares every two fields that merge under one response key, so a
|
|
3
|
+
// document whose merged fields make more than `maxPairs` such pairs is
|
|
4
|
+
// refused before it is validated.
|
|
5
|
+
import type {
|
|
6
|
+
DocumentNode,
|
|
7
|
+
FieldNode,
|
|
8
|
+
FragmentDefinitionNode,
|
|
9
|
+
SelectionSetNode,
|
|
10
|
+
} from 'graphql'
|
|
11
|
+
import { GraphQLError, Kind } from 'graphql'
|
|
12
|
+
|
|
13
|
+
/** At about 28 µs a pair, the rule's slowest, about 30 ms of validation. */
|
|
14
|
+
const maxPairs = 1000
|
|
15
|
+
|
|
16
|
+
/** Fields of one key, and how many came from each place (see `merged`). */
|
|
17
|
+
interface Merged {
|
|
18
|
+
readonly fields: FieldNode[]
|
|
19
|
+
readonly from: Map<string, number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pairsOf = (fields: number) => (fields * (fields - 1)) / 2
|
|
23
|
+
|
|
24
|
+
/** The refusal of a document whose fields of one key make too many pairs. */
|
|
25
|
+
export function overlapRefusal(
|
|
26
|
+
document: DocumentNode,
|
|
27
|
+
): GraphQLError | undefined {
|
|
28
|
+
const fragments = new Map<string, FragmentDefinitionNode>()
|
|
29
|
+
for (const definition of document.definitions) {
|
|
30
|
+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
31
|
+
fragments.set(definition.name.value, definition)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// A selection set merged with no other is counted once, wherever it is
|
|
35
|
+
// reached, as the rule compares its fields once.
|
|
36
|
+
const counted = new Set<SelectionSetNode>()
|
|
37
|
+
let pairs = 0
|
|
38
|
+
/**
|
|
39
|
+
* The fields of `sets` by response key, fragments spread once, each with
|
|
40
|
+
* how many came from each fragment spread in `sets`, or from `sets`.
|
|
41
|
+
*/
|
|
42
|
+
const merged = (sets: readonly SelectionSetNode[]) => {
|
|
43
|
+
const byKey = new Map<string, Merged>()
|
|
44
|
+
const spread = new Set<string>()
|
|
45
|
+
const collect = (set: SelectionSetNode, from: string): void => {
|
|
46
|
+
for (const selection of set.selections) {
|
|
47
|
+
if (selection.kind === Kind.FIELD) {
|
|
48
|
+
const key = selection.alias?.value ?? selection.name.value
|
|
49
|
+
const same: Merged = byKey.get(key) ?? { fields: [], from: new Map() }
|
|
50
|
+
byKey.set(key, same)
|
|
51
|
+
same.fields.push(selection)
|
|
52
|
+
same.from.set(from, (same.from.get(from) ?? 0) + 1)
|
|
53
|
+
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
54
|
+
collect(selection.selectionSet, from)
|
|
55
|
+
} else {
|
|
56
|
+
const name = selection.name.value
|
|
57
|
+
const fragment = fragments.get(name)
|
|
58
|
+
if (fragment === undefined || spread.has(name)) continue
|
|
59
|
+
spread.add(name)
|
|
60
|
+
collect(fragment.selectionSet, from === '' ? name : from)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const set of sets) collect(set, '')
|
|
65
|
+
return byKey.values()
|
|
66
|
+
}
|
|
67
|
+
const walk = (sets: readonly SelectionSetNode[]): void => {
|
|
68
|
+
const [only] = sets
|
|
69
|
+
if (sets.length === 1 && only !== undefined) {
|
|
70
|
+
if (counted.has(only)) return
|
|
71
|
+
counted.add(only)
|
|
72
|
+
}
|
|
73
|
+
for (const { fields, from } of merged(sets)) {
|
|
74
|
+
if (pairs > maxPairs) return
|
|
75
|
+
pairs += pairsOf(fields.length)
|
|
76
|
+
// The pairs within a fragment are counted where it is defined.
|
|
77
|
+
for (const [fragment, count] of from) {
|
|
78
|
+
if (fragment !== '') pairs -= pairsOf(count)
|
|
79
|
+
}
|
|
80
|
+
if (from.size === 1 && !from.has('')) continue
|
|
81
|
+
const below = fields.flatMap((field) => field.selectionSet ?? [])
|
|
82
|
+
if (below.length > 0) walk(below)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const definition of document.definitions) {
|
|
86
|
+
if (
|
|
87
|
+
definition.kind === Kind.OPERATION_DEFINITION ||
|
|
88
|
+
definition.kind === Kind.FRAGMENT_DEFINITION
|
|
89
|
+
) {
|
|
90
|
+
walk([definition.selectionSet])
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (pairs <= maxPairs) return undefined
|
|
94
|
+
return new GraphQLError(
|
|
95
|
+
'The document repeats fields under one name too often. Ask for each field once.',
|
|
96
|
+
)
|
|
97
|
+
}
|