@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
package/src/pages.ts ADDED
@@ -0,0 +1,39 @@
1
+ // A list's kernel read (D-55, D-107): its `where`, its order and its page,
2
+ // read with the arguments REST reads (D-139).
3
+ import type { Page } from '@ultimabase/http/internal'
4
+ import { pageOf } from '@ultimabase/http/internal'
5
+ import { plainWhere } from './json-fields.ts'
6
+ import type { Args, Registry } from './registry.ts'
7
+ import type { Entry } from './registry.ts'
8
+
9
+ /** The kernel's `where` of a list or count's arguments. */
10
+ export function whereOf(registry: Registry, model: string, args: Args): Args {
11
+ const { where } = args
12
+ return typeof where === 'object' && where !== null
13
+ ? { where: plainWhere(registry, model, { ...where }) }
14
+ : {}
15
+ }
16
+
17
+ /** A list's kernel read, and its Relay page unless `pagination: 'list'`. */
18
+ export function listRead(
19
+ registry: Registry,
20
+ entry: Entry,
21
+ args: Args,
22
+ master: boolean,
23
+ ): { readonly read: Args; readonly page: Page['relay'] } {
24
+ const { orderBy } = args
25
+ const { read, relay } = pageOf(
26
+ registry.settings.pagination,
27
+ entry,
28
+ args,
29
+ master,
30
+ )
31
+ return {
32
+ read: {
33
+ ...whereOf(registry, entry.model.name, args),
34
+ ...(orderBy === undefined || orderBy === null ? {} : { orderBy }),
35
+ ...read,
36
+ },
37
+ page: relay,
38
+ }
39
+ }
@@ -0,0 +1,189 @@
1
+ // What the schema is built from (D-107): each model with its names and
2
+ // limits, the plugin's settings, and each type built once, when first used.
3
+ import type {
4
+ GraphQLFieldConfigMap,
5
+ GraphQLInputObjectType,
6
+ GraphQLObjectType,
7
+ GraphQLResolveInfo,
8
+ GraphQLScalarType,
9
+ } from 'graphql'
10
+ import {
11
+ GraphQLBoolean,
12
+ GraphQLEnumType,
13
+ GraphQLFloat,
14
+ GraphQLInt,
15
+ GraphQLList,
16
+ GraphQLNonNull,
17
+ GraphQLString,
18
+ } from 'graphql'
19
+ import type {
20
+ Caller,
21
+ Client,
22
+ ExposedModel,
23
+ SchemaField,
24
+ SchemaModel,
25
+ } from 'ultimabase'
26
+ import { DataTypes } from './data-types.ts'
27
+ import { upper } from './names.ts'
28
+ import { DateScalar, DateTime, DecimalScalar, JSONScalar } from './scalars.ts'
29
+
30
+ /** A model's key of `.with` (D-107). */
31
+ export type GraphqlWith =
32
+ { readonly plural?: string; readonly maxTake?: number } | false
33
+
34
+ /** What every resolver receives, the generated ones and those `extend` adds. */
35
+ export type Context = {
36
+ readonly caller: Caller
37
+ /** The request's client, which a function's `run` receives (D-113). */
38
+ readonly client: Client
39
+ /**
40
+ * The kernel's `select` for what the client asks of the rows a field
41
+ * returns, relations and their pages included; spread it to add a field.
42
+ * Its type names `id` only: the rest depends on the document.
43
+ */
44
+ readonly select: (info: GraphQLResolveInfo) => { readonly id: true }
45
+ }
46
+ /** A kernel query's options: run as the request's caller (D-106). */
47
+ export const as = (context: Context): { readonly as: Caller } => ({
48
+ as: context.caller,
49
+ })
50
+ export type Args = { readonly [arg: string]: unknown }
51
+ export type Scalar = Extract<SchemaField, { readonly kind: 'scalar' }>
52
+
53
+ export type Entry = {
54
+ readonly exposed: ExposedModel<GraphqlWith>
55
+ readonly model: SchemaModel
56
+ /** In the root fields; a hidden model is reached by relations only. */
57
+ readonly visible: boolean
58
+ readonly single: string
59
+ readonly list: string
60
+ /** The rows a list returns without `first`, `last` or `take`. */
61
+ readonly take: number
62
+ readonly maxTake: number
63
+ }
64
+
65
+ export type Settings = {
66
+ readonly pagination: 'connection' | 'list'
67
+ }
68
+
69
+ function memo<T>(cache: Map<string, T>, name: string, build: () => T): T {
70
+ const found = cache.get(name)
71
+ if (found !== undefined) return found
72
+ const built = build()
73
+ cache.set(name, built)
74
+ return built
75
+ }
76
+
77
+ export class Registry {
78
+ readonly #objects = new Map<string, GraphQLObjectType>()
79
+ readonly #inputs = new Map<string, GraphQLInputObjectType>()
80
+ readonly #enums = new Map<string, GraphQLEnumType>()
81
+ readonly entries: ReadonlyMap<string, Entry>
82
+ readonly settings: Settings
83
+ /** The types functions and json fields give, shared by both (D-125). */
84
+ readonly data: DataTypes
85
+ /** A model's virtual fields, which the functions' outputs build (D-151). */
86
+ virtualFields: (model: string) => GraphQLFieldConfigMap<unknown, Context> =
87
+ () => ({})
88
+
89
+ constructor(entries: ReadonlyMap<string, Entry>, settings: Settings) {
90
+ this.entries = entries
91
+ this.settings = settings
92
+ this.data = new DataTypes(entries.keys())
93
+ // Before any function's: a `defineEnum` keeps its own description.
94
+ for (const [model, { exposed }] of entries) {
95
+ for (const [field, shared] of Object.entries(exposed.enums)) {
96
+ const { name, values, description } = shared
97
+ this.data.enum(name, values, `${model}.${field}`, description)
98
+ }
99
+ }
100
+ }
101
+
102
+ entry(model: string): Entry {
103
+ const entry = this.entries.get(model)
104
+ if (entry === undefined) throw new Error(`Unknown model ${model}.`)
105
+ return entry
106
+ }
107
+
108
+ object(name: string, build: () => GraphQLObjectType): GraphQLObjectType {
109
+ return memo(this.#objects, name, build)
110
+ }
111
+
112
+ input(
113
+ name: string,
114
+ build: () => GraphQLInputObjectType,
115
+ ): GraphQLInputObjectType {
116
+ return memo(this.#inputs, name, build)
117
+ }
118
+
119
+ enum(name: string, build: () => GraphQLEnumType): GraphQLEnumType {
120
+ return memo(this.#enums, name, build)
121
+ }
122
+ }
123
+
124
+ /** Each field's `description` (D-150), on the fields of a type or an input. */
125
+ export function described<
126
+ C extends { readonly description?: string | null | undefined },
127
+ >(entry: Entry, fields: { [field: string]: C }): { [field: string]: C } {
128
+ for (const [field, description] of Object.entries(
129
+ entry.exposed.descriptions,
130
+ )) {
131
+ const config = fields[field]
132
+ if (config !== undefined) fields[field] = { ...config, description }
133
+ }
134
+ return fields
135
+ }
136
+
137
+ export const scalarTypes: {
138
+ readonly [T in Exclude<Scalar['type'], 'enum'>]: GraphQLScalarType
139
+ } = {
140
+ string: GraphQLString,
141
+ number: GraphQLFloat,
142
+ decimal: DecimalScalar,
143
+ boolean: GraphQLBoolean,
144
+ dateTime: DateTime,
145
+ date: DateScalar,
146
+ json: JSONScalar,
147
+ }
148
+
149
+ /**
150
+ * A scalar field's type; an enum's is `PostStatus`, built once, and a
151
+ * `defineEnum`'s its own name, one type for every field (D-150).
152
+ */
153
+ export function scalarType(
154
+ registry: Registry,
155
+ model: string,
156
+ field: Scalar,
157
+ ): GraphQLScalarType | GraphQLEnumType {
158
+ if (field.integer === true) return GraphQLInt
159
+ if (field.type !== 'enum') return scalarTypes[field.type]
160
+ const shared = registry.entry(model).exposed.enums[field.name]
161
+ if (shared !== undefined) {
162
+ const { name, values } = shared
163
+ return registry.data.enum(name, values, `${model}.${field.name}`)
164
+ }
165
+ const name = `${model}${upper(field.name)}`
166
+ return registry.enum(
167
+ name,
168
+ () =>
169
+ new GraphQLEnumType({
170
+ name,
171
+ values: Object.fromEntries(
172
+ (field.values ?? []).map((value) => [value, { value }]),
173
+ ),
174
+ }),
175
+ )
176
+ }
177
+
178
+ /** A scalar field's value, read or written: a list's is `[Float!]` (D-127). */
179
+ export const valueType = (
180
+ registry: Registry,
181
+ model: string,
182
+ field: Scalar,
183
+ ):
184
+ | GraphQLScalarType
185
+ | GraphQLEnumType
186
+ | GraphQLList<GraphQLNonNull<GraphQLScalarType | GraphQLEnumType>> => {
187
+ const item = scalarType(registry, model, field)
188
+ return field.list ? new GraphQLList(new GraphQLNonNull(item)) : item
189
+ }
package/src/reread.ts ADDED
@@ -0,0 +1,159 @@
1
+ // A function's output as the client reads it (D-109): its scalars as `run`
2
+ // returned them, its rows' relations re-read by id as the caller, in one
3
+ // query per model and only when a relation is selected.
4
+ import { badInput, rowsOf, run } from '@ultimabase/http/internal'
5
+ import type { FieldNode, GraphQLResolveInfo } from 'graphql'
6
+ import type { ExposedOutput } from 'ultimabase'
7
+ import { isMaster, objectType } from './outputs.ts'
8
+ import type { Args, Context, Registry } from './registry.ts'
9
+ import { as } from './registry.ts'
10
+ import type { Fields } from './select.ts'
11
+ import { grouped, selectOf } from './select.ts'
12
+
13
+ type Row = { [field: string]: unknown }
14
+ type Named = Exclude<ExposedOutput, { readonly kind: 'schema' }>
15
+ /** The rows of one model to re-read, and the relations they select. */
16
+ type Pending = {
17
+ readonly select: { [relation: string]: unknown }
18
+ readonly rows: Map<string, Row[]>
19
+ }
20
+
21
+ const isRecord = (value: unknown): value is object =>
22
+ typeof value === 'object' && value !== null
23
+
24
+ /** `run`'s value, its rows' relations read as the caller. */
25
+ export async function reread(
26
+ registry: Registry,
27
+ output: ExposedOutput,
28
+ value: unknown,
29
+ context: Context,
30
+ info: GraphQLResolveInfo,
31
+ call: { readonly name: string; readonly take: number },
32
+ ): Promise<unknown> {
33
+ const master = isMaster(context)
34
+ // The rows whose relations are read, which the row budget counts.
35
+ let counted = 0
36
+ const pending = new Map<string, Pending>()
37
+ // A list's items share their nodes: each selection is read once.
38
+ const relations = new Map<readonly FieldNode[], Args>()
39
+ const fields = new Map<readonly FieldNode[], ReadonlyMap<string, Fields>>()
40
+
41
+ const relationsOf = (model: string, nodes: readonly FieldNode[]): Args => {
42
+ const known = relations.get(nodes)
43
+ if (known !== undefined) return known
44
+ const type = objectType(registry, model)
45
+ const select = selectOf(registry, info, type, nodes, master)
46
+ // A virtual is computed when its row is read (D-151).
47
+ const { virtuals } = registry.entry(model).exposed
48
+ const found = Object.fromEntries(
49
+ Object.entries(select).filter(
50
+ ([name, read]) =>
51
+ read !== true || virtuals.some((virtual) => virtual.name === name),
52
+ ),
53
+ )
54
+ relations.set(nodes, found)
55
+ return found
56
+ }
57
+
58
+ const fieldsOf = (nodes: readonly FieldNode[]) => {
59
+ const known = fields.get(nodes)
60
+ if (known !== undefined) return known
61
+ const found = grouped(info, nodes)
62
+ fields.set(nodes, found)
63
+ return found
64
+ }
65
+
66
+ const row = (model: string, item: object, nodes: readonly FieldNode[]) => {
67
+ const select = relationsOf(model, nodes)
68
+ if (Object.keys(select).length === 0) return item
69
+ const copy: Row = { ...item }
70
+ const { id } = copy
71
+ if (typeof id !== 'string') {
72
+ throw new TypeError(`A ${model} was returned without its id.`)
73
+ }
74
+ const reading: Pending = pending.get(model) ?? {
75
+ select: {},
76
+ rows: new Map(),
77
+ }
78
+ pending.set(model, reading)
79
+ for (const [name, read] of Object.entries(select)) {
80
+ const known = reading.select[name]
81
+ if (
82
+ known !== undefined &&
83
+ JSON.stringify(known) !== JSON.stringify(read)
84
+ ) {
85
+ throw badInput(
86
+ `${model}.${name} is selected in two ways; select it the same way everywhere.`,
87
+ )
88
+ }
89
+ reading.select[name] = read
90
+ }
91
+ reading.rows.set(id, [...(reading.rows.get(id) ?? []), copy])
92
+ counted += 1
93
+ return copy
94
+ }
95
+
96
+ const one = (named: Named, item: unknown, nodes: readonly FieldNode[]) => {
97
+ if (!isRecord(item)) return item
98
+ if (named.kind === 'model') return row(named.name, item, nodes)
99
+ const copy: Row = { ...item }
100
+ const selected = fieldsOf(nodes)
101
+ for (const [name, field] of named.type.fields) {
102
+ const below = selected.get(name)
103
+ if (below !== undefined) {
104
+ copy[name] = walk(field, Reflect.get(item, name), below)
105
+ }
106
+ }
107
+ return copy
108
+ }
109
+
110
+ const walk = (
111
+ at: ExposedOutput,
112
+ item: unknown,
113
+ nodes: readonly FieldNode[],
114
+ ): unknown => {
115
+ if (at.kind === 'schema') return item
116
+ if (at.form !== 'list' || !Array.isArray(item)) {
117
+ return one(at, item, nodes)
118
+ }
119
+ const before = counted
120
+ const items = item.map((each: unknown) => one(at, each, nodes))
121
+ // The budget counts `take` rows in a list (D-109).
122
+ if (items.length > call.take && counted > before) {
123
+ throw new RangeError(
124
+ `${call.name} returned ${items.length} rows; a list counts take (${call.take}) rows, so it holds no more under a selected relation.`,
125
+ )
126
+ }
127
+ return items
128
+ }
129
+
130
+ const result = walk(output, value, info.fieldNodes)
131
+ for (const [model, { select, rows }] of pending) {
132
+ const entry = registry.entry(model)
133
+ const ids = [...rows.keys()]
134
+ const read = await run(() =>
135
+ entry.exposed.queries.findMany(
136
+ {
137
+ where: { id: { in: ids } },
138
+ take: ids.length,
139
+ select: { id: true, ...select },
140
+ },
141
+ as(context),
142
+ ),
143
+ )
144
+ const byId = new Map(rowsOf(read).map((found) => [found.id, found]))
145
+ for (const [id, copies] of rows) {
146
+ const found = byId.get(id)
147
+ for (const name of Object.keys(select)) {
148
+ // A row the caller cannot read has no relations (D-109).
149
+ const many = entry.model.fields.some(
150
+ (field) => field.name === name && field.kind === 'many',
151
+ )
152
+ const empty = many ? [] : null
153
+ const relation = found === undefined ? empty : Reflect.get(found, name)
154
+ for (const copy of copies) copy[name] = relation
155
+ }
156
+ }
157
+ }
158
+ return result
159
+ }
package/src/rows.ts ADDED
@@ -0,0 +1,151 @@
1
+ // The row budget (D-107): a document that may return more than `maxRows`
2
+ // rows, or reads and writes more than `maxRoots` times at its root, is
3
+ // refused before it runs. Each list counts the rows it asks for, times the
4
+ // rows of the lists above it. Each relation a list's `orderBy` goes through
5
+ // counts one level of `maxDepth` under the list (D-134).
6
+ import type {
7
+ FieldNode,
8
+ GraphQLObjectType,
9
+ ValidatedExecutionArgs,
10
+ } from 'graphql'
11
+ import {
12
+ getArgumentValues,
13
+ getNamedType,
14
+ GraphQLError,
15
+ isObjectType,
16
+ } from 'graphql'
17
+ import type { Info, Selecting } from './select.ts'
18
+ import { grouped } from './select.ts'
19
+ import type { Entry, Registry } from './registry.ts'
20
+
21
+ /**
22
+ * What a field reads: one row, a list of a model's rows, or a function's
23
+ * list, which counts `take` rows (D-109).
24
+ */
25
+ export type Read =
26
+ | { readonly kind: 'one' }
27
+ | { readonly kind: 'many'; readonly entry: Entry }
28
+ | { readonly kind: 'list'; readonly size: number }
29
+
30
+ const one: Read = { kind: 'one' }
31
+ /** Each a query or a write of its own, a count one more. */
32
+ const maxRoots = 20
33
+ const responseKey = (node: FieldNode) => node.alias?.value ?? node.name.value
34
+
35
+ // `take` for none or a negative number; above `maxTake`, the list refuses.
36
+ const sizeOf = (entry: Entry, asked: unknown): number =>
37
+ typeof asked === 'number' && asked >= 0
38
+ ? Math.min(asked, entry.maxTake)
39
+ : entry.take
40
+
41
+ /** The reads of the root query, by field name. */
42
+ function rootReads(registry: Registry): ReadonlyMap<string, Read> {
43
+ const reads = new Map<string, Read>()
44
+ for (const entry of registry.entries.values()) {
45
+ if (!entry.visible) continue
46
+ reads.set(entry.single, one)
47
+ reads.set(entry.list, { kind: 'many', entry })
48
+ }
49
+ return reads
50
+ }
51
+
52
+ /** The relations an `orderBy` goes through, the deepest path's. */
53
+ function sortDepth(orderBy: unknown): number {
54
+ if (Array.isArray(orderBy)) return Math.max(0, ...orderBy.map(sortDepth))
55
+ if (typeof orderBy !== 'object' || orderBy === null) return 0
56
+ return Math.max(
57
+ 0,
58
+ ...Object.values(orderBy).map((value: unknown) =>
59
+ typeof value === 'object' && value !== null ? 1 + sortDepth(value) : 0,
60
+ ),
61
+ )
62
+ }
63
+
64
+ export type Limits = { readonly maxRows: number; readonly maxDepth: number }
65
+
66
+ /** The refusal of a document that may return more than `maxRows` rows. */
67
+ export function rowBudget(
68
+ registry: Registry,
69
+ { maxRows, maxDepth }: Limits,
70
+ functions: ReadonlyMap<string, ReadonlyMap<string, Read>>,
71
+ ): (args: ValidatedExecutionArgs) => GraphQLError | undefined {
72
+ const roots = rootReads(registry)
73
+ const list = registry.settings.pagination === 'list'
74
+ const readOf = (
75
+ type: GraphQLObjectType,
76
+ root: string,
77
+ name: string,
78
+ ): Read | undefined => {
79
+ const called = functions.get(type.name)
80
+ if (called?.has(name) === true) return called.get(name)
81
+ if (root === 'mutation') return one
82
+ if (root === 'query') return roots.get(name)
83
+ const entry = registry.entries.get(type.name)
84
+ const field = entry?.model.fields.find((f) => f.name === name)
85
+ if (field === undefined || field.kind === 'scalar') return undefined
86
+ if (field.kind === 'one') return one
87
+ return { kind: 'many', entry: registry.entry(field.target) }
88
+ }
89
+ return (args) => {
90
+ const info: Info = {
91
+ fragments: args.fragmentDefinitions,
92
+ variableValues: args.variableValues,
93
+ }
94
+ let rows = 0
95
+ let rootFields = 0
96
+ let deepest = 0
97
+ const walk = (
98
+ type: GraphQLObjectType,
99
+ nodes: readonly Selecting[],
100
+ times: number,
101
+ root: string,
102
+ level: number,
103
+ ): void => {
104
+ for (const fields of grouped(info, nodes, responseKey).values()) {
105
+ if (rows > maxRows) return
106
+ const [node] = fields
107
+ const field = type.getFields()[node.name.value]
108
+ if (field === undefined) continue
109
+ const read = readOf(type, root, node.name.value)
110
+ let each = times
111
+ if (read?.kind === 'many') {
112
+ const asked = getArgumentValues(field, node, args.variableValues)
113
+ const size = sizeOf(
114
+ read.entry,
115
+ list ? asked['take'] : (asked['last'] ?? asked['first']),
116
+ )
117
+ // A count reads the rows it counts, however few the page returns.
118
+ deepest = Math.max(deepest, level + sortDepth(asked['orderBy']))
119
+ const counted = grouped(info, fields).has('totalCount')
120
+ each *= counted ? Math.max(size, read.entry.take) : size
121
+ if (counted && root !== '') rootFields += 1
122
+ } else if (read?.kind === 'list') each *= read.size
123
+ if (read !== undefined) rows += each
124
+ if (root !== '') rootFields += 1
125
+ const named = getNamedType(field.type)
126
+ if (isObjectType(named) && each > 0) {
127
+ walk(named, fields, each, '', level + 1)
128
+ }
129
+ }
130
+ }
131
+ const { operation } = args
132
+ const type = args.schema.getRootType(operation.operation)
133
+ // `validateExecutionArgs` refused an operation the schema has no root for.
134
+ if (type) walk(type, [operation], 1, operation.operation, 1)
135
+ if (deepest > maxDepth) {
136
+ return new GraphQLError(
137
+ `The document is nested ${deepest} levels deep, its orderBy included; the most is ${maxDepth}.`,
138
+ )
139
+ }
140
+ if (rootFields > maxRoots) {
141
+ return new GraphQLError(
142
+ `The document asks for more than ${maxRoots} root fields, a totalCount counted as one more. Send several documents.`,
143
+ )
144
+ }
145
+ if (rows <= maxRows) return undefined
146
+ const fewer = list ? 'take' : 'first or last'
147
+ return new GraphQLError(
148
+ `The document asks for more than ${maxRows.toLocaleString('en-US')} rows. Ask for fewer with ${fewer}.`,
149
+ )
150
+ }
151
+ }
package/src/scalars.ts ADDED
@@ -0,0 +1,110 @@
1
+ // `DateTime` and `JSON` (D-107): a date as an ISO 8601 string, and any JSON
2
+ // value as the kernel stores it; `Decimal` (D-145), an exact number, and
3
+ // `Date` (D-150), a day, as strings, which the kernel checks.
4
+ import type { ConstValueNode } from 'graphql'
5
+ import { GraphQLError, GraphQLScalarType, Kind } from 'graphql'
6
+
7
+ const iso =
8
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/
9
+
10
+ /** A date a client sent, which keeps its text for a function (D-109). */
11
+ export class SentDate extends Date {
12
+ readonly sent: string
13
+
14
+ constructor(sent: string) {
15
+ super(sent)
16
+ this.sent = sent
17
+ }
18
+ }
19
+
20
+ function date(value: unknown): Date {
21
+ if (typeof value === 'string' && iso.test(value)) {
22
+ const parsed = new SentDate(value)
23
+ if (!Number.isNaN(parsed.getTime())) return parsed
24
+ }
25
+ throw new GraphQLError(
26
+ 'A DateTime is an ISO 8601 string, such as 2026-01-31T12:00:00.000Z.',
27
+ )
28
+ }
29
+
30
+ export const DateTime: GraphQLScalarType<Date, string> = new GraphQLScalarType<
31
+ Date,
32
+ string
33
+ >({
34
+ name: 'DateTime',
35
+ description: 'A date and time, as an ISO 8601 string.',
36
+ coerceOutputValue: (value) => {
37
+ if (value instanceof Date) return value.toISOString()
38
+ // A function's schema output holds its dates as strings (D-109).
39
+ if (typeof value === 'string') return date(value).toISOString()
40
+ throw new GraphQLError('A DateTime field returned no date.')
41
+ },
42
+ coerceInputValue: date,
43
+ coerceInputLiteral: (node) =>
44
+ date(node.kind === Kind.STRING ? node.value : undefined),
45
+ })
46
+
47
+ function decimal(value: unknown): string {
48
+ if (typeof value === 'string') return value
49
+ throw new GraphQLError("A Decimal is a string, such as '12.50'.")
50
+ }
51
+
52
+ export const DecimalScalar: GraphQLScalarType<string, string> =
53
+ new GraphQLScalarType<string, string>({
54
+ name: 'Decimal',
55
+ description: "An exact number, as a string such as '12.50'.",
56
+ coerceOutputValue: decimal,
57
+ coerceInputValue: decimal,
58
+ coerceInputLiteral: (node) =>
59
+ decimal(node.kind === Kind.STRING ? node.value : undefined),
60
+ })
61
+
62
+ function day(value: unknown): string {
63
+ if (typeof value === 'string') return value
64
+ throw new GraphQLError("A Date is a day as a string, such as '2026-07-31'.")
65
+ }
66
+
67
+ export const DateScalar: GraphQLScalarType<string, string> =
68
+ new GraphQLScalarType<string, string>({
69
+ name: 'Date',
70
+ description: "A day, as an ISO 8601 string such as '2026-07-31'.",
71
+ coerceOutputValue: day,
72
+ coerceInputValue: day,
73
+ coerceInputLiteral: (node) =>
74
+ day(node.kind === Kind.STRING ? node.value : undefined),
75
+ })
76
+
77
+ /** Dates back to their text: a validator reads what the client sent. */
78
+ export function plain(value: unknown): unknown {
79
+ if (value instanceof SentDate) return value.sent
80
+ if (Array.isArray(value)) return value.map(plain)
81
+ if (typeof value !== 'object' || value === null) return value
82
+ return Object.fromEntries(
83
+ Object.entries(value).map(([key, item]) => [key, plain(item)]),
84
+ )
85
+ }
86
+
87
+ /** A literal as the plain value the kernel's JSON check accepts. */
88
+ function literal(node: ConstValueNode): unknown {
89
+ if (node.kind === Kind.OBJECT) {
90
+ return Object.fromEntries(
91
+ node.fields.map((field) => [field.name.value, literal(field.value)]),
92
+ )
93
+ }
94
+ if (node.kind === Kind.LIST) return node.values.map(literal)
95
+ if (node.kind === Kind.INT || node.kind === Kind.FLOAT) {
96
+ return Number(node.value)
97
+ }
98
+ if (node.kind === Kind.NULL) return null
99
+ // A string, a boolean or an enum's name.
100
+ return node.value
101
+ }
102
+
103
+ export const JSONScalar: GraphQLScalarType<unknown, unknown> =
104
+ new GraphQLScalarType<unknown, unknown>({
105
+ name: 'JSON',
106
+ description: 'Any JSON value.',
107
+ coerceOutputValue: (value) => value,
108
+ coerceInputValue: (value) => value,
109
+ coerceInputLiteral: literal,
110
+ })