@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -2
  3. package/dist/data-types.d.ts +15 -0
  4. package/dist/data-types.d.ts.map +1 -0
  5. package/dist/data-types.js +53 -0
  6. package/dist/data.d.ts +34 -0
  7. package/dist/data.d.ts.map +1 -0
  8. package/dist/data.js +207 -0
  9. package/dist/depth.d.ts +6 -0
  10. package/dist/depth.d.ts.map +1 -0
  11. package/dist/depth.js +67 -0
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +42 -0
  15. package/dist/functions.d.ts +28 -0
  16. package/dist/functions.d.ts.map +1 -0
  17. package/dist/functions.js +195 -0
  18. package/dist/graphiql.d.ts +3 -0
  19. package/dist/graphiql.d.ts.map +1 -0
  20. package/dist/graphiql.js +46 -0
  21. package/dist/index.d.ts +47 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +165 -0
  24. package/dist/inputs.d.ts +20 -0
  25. package/dist/inputs.d.ts.map +1 -0
  26. package/dist/inputs.js +236 -0
  27. package/dist/json-fields.d.ts +12 -0
  28. package/dist/json-fields.d.ts.map +1 -0
  29. package/dist/json-fields.js +66 -0
  30. package/dist/json-schema.d.ts +7 -0
  31. package/dist/json-schema.d.ts.map +1 -0
  32. package/dist/json-schema.js +16 -0
  33. package/dist/live-stream.d.ts +59 -0
  34. package/dist/live-stream.d.ts.map +1 -0
  35. package/dist/live-stream.js +136 -0
  36. package/dist/mutations.d.ts +9 -0
  37. package/dist/mutations.d.ts.map +1 -0
  38. package/dist/mutations.js +228 -0
  39. package/dist/names.d.ts +8 -0
  40. package/dist/names.d.ts.map +1 -0
  41. package/dist/names.js +14 -0
  42. package/dist/outputs.d.ts +16 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +177 -0
  45. package/dist/overlap.d.ts +5 -0
  46. package/dist/overlap.d.ts.map +1 -0
  47. package/dist/overlap.js +82 -0
  48. package/dist/pages.d.ts +11 -0
  49. package/dist/pages.d.ts.map +1 -0
  50. package/dist/pages.js +22 -0
  51. package/dist/registry.d.ts +80 -0
  52. package/dist/registry.d.ts.map +1 -0
  53. package/dist/registry.js +97 -0
  54. package/dist/reread.d.ts +9 -0
  55. package/dist/reread.d.ts.map +1 -0
  56. package/dist/reread.js +116 -0
  57. package/dist/rows.d.ts +23 -0
  58. package/dist/rows.d.ts.map +1 -0
  59. package/dist/rows.js +106 -0
  60. package/dist/scalars.d.ts +13 -0
  61. package/dist/scalars.d.ts.map +1 -0
  62. package/dist/scalars.js +88 -0
  63. package/dist/schema.d.ts +17 -0
  64. package/dist/schema.d.ts.map +1 -0
  65. package/dist/schema.js +130 -0
  66. package/dist/select.d.ts +24 -0
  67. package/dist/select.d.ts.map +1 -0
  68. package/dist/select.js +152 -0
  69. package/dist/sockets.d.ts +25 -0
  70. package/dist/sockets.d.ts.map +1 -0
  71. package/dist/sockets.js +234 -0
  72. package/dist/subscriptions.d.ts +14 -0
  73. package/dist/subscriptions.d.ts.map +1 -0
  74. package/dist/subscriptions.js +242 -0
  75. package/package.json +58 -3
  76. package/src/data-types.ts +100 -0
  77. package/src/data.ts +366 -0
  78. package/src/depth.ts +98 -0
  79. package/src/errors.ts +48 -0
  80. package/src/functions.ts +291 -0
  81. package/src/graphiql.ts +54 -0
  82. package/src/index.ts +231 -0
  83. package/src/inputs.ts +348 -0
  84. package/src/json-fields.ts +95 -0
  85. package/src/json-schema.ts +21 -0
  86. package/src/live-stream.ts +192 -0
  87. package/src/mutations.ts +317 -0
  88. package/src/names.ts +25 -0
  89. package/src/outputs.ts +267 -0
  90. package/src/overlap.ts +97 -0
  91. package/src/pages.ts +39 -0
  92. package/src/registry.ts +189 -0
  93. package/src/reread.ts +159 -0
  94. package/src/rows.ts +151 -0
  95. package/src/scalars.ts +110 -0
  96. package/src/schema.ts +189 -0
  97. package/src/select.ts +230 -0
  98. package/src/sockets.ts +291 -0
  99. package/src/subscriptions.ts +334 -0
@@ -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
+ }