Skip to content

Commit

Permalink
Add missing docs, deprecate @discriminationAlias
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitriy Lazarev <[email protected]>
  • Loading branch information
wKich committed Sep 13, 2024
1 parent 9b1fc21 commit 33b1a42
Show file tree
Hide file tree
Showing 28 changed files with 1,132 additions and 572 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ types and how to resolve them.
- [`@field`](#field)
- [`@implements`](#implements)
- [`@discriminates`](#discriminates)
- [`@discriminationAlias`](#discriminationalias)
- [`@resolve`](#resolve)
- [Getting started](#getting-started)
- [GraphQL Application](#graphql-application)
Expand Down Expand Up @@ -122,6 +121,8 @@ type Service @implements(interface: "Entity") {
_NOTE: In this example if we have data of `Entity` type and it has `kind` field_
_with `Component` value, that means data will be resolved to `Component` type_

#### `opaqueType`

There is a special case when your runtime data doesn't have a value
that can be used to discriminate the interface or there is no type
that matches the value. In this case, you can define `opaqueType` argument
Expand All @@ -141,26 +142,25 @@ plugin will generate it for you.
There is another way to define opaque types for all interfaces by using `generateOpaqueTypes`
option for GraphQL plugin.

### `@discriminationAlias`
#### `aliases`

By default value from `with` argument is used to find a type as-is or converted to PascalCase.
Sometimes you need to match the value with a type that has a different name.
In this case, you can use `@discriminationAlias` directive.
By default value from `with` argument is used to find a type as-is or converted to PascalCase.
Sometimes you need to match the value with a type that has a different name.
In this case, you can define `aliases` argument.

```graphql
interface API
@implements(interface: "Node")
@discriminates(with: "spec.type")
@discriminationAlias(value: "openapi", type: "OpenAPI") {
@discriminates(with: "spec.type", aliases: [{ value: "grpc", type: "GrpcAPI" }]) {
# ...
}

type OpenAPI @implements(interface: "API") {
type GrpcAPI @implements(interface: "API") {
# ...
}
```

This means, when `spec.type` equals to `openapi`, the `API` interface will be resolved to `OpenAPI` type.
This means, when `spec.type` equals to `grpc`, the `API` interface will be resolved to `GrpcAPI` type.

### `@resolve`

Expand Down
8 changes: 7 additions & 1 deletion src/__snapshots__/schema.graphql.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
directive @discriminates(opaqueType: String, with: _DirectiveArgument_) on INTERFACE
directive @discriminates(aliases: [DiscriminationAlias!], opaqueType: String, with: _DirectiveArgument_) on INTERFACE

"""@deprecated Please use `@discriminates(aliases: [...])`"""
directive @discriminationAlias(type: String!, value: String!) repeatable on INTERFACE

directive @field(at: _DirectiveArgument_, default: _DirectiveArgument_) on FIELD_DEFINITION
Expand All @@ -14,6 +15,11 @@ interface Connection {
pageInfo: PageInfo!
}

input DiscriminationAlias {
type: String!
value: String!
}

interface Edge {
cursor: String!
node: Node!
Expand Down
8 changes: 8 additions & 0 deletions src/__snapshots__/types.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export type Connection = {
pageInfo: PageInfo;
};

export type DiscriminationAlias = {
type: Scalars['String']['input'];
value: Scalars['String']['input'];
};

export type Edge = {
cursor: Scalars['String']['output'];
node: Node;
Expand Down Expand Up @@ -137,6 +142,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = {
export type ResolversTypes = {
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
Connection: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Connection']>;
DiscriminationAlias: DiscriminationAlias;
Edge: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Edge']>;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
Expand All @@ -151,6 +157,7 @@ export type ResolversTypes = {
export type ResolversParentTypes = {
Boolean: Scalars['Boolean']['output'];
Connection: ResolversInterfaceTypes<ResolversParentTypes>['Connection'];
DiscriminationAlias: DiscriminationAlias;
Edge: ResolversInterfaceTypes<ResolversParentTypes>['Edge'];
ID: Scalars['ID']['output'];
Int: Scalars['Int']['output'];
Expand All @@ -162,6 +169,7 @@ export type ResolversParentTypes = {
};

export type DiscriminatesDirectiveArgs = {
aliases?: Maybe<Array<DiscriminationAlias>>;
opaqueType?: Maybe<Scalars['String']['input']>;
with?: Maybe<Scalars['_DirectiveArgument_']['input']>;
};
Expand Down
9 changes: 9 additions & 0 deletions src/core/core.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ directive @field(
directive @discriminates(
with: _DirectiveArgument_
opaqueType: String
aliases: [DiscriminationAlias!]
) on INTERFACE
"""
@deprecated Please use `@discriminates(aliases: [...])`
"""
directive @discriminationAlias(
value: String!
type: String!
Expand All @@ -15,6 +19,11 @@ directive @resolve(at: _DirectiveArgument_, nodeType: String, from: String) on F

scalar _DirectiveArgument_

input DiscriminationAlias {
value: String!
type: String!
}

interface Node {
id: ID!
}
Expand Down
27 changes: 17 additions & 10 deletions src/core/resolveDirectiveMapper.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import _ from "lodash";
import { connectionFromArray, ConnectionArguments } from "graphql-relay";
import {
GraphQLInputObjectType,
type GraphQLFieldConfig,
type GraphQLInterfaceType,
GraphQLInt,
GraphQLString,
type GraphQLFieldConfig,
type GraphQLInterfaceType,
} from "graphql";
import type {
DirectiveMapperAPI,
FieldResolver,
ResolverContext,
} from "../types.js";
import { ConnectionArguments, connectionFromArray } from "graphql-relay";
import _ from "lodash";
import { HYDRAPHQL_EXTENSION } from "src/constants.js";
import {
createConnectionType,
decodeId,
Expand All @@ -21,7 +17,11 @@ import {
isNamedListType,
unboxNamedType,
} from "../helpers.js";
import { HYDRAPHQL_EXTENSION } from "src/constants.js";
import type {
DirectiveMapperAPI,
FieldResolver,
ResolverContext,
} from "../types.js";

export function resolveDirectiveMapper(
fieldName: string,
Expand Down Expand Up @@ -97,6 +97,11 @@ export function resolveDirectiveMapper(
}
}

// FIXME: This doesn't work if a single ref is resolved to multiple nodes
// We need to load all nodes
// So here we might have a single connection or array of connections
// TODO Throw an error if we have an array of refs and a single connection
// TODO Throw an error if we have a single ref and an array of connections
const ids = ((ref ?? []) as string[]).map((r) => ({
id: encodeId({
source,
Expand All @@ -108,6 +113,7 @@ export function resolveDirectiveMapper(
}),
}));

// FIXME: We need to apply connection
return {
...connectionFromArray(ids, args as ConnectionArguments),
count: ids.length,
Expand Down Expand Up @@ -176,6 +182,7 @@ export function resolveDirectiveMapper(
fieldResolver,
},
};
// TODO Add test case of handling [Connection] type (array of connections)
field.resolve = async ({ id }, args, context, info) => {
if (directive.at === "id") return { id };

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./createLoader.js";
export * from "./createGraphQLApp.js";
export * from "./core/core.js";
export * from "./helpers.js";
export * from "./loadSchema.js";
export { transformSchema } from "./transformSchema.js";
export type {
GraphQLContext,
Expand Down
22 changes: 16 additions & 6 deletions src/loadSchema.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { CodeFileLoader } from "@graphql-tools/code-file-loader";
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
import { loadTypedefs } from "@graphql-tools/load";
import { loadTypedefs, loadTypedefsSync } from "@graphql-tools/load";
import {
getResolversFromSchema,
printSchemaWithDirectives,
Source,
} from "@graphql-tools/utils";
import { createModule, gql } from "graphql-modules";

export async function loadSchema(schema: string | string[]) {
const sources = await loadTypedefs(schema, {
sort: true,
loaders: [new CodeFileLoader(), new GraphQLFileLoader()],
});
const loadTypeDefsOptions = {
sort: true,
loaders: [new CodeFileLoader(), new GraphQLFileLoader()],
};

function sources2modules(sources: Source[]) {
return sources.map((source, index) =>
createModule({
id: source.location ?? `unknown_${index}`,
Expand All @@ -24,3 +26,11 @@ export async function loadSchema(schema: string | string[]) {
}),
);
}

export async function loadSchema(schema: string | string[]) {
return sources2modules(await loadTypedefs(schema, loadTypeDefsOptions));
}

export function loadSchemaSync(schema: string | string[]) {
return sources2modules(loadTypedefsSync(schema, loadTypeDefsOptions));
}
84 changes: 47 additions & 37 deletions src/mapDirectives.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ describe("mapDirectives", () => {
const schema = transform(gql`
interface Entity
@implements(interface: "Node")
@discriminates(with: "kind")
@discriminationAlias(value: "component", type: "Component")
@discriminationAlias(value: "template", type: "Template")
@discriminationAlias(value: "location", type: "Location") {
@discriminates(
with: "kind"
aliases: [
{ value: "component", type: "Component" }
{ value: "template", type: "Template" }
{ value: "location", type: "Location" }
]
) {
totalCount: Int!
}
Expand Down Expand Up @@ -390,14 +394,18 @@ describe("mapDirectives", () => {
);
});

void test(`should fail if @discriminationAlias has ambiguous types`, () => {
void test(`should fail if discrimination aliases have ambiguous types`, () => {
expect(() =>
transform(gql`
interface Entity
@implements(interface: "Node")
@discriminates(with: "kind")
@discriminationAlias(value: "component", type: "EntityComponent")
@discriminationAlias(value: "component", type: "Component") {
@discriminates(
with: "kind"
aliases: [
{ value: "component", type: "EntityComponent" }
{ value: "component", type: "Component" }
]
) {
name: String!
}
Expand All @@ -414,20 +422,6 @@ describe("mapDirectives", () => {
);
});

void test(`should fail if @discriminationAlias is used without @discriminates`, () => {
expect(() =>
transform(gql`
interface Entity
@implements(interface: "Node")
@discriminationAlias(value: "component", type: "EntityComponent") {
name: String!
}
`),
).toThrow(
`The "Entity" interface has @discriminationAlias directive but doesn't have @discriminates directive`,
);
});

void test(`should fail if interface has multiple implementations and @discriminates is not specified`, () => {
expect(() =>
transform(gql`
Expand Down Expand Up @@ -565,9 +559,11 @@ describe("mapDirectives", () => {
expect(() =>
transform(gql`
interface Entity
@discriminates(with: "kind")
@implements(interface: "Node")
@discriminationAlias(value: "component", type: "Component") {
@discriminates(
with: "kind"
aliases: [{ value: "component", type: "Component" }]
)
@implements(interface: "Node") {
name: String!
}
type Resource @implements(interface: "Entity") {
Expand All @@ -580,7 +576,7 @@ describe("mapDirectives", () => {
}
`),
).toThrow(
'Type(-s) "Component" in `interface Entity @discriminationAlias(value: ..., type: ...)` must implement "Entity" interface by using @implements directive',
'Type(-s) "Component" in `interface Entity @discriminates(aliases: [...])` must implement "Entity" interface by using @implements directive',
);
});

Expand Down Expand Up @@ -895,9 +891,13 @@ describe("mapDirectives", () => {
id: "test",
typeDefs: gql`
interface Node
@discriminates(with: "__source")
@discriminationAlias(value: "Mock", type: "Entity")
@discriminationAlias(value: "GraphQL", type: "GraphQLEntity")
@discriminates(
with: "__source"
aliases: [
{ value: "Mock", type: "Entity" }
{ value: "GraphQL", type: "GraphQLEntity" }
]
)
type Entity @implements(interface: "Node") {
parent: GraphQLEntity @resolve(at: "spec.parentId", from: "GraphQL")
Expand Down Expand Up @@ -965,9 +965,13 @@ describe("mapDirectives", () => {
id: "test",
typeDefs: gql`
interface Node
@discriminates(with: "__source")
@discriminationAlias(value: "Mock", type: "Entity")
@discriminationAlias(value: "Tasks", type: "TaskProperty")
@discriminates(
with: "__source"
aliases: [
{ value: "Mock", type: "Entity" }
{ value: "Tasks", type: "TaskProperty" }
]
)
type Entity @implements(interface: "Node") {
property(name: String!): TaskProperty
Expand Down Expand Up @@ -1051,9 +1055,13 @@ describe("mapDirectives", () => {
id: "test",
typeDefs: gql`
interface Node
@discriminates(with: "__source")
@discriminationAlias(value: "Mock", type: "Entity")
@discriminationAlias(value: "Tasks", type: "Task")
@discriminates(
with: "__source"
aliases: [
{ value: "Mock", type: "Entity" }
{ value: "Tasks", type: "Task" }
]
)
type Entity @implements(interface: "Node") {
task(taskId: ID!): Task @resolve(from: "Tasks")
Expand Down Expand Up @@ -1202,8 +1210,10 @@ describe("mapDirectives", () => {
typeDefs: gql`
interface Entity
@implements(interface: "Node")
@discriminates(with: "kind")
@discriminationAlias(value: "User", type: "Employee") {
@discriminates(
with: "kind"
aliases: [{ value: "User", type: "Employee" }]
) {
name: String! @field(at: "name")
}
Expand Down
Loading

0 comments on commit 33b1a42

Please sign in to comment.