Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add frontside.com documentation minisite #15

Merged
merged 5 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist
examples
www
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
Loading