@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLObjectType, } from 'graphql';
|
|
2
|
+
import { readableVirtuals } from '@ultimabase/http/internal';
|
|
3
|
+
import { Refused } from 'ultimabase';
|
|
4
|
+
import { checkName, inputArgs, outputData } from './data.js';
|
|
5
|
+
import { upper } from './names.js';
|
|
6
|
+
import { isMaster, objectType } from './outputs.js';
|
|
7
|
+
import { selectOf } from './select.js';
|
|
8
|
+
import { followed } from './subscriptions.js';
|
|
9
|
+
import { as } from './registry.js';
|
|
10
|
+
import { reread } from './reread.js';
|
|
11
|
+
import { plain } from './scalars.js';
|
|
12
|
+
const at = (owner, name, place) => ({
|
|
13
|
+
owner,
|
|
14
|
+
name,
|
|
15
|
+
suffix: '',
|
|
16
|
+
at: place,
|
|
17
|
+
});
|
|
18
|
+
/** Whether a model's rows, as `info` selects them, read a relation. */
|
|
19
|
+
function relates(registry, model, info, master) {
|
|
20
|
+
const type = objectType(registry, model);
|
|
21
|
+
const select = selectOf(registry, info, type, info.fieldNodes, master);
|
|
22
|
+
return Object.values(select).some((read) => read !== true);
|
|
23
|
+
}
|
|
24
|
+
class Builder {
|
|
25
|
+
#types = new Map();
|
|
26
|
+
#registry;
|
|
27
|
+
#data;
|
|
28
|
+
#take;
|
|
29
|
+
/** What each field reads, by type then field, for the row budget. */
|
|
30
|
+
reads = new Map();
|
|
31
|
+
/** Each named type, and the first function to give it. */
|
|
32
|
+
owners;
|
|
33
|
+
constructor(registry, take) {
|
|
34
|
+
this.#registry = registry;
|
|
35
|
+
this.#data = registry.data;
|
|
36
|
+
this.owners = this.#data.owners;
|
|
37
|
+
this.#take = take;
|
|
38
|
+
}
|
|
39
|
+
/** A model or a type is non-null unless `.nullable()`; a list too. */
|
|
40
|
+
output(output, path, parent, field) {
|
|
41
|
+
if (output.kind === 'schema') {
|
|
42
|
+
const { type, nullable } = outputData(output.schema, path, this.#data);
|
|
43
|
+
return nullable ? type : new GraphQLNonNull(type);
|
|
44
|
+
}
|
|
45
|
+
const reads = this.reads.get(parent) ?? new Map();
|
|
46
|
+
this.reads.set(parent, reads);
|
|
47
|
+
const list = output.form === 'list';
|
|
48
|
+
reads.set(field, list ? { kind: 'list', size: this.#take } : { kind: 'one' });
|
|
49
|
+
const named = output.kind === 'model'
|
|
50
|
+
? objectType(this.#registry, output.name)
|
|
51
|
+
: this.#type(output.type, path.owner);
|
|
52
|
+
if (output.form === 'nullable')
|
|
53
|
+
return named;
|
|
54
|
+
const item = new GraphQLNonNull(named);
|
|
55
|
+
return list ? new GraphQLNonNull(new GraphQLList(item)) : item;
|
|
56
|
+
}
|
|
57
|
+
/** The type named `name`, if a function, a virtual or a field gave it. */
|
|
58
|
+
named(name) {
|
|
59
|
+
return this.#types.get(name) ?? this.#data.named(name);
|
|
60
|
+
}
|
|
61
|
+
/** A `defineType`, built once; its fields cannot refer back to it. */
|
|
62
|
+
#type(type, owner) {
|
|
63
|
+
const known = this.#types.get(type.name);
|
|
64
|
+
if (known !== undefined)
|
|
65
|
+
return known;
|
|
66
|
+
const { name } = type;
|
|
67
|
+
checkName(name, `defineType ${name}`);
|
|
68
|
+
const other = this.owners.get(name);
|
|
69
|
+
if (other !== undefined) {
|
|
70
|
+
throw new Refused(`${other} and ${owner} give the type ${name} two shapes; name each once.`);
|
|
71
|
+
}
|
|
72
|
+
this.owners.set(name, owner);
|
|
73
|
+
const fields = [...type.fields].map(([field, output]) => {
|
|
74
|
+
checkName(field, `${name}.${field}`);
|
|
75
|
+
const path = at(name, `${name}${upper(field)}`, field);
|
|
76
|
+
return [field, { type: this.output(output, path, name, field) }];
|
|
77
|
+
});
|
|
78
|
+
const built = new GraphQLObjectType({
|
|
79
|
+
name,
|
|
80
|
+
description: type.description,
|
|
81
|
+
fields: Object.fromEntries(fields),
|
|
82
|
+
});
|
|
83
|
+
this.#types.set(name, built);
|
|
84
|
+
return built;
|
|
85
|
+
}
|
|
86
|
+
/** A virtual's field: an argument per input property, its output (D-151). */
|
|
87
|
+
virtual(model, virtual, ruled) {
|
|
88
|
+
const { name, input, output } = virtual;
|
|
89
|
+
const owner = `${model}.${name}`;
|
|
90
|
+
const base = `${model}${upper(name)}`;
|
|
91
|
+
const given = input === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: inputArgs(input, at(owner, `${base}Input`, 'input'), this.#data);
|
|
94
|
+
if (given?.flat === false) {
|
|
95
|
+
throw new Refused(`${owner}: its input is an object schema, whose properties are its arguments.`);
|
|
96
|
+
}
|
|
97
|
+
const type = this.output(output, at(owner, `${base}Output`, name), model, name);
|
|
98
|
+
const take = this.#take;
|
|
99
|
+
const registry = this.#registry;
|
|
100
|
+
return {
|
|
101
|
+
// Absent when a read rule hides it (D-44).
|
|
102
|
+
type: ruled && type instanceof GraphQLNonNull ? type.ofType : type,
|
|
103
|
+
description: virtual.description,
|
|
104
|
+
args: given?.args ?? {},
|
|
105
|
+
resolve: (source, _, context, info) => {
|
|
106
|
+
const value = Reflect.get(Object(source), name);
|
|
107
|
+
// The budget counts `take` rows in a list (D-109).
|
|
108
|
+
if (output.kind === 'model' &&
|
|
109
|
+
Array.isArray(value) &&
|
|
110
|
+
value.length > take &&
|
|
111
|
+
relates(registry, output.name, info, isMaster(context))) {
|
|
112
|
+
throw new RangeError(`${owner} returned ${value.length} rows; a list counts take (${take}) rows, so it holds no more under a selected relation.`);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A function's field, and its subscription when it is `live` (D-154),
|
|
120
|
+
* whose rows the budget counts as the query's.
|
|
121
|
+
*/
|
|
122
|
+
field(fn, name, owner, live) {
|
|
123
|
+
const { input, output } = fn;
|
|
124
|
+
const base = upper(name);
|
|
125
|
+
const given = input === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: inputArgs(input, at(owner, `${base}Input`, 'input'), this.#data);
|
|
128
|
+
const root = fn.query ? 'Query' : 'Mutation';
|
|
129
|
+
const type = output === undefined
|
|
130
|
+
? new GraphQLNonNull(GraphQLBoolean)
|
|
131
|
+
: this.output(output, at(owner, `${base}Output`, 'output'), root, name);
|
|
132
|
+
const registry = this.#registry;
|
|
133
|
+
const take = this.#take;
|
|
134
|
+
const value = async (args, context, info) => {
|
|
135
|
+
const sent = given?.flat === true ? args : args['input'];
|
|
136
|
+
const result = await fn.run(plain(sent), as(context), context.client);
|
|
137
|
+
if (output === undefined)
|
|
138
|
+
return true;
|
|
139
|
+
if (result === undefined && type instanceof GraphQLNonNull) {
|
|
140
|
+
throw new TypeError(`${fn.name} returned undefined; make its output .nullable() and return null.`);
|
|
141
|
+
}
|
|
142
|
+
const call = { name: fn.name, take };
|
|
143
|
+
return reread(registry, output, result, context, info, call);
|
|
144
|
+
};
|
|
145
|
+
const config = {
|
|
146
|
+
type,
|
|
147
|
+
description: fn.description,
|
|
148
|
+
args: given?.args ?? {},
|
|
149
|
+
};
|
|
150
|
+
const field = {
|
|
151
|
+
...config,
|
|
152
|
+
resolve: async (_, args, context, info) => value(args, context, info),
|
|
153
|
+
};
|
|
154
|
+
if (!live)
|
|
155
|
+
return { field };
|
|
156
|
+
const read = this.reads.get(root)?.get(name);
|
|
157
|
+
if (read !== undefined) {
|
|
158
|
+
const reads = this.reads.get('Subscription') ?? new Map();
|
|
159
|
+
this.reads.set('Subscription', reads.set(name, read));
|
|
160
|
+
}
|
|
161
|
+
return { field, live: followed(owner, config, value) };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** The functions' root fields, and what each reads (D-109). */
|
|
165
|
+
export function functionFields(registry, functions, take, live) {
|
|
166
|
+
const builder = new Builder(registry, take);
|
|
167
|
+
registry.virtualFields = (model) => {
|
|
168
|
+
const entry = registry.entry(model);
|
|
169
|
+
const { readRules } = entry.exposed;
|
|
170
|
+
return Object.fromEntries(readableVirtuals(entry).map((virtual) => [
|
|
171
|
+
virtual.name,
|
|
172
|
+
builder.virtual(model, virtual, readRules[virtual.name] === 'rule'),
|
|
173
|
+
]));
|
|
174
|
+
};
|
|
175
|
+
const fields = [];
|
|
176
|
+
const subscriptions = {};
|
|
177
|
+
for (const fn of functions) {
|
|
178
|
+
if (fn.with === false)
|
|
179
|
+
continue;
|
|
180
|
+
const name = checkName(fn.with?.name ?? fn.name, fn.name);
|
|
181
|
+
// A plugin's function is named with its plugin (D-113).
|
|
182
|
+
const owner = fn.plugin === undefined ? fn.name : `${fn.plugin}.${fn.name}`;
|
|
183
|
+
const built = builder.field(fn, name, owner, live(owner));
|
|
184
|
+
fields.push({ query: fn.query, name, owner, field: built.field });
|
|
185
|
+
if (built.live !== undefined)
|
|
186
|
+
subscriptions[name] = built.live;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
fields,
|
|
190
|
+
subscriptions,
|
|
191
|
+
reads: builder.reads,
|
|
192
|
+
owners: builder.owners,
|
|
193
|
+
named: (name) => builder.named(name),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphiql.d.ts","sourceRoot":"","sources":["../src/graphiql.ts"],"names":[],"mappings":"AAuBA,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,QAAQ,CA6BzD"}
|
package/dist/graphiql.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// GraphiQL 5 (D-107): the page a browser gets at `/graphql` outside
|
|
2
|
+
// production, its modules loaded from esm.sh at pinned versions.
|
|
3
|
+
const imports = {
|
|
4
|
+
react: 'https://esm.sh/react@19.3.1',
|
|
5
|
+
'react/': 'https://esm.sh/react@19.3.1/',
|
|
6
|
+
'react-dom': 'https://esm.sh/react-dom@19.3.1',
|
|
7
|
+
'react-dom/': 'https://esm.sh/react-dom@19.3.1/',
|
|
8
|
+
graphiql: 'https://esm.sh/graphiql@5.4.0?standalone&external=react,react-dom,@graphiql/react,graphql',
|
|
9
|
+
'graphiql/': 'https://esm.sh/graphiql@5.4.0/',
|
|
10
|
+
'@graphiql/react': 'https://esm.sh/@graphiql/react@0.39.1?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid',
|
|
11
|
+
'@graphiql/toolkit': 'https://esm.sh/@graphiql/toolkit@0.12.1?standalone&external=graphql',
|
|
12
|
+
graphql: 'https://esm.sh/graphql@17.0.2',
|
|
13
|
+
'@emotion/is-prop-valid': 'data:text/javascript,',
|
|
14
|
+
};
|
|
15
|
+
/** A value written into a script, unable to close it. */
|
|
16
|
+
const inline = (value) => JSON.stringify(value).replaceAll('<', '\\u003c');
|
|
17
|
+
/** The page, served at `path`, that sends its documents to `path`. */
|
|
18
|
+
export function graphiqlPage(path) {
|
|
19
|
+
const html = `<!doctype html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="utf-8">
|
|
23
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
24
|
+
<title>UltimaBase GraphiQL</title>
|
|
25
|
+
<style>html,body,#graphiql{height:100%;margin:0}</style>
|
|
26
|
+
<link rel="stylesheet" href="https://esm.sh/graphiql@5.4.0/dist/style.css">
|
|
27
|
+
<script type="importmap">${inline({ imports })}</script>
|
|
28
|
+
<script type="module">
|
|
29
|
+
import React from 'react'
|
|
30
|
+
import ReactDOM from 'react-dom/client'
|
|
31
|
+
import { GraphiQL } from 'graphiql'
|
|
32
|
+
import { createGraphiQLFetcher } from '@graphiql/toolkit'
|
|
33
|
+
import 'graphiql/setup-workers/esm.sh'
|
|
34
|
+
const fetcher = createGraphiQLFetcher({ url: ${inline(path)} })
|
|
35
|
+
ReactDOM.createRoot(document.getElementById('graphiql')).render(
|
|
36
|
+
React.createElement(GraphiQL, { fetcher }),
|
|
37
|
+
)
|
|
38
|
+
</script>
|
|
39
|
+
</head>
|
|
40
|
+
<body><div id="graphiql"></div></body>
|
|
41
|
+
</html>
|
|
42
|
+
`;
|
|
43
|
+
return () => new Response(html, {
|
|
44
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { Http } from '@ultimabase/http';
|
|
3
|
+
import type { Realtime } from '@ultimabase/realtime';
|
|
4
|
+
import type { PluginOf, PluginTypes, UseOf } from 'ultimabase';
|
|
5
|
+
import type { GraphqlFunctionWith } from './functions.ts';
|
|
6
|
+
import type { GraphqlWith } from './registry.ts';
|
|
7
|
+
export type { GraphqlFunctionWith } from './functions.ts';
|
|
8
|
+
export type { Context as GraphqlContext, GraphqlWith } from './registry.ts';
|
|
9
|
+
/** What `graphqlPlugin` takes (D-107). */
|
|
10
|
+
export interface GraphqlOptions {
|
|
11
|
+
/** `/graphql` by default. */
|
|
12
|
+
readonly path?: string;
|
|
13
|
+
/** Relay connections, or `take` and `skip` lists. */
|
|
14
|
+
readonly pagination?: 'connection' | 'list';
|
|
15
|
+
/** The rows a list returns when none is asked for; 100 by default (D-55). */
|
|
16
|
+
readonly take?: number;
|
|
17
|
+
/** The rows a caller other than Master may ask for; 100 by default. */
|
|
18
|
+
readonly maxTake?: number;
|
|
19
|
+
/** The deepest a document of a caller other than Master nests; 10 by default. */
|
|
20
|
+
readonly maxDepth?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The most rows a document of a caller other than Master may return, each
|
|
23
|
+
* list times the lists above it; 20 000 by default.
|
|
24
|
+
*/
|
|
25
|
+
readonly maxRows?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Introspection and "Did you mean" suggestions: closed in production,
|
|
28
|
+
* open elsewhere, unless set.
|
|
29
|
+
*/
|
|
30
|
+
readonly introspection?: boolean;
|
|
31
|
+
/** Replaces the generated schema with one built from it (D-45). */
|
|
32
|
+
readonly extend?: (schema: GraphQLSchema) => GraphQLSchema;
|
|
33
|
+
}
|
|
34
|
+
/** The GraphQL plugin's types (D-57). */
|
|
35
|
+
export interface Graphql extends PluginTypes {
|
|
36
|
+
readonly name: 'graphql';
|
|
37
|
+
readonly appOptions: GraphqlOptions;
|
|
38
|
+
readonly requires: Http;
|
|
39
|
+
readonly optional: Realtime;
|
|
40
|
+
readonly with: GraphqlWith;
|
|
41
|
+
readonly functionWith: GraphqlFunctionWith;
|
|
42
|
+
}
|
|
43
|
+
/** The application's GraphQL API, served by `httpPlugin` (D-107). */
|
|
44
|
+
export declare const graphqlPlugin: PluginOf<Graphql>;
|
|
45
|
+
/** A model's `.with({ graphql })`, from its `use` (D-107). */
|
|
46
|
+
export declare const graphql: UseOf<Graphql>;
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAW5C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAEpD,OAAO,KAAK,EAAkB,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAK9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAKzD,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAA;AAShE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACzD,YAAY,EAAE,OAAO,IAAI,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE3E,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC9B,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,qDAAqD;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAA;IAC3C,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,aAAa,CAAA;CAC1D;AAED,yCAAyC;AACzC,MAAM,WAAW,OAAQ,SAAQ,WAAW;IAC3C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAA;CAC1C;AAwJD,qEAAqE;AACrE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAiB,CAAA;AAE7D,8DAA8D;AAC9D,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,OAAO,CAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { executeRootSelectionSet, MaxIntrospectionDepthRule, NoSchemaIntrospectionCustomRule, parse, specifiedRules, validate, validateExecutionArgs, } from 'graphql';
|
|
2
|
+
import { createHandler } from 'graphql-http/lib/use/fetch';
|
|
3
|
+
import { clientOf, httpPlugin, routes } from '@ultimabase/http';
|
|
4
|
+
import { realtimePlugin } from '@ultimabase/realtime';
|
|
5
|
+
import { definePlugin } from 'ultimabase';
|
|
6
|
+
import manifest from '../package.json' with { type: 'json' };
|
|
7
|
+
import { depthRule, introspectionDepthRule } from './depth.js';
|
|
8
|
+
import { formatter } from './errors.js';
|
|
9
|
+
import { functionFields } from './functions.js';
|
|
10
|
+
import { graphiqlPage } from './graphiql.js';
|
|
11
|
+
import { overlapRefusal } from './overlap.js';
|
|
12
|
+
import { rootNames } from './names.js';
|
|
13
|
+
import { Registry } from './registry.js';
|
|
14
|
+
import { rowBudget } from './rows.js';
|
|
15
|
+
import { schemaOf } from './schema.js';
|
|
16
|
+
import { selectFor } from './select.js';
|
|
17
|
+
import { serveSockets } from './sockets.js';
|
|
18
|
+
import { subscriptionFields } from './subscriptions.js';
|
|
19
|
+
/** About ten times the standard introspection query. */
|
|
20
|
+
const maxTokens = 2000;
|
|
21
|
+
// What closes each running application's WebSockets, by its plugin's context.
|
|
22
|
+
const closers = new WeakMap();
|
|
23
|
+
const plugin = definePlugin({
|
|
24
|
+
name: 'graphql',
|
|
25
|
+
manifest,
|
|
26
|
+
requires: { http: httpPlugin },
|
|
27
|
+
optional: { realtime: realtimePlugin },
|
|
28
|
+
api: (pluginContext) => {
|
|
29
|
+
const { plugins, models, functions, options, logger } = pluginContext;
|
|
30
|
+
const { realtime } = plugins;
|
|
31
|
+
const take = options.take ?? 100;
|
|
32
|
+
const entries = new Map();
|
|
33
|
+
for (const exposed of models) {
|
|
34
|
+
const name = exposed.schema.name;
|
|
35
|
+
const own = exposed.with === false ? {} : (exposed.with ?? {});
|
|
36
|
+
entries.set(name, {
|
|
37
|
+
exposed,
|
|
38
|
+
model: exposed.schema,
|
|
39
|
+
visible: exposed.with !== false,
|
|
40
|
+
...rootNames(name, own.plural),
|
|
41
|
+
take,
|
|
42
|
+
maxTake: own.maxTake ?? options.maxTake ?? 100,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const registry = new Registry(entries, {
|
|
46
|
+
pagination: options.pagination ?? 'connection',
|
|
47
|
+
});
|
|
48
|
+
const liveFunction = realtime?.liveFunction ?? (() => false);
|
|
49
|
+
const called = functionFields(registry, functions, take, liveFunction);
|
|
50
|
+
const live = realtime?.live ?? (() => false);
|
|
51
|
+
const generated = schemaOf(registry, {
|
|
52
|
+
...called,
|
|
53
|
+
subscriptions: {
|
|
54
|
+
...subscriptionFields(registry, live),
|
|
55
|
+
...called.subscriptions,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const schema = options.extend?.(generated) ?? generated;
|
|
59
|
+
const production = process.env['NODE_ENV'] === 'production';
|
|
60
|
+
const hideSuggestions = !(options.introspection ?? !production);
|
|
61
|
+
const closed = hideSuggestions ? [NoSchemaIntrospectionCustomRule] : [];
|
|
62
|
+
const maxDepth = options.maxDepth ?? 10;
|
|
63
|
+
const depth = depthRule(maxDepth);
|
|
64
|
+
const maxRows = options.maxRows ?? 20_000;
|
|
65
|
+
const budget = rowBudget(registry, { maxRows, maxDepth }, called.reads);
|
|
66
|
+
// The contexts of Master's requests, which no budget limits.
|
|
67
|
+
const exempt = new WeakSet();
|
|
68
|
+
// The caller the HTTP plugin read and the client, by its request.
|
|
69
|
+
const requests = new WeakMap();
|
|
70
|
+
const heldOf = (request) => {
|
|
71
|
+
const held = requests.get(request);
|
|
72
|
+
if (held === undefined)
|
|
73
|
+
throw new Error('No caller was read.');
|
|
74
|
+
return held;
|
|
75
|
+
};
|
|
76
|
+
const operations = {
|
|
77
|
+
schema,
|
|
78
|
+
// Validation is quadratic in a field repeated under one key, so a
|
|
79
|
+
// document is bounded before it is validated.
|
|
80
|
+
parse: (source) => parse(source, { maxTokens }),
|
|
81
|
+
rules: (master) => [
|
|
82
|
+
...specifiedRules.filter((rule) => rule !== MaxIntrospectionDepthRule),
|
|
83
|
+
introspectionDepthRule,
|
|
84
|
+
...closed,
|
|
85
|
+
...(master ? [] : [depth]),
|
|
86
|
+
],
|
|
87
|
+
validate: (document, rules) => {
|
|
88
|
+
const refused = overlapRefusal(document);
|
|
89
|
+
if (refused !== undefined)
|
|
90
|
+
return [refused];
|
|
91
|
+
return validate(schema, document, rules, { hideSuggestions });
|
|
92
|
+
},
|
|
93
|
+
execute: async (args) => {
|
|
94
|
+
const validated = validateExecutionArgs({ ...args, hideSuggestions });
|
|
95
|
+
if (!('schema' in validated))
|
|
96
|
+
return { errors: validated };
|
|
97
|
+
const { contextValue } = validated;
|
|
98
|
+
const master = typeof contextValue === 'object' &&
|
|
99
|
+
contextValue !== null &&
|
|
100
|
+
exempt.has(contextValue);
|
|
101
|
+
const refused = master ? undefined : budget(validated);
|
|
102
|
+
if (refused !== undefined)
|
|
103
|
+
return { errors: [refused] };
|
|
104
|
+
return executeRootSelectionSet(validated);
|
|
105
|
+
},
|
|
106
|
+
context: (caller, client) => {
|
|
107
|
+
const master = caller.kind === 'master';
|
|
108
|
+
const select = selectFor(registry, master);
|
|
109
|
+
const context = { caller, client, select };
|
|
110
|
+
if (master)
|
|
111
|
+
exempt.add(context);
|
|
112
|
+
return context;
|
|
113
|
+
},
|
|
114
|
+
hideSuggestions,
|
|
115
|
+
format: formatter(logger),
|
|
116
|
+
logger,
|
|
117
|
+
};
|
|
118
|
+
const handler = createHandler({
|
|
119
|
+
schema,
|
|
120
|
+
parse: operations.parse,
|
|
121
|
+
context: (request) => {
|
|
122
|
+
const { caller, client } = heldOf(request.raw);
|
|
123
|
+
return operations.context(caller, client);
|
|
124
|
+
},
|
|
125
|
+
validationRules: (request) => operations.rules(heldOf(request.raw).caller.kind === 'master'),
|
|
126
|
+
validate: (_, document, rules) => operations.validate(document, rules),
|
|
127
|
+
execute: operations.execute,
|
|
128
|
+
formatError: operations.format,
|
|
129
|
+
});
|
|
130
|
+
const path = options.path ?? '/graphql';
|
|
131
|
+
const page = production ? undefined : graphiqlPage(path);
|
|
132
|
+
plugins.http.mount(routes().on(['GET', 'POST'], path, async (c) => {
|
|
133
|
+
// A browser opening the URL, not a client sending a document.
|
|
134
|
+
const browsing = c.req.method === 'GET' &&
|
|
135
|
+
c.req.header('accept')?.includes('text/html') === true;
|
|
136
|
+
if (page !== undefined && browsing)
|
|
137
|
+
return page();
|
|
138
|
+
// A middleware may have read the body: Hono kept it, not the request.
|
|
139
|
+
const request = c.req.method === 'POST' && c.req.raw.bodyUsed
|
|
140
|
+
? new Request(c.req.raw, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body: await c.req.text(),
|
|
143
|
+
})
|
|
144
|
+
: c.req.raw;
|
|
145
|
+
const { client, sent } = clientOf(c);
|
|
146
|
+
requests.set(request, { caller: c.var.caller, client });
|
|
147
|
+
return sent(await handler(request));
|
|
148
|
+
}));
|
|
149
|
+
if (realtime !== undefined) {
|
|
150
|
+
const close = serveSockets(plugins.http, path, realtime, operations);
|
|
151
|
+
closers.set(pluginContext, close);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
start: ({ plugins, options, logger }) => {
|
|
155
|
+
if (process.env['NODE_ENV'] === 'production')
|
|
156
|
+
return;
|
|
157
|
+
logger.info(`GraphiQL on ${plugins.http.url()}${options.path ?? '/graphql'}.`);
|
|
158
|
+
},
|
|
159
|
+
// Before the HTTP plugin's stop, which waits for the sockets to close.
|
|
160
|
+
stop: async (context) => closers.get(context)?.(),
|
|
161
|
+
});
|
|
162
|
+
/** The application's GraphQL API, served by `httpPlugin` (D-107). */
|
|
163
|
+
export const graphqlPlugin = plugin.plugin;
|
|
164
|
+
/** A model's `.with({ graphql })`, from its `use` (D-107). */
|
|
165
|
+
export const graphql = plugin.use;
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GraphQLInputObjectType } from 'graphql';
|
|
2
|
+
import type { Args, Registry } from './registry.ts';
|
|
3
|
+
/** An input object's value, as the kernel's arguments. */
|
|
4
|
+
export declare const objectOf: (value: unknown) => Args;
|
|
5
|
+
/** `PostWhereInput`. */
|
|
6
|
+
export declare function whereInput(registry: Registry, model: string): GraphQLInputObjectType;
|
|
7
|
+
/**
|
|
8
|
+
* `PostOrderByInput`: scalar fields but json, lists and plugin fields, as
|
|
9
|
+
* the kernel sorts (D-94, D-120, D-122); a `one` relation as its target's,
|
|
10
|
+
* a `many` one by its count (D-134).
|
|
11
|
+
*/
|
|
12
|
+
export declare function orderByInput(registry: Registry, model: string): GraphQLInputObjectType;
|
|
13
|
+
/**
|
|
14
|
+
* `PostWhereUniqueInput @oneOf`: `id`, each unique field, and each unique
|
|
15
|
+
* index as `author_slug: { author, slug }`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function uniqueInput(registry: Registry, model: string): GraphQLInputObjectType;
|
|
18
|
+
/** A `@oneOf` unique target as the kernel's flat `where`. */
|
|
19
|
+
export declare function uniqueWhere(registry: Registry, model: string, where: Args): Args;
|
|
20
|
+
//# sourceMappingURL=inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputs.d.ts","sourceRoot":"","sources":["../src/inputs.ts"],"names":[],"mappings":"AASA,OAAO,EAIN,sBAAsB,EAItB,MAAM,SAAS,CAAA;AAGhB,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAU,MAAM,eAAe,CAAA;AAI3D,0DAA0D;AAC1D,eAAO,MAAM,QAAQ,UAAW,OAAO,KAAG,IACsB,CAAA;AA4IhE,wBAAwB;AACxB,wBAAgB,UAAU,CACzB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,sBAAsB,CAmCxB;AAuBD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,sBAAsB,CAoCxB;AAgBD;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,GACX,sBAAsB,CA+BxB;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,GACT,IAAI,CAcN"}
|