envio 3.7.0 → 3.9.0-resolvers.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/README.md +2 -2
- package/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +674 -161
- package/index.js +10 -0
- package/package.json +6 -7
- package/src/AddressRows.res +121 -0
- package/src/AddressRows.res.mjs +143 -0
- package/src/Batch.res +11 -0
- package/src/Batch.res.mjs +2 -1
- package/src/Bin.res +47 -0
- package/src/Bin.res.mjs +34 -1
- package/src/ChainState.res +108 -98
- package/src/ChainState.res.mjs +86 -67
- package/src/ChainState.resi +28 -15
- package/src/Config.res +61 -131
- package/src/Config.res.mjs +47 -79
- package/src/ContractMapping.res +66 -0
- package/src/ContractMapping.res.mjs +73 -0
- package/src/Core.res +24 -0
- package/src/Core.res.mjs +4 -0
- package/src/Ecosystem.res +7 -2
- package/src/Env.res +128 -1
- package/src/Env.res.mjs +123 -1
- package/src/Envio.res +54 -76
- package/src/EventConfigBuilder.res +316 -46
- package/src/EventConfigBuilder.res.mjs +231 -28
- package/src/FetchState.res +59 -67
- package/src/FetchState.res.mjs +34 -44
- package/src/HandlerRegister.res +26 -15
- package/src/HandlerRegister.res.mjs +27 -7
- package/src/Hasura.res +30 -22
- package/src/Hasura.res.mjs +11 -5
- package/src/InMemoryStore.res +23 -38
- package/src/InMemoryStore.res.mjs +13 -27
- package/src/IndexerState.res +40 -3
- package/src/IndexerState.res.mjs +30 -4
- package/src/IndexerState.resi +4 -1
- package/src/Internal.res +49 -30
- package/src/Internal.res.mjs +12 -16
- package/src/Main.res +27 -20
- package/src/Main.res.mjs +19 -14
- package/src/Metrics.res +15 -1
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +46 -21
- package/src/Persistence.res.mjs +8 -5
- package/src/PgStorage.res +238 -79
- package/src/PgStorage.res.mjs +115 -73
- package/src/PruneStaleHistory.res +1 -1
- package/src/PruneStaleHistory.res.mjs +1 -1
- package/src/Rollback.res +62 -41
- package/src/Rollback.res.mjs +61 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SimulateItems.res +254 -9
- package/src/SimulateItems.res.mjs +214 -47
- package/src/Sink.res +35 -26
- package/src/Sink.res.mjs +20 -16
- package/src/TestIndexer.res +121 -123
- package/src/TestIndexer.res.mjs +97 -102
- package/src/Utils.res +38 -0
- package/src/Utils.res.mjs +39 -2
- package/src/Writing.res +4 -2
- package/src/Writing.res.mjs +3 -2
- package/src/bindings/ClickHouse.res +311 -656
- package/src/bindings/ClickHouse.res.mjs +294 -395
- package/src/bindings/ClickHouseSink.res +303 -0
- package/src/bindings/ClickHouseSink.res.mjs +216 -0
- package/src/bindings/NodeJs.res +9 -0
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/bindings/Postgres.res +9 -0
- package/src/bindings/Postgres.res.mjs +3 -0
- package/src/db/EntityHistory.res +29 -24
- package/src/db/EntityHistory.res.mjs +11 -19
- package/src/db/InternalTable.res +258 -83
- package/src/db/InternalTable.res.mjs +223 -85
- package/src/db/RollbackScope.res +29 -0
- package/src/db/RollbackScope.res.mjs +31 -0
- package/src/db/Table.res +28 -1
- package/src/db/Table.res.mjs +26 -2
- package/src/resolvers/ResolverProcess.res +809 -0
- package/src/resolvers/ResolverProcess.res.mjs +667 -0
- package/src/resolvers/ResolverQuery.res +317 -0
- package/src/resolvers/ResolverQuery.res.mjs +220 -0
- package/src/resolvers/collisions.js +108 -0
- package/src/resolvers/db.js +363 -0
- package/src/resolvers/dispatch.js +165 -0
- package/src/resolvers/errors.js +30 -0
- package/src/resolvers/graphqlSelection.js +292 -0
- package/src/resolvers/hasuraAction.js +84 -0
- package/src/resolvers/hasuraApply.js +323 -0
- package/src/resolvers/hasuraMetadata.js +119 -0
- package/src/resolvers/index.js +95 -0
- package/src/resolvers/manifest.js +370 -0
- package/src/resolvers/server.js +270 -0
- package/src/sources/AddressSet.res +0 -22
- package/src/sources/AddressStore.res +48 -88
- package/src/sources/AddressStore.res.mjs +11 -22
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -2
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/SimulateSource.res +8 -4
- package/src/sources/SimulateSource.res.mjs +7 -3
- package/src/sources/Svm.res +36 -7
- package/src/sources/Svm.res.mjs +34 -4
- package/src/sources/SvmHyperSyncClient.res +20 -116
- package/src/sources/SvmHyperSyncClient.res.mjs +5 -7
- package/src/sources/SvmHyperSyncSource.res +89 -37
- package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
- package/src/sources/TransactionStore.res +38 -0
- package/src/sources/TransactionStore.res.mjs +5 -0
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +1 -1
- package/svm.schema.json +44 -156
- package/src/MemoryStorage.res +0 -724
- package/src/MemoryStorage.res.mjs +0 -601
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The manifest, expressed as the Hasura metadata that makes each resolver a
|
|
2
|
+
// queryable root field.
|
|
3
|
+
//
|
|
4
|
+
// Hasura cannot execute user code, so an action is a declaration: the field's
|
|
5
|
+
// name, its arguments, its result type, and a URL to POST to. Everything here
|
|
6
|
+
// is derived from the manifest `envio codegen` already writes, so the schema
|
|
7
|
+
// Hasura publishes and the code that answers it come from one source.
|
|
8
|
+
//
|
|
9
|
+
// Pure on purpose -- manifest in, JSON out, no network. Applying it is
|
|
10
|
+
// `hasuraApply.js`; keeping the two apart is what makes the shape testable
|
|
11
|
+
// without a Hasura to point at.
|
|
12
|
+
|
|
13
|
+
// Hasura's own; anything else a resolver names has to be declared.
|
|
14
|
+
const BUILTIN_SCALARS = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
15
|
+
|
|
16
|
+
/** The header Hasura presents so the handler can tell it apart from anyone
|
|
17
|
+
* else who can reach the socket. */
|
|
18
|
+
export const RESOLVER_SECRET_HEADER = "x-envio-resolver-secret";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param manifest the parsed `.envio/resolvers.json`
|
|
22
|
+
* @param handlerUrl the URL *Hasura* posts to -- reachable from Hasura, which
|
|
23
|
+
* is not necessarily the address the service binds
|
|
24
|
+
* @param publicRole the role a non-admin caller runs as (Hasura's
|
|
25
|
+
* `unauthorized_role`)
|
|
26
|
+
*/
|
|
27
|
+
export function buildHasuraMetadata(manifest, { handlerUrl, publicRole = "public", actionSecret }) {
|
|
28
|
+
if (!handlerUrl) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"buildHasuraMetadata requires a handlerUrl: it is baked into every action, and Hasura has no other way to reach the resolvers."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scalars = [];
|
|
35
|
+
const enums = [];
|
|
36
|
+
const objects = [];
|
|
37
|
+
const inputObjects = [];
|
|
38
|
+
|
|
39
|
+
for (const type of manifest.types ?? []) {
|
|
40
|
+
switch (type.kind) {
|
|
41
|
+
case "scalar":
|
|
42
|
+
// A resolver declaring `String` as a custom scalar would shadow
|
|
43
|
+
// Hasura's own and make the metadata inconsistent.
|
|
44
|
+
if (BUILTIN_SCALARS.has(type.name)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Custom scalar '${type.name}' collides with a GraphQL built-in scalar.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
scalars.push({ name: type.name });
|
|
50
|
+
break;
|
|
51
|
+
case "enum":
|
|
52
|
+
enums.push({
|
|
53
|
+
name: type.name,
|
|
54
|
+
// The manifest names an enum value `name`; Hasura calls it `value`.
|
|
55
|
+
values: type.values.map(({ name }) => ({ value: name })),
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
case "object":
|
|
59
|
+
objects.push({ name: type.name, fields: type.fields });
|
|
60
|
+
break;
|
|
61
|
+
case "input_object":
|
|
62
|
+
inputObjects.push({ name: type.name, fields: type.fields });
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
// Emitting a partial set would leave Hasura with an action whose types
|
|
66
|
+
// are missing, which fails at apply time with a worse message than this.
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Cannot express manifest type '${type.name}' of kind '${type.kind}' as a Hasura custom type.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const actions = [];
|
|
74
|
+
const permissions = [];
|
|
75
|
+
|
|
76
|
+
for (const resolver of manifest.resolvers ?? []) {
|
|
77
|
+
const definition = {
|
|
78
|
+
// An action is a mutation unless it says otherwise, and a resolver is a
|
|
79
|
+
// read: the manifest's own SDL says `extend type Query`.
|
|
80
|
+
type: "query",
|
|
81
|
+
kind: "synchronous",
|
|
82
|
+
handler: handlerUrl,
|
|
83
|
+
arguments: resolver.args ?? [],
|
|
84
|
+
output_type: resolver.type,
|
|
85
|
+
// Hasura's timeout is whole seconds and bounds the whole HTTP call; the
|
|
86
|
+
// resolver's own `timeoutMs` bounds only the queries inside it. Acquiring
|
|
87
|
+
// a connection, parsing arguments and serializing the result spend
|
|
88
|
+
// Hasura's budget without spending the resolver's, so the extra second is
|
|
89
|
+
// what keeps Hasura from being the first to give up on a request the
|
|
90
|
+
// resolver still considers live -- which the client would see as an
|
|
91
|
+
// unreachable webhook rather than as a timeout.
|
|
92
|
+
timeout: Math.ceil(resolver.timeoutMs / 1000) + 1,
|
|
93
|
+
// The role reaches the handler inside the request body, so on its own it
|
|
94
|
+
// is the caller's claim rather than a fact. This header is the only thing
|
|
95
|
+
// that makes it one: Hasura sends it because the action declares it, and
|
|
96
|
+
// the value is literal, so Hasura itself needs no configuration.
|
|
97
|
+
...(actionSecret
|
|
98
|
+
? { headers: [{ name: RESOLVER_SECRET_HEADER, value: actionSecret }] }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
const action = { name: resolver.name, definition };
|
|
102
|
+
if (resolver.description) {
|
|
103
|
+
action.comment = resolver.description;
|
|
104
|
+
}
|
|
105
|
+
actions.push(action);
|
|
106
|
+
|
|
107
|
+
// Admin has access to everything in Hasura by definition, so an admin-only
|
|
108
|
+
// resolver is one with no public permission -- there is nothing to grant.
|
|
109
|
+
if (!resolver.admin) {
|
|
110
|
+
permissions.push({ action: resolver.name, role: publicRole });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
customTypes: { scalars, enums, input_objects: inputObjects, objects },
|
|
116
|
+
actions,
|
|
117
|
+
permissions,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The user-facing resolver API.
|
|
2
|
+
//
|
|
3
|
+
// Declaring a resolver registers it, the same way importing a handler module
|
|
4
|
+
// registers its handlers. `envio resolvers manifest` imports the project's
|
|
5
|
+
// resolver module and reads the registry back out; the runtime reads the same
|
|
6
|
+
// registry to dispatch. One registration, two consumers, no separate export
|
|
7
|
+
// list for the user to keep in sync.
|
|
8
|
+
|
|
9
|
+
import { buildManifest, toSDL } from "./manifest.js";
|
|
10
|
+
|
|
11
|
+
export { ResolverError } from "./errors.js";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
buildManifest,
|
|
15
|
+
defineEnum,
|
|
16
|
+
defineInput,
|
|
17
|
+
defineScalar,
|
|
18
|
+
defineType,
|
|
19
|
+
toSDL,
|
|
20
|
+
SCHEMA_VERSION,
|
|
21
|
+
} from "./manifest.js";
|
|
22
|
+
|
|
23
|
+
const registry = [];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Declares a custom GraphQL resolver.
|
|
27
|
+
*
|
|
28
|
+
* Validation happens here rather than at manifest time so the error points at
|
|
29
|
+
* the declaration that caused it, while the stack still says which file it
|
|
30
|
+
* came from.
|
|
31
|
+
*/
|
|
32
|
+
export function createResolver(options) {
|
|
33
|
+
if (!options || typeof options !== "object") {
|
|
34
|
+
throw new Error("createResolver expects an options object");
|
|
35
|
+
}
|
|
36
|
+
const { name, output, handler, timeoutMs } = options;
|
|
37
|
+
|
|
38
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
39
|
+
throw new Error("createResolver requires a non-empty `name`");
|
|
40
|
+
}
|
|
41
|
+
if (!output || typeof output.t === "undefined") {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Resolver '${name}' requires an \`output\` schema, e.g. output: S.array(MyType)`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (typeof handler !== "function") {
|
|
47
|
+
throw new Error(`Resolver '${name}' requires a \`handler\` function`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof timeoutMs !== "number" || timeoutMs <= 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Resolver '${name}' requires a positive \`timeoutMs\`. It becomes the statement_timeout on every query the resolver makes, which is the only thing bounding a runaway one.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (registry.some((r) => r.name === name)) {
|
|
55
|
+
throw new Error(`Resolver '${name}' is declared more than once`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolver = {
|
|
59
|
+
name,
|
|
60
|
+
// Passed through so `buildManifest` below refuses it with a message that
|
|
61
|
+
// names the resolver, rather than it being silently dropped here.
|
|
62
|
+
cacheTtlMs: options.cacheTtlMs,
|
|
63
|
+
description: options.description,
|
|
64
|
+
args: options.args ?? {},
|
|
65
|
+
output,
|
|
66
|
+
admin: options.admin === true,
|
|
67
|
+
timeoutMs,
|
|
68
|
+
handler,
|
|
69
|
+
};
|
|
70
|
+
// Built before registering, not after: a declaration whose schemas cannot
|
|
71
|
+
// be represented in GraphQL must leave the registry untouched, or the
|
|
72
|
+
// failure follows every later build. Doing it here at all is what surfaces
|
|
73
|
+
// the problem at the declaration rather than at the end of codegen, where
|
|
74
|
+
// the stack no longer says which resolver was at fault.
|
|
75
|
+
buildManifest([resolver]);
|
|
76
|
+
|
|
77
|
+
registry.push(resolver);
|
|
78
|
+
return resolver;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Every resolver declared so far, in declaration order. */
|
|
82
|
+
export function getRegisteredResolvers() {
|
|
83
|
+
return [...registry];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Test-only: forgets every declaration. */
|
|
87
|
+
export function resetResolvers() {
|
|
88
|
+
registry.length = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The manifest and SDL for everything declared so far. */
|
|
92
|
+
export function buildRegisteredManifest() {
|
|
93
|
+
const manifest = buildManifest(registry);
|
|
94
|
+
return { manifest, sdl: toSDL(manifest) };
|
|
95
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// Builds the resolver manifest `envio serve` reads.
|
|
2
|
+
//
|
|
3
|
+
// Resolvers declare their arguments and result with the same `S` schemas
|
|
4
|
+
// effects already use, so there is one schema language in the project rather
|
|
5
|
+
// than a second one invented for GraphQL. This module walks those schemas
|
|
6
|
+
// into GraphQL types.
|
|
7
|
+
//
|
|
8
|
+
// The output shape is a contract with envio-serve's `resolvers/manifest.rs`,
|
|
9
|
+
// which validates it and fails startup on anything it does not understand.
|
|
10
|
+
// SCHEMA_VERSION must be bumped in both places together.
|
|
11
|
+
|
|
12
|
+
import * as S from "rescript-schema";
|
|
13
|
+
import { $$BigInt as UtilsBigInt } from "../Utils.res.mjs";
|
|
14
|
+
import { schema as bigDecimalSchema } from "../bindings/BigDecimal.res.mjs";
|
|
15
|
+
|
|
16
|
+
export const SCHEMA_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
const GQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
19
|
+
|
|
20
|
+
// Sury tags for the scalars that map onto a GraphQL built-in. Everything
|
|
21
|
+
// else has to be named explicitly, because GraphQL has no anonymous types
|
|
22
|
+
// and a generated name would leak into a public API.
|
|
23
|
+
const BUILTIN_SCALARS = {
|
|
24
|
+
string: "String",
|
|
25
|
+
int32: "Int",
|
|
26
|
+
number: "Float",
|
|
27
|
+
boolean: "Boolean",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const NAMED = Symbol.for("envio.resolvers.graphqlType");
|
|
31
|
+
|
|
32
|
+
// `S.bigint` and `S.bigDecimal` are transformed strings, so the tag walk below
|
|
33
|
+
// would call them String -- turning a 30-digit PnL figure into an untyped one
|
|
34
|
+
// in the published schema. They are the vocabulary handlers already write in,
|
|
35
|
+
// so they get their scalars without the user naming them again.
|
|
36
|
+
const ENVIO_SCALARS = new Map([
|
|
37
|
+
[UtilsBigInt.schema, "BigInt"],
|
|
38
|
+
[bigDecimalSchema, "BigDecimal"],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Tagging is never applied to the caller's schema object. `S.string` and
|
|
42
|
+
// friends are shared singletons, so `defineScalar("BigInt", S.string)` would
|
|
43
|
+
// otherwise rename every string in the project to BigInt. Cloning keeps the
|
|
44
|
+
// tag local to the declaration.
|
|
45
|
+
function tagged(schema, named) {
|
|
46
|
+
const copy = Object.create(Object.getPrototypeOf(schema));
|
|
47
|
+
Object.assign(copy, schema);
|
|
48
|
+
copy[NAMED] = named;
|
|
49
|
+
return copy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// S.name throws on some schema shapes, and this only ever runs while
|
|
53
|
+
// building an error message -- a failure here would replace a useful message
|
|
54
|
+
// with a confusing one.
|
|
55
|
+
function describe(schema) {
|
|
56
|
+
try {
|
|
57
|
+
return S.name(schema);
|
|
58
|
+
} catch {
|
|
59
|
+
return JSON.stringify(schema?.t) ?? "unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Names a schema so it can appear in the GraphQL schema. */
|
|
64
|
+
export function defineType(name, fields) {
|
|
65
|
+
if (!GQL_NAME.test(name)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Invalid GraphQL type name '${name}': must match ${GQL_NAME.source}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
// S.schema() mutates the object it is given, replacing each schema value
|
|
71
|
+
// in place, so the field map has to be snapshotted before handing it over
|
|
72
|
+
// or the GraphQL walk below finds gutted entries.
|
|
73
|
+
const declared = { ...fields };
|
|
74
|
+
return tagged(S.schema(fields), { name, fields: declared });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Names a schema that appears in an argument.
|
|
79
|
+
*
|
|
80
|
+
* GraphQL keeps input and output types in separate namespaces: a `where`
|
|
81
|
+
* argument is an `input`, and an object type declared with defineType cannot
|
|
82
|
+
* stand in for one.
|
|
83
|
+
*/
|
|
84
|
+
export function defineInput(name, fields) {
|
|
85
|
+
if (!GQL_NAME.test(name)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid GraphQL input type name '${name}': must match ${GQL_NAME.source}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const declared = { ...fields };
|
|
91
|
+
return tagged(S.schema(fields), { name, fields: declared, input: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Names a schema representing a GraphQL enum. */
|
|
95
|
+
export function defineEnum(name, values) {
|
|
96
|
+
if (!GQL_NAME.test(name)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Invalid GraphQL enum name '${name}': must match ${GQL_NAME.source}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
102
|
+
throw new Error(`Enum '${name}' must declare at least one value`);
|
|
103
|
+
}
|
|
104
|
+
for (const v of values) {
|
|
105
|
+
if (!GQL_NAME.test(v)) {
|
|
106
|
+
throw new Error(`Invalid enum value '${v}' on '${name}'`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return tagged(S.union(values.map((v) => S.schema(v))), {
|
|
110
|
+
name,
|
|
111
|
+
enumValues: values,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Names a schema representing a custom GraphQL scalar. */
|
|
116
|
+
export function defineScalar(name, schema) {
|
|
117
|
+
if (!GQL_NAME.test(name)) {
|
|
118
|
+
throw new Error(`Invalid GraphQL scalar name '${name}'`);
|
|
119
|
+
}
|
|
120
|
+
return tagged(schema, { name, scalar: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A root-level optional, unwrapped.
|
|
125
|
+
*
|
|
126
|
+
* `undefined` has no JSON form, so Sury refuses to convert an `S.optional(x)`
|
|
127
|
+
* at the root at all — not just when the value is absent. A resolver
|
|
128
|
+
* declaring a nullable result is the ordinary case (nothing found, or a
|
|
129
|
+
* failure that shouldn't take the rest of the operation down), so the absent
|
|
130
|
+
* value becomes `null` on the wire and a present one converts through the
|
|
131
|
+
* inner schema. Optionals *inside* an object are untouched: Sury omits those
|
|
132
|
+
* fields, which is what GraphQL wants.
|
|
133
|
+
*/
|
|
134
|
+
export function unwrapNullableOutput(schema) {
|
|
135
|
+
const tag = tagOf(schema);
|
|
136
|
+
if (tag.kind === "option" || tag.kind === "null") {
|
|
137
|
+
return { inner: tag.node._0, nullable: true };
|
|
138
|
+
}
|
|
139
|
+
return { inner: schema, nullable: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function tagOf(schema) {
|
|
143
|
+
const t = schema.t;
|
|
144
|
+
if (typeof t === "string") return { kind: t };
|
|
145
|
+
if (t && typeof t === "object" && t.TAG) return { kind: t.TAG, node: t };
|
|
146
|
+
return { kind: "unknown" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Walks a schema into a GraphQL type string, registering any named composite
|
|
151
|
+
* types it encounters into `types`.
|
|
152
|
+
*
|
|
153
|
+
* Nullability is inverted relative to Sury: a GraphQL field is non-null
|
|
154
|
+
* unless the schema is optional, which matches how both systems are read in
|
|
155
|
+
* practice — `S.string` means "a string", `S.optional(S.string)` means "or
|
|
156
|
+
* absent".
|
|
157
|
+
*/
|
|
158
|
+
export function toGraphQLType(schema, types, path, position = "output") {
|
|
159
|
+
const tag = tagOf(schema);
|
|
160
|
+
|
|
161
|
+
if (tag.kind === "option" || tag.kind === "null") {
|
|
162
|
+
// Already nullable: unwrap without adding `!`.
|
|
163
|
+
return nullableOf(tag.node._0, types, path);
|
|
164
|
+
}
|
|
165
|
+
return `${nullableOf(schema, types, path)}!`;
|
|
166
|
+
|
|
167
|
+
function nullableOf(inner, types, path) {
|
|
168
|
+
const innerNamed = inner[NAMED];
|
|
169
|
+
const innerTag = tagOf(inner);
|
|
170
|
+
|
|
171
|
+
if (innerTag.kind === "option" || innerTag.kind === "null") {
|
|
172
|
+
// `S.optional(S.optional(x))` — collapse; GraphQL has one null.
|
|
173
|
+
return nullableOf(innerTag.node._0, types, path);
|
|
174
|
+
}
|
|
175
|
+
if (innerNamed) {
|
|
176
|
+
register(inner, innerNamed, types, path, position);
|
|
177
|
+
return innerNamed.name;
|
|
178
|
+
}
|
|
179
|
+
// After the explicit name, so `defineScalar` on a clone still wins.
|
|
180
|
+
const envioScalar = ENVIO_SCALARS.get(inner);
|
|
181
|
+
if (envioScalar !== undefined) {
|
|
182
|
+
register(inner, { name: envioScalar, scalar: true }, types, path, position);
|
|
183
|
+
return envioScalar;
|
|
184
|
+
}
|
|
185
|
+
if (innerTag.kind === "array") {
|
|
186
|
+
return `[${toGraphQLType(innerTag.node._0, types, `${path}[]`, position)}]`;
|
|
187
|
+
}
|
|
188
|
+
const builtin = BUILTIN_SCALARS[innerTag.kind];
|
|
189
|
+
if (builtin) return builtin;
|
|
190
|
+
|
|
191
|
+
if (innerTag.kind === "object") {
|
|
192
|
+
const define = position === "input" ? "defineInput" : "defineType";
|
|
193
|
+
throw new Error(
|
|
194
|
+
`${path}: object types must be named. Wrap it in ${define}("SomeName", {...}) — ` +
|
|
195
|
+
`GraphQL has no anonymous types, and a generated name would leak into your public API.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${path}: unsupported schema '${describe(inner)}'. Supported: string, int32, number, ` +
|
|
200
|
+
`boolean, arrays, optionals, and schemas named with defineType / defineInput / ` +
|
|
201
|
+
`defineEnum / defineScalar.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function register(schema, named, types, path, position) {
|
|
207
|
+
// Checked before the registration below returns early, so a type reused in
|
|
208
|
+
// the wrong position is caught even when it is already registered from the
|
|
209
|
+
// right one. Scalars and enums are legal in both.
|
|
210
|
+
if (named.fields) {
|
|
211
|
+
if (named.input && position === "output") {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`${path}: '${named.name}' is an input object and cannot appear in a result. ` +
|
|
214
|
+
`Declare the result's type with defineType("${named.name}Result", {...}).`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!named.input && position === "input") {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`${path}: '${named.name}' is an object type and cannot be an argument. ` +
|
|
220
|
+
`Declare it with defineInput("${named.name}Input", {...}) — GraphQL keeps input ` +
|
|
221
|
+
`and output types in separate namespaces.`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const existing = types.get(named.name);
|
|
227
|
+
if (existing) {
|
|
228
|
+
// The same declaration used twice is fine; two different ones sharing a
|
|
229
|
+
// name is a collision the user has to resolve, and saying so here beats
|
|
230
|
+
// serve rejecting the manifest later with less context.
|
|
231
|
+
if (existing.schema !== schema) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`${path}: two different types are both named '${named.name}'`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Reserve the name before walking fields so a self-referential type
|
|
239
|
+
// terminates instead of recursing forever.
|
|
240
|
+
const entry = { schema, def: null };
|
|
241
|
+
types.set(named.name, entry);
|
|
242
|
+
|
|
243
|
+
if (named.scalar) {
|
|
244
|
+
entry.def = { kind: "scalar", name: named.name };
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (named.enumValues) {
|
|
248
|
+
entry.def = {
|
|
249
|
+
kind: "enum",
|
|
250
|
+
name: named.name,
|
|
251
|
+
values: named.enumValues.map((v) => ({ name: v })),
|
|
252
|
+
};
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
entry.def = {
|
|
256
|
+
kind: named.input ? "input_object" : "object",
|
|
257
|
+
name: named.name,
|
|
258
|
+
fields: Object.entries(named.fields).map(([fieldName, fieldSchema]) => {
|
|
259
|
+
if (!GQL_NAME.test(fieldName)) {
|
|
260
|
+
throw new Error(`Invalid field name '${named.name}.${fieldName}'`);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
name: fieldName,
|
|
264
|
+
type: toGraphQLType(
|
|
265
|
+
fieldSchema,
|
|
266
|
+
types,
|
|
267
|
+
`${named.name}.${fieldName}`,
|
|
268
|
+
named.input ? "input" : "output"
|
|
269
|
+
),
|
|
270
|
+
};
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Builds the manifest for a set of resolver declarations. */
|
|
276
|
+
export function buildManifest(resolvers) {
|
|
277
|
+
const types = new Map();
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
const entries = [];
|
|
280
|
+
|
|
281
|
+
for (const resolver of resolvers) {
|
|
282
|
+
const { name } = resolver;
|
|
283
|
+
if (!GQL_NAME.test(name)) {
|
|
284
|
+
throw new Error(`Invalid resolver name '${name}'`);
|
|
285
|
+
}
|
|
286
|
+
if (name.startsWith("__")) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Resolver '${name}' uses the reserved '__' prefix, which GraphQL introspection owns`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (seen.has(name)) {
|
|
292
|
+
throw new Error(`Duplicate resolver '${name}'`);
|
|
293
|
+
}
|
|
294
|
+
seen.add(name);
|
|
295
|
+
// Refused rather than ignored. It was carried through the manifest and read
|
|
296
|
+
// by nothing, so a resolver author could set it, see no error, and believe
|
|
297
|
+
// an expensive query was being cached.
|
|
298
|
+
if (resolver.cacheTtlMs !== undefined) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Resolver '${name}' sets cacheTtlMs, which is not implemented -- nothing caches resolver ` +
|
|
301
|
+
`responses today. Remove it; it would otherwise read as caching that is not happening.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (!resolver.timeoutMs || resolver.timeoutMs <= 0) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Resolver '${name}' must set a positive timeoutMs. It becomes the statement_timeout on every query the resolver makes, which is the only thing bounding a runaway one.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
entries.push({
|
|
311
|
+
name,
|
|
312
|
+
...(resolver.description ? { description: resolver.description } : {}),
|
|
313
|
+
args: Object.entries(resolver.args ?? {}).map(([argName, argSchema]) => {
|
|
314
|
+
if (!GQL_NAME.test(argName)) {
|
|
315
|
+
throw new Error(`Invalid argument name '${name}.${argName}'`);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
name: argName,
|
|
319
|
+
type: toGraphQLType(argSchema, types, `${name}.${argName}`, "input"),
|
|
320
|
+
};
|
|
321
|
+
}),
|
|
322
|
+
type: toGraphQLType(resolver.output, types, `${name} result`),
|
|
323
|
+
admin: resolver.admin === true,
|
|
324
|
+
timeoutMs: resolver.timeoutMs,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
schemaVersion: SCHEMA_VERSION,
|
|
330
|
+
resolvers: entries,
|
|
331
|
+
// Sorted so the manifest is stable across runs: it is written into the
|
|
332
|
+
// image and diffed, and Map order would otherwise depend on declaration
|
|
333
|
+
// order across files.
|
|
334
|
+
types: [...types.values()]
|
|
335
|
+
.map((e) => e.def)
|
|
336
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Renders the manifest as SDL, for humans and for `envio dev` to diff. */
|
|
341
|
+
export function toSDL(manifest) {
|
|
342
|
+
const lines = [];
|
|
343
|
+
for (const type of manifest.types) {
|
|
344
|
+
if (type.kind === "scalar") {
|
|
345
|
+
lines.push(`scalar ${type.name}`, "");
|
|
346
|
+
} else if (type.kind === "enum") {
|
|
347
|
+
lines.push(`enum ${type.name} {`);
|
|
348
|
+
for (const v of type.values) lines.push(` ${v.name}`);
|
|
349
|
+
lines.push("}", "");
|
|
350
|
+
} else {
|
|
351
|
+
lines.push(`${type.kind === "input_object" ? "input" : "type"} ${type.name} {`);
|
|
352
|
+
for (const f of type.fields) lines.push(` ${f.name}: ${f.type}`);
|
|
353
|
+
lines.push("}", "");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// A project with no resolvers gets no Query extension at all: an empty
|
|
357
|
+
// `extend type Query { }` block is not valid SDL.
|
|
358
|
+
if (manifest.resolvers.length === 0) {
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
lines.push("extend type Query {");
|
|
362
|
+
for (const r of manifest.resolvers) {
|
|
363
|
+
const args = r.args.length
|
|
364
|
+
? `(${r.args.map((a) => `${a.name}: ${a.type}`).join(", ")})`
|
|
365
|
+
: "";
|
|
366
|
+
lines.push(` ${r.name}${args}: ${r.type}`);
|
|
367
|
+
}
|
|
368
|
+
lines.push("}", "");
|
|
369
|
+
return lines.join("\n");
|
|
370
|
+
}
|