@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.
Files changed (42) hide show
  1. package/README.md +288 -5
  2. package/dist/compiler.d.mts +24 -3
  3. package/dist/compiler.mjs +1876 -109
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/index.d.mts +49 -8
  6. package/dist/index.mjs +10 -4
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/names-BlzDM5cF.mjs.map +1 -1
  9. package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
  10. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  11. package/dist/testing.d.mts +1 -1
  12. package/dist/testing.mjs +1 -1
  13. package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
  14. package/package.json +8 -8
  15. package/src/compiler/analyze.ts +129 -10
  16. package/src/compiler/builder.ts +6 -85
  17. package/src/compiler/checks.ts +105 -0
  18. package/src/compiler/conformance.ts +1068 -0
  19. package/src/compiler/constants.ts +6 -0
  20. package/src/compiler/data.ts +10 -0
  21. package/src/compiler/generate.ts +41 -27
  22. package/src/compiler/graphql-js.ts +42 -0
  23. package/src/compiler/index.ts +8 -3
  24. package/src/compiler/locate.ts +39 -1
  25. package/src/compiler/model.ts +16 -1
  26. package/src/compiler/options.ts +39 -8
  27. package/src/compiler/schema-first.ts +595 -0
  28. package/src/compiler/sdl-source.ts +529 -0
  29. package/src/compiler/sdl-view.ts +3 -1
  30. package/src/compiler/suggest.ts +39 -0
  31. package/src/decorators.ts +9 -3
  32. package/src/index.ts +5 -0
  33. package/src/metadata.ts +28 -1
  34. package/src/options.ts +23 -1
  35. package/src/pipeline.ts +66 -5
  36. package/src/plugin.ts +27 -5
  37. package/src/results.ts +206 -0
  38. package/src/scalars.ts +10 -3
  39. package/src/schema.ts +90 -12
  40. package/src/sdl.ts +17 -2
  41. package/src/types.ts +35 -0
  42. 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
- | { kind: 'scalar'; name: string; scalar: ScalarDefinition<unknown> | GraphQLScalarType } // imported from user code
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
- - **No GraphQL interfaces** in this version: unions cover polymorphic results. No custom
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
@@ -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), given with `graphql.configure({ ... })` in
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> {