@ultimabase/graphql 0.0.0-stage → 0.1.1
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 +24 -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 +53 -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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @ultimabase/graphql
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The published starter starts: `ultimabase dev` runs `dist/serve.js`, and `@ultimabase/realtime`, which `@ultimabase/graphql` and `@ultimabase/rest` import, is a required peer the starter lists.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- ultimabase@0.1.1
|
|
10
|
+
- @ultimabase/http@0.1.1
|
|
11
|
+
- @ultimabase/realtime@0.1.1
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0e885c4: The first release: the kernel, its adapters, transports and plugins, the testing helpers and the starter.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [0e885c4]
|
|
22
|
+
- @ultimabase/http@0.1.0
|
|
23
|
+
- @ultimabase/realtime@0.1.0
|
|
24
|
+
- ultimabase@0.1.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ultimabase/graphql
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A GraphQL API generated from the models and the functions, served by `@ultimabase/http`. Each request runs as its caller, under the models' access rules, within limits on depth and rows.
|
|
4
|
+
|
|
5
|
+
Documentation: [GraphQL](https://ultimabase.dev/docs/graphql).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GraphQLInputObjectType, GraphQLNamedType, GraphQLObjectType } from 'graphql';
|
|
2
|
+
import { GraphQLEnumType } from 'graphql';
|
|
3
|
+
/** The named types of functions and json fields, each given one shape. */
|
|
4
|
+
export declare class DataTypes {
|
|
5
|
+
#private;
|
|
6
|
+
/** Each named type, and the first function or field to give it. */
|
|
7
|
+
readonly owners: Map<string, string>;
|
|
8
|
+
constructor(models: Iterable<string>);
|
|
9
|
+
object(name: string, key: string, owner: string, build: () => GraphQLObjectType): GraphQLObjectType;
|
|
10
|
+
input(name: string, key: string, owner: string, build: () => GraphQLInputObjectType): GraphQLInputObjectType;
|
|
11
|
+
enum(name: string, values: readonly string[], owner: string, description?: string): GraphQLEnumType;
|
|
12
|
+
/** The type named `name`, if one was given. */
|
|
13
|
+
named(name: string): GraphQLNamedType | undefined;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=data-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-types.d.ts","sourceRoot":"","sources":["../src/data-types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AASzC,0EAA0E;AAC1E,qBAAa,SAAS;;IAKrB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAY;IAEhD,YAAY,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAEnC;IAED,MAAM,CACL,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,iBAAiB,GAC5B,iBAAiB,CAEnB;IAED,KAAK,CACJ,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,sBAAsB,GACjC,sBAAsB,CAExB;IAED,IAAI,CACH,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GAClB,eAAe,CAQjB;IAED,+CAA+C;IAC/C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAMhD;CA8BD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { GraphQLEnumType } from 'graphql';
|
|
2
|
+
import { Refused } from 'ultimabase';
|
|
3
|
+
/** The named types of functions and json fields, each given one shape. */
|
|
4
|
+
export class DataTypes {
|
|
5
|
+
#objects = new Map();
|
|
6
|
+
#inputs = new Map();
|
|
7
|
+
#enums = new Map();
|
|
8
|
+
#models;
|
|
9
|
+
/** Each named type, and the first function or field to give it. */
|
|
10
|
+
owners = new Map();
|
|
11
|
+
constructor(models) {
|
|
12
|
+
this.#models = new Set(models);
|
|
13
|
+
}
|
|
14
|
+
object(name, key, owner, build) {
|
|
15
|
+
return this.#shaped(this.#objects, name, key, owner, build);
|
|
16
|
+
}
|
|
17
|
+
input(name, key, owner, build) {
|
|
18
|
+
return this.#shaped(this.#inputs, name, key, owner, build);
|
|
19
|
+
}
|
|
20
|
+
enum(name, values, owner, description) {
|
|
21
|
+
const build = () => new GraphQLEnumType({
|
|
22
|
+
name,
|
|
23
|
+
description,
|
|
24
|
+
values: Object.fromEntries(values.map((value) => [value, { value }])),
|
|
25
|
+
});
|
|
26
|
+
return this.#shaped(this.#enums, name, values.join(), owner, build);
|
|
27
|
+
}
|
|
28
|
+
/** The type named `name`, if one was given. */
|
|
29
|
+
named(name) {
|
|
30
|
+
return (this.#objects.get(name)?.type ??
|
|
31
|
+
this.#inputs.get(name)?.type ??
|
|
32
|
+
this.#enums.get(name)?.type);
|
|
33
|
+
}
|
|
34
|
+
#shaped(cache, name, key, owner, build) {
|
|
35
|
+
if (this.#models.has(name)) {
|
|
36
|
+
throw new Refused(`${owner}: the type ${name} is named like a model.`);
|
|
37
|
+
}
|
|
38
|
+
const known = cache.get(name);
|
|
39
|
+
const other = this.owners.get(name);
|
|
40
|
+
if (known === undefined && other !== undefined) {
|
|
41
|
+
throw new Refused(`${other} and ${owner} give the type ${name} two shapes; name each once.`);
|
|
42
|
+
}
|
|
43
|
+
if (known === undefined) {
|
|
44
|
+
const type = build();
|
|
45
|
+
cache.set(name, { key, owner, type });
|
|
46
|
+
this.owners.set(name, owner);
|
|
47
|
+
return type;
|
|
48
|
+
}
|
|
49
|
+
if (known.key === key)
|
|
50
|
+
return known.type;
|
|
51
|
+
throw new Refused(`${known.owner} and ${owner} give the type ${name} two shapes; name each once.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GraphQLFieldConfigArgumentMap, GraphQLNullableInputType, GraphQLNullableOutputType } from 'graphql';
|
|
2
|
+
import type { DataTypes } from './data-types.ts';
|
|
3
|
+
/** Where a node is: who a message names, a type's name, and the path. */
|
|
4
|
+
export type Path = {
|
|
5
|
+
readonly owner: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** `Input` under a shared input type: `AddressInput`, `AddressGeoInput`. */
|
|
8
|
+
readonly suffix: string;
|
|
9
|
+
readonly at: string;
|
|
10
|
+
};
|
|
11
|
+
/** Stops startup on a name GraphQL refuses. */
|
|
12
|
+
export declare function checkName(name: string, where: string): string;
|
|
13
|
+
export type Typed<T> = {
|
|
14
|
+
readonly type: T;
|
|
15
|
+
readonly nullable: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** An output schema's type, nullable when its JSON Schema accepts `null`. */
|
|
18
|
+
export declare function outputData(schema: object, path: Path, types: DataTypes): Typed<GraphQLNullableOutputType>;
|
|
19
|
+
/**
|
|
20
|
+
* An object input's properties as arguments, `flat`; any other input as one
|
|
21
|
+
* `input` argument (D-109).
|
|
22
|
+
*/
|
|
23
|
+
export declare function inputArgs(schema: object, path: Path, types: DataTypes): {
|
|
24
|
+
readonly args: GraphQLFieldConfigArgumentMap;
|
|
25
|
+
readonly flat: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A json field's type when read, from its schema's output side (D-125);
|
|
29
|
+
* `undefined` when the schema has no JSON Schema export.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fieldOutput(schema: object, path: Path, types: DataTypes): Typed<GraphQLNullableOutputType> | undefined;
|
|
32
|
+
/** A json field's type when written, from its schema's input side. */
|
|
33
|
+
export declare function fieldInput(schema: object, path: Path, types: DataTypes): Typed<GraphQLNullableInputType> | undefined;
|
|
34
|
+
//# sourceMappingURL=data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEX,6BAA6B,EAC7B,wBAAwB,EACxB,yBAAyB,EAGzB,MAAM,SAAS,CAAA;AAgBhB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAgBhD,yEAAyE;AACzE,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,+CAA+C;AAC/C,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG7D;AAGD,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAqPvE,6EAA6E;AAC7E,wBAAgB,UAAU,CACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,GACd,KAAK,CAAC,yBAAyB,CAAC,CAGlC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAkB1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,GACd,KAAK,CAAC,yBAAyB,CAAC,GAAG,SAAS,CAG9C;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,GACd,KAAK,CAAC,wBAAwB,CAAC,GAAG,SAAS,CAG7C"}
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { isNode, jsonSchemaOf } from '@ultimabase/http/internal';
|
|
2
|
+
import { GraphQLBoolean, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
|
|
3
|
+
import { defsOf, exportOf } from './json-schema.js';
|
|
4
|
+
import { upper } from './names.js';
|
|
5
|
+
import { DateScalar, DateTime, JSONScalar } from './scalars.js';
|
|
6
|
+
import { Refused } from 'ultimabase';
|
|
7
|
+
const reserved = new Set(['true', 'false', 'null']);
|
|
8
|
+
const named = (value) => /^[_A-Za-z][_0-9A-Za-z]*$/.test(value) && !value.startsWith('__');
|
|
9
|
+
const isName = (value) => typeof value === 'string' && named(value);
|
|
10
|
+
const isValue = (value) => isName(value) && !reserved.has(value);
|
|
11
|
+
/** A node's description, as `.describe()` writes it (D-150). */
|
|
12
|
+
const describedBy = (node) => {
|
|
13
|
+
const { description } = node;
|
|
14
|
+
return typeof description === 'string' ? description : undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Stops startup on a name GraphQL refuses. */
|
|
17
|
+
export function checkName(name, where) {
|
|
18
|
+
if (named(name))
|
|
19
|
+
return name;
|
|
20
|
+
throw new Refused(`${where}: ${name} is not a GraphQL name.`);
|
|
21
|
+
}
|
|
22
|
+
const union = (path) => new Refused(`${path.owner}: ${path.at} is a union, which GraphQL cannot type; keep one type, or one and null.`);
|
|
23
|
+
/** `JSON` in a json field's types, or startup stops for a function's. */
|
|
24
|
+
function untyped(scope, nullable, refused) {
|
|
25
|
+
if (!scope.lenient)
|
|
26
|
+
throw refused();
|
|
27
|
+
return { type: scope.leaf(JSONScalar), nullable };
|
|
28
|
+
}
|
|
29
|
+
function refType(ref, path, scope) {
|
|
30
|
+
if (ref === '#') {
|
|
31
|
+
return untyped(scope, false, () => new Refused(`${path.owner}: ${path.at} refers to its own schema; name it with .meta({ id }).`));
|
|
32
|
+
}
|
|
33
|
+
const id = ref.startsWith('#/$defs/') ? ref.slice(8) : '';
|
|
34
|
+
const node = scope.defs[id];
|
|
35
|
+
if (!isNode(node)) {
|
|
36
|
+
return untyped(scope, false, () => new Refused(`${path.owner}: ${path.at} refers to ${ref}, not a $defs.`));
|
|
37
|
+
}
|
|
38
|
+
if (!named(id)) {
|
|
39
|
+
return untyped(scope, false, () => new Refused(`${path.owner}: ${path.at} refers to ${id}, which is not a GraphQL name; name it with .meta({ id }).`));
|
|
40
|
+
}
|
|
41
|
+
const suffix = scope.side === 'input' ? 'Input' : '';
|
|
42
|
+
return dataType(node, { ...path, name: id, suffix }, scope);
|
|
43
|
+
}
|
|
44
|
+
function enumType(node, values, path, types) {
|
|
45
|
+
const names = values.filter((value) => value !== null);
|
|
46
|
+
if (!names.every(isValue))
|
|
47
|
+
return GraphQLString;
|
|
48
|
+
return types.enum(path.name, names, path.owner, describedBy(node));
|
|
49
|
+
}
|
|
50
|
+
function typeOf(type, node, path, scope) {
|
|
51
|
+
const { const: only, enum: listed } = node;
|
|
52
|
+
const values = only === undefined ? (Array.isArray(listed) ? listed : []) : [only];
|
|
53
|
+
if (type === 'boolean')
|
|
54
|
+
return scope.leaf(GraphQLBoolean);
|
|
55
|
+
// A literal number is an `Int` when each of its values is whole.
|
|
56
|
+
if (type === 'integer' || type === 'number') {
|
|
57
|
+
const whole = values.length > 0 && values.every(Number.isInteger);
|
|
58
|
+
return scope.leaf(type === 'integer' || whole ? GraphQLInt : GraphQLFloat);
|
|
59
|
+
}
|
|
60
|
+
if (only !== undefined)
|
|
61
|
+
return scope.leaf(GraphQLString);
|
|
62
|
+
if (values.length > 0)
|
|
63
|
+
return scope.leaf(enumType(node, values, path, scope.types));
|
|
64
|
+
if (type === 'string') {
|
|
65
|
+
const format = node['format'];
|
|
66
|
+
return scope.leaf(format === 'date-time'
|
|
67
|
+
? DateTime
|
|
68
|
+
: format === 'date'
|
|
69
|
+
? DateScalar
|
|
70
|
+
: GraphQLString);
|
|
71
|
+
}
|
|
72
|
+
if (type === 'array') {
|
|
73
|
+
const items = isNode(node['items']) ? node['items'] : {};
|
|
74
|
+
return scope.list(dataType(items, { ...path, at: `${path.at}[]` }, scope));
|
|
75
|
+
}
|
|
76
|
+
const { properties } = node;
|
|
77
|
+
if (type === 'object' && isNode(properties)) {
|
|
78
|
+
const keys = Object.keys(properties);
|
|
79
|
+
// A json field's key GraphQL cannot name makes its object `JSON`.
|
|
80
|
+
const typed = !scope.lenient || keys.every(named);
|
|
81
|
+
if (keys.length > 0 && typed)
|
|
82
|
+
return scope.object(node, properties, path);
|
|
83
|
+
}
|
|
84
|
+
return scope.leaf(JSONScalar);
|
|
85
|
+
}
|
|
86
|
+
/** A node's GraphQL type, and whether it accepts `null`. */
|
|
87
|
+
function dataType(node, path, scope) {
|
|
88
|
+
const ref = node['$ref'];
|
|
89
|
+
if (typeof ref === 'string')
|
|
90
|
+
return refType(ref, path, scope);
|
|
91
|
+
const variants = node['anyOf'] ?? node['oneOf'];
|
|
92
|
+
if (Array.isArray(variants)) {
|
|
93
|
+
const others = variants.filter((variant) => !isNode(variant) || variant['type'] !== 'null');
|
|
94
|
+
const [only] = others;
|
|
95
|
+
if (others.length !== 1 || variants.length !== 2 || !isNode(only)) {
|
|
96
|
+
return untyped(scope, others.length < variants.length, () => union(path));
|
|
97
|
+
}
|
|
98
|
+
return { type: dataType(only, path, scope).type, nullable: true };
|
|
99
|
+
}
|
|
100
|
+
const listed = Array.isArray(node['type'])
|
|
101
|
+
? node['type']
|
|
102
|
+
: [node['type']];
|
|
103
|
+
const types = listed.filter((type) => type !== undefined && type !== 'null');
|
|
104
|
+
const nullable = types.length < listed.length;
|
|
105
|
+
if (types.length > 1)
|
|
106
|
+
return untyped(scope, nullable, () => union(path));
|
|
107
|
+
return { type: typeOf(types[0], node, path, scope), nullable };
|
|
108
|
+
}
|
|
109
|
+
function propertiesOf(node, properties, path, scope) {
|
|
110
|
+
const listed = Array.isArray(node['required'])
|
|
111
|
+
? node['required']
|
|
112
|
+
: [];
|
|
113
|
+
return Object.entries(properties).map(([key, value]) => {
|
|
114
|
+
const at = `${path.at}.${key}`;
|
|
115
|
+
checkName(key, `${path.owner}: ${at}`);
|
|
116
|
+
const child = { ...path, name: `${path.name}${upper(key)}`, at };
|
|
117
|
+
const property = isNode(value) ? value : {};
|
|
118
|
+
const { type, nullable } = dataType(property, child, scope);
|
|
119
|
+
// A default fills an input the client leaves out.
|
|
120
|
+
const defaulted = scope.side === 'input' && property['default'] !== undefined;
|
|
121
|
+
const required = listed.includes(key) && !nullable && !defaulted;
|
|
122
|
+
return [
|
|
123
|
+
key,
|
|
124
|
+
{
|
|
125
|
+
type: required ? new GraphQLNonNull(type) : type,
|
|
126
|
+
description: describedBy(property),
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** An object type's fields, read when the schema is built. */
|
|
132
|
+
const fieldsOf = (node, properties, path, scope) => () => Object.fromEntries(propertiesOf(node, properties, path, scope));
|
|
133
|
+
const listOf = ({ type, nullable }) => new GraphQLList(nullable ? type : new GraphQLNonNull(type));
|
|
134
|
+
/** Each side's scope for `root`'s types. */
|
|
135
|
+
function scopesOf(root, types, lenient) {
|
|
136
|
+
const shared = {
|
|
137
|
+
lenient,
|
|
138
|
+
defs: defsOf(root),
|
|
139
|
+
types,
|
|
140
|
+
leaf: (type) => type,
|
|
141
|
+
};
|
|
142
|
+
const output = {
|
|
143
|
+
...shared,
|
|
144
|
+
side: 'output',
|
|
145
|
+
list: listOf,
|
|
146
|
+
object: (node, properties, path) => types.object(path.name, JSON.stringify(node), path.owner, () => new GraphQLObjectType({
|
|
147
|
+
name: path.name,
|
|
148
|
+
description: describedBy(node),
|
|
149
|
+
fields: fieldsOf(node, properties, path, output),
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
const input = {
|
|
153
|
+
...shared,
|
|
154
|
+
side: 'input',
|
|
155
|
+
list: listOf,
|
|
156
|
+
object: (node, properties, path) => {
|
|
157
|
+
const name = `${path.name}${path.suffix}`;
|
|
158
|
+
return types.input(name, JSON.stringify(node), path.owner, () => new GraphQLInputObjectType({
|
|
159
|
+
name,
|
|
160
|
+
description: describedBy(node),
|
|
161
|
+
fields: fieldsOf(node, properties, path, input),
|
|
162
|
+
}));
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
return { output, input };
|
|
166
|
+
}
|
|
167
|
+
/** An output schema's type, nullable when its JSON Schema accepts `null`. */
|
|
168
|
+
export function outputData(schema, path, types) {
|
|
169
|
+
const root = jsonSchemaOf(schema, 'output', `${path.owner}: ${path.at}`);
|
|
170
|
+
return dataType(root, path, scopesOf(root, types, false).output);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* An object input's properties as arguments, `flat`; any other input as one
|
|
174
|
+
* `input` argument (D-109).
|
|
175
|
+
*/
|
|
176
|
+
export function inputArgs(schema, path, types) {
|
|
177
|
+
const root = jsonSchemaOf(schema, 'input', `${path.owner}: ${path.at}`);
|
|
178
|
+
const scope = scopesOf(root, types, false).input;
|
|
179
|
+
const ref = root['$ref'];
|
|
180
|
+
const object = typeof ref === 'string' ? scope.defs[ref.slice(8)] : root;
|
|
181
|
+
const properties = isNode(object) ? object['properties'] : undefined;
|
|
182
|
+
if (isNode(object) && object['type'] === 'object' && isNode(properties)) {
|
|
183
|
+
const args = propertiesOf(object, properties, path, scope);
|
|
184
|
+
return { args: Object.fromEntries(args), flat: true };
|
|
185
|
+
}
|
|
186
|
+
const { type, nullable } = dataType(root, path, scope);
|
|
187
|
+
const description = describedBy(root);
|
|
188
|
+
return {
|
|
189
|
+
args: {
|
|
190
|
+
input: { type: nullable ? type : new GraphQLNonNull(type), description },
|
|
191
|
+
},
|
|
192
|
+
flat: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A json field's type when read, from its schema's output side (D-125);
|
|
197
|
+
* `undefined` when the schema has no JSON Schema export.
|
|
198
|
+
*/
|
|
199
|
+
export function fieldOutput(schema, path, types) {
|
|
200
|
+
const root = exportOf(schema, 'output');
|
|
201
|
+
return root && dataType(root, path, scopesOf(root, types, true).output);
|
|
202
|
+
}
|
|
203
|
+
/** A json field's type when written, from its schema's input side. */
|
|
204
|
+
export function fieldInput(schema, path, types) {
|
|
205
|
+
const root = exportOf(schema, 'input');
|
|
206
|
+
return root && dataType(root, path, scopesOf(root, types, true).input);
|
|
207
|
+
}
|
package/dist/depth.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ValidationRule } from 'graphql';
|
|
2
|
+
/** The validation rule that refuses a document deeper than `maxDepth`. */
|
|
3
|
+
export declare function depthRule(maxDepth: number): ValidationRule;
|
|
4
|
+
/** `MaxIntrospectionDepthRule`, measuring each fragment once. */
|
|
5
|
+
export declare const introspectionDepthRule: ValidationRule;
|
|
6
|
+
//# sourceMappingURL=depth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"depth.d.ts","sourceRoot":"","sources":["../src/depth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAMX,cAAc,EACd,MAAM,SAAS,CAAA;AAyChB,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAkB1D;AAQD,iEAAiE;AACjE,eAAO,MAAM,sBAAsB,EAAE,cAiBpC,CAAA"}
|
package/dist/depth.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { GraphQLError, Kind } from 'graphql';
|
|
2
|
+
const introspection = new Set(['__schema', '__type']);
|
|
3
|
+
/** The deepest path under a selection set, each fragment measured once. */
|
|
4
|
+
function measurer(context, weigh) {
|
|
5
|
+
// One being measured counts 0, which only a cycle meets, and
|
|
6
|
+
// `NoFragmentCyclesRule` refuses a cycle.
|
|
7
|
+
const fragments = new Map();
|
|
8
|
+
const depth = (set) => {
|
|
9
|
+
let deepest = 0;
|
|
10
|
+
for (const selection of set?.selections ?? []) {
|
|
11
|
+
deepest = Math.max(deepest, depthOf(selection));
|
|
12
|
+
}
|
|
13
|
+
return deepest;
|
|
14
|
+
};
|
|
15
|
+
const depthOf = (selection) => {
|
|
16
|
+
if (selection.kind === Kind.FIELD) {
|
|
17
|
+
return weigh(selection, () => depth(selection.selectionSet));
|
|
18
|
+
}
|
|
19
|
+
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
20
|
+
return depth(selection.selectionSet);
|
|
21
|
+
}
|
|
22
|
+
const name = selection.name.value;
|
|
23
|
+
const known = fragments.get(name);
|
|
24
|
+
if (known !== undefined)
|
|
25
|
+
return known;
|
|
26
|
+
fragments.set(name, 0);
|
|
27
|
+
const measured = depth(context.getFragment(name)?.selectionSet);
|
|
28
|
+
fragments.set(name, measured);
|
|
29
|
+
return measured;
|
|
30
|
+
};
|
|
31
|
+
return depth;
|
|
32
|
+
}
|
|
33
|
+
/** The validation rule that refuses a document deeper than `maxDepth`. */
|
|
34
|
+
export function depthRule(maxDepth) {
|
|
35
|
+
return (context) => {
|
|
36
|
+
const depth = measurer(context, (field, below) => introspection.has(field.name.value) ? 0 : 1 + below());
|
|
37
|
+
return {
|
|
38
|
+
OperationDefinition: (node) => {
|
|
39
|
+
const measured = depth(node.selectionSet);
|
|
40
|
+
if (measured <= maxDepth)
|
|
41
|
+
return;
|
|
42
|
+
context.reportError(new GraphQLError(`The document is nested ${measured} levels deep; the most is ${maxDepth}.`, { nodes: node }));
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// The lists graphql-js's `MaxIntrospectionDepthRule` counts, and how many
|
|
48
|
+
// nested ones it refuses. Its own walk visits a fragment once per path, so a
|
|
49
|
+
// fragment spreading the previous one twice costs 2^n visits.
|
|
50
|
+
const lists = new Set(['fields', 'interfaces', 'possibleTypes', 'inputFields']);
|
|
51
|
+
const maxLists = 3;
|
|
52
|
+
/** `MaxIntrospectionDepthRule`, measuring each fragment once. */
|
|
53
|
+
export const introspectionDepthRule = (context) => {
|
|
54
|
+
const depth = measurer(context, (field, below) => (lists.has(field.name.value) ? 1 : 0) + below());
|
|
55
|
+
return {
|
|
56
|
+
Field: (node) => {
|
|
57
|
+
if (!introspection.has(node.name.value))
|
|
58
|
+
return undefined;
|
|
59
|
+
if (depth(node.selectionSet) < maxLists)
|
|
60
|
+
return undefined;
|
|
61
|
+
context.reportError(new GraphQLError('Maximum introspection depth exceeded', {
|
|
62
|
+
nodes: [node],
|
|
63
|
+
}));
|
|
64
|
+
return false;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GraphQLError } from 'graphql';
|
|
2
|
+
import type { Logger } from 'ultimabase';
|
|
3
|
+
type Format = (error: Readonly<GraphQLError | Error>) => GraphQLError | Error;
|
|
4
|
+
/** `graphql-http`'s `formatError`: what a client sees of each error. */
|
|
5
|
+
export declare function formatter(logger: Logger): Format;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxC,KAAK,MAAM,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,GAAG,KAAK,CAAC,KAAK,YAAY,GAAG,KAAK,CAAA;AAc7E,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwBhD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Errors as D-106 sends them, over GraphQL (D-107): a `PublicError` with its
|
|
2
|
+
// code and details, any other error masked and logged with its id, and
|
|
3
|
+
// graphql-js's own without the values they refused.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { GraphQLError } from 'graphql';
|
|
6
|
+
import { PublicError } from 'ultimabase';
|
|
7
|
+
/** graphql-js's message without the value it refused, a password perhaps. */
|
|
8
|
+
function unvalued(error) {
|
|
9
|
+
const { message, nodes, path, extensions } = error;
|
|
10
|
+
const reason = message.replace(/[,;]? [Ff]ound: [\s\S]*$|(?<= cannot represent [^:]*): [\s\S]*$/, '');
|
|
11
|
+
if (reason === message)
|
|
12
|
+
return error;
|
|
13
|
+
const ended = /[.?]$/.test(reason) ? reason : `${reason}.`;
|
|
14
|
+
return new GraphQLError(ended, { nodes, path, extensions });
|
|
15
|
+
}
|
|
16
|
+
/** `graphql-http`'s `formatError`: what a client sees of each error. */
|
|
17
|
+
export function formatter(logger) {
|
|
18
|
+
return (error) => {
|
|
19
|
+
// A request `graphql-http` could not read, or a document GraphQL
|
|
20
|
+
// refused: their messages are written for the client.
|
|
21
|
+
if (!(error instanceof GraphQLError))
|
|
22
|
+
return error;
|
|
23
|
+
const original = error.originalError;
|
|
24
|
+
if (original === undefined || original instanceof GraphQLError) {
|
|
25
|
+
return unvalued(error);
|
|
26
|
+
}
|
|
27
|
+
const at = { nodes: error.nodes, path: error.path };
|
|
28
|
+
if (original instanceof PublicError) {
|
|
29
|
+
const { code, details } = original;
|
|
30
|
+
return new GraphQLError(original.message, {
|
|
31
|
+
...at,
|
|
32
|
+
extensions: details === undefined ? { code } : { code, details },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const correlationId = randomUUID();
|
|
36
|
+
logger.error({ correlationId, err: original }, 'A GraphQL field failed.');
|
|
37
|
+
return new GraphQLError('Internal error.', {
|
|
38
|
+
...at,
|
|
39
|
+
extensions: { code: 'INTERNAL', correlationId },
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GraphQLFieldConfig, GraphQLNamedType } from 'graphql';
|
|
2
|
+
import type { ExposedFunction } from 'ultimabase';
|
|
3
|
+
import type { Args, Context, Registry } from './registry.ts';
|
|
4
|
+
import type { Read } from './rows.ts';
|
|
5
|
+
/** A function's key of `.with` (D-109): its field's name, or hidden. */
|
|
6
|
+
export type GraphqlFunctionWith = {
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
} | false;
|
|
9
|
+
export type Field = GraphQLFieldConfig<unknown, Context, Args>;
|
|
10
|
+
/** A function's root field, and the function that gives it. */
|
|
11
|
+
export type RootField = {
|
|
12
|
+
readonly query: boolean;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly field: Field;
|
|
16
|
+
};
|
|
17
|
+
/** The functions' root fields, and what each reads (D-109). */
|
|
18
|
+
export declare function functionFields(registry: Registry, functions: readonly ExposedFunction<GraphqlFunctionWith>[], take: number, live: (owner: string) => boolean): {
|
|
19
|
+
readonly fields: readonly RootField[];
|
|
20
|
+
/** The live functions' subscriptions, named as their fields (D-154). */
|
|
21
|
+
readonly subscriptions: {
|
|
22
|
+
readonly [name: string]: Field;
|
|
23
|
+
};
|
|
24
|
+
readonly reads: ReadonlyMap<string, ReadonlyMap<string, Read>>;
|
|
25
|
+
readonly owners: ReadonlyMap<string, string>;
|
|
26
|
+
readonly named: (name: string) => GraphQLNamedType | undefined;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,gBAAgB,EAGhB,MAAM,SAAS,CAAA;AAQhB,OAAO,KAAK,EACX,eAAe,EAIf,MAAM,YAAY,CAAA;AASnB,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAI5D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,wEAAwE;AACxE,MAAM,MAAM,mBAAmB,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAA;AAEpE,MAAM,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;AAE9D,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CACrB,CAAA;AAwMD,+DAA+D;AAC/D,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,SAAS,eAAe,CAAC,mBAAmB,CAAC,EAAE,EAC1D,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAC9B;IACF,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;IACrC,wEAAwE;IACxE,QAAQ,CAAC,aAAa,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;IAC1D,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAC9D,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAA;CAC9D,CA8BA"}
|