@znight/graphql 0.2.0 → 0.2.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/README.md +288 -5
- package/dist/compiler.d.mts +24 -3
- package/dist/compiler.mjs +1876 -109
- package/dist/compiler.mjs.map +1 -1
- package/dist/index.d.mts +49 -8
- package/dist/index.mjs +10 -4
- package/dist/index.mjs.map +1 -1
- package/dist/names-BlzDM5cF.mjs.map +1 -1
- package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
- package/package.json +8 -8
- package/src/compiler/analyze.ts +129 -10
- package/src/compiler/builder.ts +6 -85
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +6 -0
- package/src/compiler/data.ts +10 -0
- package/src/compiler/generate.ts +41 -27
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/index.ts +8 -3
- package/src/compiler/locate.ts +39 -1
- package/src/compiler/model.ts +16 -1
- package/src/compiler/options.ts +39 -8
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +3 -1
- package/src/compiler/suggest.ts +39 -0
- package/src/decorators.ts +9 -3
- package/src/index.ts +5 -0
- package/src/metadata.ts +28 -1
- package/src/options.ts +23 -1
- package/src/pipeline.ts +66 -5
- package/src/plugin.ts +27 -5
- package/src/results.ts +206 -0
- package/src/scalars.ts +10 -3
- package/src/schema.ts +90 -12
- package/src/sdl.ts +17 -2
- package/src/types.ts +35 -0
- package/dist/plugin-BdBxr5NQ.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -23,6 +23,11 @@ batching per execution and subscriptions over Server-Sent Events. The design is
|
|
|
23
23
|
|
|
24
24
|
No `@ObjectType`, no `@Field(() => Int)`, no `reflect-metadata`, no SDL file to keep in sync.
|
|
25
25
|
|
|
26
|
+
Your `.graphql` files are the contract? The opt-in [schema-first mode](#schema-first) reads them
|
|
27
|
+
instead, binds resolvers to their fields by name (NestJS style) and checks at build time that every
|
|
28
|
+
field is resolved and that every resolver signature conforms to the SDL
|
|
29
|
+
([RFC 0007](../../rfcs/0007-graphql-schema-first.md)).
|
|
30
|
+
|
|
26
31
|
**Example:** [`graphql-api`](https://github.com/znight-org/znight/tree/main/examples/graphql-api) (a
|
|
27
32
|
book catalog with loaders, subscriptions over SSE, per-field access decisions and a committed
|
|
28
33
|
`schema.graphql`, on Node.js and Bun).
|
|
@@ -53,11 +58,13 @@ dependencies, `dataloader` is a dependency, `@znight/testing` an optional peer (
|
|
|
53
58
|
- [Context and injection](#context-and-injection)
|
|
54
59
|
- [Authorization](#authorization)
|
|
55
60
|
- [Errors](#errors)
|
|
61
|
+
- [Errors as data](#errors-as-data)
|
|
56
62
|
- [Plugin options](#plugin-options)
|
|
57
63
|
- [GraphQL over HTTP and SSE](#graphql-over-http-and-sse)
|
|
58
64
|
- [Security defaults and limits](#security-defaults-and-limits)
|
|
59
65
|
- [Testing](#testing)
|
|
60
66
|
- [The generated SDL and the compiler options](#the-generated-sdl-and-the-compiler-options)
|
|
67
|
+
- [Schema-first](#schema-first)
|
|
61
68
|
- [Observability](#observability)
|
|
62
69
|
- [Performance](#performance)
|
|
63
70
|
- [Diagnostics](#diagnostics)
|
|
@@ -657,6 +664,104 @@ many fields. Request errors (parse, validation, variables, limits) are never log
|
|
|
657
664
|
|
|
658
665
|
Applications may use their own codes: `new GraphQLException(message, { code: 'OUT_OF_STOCK' })`.
|
|
659
666
|
|
|
667
|
+
### Errors as data
|
|
668
|
+
|
|
669
|
+
Errors a client is expected to handle can be **values** of the field instead of entries of
|
|
670
|
+
`errors[]` (the "result union" pattern, RFC 0008). The field says which ones it can return through
|
|
671
|
+
its TypeScript return type, a union; a result mapper turns errors into those values:
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
import { Injectable } from '@znight/core'
|
|
675
|
+
import {
|
|
676
|
+
Args,
|
|
677
|
+
GraphQLErrorCode,
|
|
678
|
+
GraphQLException,
|
|
679
|
+
GraphQLPlugin,
|
|
680
|
+
type GraphQLResultMapper,
|
|
681
|
+
Query,
|
|
682
|
+
Resolver,
|
|
683
|
+
} from '@znight/graphql'
|
|
684
|
+
import { NotFoundException } from '@znight/http'
|
|
685
|
+
|
|
686
|
+
export interface NotFoundRejection {
|
|
687
|
+
readonly __typename: 'NotFoundRejection'
|
|
688
|
+
readonly message: string
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export interface ValidationRejection {
|
|
692
|
+
readonly __typename: 'ValidationRejection'
|
|
693
|
+
readonly issues: readonly string[]
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export type BookResult = Book | NotFoundRejection | ValidationRejection
|
|
697
|
+
|
|
698
|
+
@Resolver()
|
|
699
|
+
export class BookResolver {
|
|
700
|
+
constructor(private readonly books: BookService) {}
|
|
701
|
+
|
|
702
|
+
@Query()
|
|
703
|
+
book(@Args('id') id: string): Promise<BookResult> {
|
|
704
|
+
return this.books.get(id) // throws NotFoundException
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@Injectable()
|
|
709
|
+
export class Rejections implements GraphQLResultMapper {
|
|
710
|
+
/** The object types this mapper produces: only fields whose union holds one of them use it. */
|
|
711
|
+
static readonly types = ['NotFoundRejection', 'ValidationRejection']
|
|
712
|
+
|
|
713
|
+
toResult(error: unknown): NotFoundRejection | ValidationRejection | undefined {
|
|
714
|
+
if (error instanceof NotFoundException) return { __typename: 'NotFoundRejection', message: error.message }
|
|
715
|
+
if (error instanceof GraphQLException && error.code === GraphQLErrorCode.BadUserInput) {
|
|
716
|
+
const issues = (error.extensions?.['issues'] ?? []) as readonly { readonly message: string }[]
|
|
717
|
+
return { __typename: 'ValidationRejection', issues: issues.map(issue => issue.message) }
|
|
718
|
+
}
|
|
719
|
+
return undefined
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
GraphQLPlugin.configure({ resultMappers: [Rejections] })
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
`{ book(id: "7") { ... on Book { title } ... on NotFoundRejection { message } } }` answers
|
|
727
|
+
`{ "data": { "book": { "message": "No book 7" } } }`, without `errors`.
|
|
728
|
+
|
|
729
|
+
- **Which fields.** A resolver-backed field whose type, without `!`, is a union (not a list) holding
|
|
730
|
+
one of the mapper's `types`; nothing else per field. This is decided at boot: every other field
|
|
731
|
+
keeps its resolve function, with no wrapper and no cost.
|
|
732
|
+
- **Which errors.** Everything the field's pipeline throws or rejects: its guards (a guard returning
|
|
733
|
+
`false` is a `GraphQLException` with code `FORBIDDEN`, a thrown `UnauthorizedException` is passed
|
|
734
|
+
as is), the Standard Schema validation of `@Args` (`BAD_USER_INPUT`, with `extensions.issues`),
|
|
735
|
+
the method (synchronously or as a rejected promise). Guards run before validation, so input
|
|
736
|
+
errors are only shown to clients allowed to call the field. `toResult(error, ctx)` receives the
|
|
737
|
+
raw error and the `ResolverContext` guards receive (the field's coordinate and metadata, the
|
|
738
|
+
arguments, the parent, the operation).
|
|
739
|
+
- **The value.** The first mapper returning a value that the union resolves to one of its members
|
|
740
|
+
(its discriminator, then `__typename`) wins. `undefined` passes to the next mapper; a value of
|
|
741
|
+
another object type is skipped (logged at `debug`), a value of no type is skipped (`warn`), a
|
|
742
|
+
mapper that throws is skipped (`error`). `toResult()` is synchronous: a returned promise is
|
|
743
|
+
skipped (`error`). When no mapper produces a member, the error goes to `errors[]` as above.
|
|
744
|
+
- **Per field.** An error of a child field stays an error of the child, even when its parent's
|
|
745
|
+
union holds the rejection; give the child a union to return a value there. A non-null union
|
|
746
|
+
field resolved to a value is no longer nulled.
|
|
747
|
+
- **Masking and logs.** Values are data chosen by the application: never masked, and an error
|
|
748
|
+
resolved as a value is logged at `debug` only. A mapper turning unexpected errors into values (an
|
|
749
|
+
`InternalErrorRejection`) must log them itself; prefer leaving them in `errors[]`.
|
|
750
|
+
- **Hook and transports.** A value is data: an operation whose only failure became a value has
|
|
751
|
+
the `success` outcome in `graphql:operation`, with no `errors`. HTTP, SSE and `plugin.execute()`
|
|
752
|
+
send it like any result. On a root subscription field, an error when the subscription starts
|
|
753
|
+
(a guard, or the method before it returns its stream) becomes a stream of that single event (one
|
|
754
|
+
`next`, then `complete`). The body of an `async *` method runs in the stream, after the start: its
|
|
755
|
+
errors stay in `errors[]`; check before returning the generator to map them.
|
|
756
|
+
- **At boot.** A result mapper is a provider of any scope (a class that is not a provider is
|
|
757
|
+
ZNIGHT001) or an instance (its `types` property, or its class's `static types`), with a
|
|
758
|
+
`toResult()` method. Its `types` must name
|
|
759
|
+
object types of the schema: a typo is ZNIGHT066.
|
|
760
|
+
|
|
761
|
+
The thrown error can be the value itself (Pothos style) when its class is a member of the union:
|
|
762
|
+
`toResult: error => (error instanceof OutOfStock ? error : undefined)` with
|
|
763
|
+
`static types = ['OutOfStock']`.
|
|
764
|
+
|
|
660
765
|
## Plugin options
|
|
661
766
|
|
|
662
767
|
`GraphQLPlugin.configure(options | (container) => options)`, name `graphql`. A function is evaluated
|
|
@@ -669,6 +774,7 @@ ZNIGHT066.
|
|
|
669
774
|
| `http` | `{}` | `false`: no HTTP transport (schema, `execute()` and `subscribe()` only). Object: `{ meta?, guards?, bodyLimit = '100kb', get = true }` |
|
|
670
775
|
| `guards` | `[]` | Global GraphQL guards (classes or instances implementing `GraphQLGuard`) |
|
|
671
776
|
| `errorMappers` | `[]` | `GraphQLErrorMapper` classes or instances, tried in order |
|
|
777
|
+
| `resultMappers` | `[]` | `GraphQLResultMapper` classes or instances, tried in order: errors as data on the fields whose union holds one of their `types` ([Errors as data](#errors-as-data)) |
|
|
672
778
|
| `exposeErrors` | `false` | Unmasked messages for unexpected errors (development) |
|
|
673
779
|
| `introspection` | `NODE_ENV !== 'production'` | Off: graphql-js `NoSchemaIntrospectionCustomRule`, and suggestions are hidden |
|
|
674
780
|
| `graphiql` | `NODE_ENV === 'development'` | `GET path` with `accept: text/html` serves GraphiQL |
|
|
@@ -845,6 +951,7 @@ export default {
|
|
|
845
951
|
| -------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
846
952
|
| `sdl` | `false` | Path of the SDL artifact, relative to the root, written with the generated module and checked by `generate --check` |
|
|
847
953
|
| `number` | `'Float'` | GraphQL type of `number` (`'Int'` suits applications whose numbers are counts and identifiers) |
|
|
954
|
+
| `schema` | `false` | Glob(s) of the `.graphql` files of the [schema-first mode](#schema-first), relative to the root |
|
|
848
955
|
|
|
849
956
|
An invalid option is a `TypeError` when `znight.config.ts` is loaded. The SDL artifact is
|
|
850
957
|
byte-identical to graphql-js `printSchema()` of the schema the plugin builds (descriptions on one
|
|
@@ -863,6 +970,161 @@ it. With Biome, exclude it in `biome.jsonc` (as this repository does for `exampl
|
|
|
863
970
|
}
|
|
864
971
|
```
|
|
865
972
|
|
|
973
|
+
## Schema-first
|
|
974
|
+
|
|
975
|
+
With `schema`, the SDL files are the schema ([RFC 0007](../../rfcs/0007-graphql-schema-first.md)):
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
import graphql from '@znight/graphql/compiler'
|
|
979
|
+
import type { ZnightOptions } from '@znight/compiler'
|
|
980
|
+
|
|
981
|
+
export default {
|
|
982
|
+
extensions: [graphql.configure({ schema: 'src/**/*.graphql', sdl: 'schema.graphql' })],
|
|
983
|
+
} satisfies ZnightOptions
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
- **Files.** The globs (a string or a list, `!` negations allowed) are relative to the project
|
|
987
|
+
root; `node_modules` and dot-directories are excluded unless a pattern names them; the files are
|
|
988
|
+
read in path order, files without definitions skipped, and the `sdl` artifact is never read back.
|
|
989
|
+
They are inputs of the compile (`watchFiles`): editing, creating or deleting one recompiles in
|
|
990
|
+
`znight dev`, the Vite plugin and `znight generate --watch` (the Bun preload recompiles on edits,
|
|
991
|
+
and picks a new file up at its next start).
|
|
992
|
+
- **One source of truth.** TypeScript types never add to the schema: JSDoc, `@gqlName`,
|
|
993
|
+
`@gqlHidden`, `@default` and `number` are ignored. Descriptions, deprecations and default values
|
|
994
|
+
come from the SDL. The runtime is the code-first one: same metadata, `schemaHash` and `sdl`
|
|
995
|
+
artifact (the served schema, semantically equal to the SDL without its custom directives).
|
|
996
|
+
- **SDL.** Parsed, merged (`extend` of every kind, across files) and validated with graphql-js at
|
|
997
|
+
build time (`graphql/language`, `graphql/type`, `graphql/utilities`, loaded lazily in this mode
|
|
998
|
+
only), with the SDL rules graphql-js keeps internal (directive locations, arguments and
|
|
999
|
+
repetitions, unique names) checked too. Object types, interfaces (implementing interfaces too), unions, enums, input objects,
|
|
1000
|
+
`schema { ... }` root names, `@deprecated` and `@specifiedBy` are supported. Custom directives
|
|
1001
|
+
are a ZNIGHT067 warning and left out; `@oneOf` inputs are not supported yet (ZNIGHT067).
|
|
1002
|
+
- **Scalars.** A scalar of the SDL binds the `scalar<T>('Name', definition)` declaration of its
|
|
1003
|
+
name (its description and `@specifiedBy` come from the SDL, emitted in the metadata with
|
|
1004
|
+
`sdl: true`: they replace the definition's, absent ones included, so the served schema is the
|
|
1005
|
+
artifact). Without one, `DateTime`, `BigInt` and `JSON` are the built-in scalars;
|
|
1006
|
+
any other scalar is ZNIGHT067.
|
|
1007
|
+
- **Binding by name.** `@Query()`, `@Mutation()`, `@Subscription()` bind the root field named by the
|
|
1008
|
+
decorator, else by the method. `@ResolveField()` binds a field of the object type named by the
|
|
1009
|
+
class decorator: `@Resolver('Book')` (`@Resolver('Book', { scope: 'request' })` with a scope);
|
|
1010
|
+
`@Parent()` is optional. A class may hold both. `@Args('name')` receives one argument, `@Args()`
|
|
1011
|
+
every argument as an object, `@Args('name', Schema)` validates with a Standard Schema.
|
|
1012
|
+
- **Enums and abstract types.** An enum value is its SDL name on both sides (graphql-codegen's
|
|
1013
|
+
enums compare by value, so any `namingConvention` works). Unions and interfaces resolve their
|
|
1014
|
+
members with `instanceof` for the exported classes the resolvers return (a class is matched to a
|
|
1015
|
+
member by its name, or its `__typename`), then from `__typename`. graphql-codegen declares
|
|
1016
|
+
`__typename` optional: the check accepts it, but the returned objects must carry it.
|
|
1017
|
+
|
|
1018
|
+
```graphql
|
|
1019
|
+
type Book {
|
|
1020
|
+
id: ID!
|
|
1021
|
+
title: String!
|
|
1022
|
+
author: Author!
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
extend type Query {
|
|
1026
|
+
book(id: ID!): Book
|
|
1027
|
+
}
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
import { Args, Loader, Parent, Query, ResolveField, Resolver } from '@znight/graphql'
|
|
1032
|
+
|
|
1033
|
+
@Resolver('Book')
|
|
1034
|
+
export class BookResolver {
|
|
1035
|
+
constructor(private readonly catalog: CatalogService) {}
|
|
1036
|
+
|
|
1037
|
+
@Query()
|
|
1038
|
+
book(@Args('id') id: QueryBookArgs['id']): Promise<Maybe<Book>> {
|
|
1039
|
+
return this.catalog.find(id)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
@ResolveField()
|
|
1043
|
+
author(@Parent() book: BookModel, @Loader() authors: Loader<AuthorsById>): Promise<Author> {
|
|
1044
|
+
return authors.load(book.authorId)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
**Coverage (ZNIGHT068).** A root field without resolver; a method naming no field (with a "did you
|
|
1050
|
+
mean", or the root type that declares it); `@Resolver('X')` naming no type, an interface, a
|
|
1051
|
+
non-object type, or a root type with field resolvers; `@Args('x')` or an `@Args()` property naming
|
|
1052
|
+
no argument; an object field without resolver that the TypeScript type returned for its parent
|
|
1053
|
+
lacks. Two methods bound to one field are ZNIGHT061; `@ResolveField` without a type name is
|
|
1054
|
+
ZNIGHT062.
|
|
1055
|
+
|
|
1056
|
+
**Type conformance (ZNIGHT068).** Types are read with the compiler's type shapes. From every
|
|
1057
|
+
binding, the return type (`Promise<T>` unwrapped, the events of a `@Subscription`), the `@Args`
|
|
1058
|
+
parameters and the `@Parent()` type are compared with the SDL, following the fields without
|
|
1059
|
+
resolver through the properties of the returned types:
|
|
1060
|
+
|
|
1061
|
+
| SDL | Output (return types, properties): assignable | Input (arguments, input fields): accepts every value |
|
|
1062
|
+
| ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
1063
|
+
| `T!` / `T` | `T!` rejects `null`, `undefined`, optional properties | `T` without default: accept `undefined`; with one: `null` or `undefined`; a nullable item: `null` |
|
|
1064
|
+
| `[T]` | arrays, tuples, `Set`, `ReadonlySet`, `Iterable` | arrays |
|
|
1065
|
+
| `String`, `ID` | strings, string literals and enums, branded strings (`ID`: numbers too) | `string` (a literal union is narrower) |
|
|
1066
|
+
| `Int`, `Float` | numbers, number literals and enums, `Int` / `Float` | `number` |
|
|
1067
|
+
| `Boolean` | `boolean`, `true`, `false` | `boolean` |
|
|
1068
|
+
| `DateTime`, `BigInt`, `JSON` | `Date`; `bigint` or number; anything | `Date`; `bigint`; anything |
|
|
1069
|
+
| custom scalar | the `T` of its `scalar<T>()` | the `T` of its `scalar<T>()` |
|
|
1070
|
+
| enum | literals or an enum whose values are SDL values | `string`, or literals / an enum covering every value |
|
|
1071
|
+
| object | an object type with a conforming property for each field without resolver | – |
|
|
1072
|
+
| union, interface | each member matched by its `__typename` literal, else by name | – |
|
|
1073
|
+
| input object | – | an object type whose properties are fields of the input |
|
|
1074
|
+
|
|
1075
|
+
When a nullable argument or input field is read by TypeScript types that accept `undefined` but not
|
|
1076
|
+
`null`, the runtime turns `null` into `undefined` (`nullAsUndefined`), as in code-first (never for
|
|
1077
|
+
list items). The `T` of another `scalar<T>()` declaration (a branded id for an `ID`) is accepted for
|
|
1078
|
+
`String` and `ID`.
|
|
1079
|
+
`any`, `unknown`, opaque types, missing annotations and arguments validated by a Standard Schema are
|
|
1080
|
+
not checked. Each mismatch is one diagnostic on the TypeScript annotation, the SDL definition
|
|
1081
|
+
related:
|
|
1082
|
+
|
|
1083
|
+
```txt
|
|
1084
|
+
✗ ZNIGHT068: BookResolver.book returns Book | null, which may be null, but Query.book is Book! (non-null)
|
|
1085
|
+
--> src/book.resolver.ts:7:33
|
|
1086
|
+
|
|
|
1087
|
+
7 | book(@Args('id') id: string): Promise<Book | null> {
|
|
1088
|
+
| ^^^^^^^^^^^^^^^^^^^^
|
|
1089
|
+
|
|
|
1090
|
+
::: src/schema.graphql:7:3
|
|
1091
|
+
|
|
|
1092
|
+
7 | book(id: ID!): Book!
|
|
1093
|
+
| ---- Query.book is declared here
|
|
1094
|
+
= hint: Return a value in every case, or make Query.book nullable in the SDL (Book)
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
**graphql-codegen.** The `typescript` plugin of GraphQL Code Generator writes the TypeScript types
|
|
1098
|
+
of the SDL; map every custom scalar to the `T` of its `scalar<T>()` declaration:
|
|
1099
|
+
|
|
1100
|
+
```yaml
|
|
1101
|
+
# codegen.yml
|
|
1102
|
+
schema: src/**/*.graphql
|
|
1103
|
+
generates:
|
|
1104
|
+
src/graphql.generated.ts:
|
|
1105
|
+
plugins: [ typescript ]
|
|
1106
|
+
config:
|
|
1107
|
+
scalars:
|
|
1108
|
+
DateTime: Date
|
|
1109
|
+
EventId: ./ids.ts#EventId
|
|
1110
|
+
namingConvention:
|
|
1111
|
+
enumValues: keep
|
|
1112
|
+
useTypeImports: true
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Its output (`Scalars['X']['output']`, `Maybe<T>`, `InputMaybe<T>`, the `QueryXxxArgs` types,
|
|
1116
|
+
`__typename` on object types, intersections with interface types) passes the checks as is; the
|
|
1117
|
+
repository tests it with the verbatim output of graphql-codegen 7.4.6
|
|
1118
|
+
([`test/fixtures/codegen`](./test/fixtures/codegen)). The `typescript-resolvers` plugin is optional:
|
|
1119
|
+
decorated methods are checked directly. Models that replace the fields a field resolver computes
|
|
1120
|
+
(`authorId` instead of `author`) conform, since only the fields without resolver must be properties.
|
|
1121
|
+
|
|
1122
|
+
**Performance.** The SDL is parsed and validated once per compile; the conformance walk visits each
|
|
1123
|
+
(SDL type, TypeScript declaration) pair once, on shapes the compiler caches. An SDL of about 560
|
|
1124
|
+
types (80 SDL files, 160 TypeScript files) compiles in about 200 ms on Bun
|
|
1125
|
+
(`test/compiler/schema-first-performance.test.ts`).
|
|
1126
|
+
Boot and execution are those of code-first.
|
|
1127
|
+
|
|
866
1128
|
## Observability
|
|
867
1129
|
|
|
868
1130
|
Every operation goes through the `graphql:operation` hook of `@znight/core` (`GraphQLOperationEvent`:
|
|
@@ -935,6 +1197,8 @@ Build-time diagnostics carry a location, a hint and, for types, the path from th
|
|
|
935
1197
|
| ZNIGHT064 | Union members that cannot be told apart (no exported classes, no `__typename`, no literal property with a distinct value per member) |
|
|
936
1198
|
| ZNIGHT065 | A `@Loader()` parameter not typed `Loader<L>`, `L` not implementing `BatchLoad<K, V>`; at runtime, a batch returning another number of values than keys, invalid loader options |
|
|
937
1199
|
| ZNIGHT066 | Boot: malformed or version-skewed `extensions.graphql` (every problem listed in one error), graphql-js schema validation, invalid options, no `HttpPlugin` with the HTTP transport, a scalar implemented under another name |
|
|
1200
|
+
| ZNIGHT067 | Schema-first SDL: no file matches, syntax errors, duplicate or unknown types, fields and directives, invalid extensions, graphql-js schema validation, `@oneOf`, a scalar without `scalar()`; custom directives and built-in scalar overrides (warnings) |
|
|
1201
|
+
| ZNIGHT068 | Schema-first: a field without resolver, a resolver, type or argument the SDL does not declare, a field nothing provides, a signature that does not conform to the SDL |
|
|
938
1202
|
|
|
939
1203
|
## Compiler contract
|
|
940
1204
|
|
|
@@ -952,12 +1216,27 @@ interface GraphQLMetadata {
|
|
|
952
1216
|
}
|
|
953
1217
|
|
|
954
1218
|
type GraphQLTypeDefinition =
|
|
955
|
-
| { kind: 'object'; name: string; description?: string; fields: GraphQLFieldDefinition[] }
|
|
1219
|
+
| { kind: 'object'; name: string; description?: string; interfaces?: string[]; fields: GraphQLFieldDefinition[] }
|
|
1220
|
+
| {
|
|
1221
|
+
kind: 'interface' // schema-first only; its fields have no resolver
|
|
1222
|
+
name: string
|
|
1223
|
+
description?: string
|
|
1224
|
+
interfaces?: string[]
|
|
1225
|
+
fields: GraphQLFieldDefinition[]
|
|
1226
|
+
discriminator: GraphQLDiscriminator
|
|
1227
|
+
}
|
|
956
1228
|
| { kind: 'input'; name: string; description?: string; fields: GraphQLInputFieldDefinition[] }
|
|
957
1229
|
| { kind: 'enum'; name: string; description?: string; values: GraphQLEnumValueDefinition[] }
|
|
958
1230
|
| { kind: 'union'; name: string; description?: string; members: string[]; discriminator: GraphQLDiscriminator }
|
|
959
1231
|
| { kind: 'scalar'; name: string; description?: string; builtin: true } // DateTime, BigInt, JSON
|
|
960
|
-
| {
|
|
1232
|
+
| {
|
|
1233
|
+
kind: 'scalar'
|
|
1234
|
+
name: string
|
|
1235
|
+
scalar: ScalarDefinition<unknown> | GraphQLScalarType // imported from user code
|
|
1236
|
+
description?: string // wins over the definition's
|
|
1237
|
+
specifiedByURL?: string
|
|
1238
|
+
sdl?: true // schema-first: description and specifiedByURL replace the definition's, absent ones included
|
|
1239
|
+
}
|
|
961
1240
|
|
|
962
1241
|
interface GraphQLFieldDefinition {
|
|
963
1242
|
name: string
|
|
@@ -1037,12 +1316,16 @@ with field resolvers after the properties, enum values in declaration order, the
|
|
|
1037
1316
|
`build` and `serve`, from sources or `dist`. Guards (`@UseGuard`) and loaders (`Loader<L>`) are
|
|
1038
1317
|
bound with lazy edges (no ZNIGHT003). The extension imports no `graphql`, `dataloader` nor runtime
|
|
1039
1318
|
entry; of `@znight/http` it only imports `@znight/http/compiler` (`readRouteMeta`,
|
|
1040
|
-
`effectiveRouteMeta`, `requiredRouteGroups`).
|
|
1319
|
+
`effectiveRouteMeta`, `requiredRouteGroups`). In schema-first mode only, it loads
|
|
1320
|
+
`graphql/language`, `graphql/type` and `graphql/utilities` lazily (never the executor). Interfaces
|
|
1321
|
+
are emitted by schema-first only: code-first sections, hashes and artifacts are unchanged.
|
|
1041
1322
|
|
|
1042
1323
|
## Limitations
|
|
1043
1324
|
|
|
1044
|
-
- **
|
|
1045
|
-
directives, no `@oneOf`.
|
|
1325
|
+
- **Code-first maps no TypeScript type to a GraphQL interface**: unions cover polymorphic results
|
|
1326
|
+
(the schema-first mode supports interfaces). No custom directives, no `@oneOf`.
|
|
1327
|
+
- Schema-first: no `__resolveType` methods (return objects carrying `__typename`, or classes), no
|
|
1328
|
+
custom internal enum values (an enum value is its SDL name).
|
|
1046
1329
|
- **Subscriptions over SSE only.** No `graphql-ws` (WebSocket), no SSE single connection mode, no
|
|
1047
1330
|
multipart subscriptions.
|
|
1048
1331
|
- No `@defer` / `@stream`, no persisted or trusted documents, no query cost analysis, no response
|
package/dist/compiler.d.mts
CHANGED
|
@@ -11,6 +11,19 @@ interface ResolverData {
|
|
|
11
11
|
readonly scope: Scope;
|
|
12
12
|
/** Location of `@Resolver`. */
|
|
13
13
|
readonly location: Location;
|
|
14
|
+
/**
|
|
15
|
+
* `@Resolver('Event')`: the SDL object type its `@ResolveField` methods belong to (schema-first,
|
|
16
|
+
* RFC 0007), with the location of the argument.
|
|
17
|
+
*/
|
|
18
|
+
readonly typeName?: {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly location: Location;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Schema-first: a class the analysis rejected (an error was reported); it only tells which fields
|
|
24
|
+
* its methods name (never bound, never a provider).
|
|
25
|
+
*/
|
|
26
|
+
readonly invalid?: true;
|
|
14
27
|
/** Class `@UseGuard` guards. */
|
|
15
28
|
readonly guards: readonly ClassRef[];
|
|
16
29
|
/** One entry per field decorator of the class's methods, in source order. */
|
|
@@ -62,8 +75,8 @@ type ParamData = {
|
|
|
62
75
|
/** Everything the extension stores. */
|
|
63
76
|
type GraphQLData = ResolverData;
|
|
64
77
|
/**
|
|
65
|
-
* Options of the compiler extension (RFC 0005 §6.7
|
|
66
|
-
* `znight.config.ts`. Import-free.
|
|
78
|
+
* Options of the compiler extension (RFC 0005 §6.7, RFC 0007 §4.1), given with
|
|
79
|
+
* `graphql.configure({ ... })` in `znight.config.ts`. Import-free.
|
|
67
80
|
*/
|
|
68
81
|
/** Options of `graphql.configure()`. */
|
|
69
82
|
interface GraphQLCompilerOptions {
|
|
@@ -72,13 +85,21 @@ interface GraphQLCompilerOptions {
|
|
|
72
85
|
* generated module and checked by `znight generate --check`. Default `false`: no file.
|
|
73
86
|
*/
|
|
74
87
|
readonly sdl?: string | false;
|
|
75
|
-
/** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. */
|
|
88
|
+
/** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. Code-first only. */
|
|
76
89
|
readonly number?: "Float" | "Int";
|
|
90
|
+
/**
|
|
91
|
+
* Schema-first mode (RFC 0007): globs of the `.graphql` files that define the schema, relative to
|
|
92
|
+
* the project root (`'src/**\/*.graphql'`, `!` negations allowed). Resolvers are bound to their
|
|
93
|
+
* fields by name and checked against the SDL. Default `false`: code-first.
|
|
94
|
+
*/
|
|
95
|
+
readonly schema?: string | readonly string[] | false;
|
|
77
96
|
}
|
|
78
97
|
/** {@link GraphQLCompilerOptions} with their defaults. */
|
|
79
98
|
interface ResolvedGraphQLCompilerOptions {
|
|
80
99
|
readonly sdl: string | false;
|
|
81
100
|
readonly number: "Float" | "Int";
|
|
101
|
+
/** The schema-first globs; `false`: code-first. */
|
|
102
|
+
readonly schema: readonly string[] | false;
|
|
82
103
|
}
|
|
83
104
|
/** The `@znight/graphql` compiler extension, with its options and `configure()`. */
|
|
84
105
|
export interface GraphQLCompilerExtension extends CompilerExtension<GraphQLData> {
|